Greasy Fork is available in English.

M3Unator - Web Directory Playlist Creator

Create M3U/M3U8 playlists from directory listing pages. Automatically finds video and audio files in web server indexes.

  1. // ==UserScript==
  2. // @name M3Unator - Web Directory Playlist Creator
  3. // @namespace https://github.com/hasanbeder/M3Unator
  4. // @version 1.0.2
  5. // @description Create M3U/M3U8 playlists from directory listing pages. Automatically finds video and audio files in web server indexes.
  6. // @author Hasan Beder
  7. // @license GPL-3.0
  8. // @match *://*/*
  9. // @grant GM_addStyle
  10. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWMyZTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjIzIDcgMTYgMTIgMjMgMTcgMjMgNyIvPjxyZWN0IHg9IjEiIHk9IjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNCIgcng9IjIiIHJ5PSIyIi8+PC9zdmc+
  11. // @homepageURL https://github.com/hasanbeder/M3Unator
  12. // @supportURL https://github.com/hasanbeder/M3Unator/issues
  13. // @run-at document-end
  14. // @noframes true
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. if (!document.title.includes('Index of') && !document.querySelector('div#table-list')) {
  21. console.log('This page is not an Index page, M3Unator disabled.');
  22. return;
  23. }
  24.  
  25. function parseLiteSpeedDirectory() {
  26. const links = [];
  27. const rows = document.querySelectorAll('#table-content tr');
  28. rows.forEach(row => {
  29. const linkElement = row.querySelector('a');
  30. if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
  31. const href = linkElement.getAttribute('href');
  32. if (href) {
  33. links.push(new URL(href, window.location.href).href);
  34. }
  35. }
  36. });
  37. return links;
  38. }
  39.  
  40. // Add LiteSpeed support to the existing getDirectoryLinks function
  41. function getDirectoryLinks() {
  42. const links = [];
  43. // LiteSpeed directory listing
  44. if (document.querySelector('div#table-list')) {
  45. const rows = document.querySelectorAll('#table-content tr');
  46. rows.forEach(row => {
  47. const linkElement = row.querySelector('a');
  48. if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
  49. const href = linkElement.getAttribute('href');
  50. if (href) {
  51. links.push(new URL(href, window.location.href).href);
  52. }
  53. }
  54. });
  55. return links;
  56. }
  57. // Apache/Nginx style directory listing
  58. const anchors = document.querySelectorAll('a');
  59. anchors.forEach(anchor => {
  60. if (!anchor.textContent.includes('Parent Directory')) {
  61. const href = anchor.getAttribute('href');
  62. if (href && !href.startsWith('?') && !href.startsWith('/')) {
  63. links.push(new URL(href, window.location.href).href);
  64. }
  65. }
  66. });
  67. return links;
  68. }
  69.  
  70. GM_addStyle(`
  71. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
  72.  
  73. [class^="M3Unator"] {
  74. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  75. }
  76.  
  77. .M3Unator-title {
  78. font-weight: 700;
  79. letter-spacing: -0.02em;
  80. }
  81.  
  82. .M3Unator-input-group label {
  83. font-weight: 500;
  84. letter-spacing: -0.01em;
  85. }
  86.  
  87. .M3Unator-input {
  88. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  89. font-size: 0.9375rem;
  90. letter-spacing: -0.01em;
  91. }
  92.  
  93. .M3Unator-button {
  94. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  95. font-weight: 600;
  96. letter-spacing: -0.01em;
  97. }
  98.  
  99. .M3Unator-control-btn {
  100. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  101. font-weight: 500;
  102. letter-spacing: -0.01em;
  103. }
  104.  
  105. .M3Unator-log {
  106. font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
  107. font-size: 0.8125rem;
  108. letter-spacing: -0.01em;
  109. line-height: 1.5;
  110. }
  111.  
  112. .M3Unator-log-counter {
  113. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  114. font-weight: 600;
  115. letter-spacing: -0.01em;
  116. }
  117.  
  118. .M3Unator-container {
  119. position: fixed;
  120. inset: 0;
  121. background: rgba(0, 0, 0, 0.75);
  122. backdrop-filter: blur(8px);
  123. display: none;
  124. place-items: center;
  125. padding: 1rem;
  126. z-index: 9999;
  127. }
  128.  
  129. .M3Unator-container[data-visible="true"] {
  130. display: grid;
  131. }
  132.  
  133. .M3Unator-overlay {
  134. position: fixed;
  135. inset: 0;
  136. background: transparent;
  137. z-index: 9998;
  138. }
  139.  
  140. body.modal-open {
  141. overflow: hidden;
  142. pointer-events: none; /* Prevent background clicks */
  143. }
  144.  
  145. body.modal-open .M3Unator-container,
  146. body.modal-open .M3Unator-popup {
  147. pointer-events: all; /* Allow clicks on modal content */
  148. }
  149.  
  150. .M3Unator-popup {
  151. background: #11111b;
  152. color: #cdd6f4;
  153. width: 100%;
  154. max-width: 480px;
  155. border-radius: 12px;
  156. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  157. overflow: hidden;
  158. animation: slideUp 0.3s ease;
  159. position: absolute;
  160. }
  161.  
  162. .M3Unator-header {
  163. padding: 1.25rem 1.618rem;
  164. background: #1e1e2e;
  165. color: #cdd6f4;
  166. display: flex;
  167. align-items: center;
  168. justify-content: space-between;
  169. cursor: move;
  170. user-select: none;
  171. border-bottom: 1px solid #313244;
  172. }
  173.  
  174. .M3Unator-title {
  175. display: flex;
  176. align-items: center;
  177. gap: 0.75rem;
  178. margin: 0;
  179. font-size: 1.25rem;
  180. font-weight: 600;
  181. line-height: 1;
  182. }
  183.  
  184. .M3Unator-title svg {
  185. width: 24px;
  186. height: 24px;
  187. color: #f5c2e7;
  188. filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
  189. flex-shrink: 0;
  190. display: flex;
  191. align-items: center;
  192. justify-content: center;
  193. margin-top: 1px;
  194. }
  195.  
  196. .M3Unator-title span {
  197. display: flex;
  198. align-items: center;
  199. line-height: 24px;
  200. background: linear-gradient(90deg,
  201. #f5c2e7,
  202. #cba6f7,
  203. #89b4fa,
  204. #a6e3a1,
  205. #f5c2e7
  206. );
  207. background-size: 300% auto;
  208. -webkit-background-clip: text;
  209. background-clip: text;
  210. -webkit-text-fill-color: transparent;
  211. animation: gradient 3s linear infinite;
  212. }
  213.  
  214. .M3Unator-close {
  215. background: rgba(203, 166, 247, 0.1);
  216. border: none;
  217. color: #cba6f7;
  218. width: 32px;
  219. height: 32px;
  220. border-radius: 8px;
  221. display: grid;
  222. place-items: center;
  223. cursor: pointer;
  224. transition: all 0.2s ease;
  225. }
  226.  
  227. .M3Unator-close:hover {
  228. background: rgba(203, 166, 247, 0.2);
  229. transform: rotate(360deg);
  230. }
  231.  
  232. .M3Unator-close svg {
  233. width: 18px;
  234. height: 18px;
  235. }
  236.  
  237. .M3Unator-content {
  238. padding: 0.75rem;
  239. display: flex;
  240. flex-direction: column;
  241. gap: 0.75rem;
  242. }
  243.  
  244. .M3Unator-input-group {
  245. margin-bottom: 0;
  246. }
  247.  
  248. .M3Unator-input-group label {
  249. display: block;
  250. margin-bottom: 0.5rem;
  251. font-weight: 500;
  252. color: #bac2de;
  253. }
  254.  
  255. .M3Unator-input {
  256. width: 100%;
  257. height: 42px; /* Same height as Create Playlist button */
  258. padding: 0 12px;
  259. border: 1px solid #45475a;
  260. border-radius: 8px;
  261. background: #1e1e2e;
  262. color: #f5c2e7;
  263. font-size: 14px;
  264. transition: all 0.2s ease;
  265. box-sizing: border-box;
  266. }
  267.  
  268. .M3Unator-input:focus {
  269. outline: none;
  270. border-color: #f5c2e7;
  271. box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1);
  272. }
  273.  
  274. .M3Unator-input::placeholder {
  275. color: #6c7086;
  276. opacity: 1;
  277. }
  278.  
  279. .M3Unator-toggle-container {
  280. position: relative;
  281. display: flex;
  282. align-items: center;
  283. justify-content: center;
  284. }
  285.  
  286. .M3Unator-toggle-container input[type="checkbox"] {
  287. display: none;
  288. }
  289.  
  290. .M3Unator-toggle-container span {
  291. width: 48px;
  292. height: 48px;
  293. background: #1e1e2e;
  294. border: 2px solid #45475a;
  295. border-radius: 12px;
  296. display: inline-flex;
  297. align-items: center;
  298. justify-content: center;
  299. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  300. position: relative;
  301. }
  302.  
  303. .M3Unator-toggle-container svg {
  304. width: 24px;
  305. height: 24px;
  306. opacity: 0.7;
  307. transition: all 0.3s ease;
  308. position: absolute;
  309. top: 50%;
  310. left: 50%;
  311. transform: translate(-50%, -50%);
  312. }
  313.  
  314. .M3Unator-toggle-container input[type="checkbox"]:checked + span {
  315. background: rgba(203, 166, 247, 0.1);
  316. border-color: #cba6f7;
  317. box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
  318. }
  319.  
  320. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  321. opacity: 1;
  322. color: #cba6f7;
  323. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
  324. }
  325.  
  326. .M3Unator-toggle-container span:hover {
  327. background: #313244;
  328. transform: translateY(-2px);
  329. }
  330.  
  331. .M3Unator-toggle-container span:active {
  332. transform: translateY(1px);
  333. }
  334.  
  335. .M3Unator-toggle-container input[type="checkbox"]:checked + span:hover {
  336. background: rgba(203, 166, 247, 0.2);
  337. }
  338.  
  339. .M3Unator-toggle-container span:active {
  340. transform: translateY(1px);
  341. }
  342.  
  343. .M3Unator-toggle-container svg {
  344. width: 24px;
  345. height: 24px;
  346. opacity: 0.8;
  347. transition: all 0.2s ease;
  348. }
  349.  
  350. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  351. opacity: 1;
  352. color: #cba6f7;
  353. }
  354.  
  355. .M3Unator-toggle-group {
  356. display: flex;
  357. gap: 0.75rem;
  358. margin: 0.75rem 0;
  359. justify-content: center;
  360. background: rgba(30, 30, 46, 0.4);
  361. padding: 0.75rem;
  362. border-radius: 12px;
  363. backdrop-filter: blur(8px);
  364. }
  365.  
  366. [title]:hover::after {
  367. content: attr(title);
  368. position: absolute;
  369. bottom: calc(100% + 5px);
  370. left: 50%;
  371. transform: translateX(-50%);
  372. padding: 0.5rem 0.75rem;
  373. background: rgba(30, 30, 46, 0.95);
  374. color: #cdd6f4;
  375. font-size: 0.875rem;
  376. white-space: nowrap;
  377. border-radius: 6px;
  378. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  379. z-index: 1000;
  380. border: 1px solid #313244;
  381. text-align: center;
  382. backdrop-filter: blur(8px);
  383. pointer-events: none;
  384. }
  385.  
  386. .M3Unator-button {
  387. width: 100%;
  388. height: 42px;
  389. padding: 0 16px;
  390. border: none;
  391. border-radius: 8px;
  392. background: #f5c2e7;
  393. color: #1e1e2e;
  394. font-weight: 600;
  395. font-size: 14px;
  396. cursor: pointer;
  397. transition: all 0.2s ease;
  398. display: flex;
  399. align-items: center;
  400. justify-content: center;
  401. gap: 8px;
  402. }
  403.  
  404. .M3Unator-button:hover {
  405. background: #f5c2e7;
  406. transform: translateY(-1px);
  407. box-shadow: 0 4px 12px rgba(245, 194, 231, 0.2);
  408. }
  409.  
  410. .M3Unator-button:active {
  411. transform: translateY(0);
  412. }
  413.  
  414. .M3Unator-button:disabled {
  415. opacity: 0.5;
  416. cursor: not-allowed;
  417. }
  418.  
  419. .M3Unator-launcher {
  420. position: fixed;
  421. top: 1rem;
  422. right: 1.618rem;
  423. height: 48px;
  424. padding: 0 1.25rem;
  425. border-radius: 12px;
  426. background: rgba(30, 30, 46, 0.95);
  427. border: 2px solid #313244;
  428. cursor: pointer;
  429. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  430. display: flex;
  431. align-items: center;
  432. gap: 0.75rem;
  433. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  434. z-index: 9998;
  435. backdrop-filter: blur(12px);
  436. }
  437.  
  438. .M3Unator-launcher:hover {
  439. background: rgba(30, 30, 46, 0.98);
  440. border-color: #45475a;
  441. transform: translateY(-2px);
  442. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
  443. }
  444.  
  445. .M3Unator-launcher svg {
  446. width: 24px;
  447. height: 24px;
  448. color: #f5c2e7;
  449. filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
  450. }
  451.  
  452. .M3Unator-launcher span {
  453. font-weight: 600;
  454. font-size: 0.95rem;
  455. background: linear-gradient(90deg,
  456. #f5c2e7,
  457. #cba6f7,
  458. #89b4fa,
  459. #a6e3a1,
  460. #f5c2e7
  461. );
  462. background-size: 300% auto;
  463. -webkit-background-clip: text;
  464. background-clip: text;
  465. -webkit-text-fill-color: transparent;
  466. animation: gradient 3s linear infinite;
  467. }
  468.  
  469. @keyframes gradient {
  470. 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
  471. 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
  472. 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
  473. }
  474.  
  475. .M3Unator-dropdown {
  476. position: relative;
  477. width: 100%;
  478. }
  479.  
  480. .M3Unator-dropdown-button {
  481. width: 100%;
  482. padding: 0.618rem;
  483. background: #1e1e2e;
  484. border: 1px solid #313244;
  485. border-radius: 8px;
  486. color: #cdd6f4;
  487. font-size: 0.875rem;
  488. text-align: left;
  489. cursor: pointer;
  490. transition: all 0.2s ease;
  491. display: flex;
  492. align-items: center;
  493. justify-content: space-between;
  494. }
  495.  
  496. .M3Unator-dropdown-button:hover {
  497. border-color: #45475a;
  498. background: rgba(30, 30, 46, 0.8);
  499. }
  500.  
  501. .M3Unator-dropdown-button svg {
  502. width: 16px;
  503. height: 16px;
  504. min-width: 16px;
  505. min-height: 16px;
  506. transition: transform 0.2s ease;
  507. }
  508.  
  509. .M3Unator-dropdown.active .M3Unator-dropdown-button {
  510. border-color: #cba6f7;
  511. border-radius: 8px 8px 0 0;
  512. }
  513.  
  514. .M3Unator-dropdown.active .M3Unator-dropdown-button svg {
  515. transform: rotate(180deg);
  516. }
  517.  
  518. .M3Unator-dropdown-menu {
  519. position: absolute;
  520. top: 100%;
  521. left: 0;
  522. right: 0;
  523. background: #1e1e2e;
  524. border: 1px solid #cba6f7;
  525. border-top: none;
  526. border-radius: 0 0 8px 8px;
  527. overflow: hidden;
  528. z-index: 1000;
  529. display: none;
  530. animation: dropdownSlide 0.2s ease;
  531. user-select: none;
  532. }
  533.  
  534. .M3Unator-dropdown.active .M3Unator-dropdown-menu {
  535. display: block;
  536. }
  537.  
  538. .M3Unator-dropdown-item {
  539. padding: 0.618rem;
  540. color: #cdd6f4;
  541. cursor: pointer;
  542. transition: all 0.2s ease;
  543. user-select: none;
  544. }
  545.  
  546. .M3Unator-dropdown-item:hover {
  547. background: rgba(203, 166, 247, 0.1);
  548. }
  549.  
  550. .M3Unator-dropdown-item.selected {
  551. background: rgba(203, 166, 247, 0.1);
  552. color: #cba6f7;
  553. }
  554.  
  555. @keyframes slideUp {
  556. from { transform: translateY(20px); opacity: 0; }
  557. to { transform: translateY(0); opacity: 1; }
  558. }
  559.  
  560. .M3Unator-log {
  561. margin-top: 0.75rem;
  562. max-height: calc(100vh - 70vh);
  563. font-size: 0.8125rem;
  564. line-height: 1.4;
  565. }
  566.  
  567. .M3Unator-log:empty {
  568. display: none;
  569. }
  570.  
  571. .M3Unator-log-entry {
  572. padding: 0.25rem 0.5rem;
  573. border-bottom: 1px solid #313244;
  574. }
  575.  
  576. .M3Unator-log-entry:last-child {
  577. border-bottom: none;
  578. }
  579.  
  580. .M3Unator-log-entry.success {
  581. color: #94e2d5;
  582. }
  583.  
  584. .M3Unator-log-entry.error {
  585. color: #f38ba8;
  586. }
  587.  
  588. .M3Unator-log-entry.warning {
  589. color: #fab387;
  590. }
  591.  
  592. .M3Unator-log-counter {
  593. display: inline-flex;
  594. align-items: center;
  595. justify-content: center;
  596. background: rgba(245, 194, 231, 0.1);
  597. color: #f5c2e7;
  598. padding: 0.25rem 0.75rem;
  599. border-radius: 8px;
  600. font-size: 0.875rem;
  601. font-weight: 500;
  602. margin-left: 0.75rem;
  603. min-width: 3rem;
  604. text-align: center;
  605. }
  606.  
  607. @keyframes gradient {
  608. 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
  609. 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
  610. 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
  611. }
  612.  
  613. .M3Unator-title span.text {
  614. display: inline-block;
  615. position: relative;
  616. padding: 0 0.25rem;
  617. }
  618.  
  619. .M3Unator-title.scanning span.text {
  620. background: linear-gradient(90deg,
  621. #f5c2e7,
  622. #cba6f7,
  623. #89b4fa,
  624. #a6e3a1,
  625. #f5c2e7
  626. );
  627. background-size: 300% auto;
  628. -webkit-background-clip: text;
  629. background-clip: text;
  630. -webkit-text-fill-color: transparent;
  631. animation: gradient 3s linear infinite;
  632. font-weight: 700;
  633. letter-spacing: 0.5px;
  634. }
  635.  
  636. .M3Unator-title.scanning span.text::after {
  637. content: '';
  638. position: absolute;
  639. bottom: -2px;
  640. left: 0;
  641. width: 100%;
  642. height: 2px;
  643. background: inherit;
  644. animation: gradient 3s linear infinite;
  645. }
  646.  
  647. .M3Unator-title.scanning svg {
  648. animation: morphAnimation 2s ease-in-out infinite;
  649. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.5));
  650. }
  651.  
  652. @keyframes morphAnimation {
  653. 0% {
  654. transform: scale(1);
  655. opacity: 1;
  656. }
  657. 50% {
  658. transform: scale(1.2);
  659. opacity: 0.7;
  660. }
  661. 100% {
  662. transform: scale(1);
  663. opacity: 1;
  664. }
  665. }
  666.  
  667. .M3Unator-controls {
  668. display: none;
  669. gap: 0.75rem;
  670. margin: 0.75rem 0;
  671. justify-content: center;
  672. }
  673.  
  674. .M3Unator-controls.active {
  675. display: flex;
  676. }
  677.  
  678. .M3Unator-control-btn {
  679. display: none;
  680. padding: 0.75rem 1.5rem;
  681. border-radius: 12px;
  682. font-weight: 600;
  683. font-size: 0.95rem;
  684. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  685. align-items: center;
  686. gap: 0.75rem;
  687. min-width: 160px;
  688. justify-content: center;
  689. background: rgba(30, 30, 46, 0.6);
  690. backdrop-filter: blur(8px);
  691. width: 160px;
  692. }
  693.  
  694. .M3Unator-control-btn:hover {
  695. background: #313244;
  696. transform: translateY(-1px);
  697. }
  698.  
  699. .M3Unator-control-btn:active {
  700. transform: translateY(1px);
  701. }
  702.  
  703. .M3Unator-control-btn.pause {
  704. border-color: #fab387;
  705. color: #fab387;
  706. }
  707.  
  708. .M3Unator-control-btn.pause:hover {
  709. background: rgba(250, 179, 135, 0.1);
  710. }
  711.  
  712. .M3Unator-control-btn.resume {
  713. border-color: #94e2d5;
  714. color: #94e2d5;
  715. }
  716.  
  717. .M3Unator-control-btn.resume:hover {
  718. background: rgba(148, 226, 213, 0.1);
  719. }
  720.  
  721. .M3Unator-control-btn.cancel {
  722. border-color: #f38ba8;
  723. color: #f38ba8;
  724. }
  725.  
  726. .M3Unator-control-btn.cancel:hover {
  727. background: rgba(243, 139, 168, 0.1);
  728. }
  729.  
  730. .M3Unator-control-btn svg {
  731. width: 14px;
  732. height: 14px;
  733. }
  734.  
  735. .M3Unator-button {
  736. width: 100%;
  737. padding: 0 1rem;
  738. background: #f5c2e7;
  739. color: #11111b;
  740. border: none;
  741. border-radius: 6px;
  742. font-weight: 600;
  743. font-size: 0.875rem;
  744. cursor: pointer;
  745. transition: all 0.2s ease;
  746. display: flex;
  747. align-items: center;
  748. justify-content: center;
  749. gap: 0.375rem;
  750. height: 48px;
  751. min-height: 48px;
  752. line-height: 1;
  753. }
  754.  
  755. .M3Unator-spinner {
  756. width: 20px;
  757. height: 20px;
  758. border: 2px solid rgba(17, 17, 27, 0.3);
  759. border-radius: 50%;
  760. border-top-color: #11111b;
  761. animation: spin 0.6s linear infinite;
  762. margin-right: 0;
  763. flex-shrink: 0;
  764. }
  765.  
  766. .M3Unator-toast-container {
  767. position: fixed;
  768. bottom: 24px;
  769. left: 50%;
  770. transform: translateX(-50%);
  771. z-index: 999999;
  772. pointer-events: none;
  773. display: flex;
  774. flex-direction: column;
  775. align-items: center;
  776. width: auto;
  777. }
  778.  
  779. .M3Unator-toast {
  780. display: flex;
  781. align-items: center;
  782. gap: 12px;
  783. padding: 12px 24px;
  784. border-radius: 12px;
  785. margin-bottom: 12px;
  786. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  787. font-size: 14px;
  788. font-weight: 500;
  789. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  790. background: rgba(17, 17, 27, 0.95);
  791. border: 2px solid;
  792. pointer-events: all;
  793. min-width: 300px;
  794. max-width: 500px;
  795. backdrop-filter: blur(16px);
  796. will-change: transform, opacity;
  797. animation: none;
  798. transform-origin: center bottom;
  799. }
  800.  
  801. .M3Unator-toast.show {
  802. animation: toastBounceIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
  803. }
  804.  
  805. .M3Unator-toast.removing {
  806. animation: toastBounceOut 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
  807. }
  808.  
  809. @keyframes toastBounceIn {
  810. 0% {
  811. opacity: 0;
  812. transform: scale(0.3) translateY(2000px);
  813. }
  814. 60% {
  815. opacity: 1;
  816. transform: scale(1.1) translateY(-20px);
  817. }
  818. 75% {
  819. transform: scale(0.95) translateY(10px);
  820. }
  821. 90% {
  822. transform: scale(1.02) translateY(-5px);
  823. }
  824. 100% {
  825. transform: scale(1) translateY(0);
  826. }
  827. }
  828.  
  829. @keyframes toastBounceOut {
  830. 0% {
  831. transform: scale(1) translateY(0);
  832. opacity: 1;
  833. }
  834. 20% {
  835. transform: scale(1.1) translateY(-20px);
  836. opacity: 0.8;
  837. }
  838. 100% {
  839. transform: scale(0.3) translateY(2000px);
  840. opacity: 0;
  841. }
  842. }
  843.  
  844. .M3Unator-toast svg {
  845. width: 20px;
  846. height: 20px;
  847. flex-shrink: 0;
  848. filter: drop-shadow(0 0 4px currentColor);
  849. animation: iconPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
  850. opacity: 0;
  851. transform: scale(0.5);
  852. }
  853.  
  854. @keyframes iconPop {
  855. 0% {
  856. opacity: 0;
  857. transform: scale(0.5) rotate(-180deg);
  858. }
  859. 100% {
  860. opacity: 1;
  861. transform: scale(1) rotate(0deg);
  862. }
  863. }
  864.  
  865. .M3Unator-toast span {
  866. opacity: 0;
  867. transform: translateX(-10px);
  868. animation: textSlide 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
  869. animation-delay: 0.15s;
  870. }
  871.  
  872. @keyframes textSlide {
  873. 0% {
  874. opacity: 0;
  875. transform: translateX(-10px);
  876. }
  877. 100% {
  878. opacity: 1;
  879. transform: translateX(0);
  880. }
  881. }
  882.  
  883. .M3Unator-input-row {
  884. display: flex;
  885. gap: 0.75rem;
  886. margin-bottom: 0.75rem;
  887. }
  888.  
  889. .M3Unator-input-row .M3Unator-input-group {
  890. margin-bottom: 0;
  891. }
  892.  
  893. .M3Unator-input-row .M3Unator-input-group:first-child {
  894. flex: 2;
  895. }
  896.  
  897. .M3Unator-input-row .M3Unator-input-group:last-child {
  898. flex: 1;
  899. }
  900.  
  901. .M3Unator-social {
  902. display: flex;
  903. gap: 8px;
  904. margin-right: 8px;
  905. }
  906.  
  907. .M3Unator-social a {
  908. width: 32px;
  909. height: 32px;
  910. border-radius: 8px;
  911. display: grid;
  912. place-items: center;
  913. color: #cdd6f4;
  914. background: rgba(205, 214, 244, 0.1);
  915. transition: all 0.2s ease;
  916. }
  917.  
  918. .M3Unator-social a:hover {
  919. background: rgba(205, 214, 244, 0.2);
  920. transform: rotate(360deg);
  921. }
  922.  
  923. .M3Unator-social svg {
  924. width: 18px;
  925. height: 18px;
  926. }
  927.  
  928. .M3Unator-advanced-settings {
  929. margin-top: 1rem;
  930. padding: 1rem;
  931. background: rgba(30, 30, 46, 0.5);
  932. border: 1px solid #313244;
  933. border-radius: 8px;
  934. display: none;
  935. }
  936.  
  937. .M3Unator-advanced-settings.active {
  938. display: block;
  939. animation: fadeIn 0.3s ease;
  940. }
  941.  
  942. .M3Unator-advanced-toggle {
  943. width: 100%;
  944. padding: 0.75rem;
  945. background: #1e1e2e;
  946. border: 1px solid #313244;
  947. border-radius: 8px;
  948. color: #cdd6f4;
  949. font-size: 0.875rem;
  950. font-weight: 500;
  951. cursor: pointer;
  952. display: flex;
  953. align-items: center;
  954. justify-content: space-between;
  955. transition: all 0.2s ease;
  956. }
  957.  
  958. .M3Unator-advanced-toggle:hover {
  959. background: #313244;
  960. }
  961.  
  962. .M3Unator-advanced-toggle svg {
  963. width: 16px;
  964. height: 16px;
  965. transition: transform 0.2s ease;
  966. }
  967.  
  968. .M3Unator-advanced-toggle.active svg {
  969. transform: rotate(180deg);
  970. }
  971.  
  972. .M3Unator-depth-slider {
  973. -webkit-appearance: none;
  974. width: 100%;
  975. height: 4px;
  976. border-radius: 2px;
  977. background: #313244;
  978. outline: none;
  979. margin: 1rem 0;
  980. }
  981.  
  982. .M3Unator-depth-slider::-webkit-slider-thumb {
  983. -webkit-appearance: none;
  984. appearance: none;
  985. width: 16px;
  986. height: 16px;
  987. border-radius: 50%;
  988. background: #cba6f7;
  989. cursor: pointer;
  990. transition: all 0.2s ease;
  991. }
  992.  
  993. .M3Unator-depth-slider::-webkit-slider-thumb:hover {
  994. transform: scale(1.2);
  995. }
  996.  
  997. .M3Unator-depth-value {
  998. text-align: center;
  999. font-size: 0.875rem;
  1000. color: #cdd6f4;
  1001. margin-top: 0.5rem;
  1002. }
  1003.  
  1004. @keyframes fadeIn {
  1005. from { opacity: 0; transform: translateY(-10px); }
  1006. to { opacity: 1; transform: translateY(0); }
  1007. }
  1008.  
  1009. .M3Unator-depth-settings {
  1010. margin-top: 0.75rem;
  1011. margin-left: 1.75rem;
  1012. padding: 0.75rem;
  1013. background: rgba(30, 30, 46, 0.3);
  1014. border-left: 2px solid #cba6f7;
  1015. border-radius: 0 8px 8px 0;
  1016. display: none;
  1017. animation: slideDown 0.3s ease;
  1018. }
  1019.  
  1020. .M3Unator-depth-settings.active {
  1021. display: block;
  1022. }
  1023.  
  1024. .M3Unator-depth-input {
  1025. position: relative;
  1026. display: flex;
  1027. align-items: center;
  1028. gap: 0.75rem;
  1029. margin-top: 0.5rem;
  1030. }
  1031.  
  1032. .M3Unator-depth-input input[type="number"] {
  1033. width: 64px;
  1034. padding: 0.25rem 0.375rem;
  1035. border: 1px solid #45475a;
  1036. border-radius: 4px;
  1037. background: rgba(30, 30, 46, 0.8);
  1038. color: #cdd6f4;
  1039. font-size: 0.875rem;
  1040. text-align: center;
  1041. margin: 0 0 0 0.5rem;
  1042. }
  1043.  
  1044. .M3Unator-depth-input input[type="number"]:focus {
  1045. outline: none;
  1046. border-color: #cba6f7;
  1047. box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
  1048. }
  1049.  
  1050. .M3Unator-depth-input input[type="number"]::-webkit-inner-spin-button {
  1051. opacity: 1;
  1052. background: #313244;
  1053. border-left: 1px solid #45475a;
  1054. border-radius: 0 4px 4px 0;
  1055. cursor: pointer;
  1056. }
  1057.  
  1058. .M3Unator-depth-toggle {
  1059. display: flex;
  1060. align-items: center;
  1061. gap: 0.5rem;
  1062. padding: 0.5rem;
  1063. background: #1e1e2e;
  1064. border: 1px solid #45475a;
  1065. border-radius: 6px;
  1066. color: #cdd6f4;
  1067. font-size: 0.875rem;
  1068. cursor: pointer;
  1069. transition: all 0.2s ease;
  1070. }
  1071.  
  1072. .M3Unator-depth-toggle:hover {
  1073. background: #313244;
  1074. border-color: #cba6f7;
  1075. }
  1076.  
  1077. .M3Unator-depth-toggle.active {
  1078. background: rgba(203, 166, 247, 0.1);
  1079. border-color: #cba6f7;
  1080. color: #cba6f7;
  1081. }
  1082.  
  1083. @keyframes slideDown {
  1084. from { opacity: 0; transform: translateY(-10px); }
  1085. to { opacity: 1; transform: translateY(0); }
  1086. }
  1087.  
  1088. .M3Unator-stats-bar {
  1089. margin: 0.75rem 0;
  1090. padding: 0.5rem;
  1091. background: rgba(30, 30, 46, 0.5);
  1092. border: 1px solid #313244;
  1093. border-radius: 8px;
  1094. display: none;
  1095. }
  1096.  
  1097. .M3Unator-stats-bar.active {
  1098. display: block;
  1099. }
  1100.  
  1101. .M3Unator-stats {
  1102. display: flex;
  1103. align-items: center;
  1104. justify-content: space-around;
  1105. gap: 0.382rem;
  1106. padding: 0.25rem;
  1107. }
  1108.  
  1109. .M3Unator-stat {
  1110. display: inline-flex;
  1111. align-items: center;
  1112. gap: 0.25rem;
  1113. font-size: 0.75rem;
  1114. color: #cdd6f4;
  1115. cursor: help;
  1116. min-width: 40px;
  1117. justify-content: flex-start;
  1118. padding: 0 0.25rem;
  1119. position: relative;
  1120. }
  1121.  
  1122. .M3Unator-stat span {
  1123. min-width: 16px;
  1124. text-align: right;
  1125. font-variant-numeric: tabular-nums;
  1126. font-size: 0.7rem;
  1127. font-weight: 500;
  1128. }
  1129.  
  1130. .M3Unator-stat svg {
  1131. opacity: 0.8;
  1132. flex-shrink: 0;
  1133. width: 14px;
  1134. height: 14px;
  1135. }
  1136.  
  1137. .M3Unator-stat.video {
  1138. color: #94e2d5;
  1139. }
  1140.  
  1141. .M3Unator-stat.audio {
  1142. color: #89b4fa;
  1143. }
  1144.  
  1145. .M3Unator-stat.dir {
  1146. color: #cba6f7;
  1147. }
  1148.  
  1149. .M3Unator-stat.error {
  1150. color: #f38ba8;
  1151. }
  1152.  
  1153. .M3Unator-stat.depth {
  1154. color: #a6e3a1;
  1155. transition: color 0.3s ease;
  1156. }
  1157.  
  1158. .M3Unator-stat.depth[data-progress="high"] {
  1159. color: #f38ba8;
  1160. }
  1161.  
  1162. .M3Unator-stat.depth[data-progress="medium"] {
  1163. color: #fab387;
  1164. }
  1165.  
  1166. .M3Unator-stat.depth[data-progress="low"] {
  1167. color: #f9e2af;
  1168. }
  1169.  
  1170. .M3Unator-stat:hover::after {
  1171. content: attr(title);
  1172. position: absolute;
  1173. bottom: calc(100% + 5px);
  1174. left: 50%;
  1175. transform: translateX(-50%);
  1176. padding: 0.5rem 0.75rem;
  1177. background: rgba(30, 30, 46, 0.95);
  1178. color: #cdd6f4;
  1179. font-size: 0.875rem;
  1180. white-space: nowrap;
  1181. border-radius: 6px;
  1182. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  1183. z-index: 1000;
  1184. border: 1px solid #313244;
  1185. text-align: center;
  1186. backdrop-filter: blur(8px);
  1187. pointer-events: none;
  1188. }
  1189.  
  1190. .M3Unator-spinner {
  1191. width: 20px;
  1192. height: 20px;
  1193. border: 2px solid rgba(17, 17, 27, 0.3);
  1194. border-radius: 50%;
  1195. border-top-color: #11111b;
  1196. animation: spin 0.6s linear infinite;
  1197. margin-right: 0;
  1198. flex-shrink: 0;
  1199. }
  1200.  
  1201. @keyframes spin {
  1202. to { transform: rotate(360deg); }
  1203. }
  1204.  
  1205. .M3Unator-toast {
  1206. animation: toastSlideUp 0.2s ease forwards;
  1207. }
  1208.  
  1209. .M3Unator-toast.removing {
  1210. animation: toastSlideDown 0.2s ease forwards;
  1211. }
  1212.  
  1213. .M3Unator-popup {
  1214. animation: slideUp 0.2s ease;
  1215. }
  1216.  
  1217. .M3Unator-stats-bar {
  1218. animation: fadeIn 0.2s ease;
  1219. }
  1220.  
  1221. .M3Unator-log {
  1222. transition: max-height 0.3s ease;
  1223. }
  1224.  
  1225. .M3Unator-log.collapsed {
  1226. max-height: 0;
  1227. overflow: hidden;
  1228. }
  1229.  
  1230. .M3Unator-log-toggle {
  1231. width: 100%;
  1232. padding: 0.5rem 0.75rem;
  1233. background: rgba(203, 166, 247, 0.05);
  1234. border: none;
  1235. color: #cdd6f4;
  1236. display: flex;
  1237. align-items: center;
  1238. justify-content: space-between;
  1239. cursor: pointer;
  1240. transition: all 0.2s ease;
  1241. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  1242. font-size: 0.875rem;
  1243. font-weight: 500;
  1244. border-radius: 6px;
  1245. }
  1246.  
  1247. .M3Unator-log-toggle:hover {
  1248. background: rgba(203, 166, 247, 0.1);
  1249. }
  1250.  
  1251. .M3Unator-activity-indicator {
  1252. width: 14px;
  1253. height: 14px;
  1254. border-radius: 50%;
  1255. background: #45475a; /* Darker gray */
  1256. margin-left: auto;
  1257. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1258. }
  1259.  
  1260. .M3Unator-activity-indicator.active {
  1261. background: #89dceb; /* Brighter blue */
  1262. box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2);
  1263. animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  1264. }
  1265.  
  1266. .M3Unator-activity-indicator.paused {
  1267. background: #f9e2af; /* More visible yellow */
  1268. box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2);
  1269. }
  1270.  
  1271. .M3Unator-activity-indicator.error {
  1272. background: #f38ba8; /* Current red is good */
  1273. box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2);
  1274. }
  1275.  
  1276. .M3Unator-activity-indicator.completed {
  1277. background: #94e2d5; /* Brighter green-turquoise */
  1278. box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2);
  1279. }
  1280.  
  1281. @keyframes pulseActive {
  1282. 0% {
  1283. box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4);
  1284. }
  1285. 70% {
  1286. box-shadow: 0 0 0 8px rgba(137, 220, 235, 0);
  1287. }
  1288. 100% {
  1289. box-shadow: 0 0 0 0 rgba(137, 220, 235, 0);
  1290. }
  1291. }
  1292.  
  1293. @keyframes pulsePaused {
  1294. 0% {
  1295. box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4);
  1296. }
  1297. 70% {
  1298. box-shadow: 0 0 0 8px rgba(249, 226, 175, 0);
  1299. }
  1300. 100% {
  1301. box-shadow: 0 0 0 0 rgba(249, 226, 175, 0);
  1302. }
  1303. }
  1304.  
  1305. @keyframes completeScale {
  1306. 0% {
  1307. transform: scale(0.8);
  1308. opacity: 0.5;
  1309. }
  1310. 50% {
  1311. transform: scale(1.2);
  1312. }
  1313. 100% {
  1314. transform: scale(1);
  1315. opacity: 1;
  1316. }
  1317. }
  1318.  
  1319. .M3Unator-toggle-container span svg .infinity-icon {
  1320. opacity: 0.5;
  1321. transition: opacity 0.2s ease;
  1322. transform: scale(0.6) translateY(4px);
  1323. transform-origin: center;
  1324. stroke-width: 1.5;
  1325. }
  1326.  
  1327. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg .infinity-icon {
  1328. opacity: 1;
  1329. }
  1330.  
  1331. .M3Unator-depth-controls {
  1332. background: rgba(30, 30, 46, 0.4);
  1333. backdrop-filter: blur(8px);
  1334. border: 1px solid #313244;
  1335. border-radius: 8px;
  1336. padding: 0.618rem;
  1337. margin-top: 1rem;
  1338. display: none;
  1339. }
  1340.  
  1341. .M3Unator-depth-controls.active {
  1342. display: block;
  1343. }
  1344.  
  1345. .M3Unator-radio-group {
  1346. display: flex;
  1347. gap: 0.75rem;
  1348. justify-content: center;
  1349. background: rgba(30, 30, 46, 0.6);
  1350. padding: 0.5rem;
  1351. border-radius: 6px;
  1352. }
  1353.  
  1354. .M3Unator-radio {
  1355. display: flex;
  1356. align-items: center;
  1357. gap: 0.5rem;
  1358. cursor: pointer;
  1359. padding: 0.5rem;
  1360. border-radius: 4px;
  1361. transition: all 0.2s ease;
  1362. background: transparent;
  1363. border: 1px solid transparent;
  1364. }
  1365.  
  1366. .M3Unator-radio:hover {
  1367. background: rgba(203, 166, 247, 0.1);
  1368. }
  1369.  
  1370. .M3Unator-radio input[type="radio"] {
  1371. display: none;
  1372. }
  1373.  
  1374. .M3Unator-radio .radio-mark {
  1375. width: 16px;
  1376. height: 16px;
  1377. border: 1.5px solid #45475a;
  1378. border-radius: 50%;
  1379. display: flex;
  1380. align-items: center;
  1381. justify-content: center;
  1382. transition: all 0.2s ease;
  1383. flex-shrink: 0;
  1384. background: rgba(30, 30, 46, 0.6);
  1385. position: relative;
  1386. }
  1387.  
  1388. .M3Unator-radio input[type="radio"]:checked + .radio-mark {
  1389. border-color: #cba6f7;
  1390. background: rgba(203, 166, 247, 0.1);
  1391. }
  1392.  
  1393. .M3Unator-radio input[type="radio"]:checked + .radio-mark::after {
  1394. content: '';
  1395. width: 8px;
  1396. height: 8px;
  1397. border-radius: 50%;
  1398. background: #cba6f7;
  1399. position: absolute;
  1400. }
  1401.  
  1402. .M3Unator-radio .radio-label {
  1403. color: #cdd6f4;
  1404. font-size: 0.875rem;
  1405. user-select: none;
  1406. display: flex;
  1407. align-items: center;
  1408. gap: 0.5rem;
  1409. }
  1410.  
  1411. .M3Unator-depth-input {
  1412. width: 64px;
  1413. padding: 0.25rem 0.375rem;
  1414. border: 1px solid #45475a;
  1415. border-radius: 4px;
  1416. background: rgba(30, 30, 46, 0.8);
  1417. color: #cdd6f4;
  1418. font-size: 0.875rem;
  1419. text-align: center;
  1420. transition: all 0.2s ease;
  1421. -moz-appearance: textfield;
  1422. margin-top: -1px;
  1423. display: inline-flex;
  1424. align-items: center;
  1425. height: 28px;
  1426. }
  1427.  
  1428. .M3Unator-depth-input::-webkit-outer-spin-button,
  1429. .M3Unator-depth-input::-webkit-inner-spin-button {
  1430. -webkit-appearance: inner-spin-button;
  1431. opacity: 1;
  1432. background: #313244;
  1433. border-left: 1px solid #45475a;
  1434. border-radius: 0 4px 4px 0;
  1435. cursor: pointer;
  1436. height: 100%;
  1437. position: absolute;
  1438. right: 0;
  1439. top: 0;
  1440. }
  1441.  
  1442. .M3Unator-depth-input:focus {
  1443. outline: none;
  1444. border-color: #cba6f7;
  1445. box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
  1446. }
  1447.  
  1448. .M3Unator-depth-input:disabled {
  1449. opacity: 0.5;
  1450. cursor: not-allowed;
  1451. background: rgba(30, 30, 46, 0.4);
  1452. }
  1453.  
  1454. .M3Unator-radio .radio-label {
  1455. display: flex;
  1456. align-items: center;
  1457. gap: 0.5rem;
  1458. color: #cdd6f4;
  1459. font-size: 0.875rem;
  1460. user-select: none;
  1461. }
  1462.  
  1463. .M3Unator-url-container {
  1464. display: flex;
  1465. align-items: center;
  1466. background: rgba(30, 30, 46, 0.6);
  1467. border: 1px solid #313244;
  1468. border-radius: 6px;
  1469. padding: 0.618rem;
  1470. margin-bottom: 1rem;
  1471. transition: all 0.2s ease;
  1472. }
  1473.  
  1474. .M3Unator-url-container:hover {
  1475. border-color: #45475a;
  1476. }
  1477.  
  1478. .M3Unator-url-icon {
  1479. color: #6c7086;
  1480. margin-right: 0.618rem;
  1481. flex-shrink: 0;
  1482. }
  1483.  
  1484. .M3Unator-url-input {
  1485. flex: 1;
  1486. background: transparent;
  1487. border: none;
  1488. color: #cdd6f4;
  1489. font-size: 0.875rem;
  1490. padding: 0;
  1491. margin: 0;
  1492. width: 100%;
  1493. }
  1494.  
  1495. .M3Unator-url-input:focus {
  1496. outline: none;
  1497. }
  1498.  
  1499. .M3Unator-url-copy {
  1500. background: transparent;
  1501. border: none;
  1502. color: #6c7086;
  1503. padding: 0.382rem;
  1504. margin-left: 0.618rem;
  1505. cursor: pointer;
  1506. border-radius: 4px;
  1507. transition: all 0.2s ease;
  1508. display: flex;
  1509. align-items: center;
  1510. justify-content: center;
  1511. }
  1512.  
  1513. .M3Unator-url-copy:hover {
  1514. color: #cdd6f4;
  1515. background: rgba(205, 214, 244, 0.1);
  1516. }
  1517.  
  1518. .M3Unator-url-copy.copied {
  1519. color: #a6e3a1;
  1520. animation: copyPulse 0.3s ease;
  1521. }
  1522.  
  1523. @keyframes copyPulse {
  1524. 0% { transform: scale(1); }
  1525. 50% { transform: scale(1.2); }
  1526. 100% { transform: scale(1); }
  1527. }
  1528.  
  1529. .M3Unator-toggle-group {
  1530. display: flex;
  1531. gap: 1.25rem;
  1532. margin: 1.5rem 0;
  1533. justify-content: center;
  1534. background: rgba(30, 30, 46, 0.4);
  1535. padding: 1.25rem;
  1536. border-radius: 16px;
  1537. backdrop-filter: blur(8px);
  1538. }
  1539.  
  1540. .M3Unator-toggle-container {
  1541. position: relative;
  1542. }
  1543.  
  1544. .M3Unator-toggle-container span {
  1545. width: 64px;
  1546. height: 64px;
  1547. background: #1e1e2e;
  1548. border: 2px solid #45475a;
  1549. border-radius: 16px;
  1550. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1551. display: flex;
  1552. align-items: center;
  1553. justify-content: center;
  1554. }
  1555.  
  1556. .M3Unator-toggle-container input[type="checkbox"]:checked + span {
  1557. background: rgba(203, 166, 247, 0.1);
  1558. border-color: #cba6f7;
  1559. box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
  1560. transform: translateY(-2px);
  1561. }
  1562.  
  1563. .M3Unator-toggle-container span:hover {
  1564. background: #313244;
  1565. transform: translateY(-2px);
  1566. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
  1567. }
  1568.  
  1569. .M3Unator-toggle-container svg {
  1570. width: 32px;
  1571. height: 32px;
  1572. opacity: 0.7;
  1573. transition: all 0.3s ease;
  1574. }
  1575.  
  1576. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  1577. opacity: 1;
  1578. color: #cba6f7;
  1579. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
  1580. }
  1581.  
  1582. .M3Unator-progress {
  1583. background: rgba(30, 30, 46, 0.6);
  1584. border-radius: 12px;
  1585. padding: 1rem;
  1586. margin: 1rem 0;
  1587. backdrop-filter: blur(8px);
  1588. border: 1px solid rgba(203, 166, 247, 0.2);
  1589. }
  1590.  
  1591. .M3Unator-progress-text {
  1592. color: #f5c2e7;
  1593. font-weight: 600;
  1594. text-align: center;
  1595. margin-bottom: 0.5rem;
  1596. font-size: 1.1rem;
  1597. }
  1598.  
  1599. .M3Unator-progress-spinner {
  1600. width: 24px;
  1601. height: 24px;
  1602. border: 3px solid rgba(245, 194, 231, 0.1);
  1603. border-top-color: #f5c2e7;
  1604. border-radius: 50%;
  1605. animation: spin 1s linear infinite;
  1606. margin: 0 auto;
  1607. }
  1608.  
  1609. .M3Unator-controls {
  1610. display: flex;
  1611. gap: 0.75rem;
  1612. margin: 0.75rem 0;
  1613. justify-content: center;
  1614. }
  1615.  
  1616. .M3Unator-control-btn {
  1617. display: none;
  1618. padding: 0.75rem 1.5rem;
  1619. border-radius: 12px;
  1620. font-weight: 600;
  1621. font-size: 0.95rem;
  1622. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1623. align-items: center;
  1624. gap: 0.75rem;
  1625. min-width: 160px;
  1626. justify-content: center;
  1627. background: rgba(30, 30, 46, 0.6);
  1628. backdrop-filter: blur(8px);
  1629. width: 160px;
  1630. }
  1631.  
  1632. .M3Unator-control-btn.pause {
  1633. background: rgba(250, 179, 135, 0.1);
  1634. border: 2px solid #fab387;
  1635. color: #fab387;
  1636. }
  1637.  
  1638. .M3Unator-control-btn.resume {
  1639. background: rgba(148, 226, 213, 0.1);
  1640. border: 2px solid #94e2d5;
  1641. color: #94e2d5;
  1642. }
  1643.  
  1644. .M3Unator-control-btn.cancel {
  1645. background: rgba(243, 139, 168, 0.1);
  1646. border: 2px solid #f38ba8;
  1647. color: #f38ba8;
  1648. }
  1649.  
  1650. .M3Unator-control-btn:hover {
  1651. transform: translateY(-2px);
  1652. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
  1653. }
  1654.  
  1655. .M3Unator-control-btn svg {
  1656. width: 20px;
  1657. height: 20px;
  1658. }
  1659.  
  1660. .M3Unator-layers-icon {
  1661. width: 20px;
  1662. height: 20px;
  1663. margin-right: 0.5rem;
  1664. }
  1665.  
  1666. .M3Unator-input:-webkit-autofill,
  1667. .M3Unator-input:-webkit-autofill:hover,
  1668. .M3Unator-input:-webkit-autofill:focus,
  1669. .M3Unator-input:-webkit-autofill:active {
  1670. -webkit-text-fill-color: #cdd6f4 !important;
  1671. -webkit-box-shadow: 0 0 0 30px #1e1e2e inset !important;
  1672. box-shadow: 0 0 0 30px #1e1e2e inset !important;
  1673. background-color: #1e1e2e !important;
  1674. color: #cdd6f4 !important;
  1675. caret-color: #cdd6f4 !important;
  1676. transition: background-color 5000s ease-in-out 0s !important;
  1677. text-decoration: none !important;
  1678. -webkit-text-decoration: none !important;
  1679. }
  1680.  
  1681. .M3Unator-input:-moz-autofill,
  1682. .M3Unator-input:-moz-autofill-preview {
  1683. background-color: #1e1e2e !important;
  1684. color: #cdd6f4 !important;
  1685. text-decoration: none !important;
  1686. }
  1687.  
  1688. .M3Unator-input:-ms-input-placeholder {
  1689. background-color: #1e1e2e !important;
  1690. color: #cdd6f4 !important;
  1691. text-decoration: none !important;
  1692. }
  1693.  
  1694. .M3Unator-log-container {
  1695. margin: 0;
  1696. }
  1697.  
  1698. .M3Unator-log-toggle {
  1699. width: 100%;
  1700. padding: 0.5rem 0.75rem;
  1701. background: rgba(203, 166, 247, 0.05);
  1702. border: none;
  1703. color: #cdd6f4;
  1704. display: flex;
  1705. align-items: center;
  1706. justify-content: space-between;
  1707. cursor: pointer;
  1708. transition: all 0.2s ease;
  1709. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  1710. font-size: 0.875rem;
  1711. font-weight: 500;
  1712. border-radius: 6px;
  1713. }
  1714.  
  1715. .M3Unator-log-toggle:hover {
  1716. background: rgba(203, 166, 247, 0.1);
  1717. }
  1718.  
  1719. .M3Unator-activity-indicator {
  1720. width: 14px;
  1721. height: 14px;
  1722. border-radius: 50%;
  1723. background: #45475a; /* Darker gray */
  1724. margin-left: auto;
  1725. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1726. }
  1727.  
  1728. .M3Unator-activity-indicator.active {
  1729. background: #89dceb; /* Brighter blue */
  1730. box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2);
  1731. animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  1732. }
  1733.  
  1734. .M3Unator-activity-indicator.paused {
  1735. background: #f9e2af; /* More visible yellow */
  1736. box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2);
  1737. }
  1738.  
  1739. .M3Unator-activity-indicator.error {
  1740. background: #f38ba8; /* Current red is good */
  1741. box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2);
  1742. }
  1743.  
  1744. .M3Unator-activity-indicator.completed {
  1745. background: #94e2d5; /* Brighter green-turquoise */
  1746. box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2);
  1747. }
  1748.  
  1749. @keyframes pulseActive {
  1750. 0% {
  1751. box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4);
  1752. }
  1753. 70% {
  1754. box-shadow: 0 0 0 8px rgba(137, 220, 235, 0);
  1755. }
  1756. 100% {
  1757. box-shadow: 0 0 0 0 rgba(137, 220, 235, 0);
  1758. }
  1759. }
  1760.  
  1761. @keyframes pulsePaused {
  1762. 0% {
  1763. box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4);
  1764. }
  1765. 70% {
  1766. box-shadow: 0 0 0 8px rgba(249, 226, 175, 0);
  1767. }
  1768. 100% {
  1769. box-shadow: 0 0 0 0 rgba(249, 226, 175, 0);
  1770. }
  1771. }
  1772.  
  1773. @keyframes completeScale {
  1774. 0% {
  1775. transform: scale(0.8);
  1776. opacity: 0.5;
  1777. }
  1778. 50% {
  1779. transform: scale(1.2);
  1780. }
  1781. 100% {
  1782. transform: scale(1);
  1783. opacity: 1;
  1784. }
  1785. }
  1786.  
  1787. .M3Unator-log-toggle:hover .M3Unator-activity-indicator {
  1788. background: #6c7086;
  1789. animation: none;
  1790. }
  1791.  
  1792. .M3Unator-log-toggle.active .M3Unator-activity-indicator {
  1793. background: #6c7086;
  1794. animation: none;
  1795. }
  1796.  
  1797. .M3Unator-log-toggle .toggle-text {
  1798. display: flex;
  1799. align-items: center;
  1800. gap: 0.5rem;
  1801. }
  1802.  
  1803. .M3Unator-log {
  1804. height: 0;
  1805. max-height: 0;
  1806. overflow: hidden;
  1807. transition: all 0.3s ease;
  1808. background: #11111b;
  1809. padding: 0;
  1810. border-top: none;
  1811. margin: 0;
  1812. }
  1813.  
  1814. .M3Unator-log.expanded {
  1815. height: auto;
  1816. max-height: 300px;
  1817. padding: 0.75rem;
  1818. border-top: 1px solid #313244;
  1819. overflow-y: auto;
  1820. }
  1821.  
  1822. .M3Unator-log-entry {
  1823. padding: 0.25rem 0.5rem;
  1824. border-bottom: 1px solid rgba(49, 50, 68, 0.5);
  1825. font-size: 0.875rem;
  1826. }
  1827.  
  1828. .M3Unator-log-entry:last-child {
  1829. border-bottom: none;
  1830. }
  1831.  
  1832. .M3Unator-log-time {
  1833. color: #6c7086;
  1834. margin-right: 0.5rem;
  1835. }
  1836.  
  1837. .M3Unator-log-entry.success {
  1838. color: #94e2d5;
  1839. }
  1840.  
  1841. .M3Unator-log-entry.error {
  1842. color: #f38ba8;
  1843. }
  1844.  
  1845. .M3Unator-log-entry.warning {
  1846. color: #fab387;
  1847. }
  1848.  
  1849. .M3Unator-log-entry.info {
  1850. color: #89b4fa;
  1851. }
  1852.  
  1853. .M3Unator-log-entry.final {
  1854. color: #a6e3a1;
  1855. font-weight: 500;
  1856. }
  1857.  
  1858. .M3Unator-log {
  1859. margin-top: 0.75rem;
  1860. max-height: calc(100vh - 70vh);
  1861. font-size: 0.8125rem;
  1862. line-height: 1.4;
  1863. }
  1864.  
  1865. .M3Unator-log-entry {
  1866. padding: 0.25rem 0.5rem;
  1867. border-radius: 4px;
  1868. }
  1869.  
  1870. .M3Unator-log-toggle {
  1871. padding: 10px 12px;
  1872. height: 42px;
  1873. display: flex;
  1874. align-items: center;
  1875. justify-content: space-between;
  1876. }
  1877.  
  1878. .M3Unator-log-counter {
  1879. padding: 0.125rem 0.375rem;
  1880. font-size: 0.75rem;
  1881. border-radius: 4px;
  1882. }
  1883.  
  1884. .M3Unator-log-time {
  1885. font-size: 0.75rem;
  1886. opacity: 0.7;
  1887. margin-right: 0.5rem;
  1888. }
  1889. `);
  1890.  
  1891. GM_addStyle(`
  1892. .M3Unator-popup {
  1893. position: fixed;
  1894. background: #11111b;
  1895. color: #cdd6f4;
  1896. width: 100%;
  1897. max-width: 480px;
  1898. border-radius: 12px;
  1899. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  1900. overflow: hidden;
  1901. animation: slideUp 0.3s ease;
  1902. z-index: 9999;
  1903. }
  1904.  
  1905. .M3Unator-header {
  1906. padding: 1rem 1.25rem;
  1907. background: #1e1e2e;
  1908. color: #cdd6f4;
  1909. display: flex;
  1910. align-items: center;
  1911. justify-content: space-between;
  1912. cursor: move;
  1913. user-select: none;
  1914. border-bottom: 1px solid #313244;
  1915. }
  1916.  
  1917. .M3Unator-container {
  1918. position: fixed;
  1919. inset: 0;
  1920. background: rgba(0, 0, 0, 0.75);
  1921. backdrop-filter: blur(8px);
  1922. display: none;
  1923. place-items: center;
  1924. z-index: 9999;
  1925. }
  1926. `);
  1927.  
  1928. GM_addStyle(`
  1929. /* Info Modal Styles */
  1930. .info-modal {
  1931. display: none;
  1932. position: fixed;
  1933. inset: 0;
  1934. background: rgba(0, 0, 0, 0.75);
  1935. backdrop-filter: blur(8px);
  1936. z-index: 10000;
  1937. }
  1938.  
  1939. .info-modal-content {
  1940. position: absolute;
  1941. top: 50%;
  1942. left: 50%;
  1943. transform: translate(-50%, -50%);
  1944. background: #1e1e2e;
  1945. border: 1px solid #45475a;
  1946. border-radius: 12px;
  1947. width: 90%;
  1948. max-width: 600px;
  1949. color: #cdd6f4;
  1950. }
  1951.  
  1952. .info-modal-header {
  1953. padding: 1rem 1.5rem;
  1954. border-bottom: 1px solid #45475a;
  1955. display: flex;
  1956. align-items: center;
  1957. justify-content: space-between;
  1958. }
  1959.  
  1960. .info-modal-header h3 {
  1961. margin: 0;
  1962. color: #f5c2e7;
  1963. font-size: 1.25rem;
  1964. }
  1965.  
  1966. .info-modal-body {
  1967. padding: 1.5rem;
  1968. line-height: 1.6;
  1969. }
  1970.  
  1971. .info-modal-body p {
  1972. margin: 0 0 1rem;
  1973. }
  1974.  
  1975. .info-modal-body h4 {
  1976. margin: 1.5rem 0 0.75rem;
  1977. color: #f5c2e7;
  1978. }
  1979.  
  1980. .info-modal-body ul {
  1981. margin: 0.75rem 0;
  1982. padding-left: 1.5rem;
  1983. }
  1984.  
  1985. .info-modal-body li {
  1986. margin: 0.5rem 0;
  1987. }
  1988.  
  1989. .info-modal-body a {
  1990. color: #89b4fa;
  1991. text-decoration: none;
  1992. }
  1993.  
  1994. .info-modal-body a:hover {
  1995. text-decoration: underline;
  1996. }
  1997.  
  1998. .info-close {
  1999. cursor: pointer;
  2000. color: #6c7086;
  2001. transition: color 0.2s ease;
  2002. }
  2003.  
  2004. .info-close:hover {
  2005. color: #f5c2e7;
  2006. }
  2007. `);
  2008.  
  2009. GM_addStyle(`
  2010. .m3unator-input-group {
  2011. position: relative;
  2012. width: 100%;
  2013. }
  2014.  
  2015. .m3unator-input {
  2016. width: 100%;
  2017. padding-right: 80px !important;
  2018. transition: all 0.2s ease;
  2019. }
  2020.  
  2021. .m3unator-dropdown {
  2022. position: absolute;
  2023. right: 8px;
  2024. top: 50%;
  2025. transform: translateY(-50%);
  2026. display: none;
  2027. z-index: 1;
  2028. width: 70px;
  2029. }
  2030.  
  2031. .m3unator-dropdown.active {
  2032. display: block;
  2033. }
  2034.  
  2035. .m3unator-dropdown-button {
  2036. width: 100%;
  2037. padding: 4px 8px;
  2038. border-radius: 6px;
  2039. background: rgba(30, 30, 46, 0.6);
  2040. border: 1px solid rgba(69, 71, 90, 0.6);
  2041. color: #f5c2e7;
  2042. cursor: pointer;
  2043. display: flex;
  2044. align-items: center;
  2045. justify-content: space-between;
  2046. gap: 4px;
  2047. transition: all 0.2s ease;
  2048. }
  2049.  
  2050. .m3unator-dropdown-button:hover {
  2051. background: rgba(30, 30, 46, 0.8);
  2052. border-color: rgba(69, 71, 90, 0.8);
  2053. }
  2054.  
  2055. .m3unator-dropdown-menu {
  2056. position: absolute;
  2057. top: 100%;
  2058. right: 0;
  2059. width: 100%;
  2060. margin-top: 4px;
  2061. background: rgba(30, 30, 46, 0.95);
  2062. border: 1px solid rgba(69, 71, 90, 0.6);
  2063. border-radius: 6px;
  2064. padding: 4px;
  2065. display: none;
  2066. }
  2067.  
  2068. .m3unator-dropdown.active .m3unator-dropdown-menu {
  2069. display: block;
  2070. }
  2071.  
  2072. .m3unator-dropdown-item {
  2073. padding: 0.618rem;
  2074. color: #cdd6f4;
  2075. cursor: pointer;
  2076. transition: all 0.2s ease;
  2077. user-select: none;
  2078. }
  2079.  
  2080. .m3unator-dropdown-item:hover {
  2081. background: rgba(203, 166, 247, 0.1);
  2082. }
  2083.  
  2084. .m3unator-dropdown-item.selected {
  2085. background: rgba(203, 166, 247, 0.1);
  2086. color: #cba6f7;
  2087. }
  2088. `);
  2089.  
  2090. GM_addStyle(`
  2091. .M3Unator-container {
  2092. max-width: 400px;
  2093. width: 100%;
  2094. background: none;
  2095. backdrop-filter: none;
  2096. }
  2097.  
  2098. .M3Unator-popup {
  2099. background: #1e1e2e;
  2100. border-radius: 12px;
  2101. box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
  2102. border: 1px solid rgba(69, 71, 90, 0.6);
  2103. }
  2104.  
  2105. .M3Unator-content {
  2106. padding: 0.75rem;
  2107. display: flex;
  2108. flex-direction: column;
  2109. gap: 0.75rem;
  2110. max-width: 100%;
  2111. overflow: hidden;
  2112. background: none;
  2113. }
  2114.  
  2115. .M3Unator-header {
  2116. padding: 0.75rem;
  2117. display: flex;
  2118. align-items: center;
  2119. justify-content: space-between;
  2120. background: none;
  2121. border-bottom: 1px solid rgba(69, 71, 90, 0.6);
  2122. }
  2123.  
  2124. .M3Unator-input {
  2125. width: 100%;
  2126. min-width: 0;
  2127. padding: 8px 80px 8px 12px;
  2128. box-sizing: border-box;
  2129. transition: all 0.2s ease;
  2130. background: #1e1e2e;
  2131. border: 1px solid rgba(69, 71, 90, 0.6);
  2132. border-radius: 6px;
  2133. color: #f5c2e7;
  2134. font-size: 14px;
  2135. }
  2136.  
  2137. .M3Unator-dropdown-button {
  2138. width: 100%;
  2139. padding: 4px 8px;
  2140. border-radius: 6px;
  2141. background: #1e1e2e;
  2142. border: 1px solid rgba(69, 71, 90, 0.6);
  2143. color: #f5c2e7;
  2144. cursor: pointer;
  2145. display: flex;
  2146. align-items: center;
  2147. justify-content: space-between;
  2148. gap: 4px;
  2149. transition: all 0.2s ease;
  2150. box-sizing: border-box;
  2151. font-size: 14px;
  2152. font-family: monospace;
  2153. }
  2154.  
  2155. .M3Unator-dropdown-button span {
  2156. min-width: 40px;
  2157. text-align: left;
  2158. }
  2159.  
  2160. .M3Unator-dropdown-button svg {
  2161. width: 16px;
  2162. height: 16px;
  2163. min-width: 16px;
  2164. min-height: 16px;
  2165. margin-left: auto;
  2166. }
  2167.  
  2168. .M3Unator-dropdown-menu {
  2169. position: absolute;
  2170. top: 100%;
  2171. right: 0;
  2172. width: 100%;
  2173. margin-top: 4px;
  2174. background: #1e1e2e;
  2175. border: 1px solid rgba(69, 71, 90, 0.6);
  2176. border-radius: 6px;
  2177. padding: 4px;
  2178. display: none;
  2179. box-sizing: border-box;
  2180. z-index: 9999;
  2181. }
  2182. `);
  2183.  
  2184. GM_addStyle(`
  2185. .M3Unator-content {
  2186. padding: 0.75rem;
  2187. display: flex;
  2188. flex-direction: column;
  2189. gap: 12px;
  2190. }
  2191.  
  2192. .M3Unator-toggle-group {
  2193. margin: 0;
  2194. display: flex;
  2195. gap: 0.75rem;
  2196. justify-content: center;
  2197. background: rgba(30, 30, 46, 0.4);
  2198. padding: 0.75rem;
  2199. border-radius: 12px;
  2200. }
  2201.  
  2202. .M3Unator-button {
  2203. margin: 0;
  2204. }
  2205.  
  2206. .M3Unator-log-container {
  2207. margin: 0;
  2208. }
  2209.  
  2210. .M3Unator-stats-bar {
  2211. margin: 0;
  2212. }
  2213. `);
  2214.  
  2215. GM_addStyle(`
  2216. /* Dropdown Styles */
  2217. .M3Unator-dropdown {
  2218. position: relative;
  2219. display: none;
  2220. }
  2221.  
  2222. .M3Unator-dropdown-button {
  2223. width: 100%;
  2224. padding: 4px 8px;
  2225. border-radius: 6px;
  2226. background: #1e1e2e;
  2227. border: 1px solid rgba(69, 71, 90, 0.6);
  2228. color: #f5c2e7;
  2229. cursor: pointer;
  2230. display: flex;
  2231. align-items: center;
  2232. justify-content: space-between;
  2233. gap: 4px;
  2234. transition: all 0.2s ease;
  2235. box-sizing: border-box;
  2236. font-size: 14px;
  2237. font-family: monospace;
  2238. }
  2239.  
  2240. .M3Unator-dropdown-button span {
  2241. min-width: 40px;
  2242. text-align: left;
  2243. }
  2244.  
  2245. .M3Unator-dropdown-button svg {
  2246. width: 16px;
  2247. height: 16px;
  2248. min-width: 16px;
  2249. min-height: 16px;
  2250. margin-left: auto;
  2251. transition: transform 0.2s ease;
  2252. }
  2253.  
  2254. .M3Unator-dropdown.active .M3Unator-dropdown-button svg {
  2255. transform: rotate(180deg);
  2256. }
  2257.  
  2258. .M3Unator-dropdown-menu {
  2259. position: absolute;
  2260. top: 100%;
  2261. left: 0;
  2262. right: 0;
  2263. margin-top: 4px;
  2264. background: #1e1e2e;
  2265. border: 1px solid rgba(69, 71, 90, 0.6);
  2266. border-radius: 6px;
  2267. overflow: hidden;
  2268. z-index: 1000;
  2269. display: none;
  2270. }
  2271.  
  2272. .M3Unator-dropdown.active .M3Unator-dropdown-menu {
  2273. display: block;
  2274. }
  2275.  
  2276. .M3Unator-dropdown-item {
  2277. padding: 6px 12px;
  2278. color: #f5c2e7;
  2279. cursor: pointer;
  2280. transition: all 0.2s ease;
  2281. font-family: monospace;
  2282. }
  2283.  
  2284. .M3Unator-dropdown-item:hover {
  2285. background: rgba(69, 71, 90, 0.3);
  2286. }
  2287.  
  2288. .M3Unator-dropdown-item:not(:last-child) {
  2289. border-bottom: 1px solid rgba(69, 71, 90, 0.3);
  2290. }
  2291.  
  2292. /* Input Styles */
  2293. .M3Unator-input {
  2294. width: 100%;
  2295. height: 42px;
  2296. padding: 0 12px;
  2297. border: 1px solid #45475a;
  2298. border-radius: 8px;
  2299. background: #1e1e2e;
  2300. color: #f5c2e7;
  2301. font-size: 14px;
  2302. transition: all 0.2s ease;
  2303. box-sizing: border-box;
  2304. }
  2305.  
  2306. .M3Unator-input:focus {
  2307. outline: none;
  2308. border-color: #f5c2e7;
  2309. box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1);
  2310. }
  2311.  
  2312. /* Button Styles */
  2313. .M3Unator-button {
  2314. height: 42px;
  2315. padding: 0 16px;
  2316. border: none;
  2317. border-radius: 8px;
  2318. background: #f5c2e7;
  2319. color: #1e1e2e;
  2320. font-weight: 600;
  2321. font-size: 14px;
  2322. cursor: pointer;
  2323. transition: all 0.2s ease;
  2324. display: flex;
  2325. align-items: center;
  2326. justify-content: center;
  2327. gap: 8px;
  2328. }
  2329.  
  2330. /* Toggle Container Styles */
  2331. .M3Unator-toggle-container {
  2332. position: relative;
  2333. display: flex;
  2334. align-items: center;
  2335. gap: 0.5rem;
  2336. transition: all 0.2s ease;
  2337. }
  2338.  
  2339. /* Control Button Styles */
  2340. .M3Unator-control-btn {
  2341. padding: 0.75rem 1.5rem;
  2342. border-radius: 12px;
  2343. font-weight: 600;
  2344. font-size: 0.95rem;
  2345. min-width: 160px;
  2346. background: rgba(30, 30, 46, 0.6);
  2347. backdrop-filter: blur(8px);
  2348. }
  2349.  
  2350. .M3Unator-control-btn.pause {
  2351. border-color: #fab387;
  2352. color: #fab387;
  2353. }
  2354.  
  2355. .M3Unator-control-btn.resume {
  2356. border-color: #94e2d5;
  2357. color: #94e2d5;
  2358. }
  2359.  
  2360. .M3Unator-control-btn.cancel {
  2361. border-color: #f38ba8;
  2362. color: #f38ba8;
  2363. }
  2364.  
  2365. /* Stats Styles */
  2366. .M3Unator-stat {
  2367. display: inline-flex;
  2368. align-items: center;
  2369. gap: 0.382rem;
  2370. font-size: 0.875rem;
  2371. cursor: help;
  2372. min-width: 52px;
  2373. padding: 0 0.382rem;
  2374. }
  2375. `);
  2376.  
  2377. GM_addStyle(`
  2378. .M3Unator-toast.success {
  2379. color: #a6e3a1;
  2380. border-color: #a6e3a1;
  2381. background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(166, 227, 161, 0.1);
  2382. }
  2383.  
  2384. .M3Unator-toast.error {
  2385. color: #f38ba8;
  2386. border-color: #f38ba8;
  2387. background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(243, 139, 168, 0.1);
  2388. }
  2389.  
  2390. .M3Unator-toast.warning {
  2391. color: #fab387;
  2392. border-color: #fab387;
  2393. background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(250, 179, 135, 0.1);
  2394. }
  2395. `);
  2396.  
  2397. class LogCache {
  2398. constructor(maxSize = 100) {
  2399. this.maxSize = maxSize;
  2400. this.logs = [];
  2401. this.stats = {
  2402. totalLogs: 0,
  2403. skippedLogs: 0
  2404. };
  2405. }
  2406.  
  2407. add(message, type = '') {
  2408. const timestamp = new Date().toLocaleTimeString();
  2409. this.logs.push({ message, type, timestamp });
  2410. this.stats.totalLogs++;
  2411. if (this.logs.length > this.maxSize) {
  2412. this.logs.shift();
  2413. this.stats.skippedLogs++;
  2414. }
  2415. }
  2416.  
  2417. getSummary() {
  2418. return {
  2419. logs: [...this.logs],
  2420. stats: { ...this.stats }
  2421. };
  2422. }
  2423.  
  2424. clear() {
  2425. this.logs = [];
  2426. this.stats.totalLogs = 0;
  2427. this.stats.skippedLogs = 0;
  2428. }
  2429. }
  2430.  
  2431. class PlaylistGenerator {
  2432. constructor() {
  2433. this.initialStats = {
  2434. directories: {
  2435. total: 0,
  2436. depth: 0
  2437. },
  2438. files: {
  2439. video: {
  2440. total: 0,
  2441. current: 0
  2442. },
  2443. audio: {
  2444. total: 0,
  2445. current: 0
  2446. }
  2447. },
  2448. errors: {
  2449. total: 0,
  2450. skipped: 0
  2451. },
  2452. totalFiles: 0
  2453. };
  2454.  
  2455. this.videoFormats = [
  2456. '.mp4', '.mkv', '.avi', '.webm', '.mov', '.flv', '.wmv',
  2457. '.m4v', '.mpg', '.mpeg', '.3gp', '.vob', '.ts', '.mts',
  2458. '.m2ts', '.divx', '.xvid', '.asf', '.ogv', '.rm', '.rmvb',
  2459. '.wtv', '.qt', '.hevc', '.f4v', '.swf', '.vro', '.ogx',
  2460. '.drc', '.gifv', '.mxf', '.roq', '.nsv'
  2461. ];
  2462.  
  2463. this.audioFormats = [
  2464. '.mp3', '.m4a', '.wav', '.flac', '.aac', '.ogg', '.wma',
  2465. '.opus', '.aiff', '.ape', '.mka', '.ac3', '.dts', '.m4b',
  2466. '.m4p', '.m4r', '.mid', '.midi', '.mp2', '.mpa', '.mpc',
  2467. '.ra', '.tta', '.voc', '.vox', '.amr', '.awb', '.dsf',
  2468. '.dff', '.alac', '.wv', '.oga', '.sln', '.aif', '.pcm'
  2469. ];
  2470.  
  2471. // Create Map for file extensions
  2472. this.extensionMap = new Map();
  2473.  
  2474. // Add video extensions to Map
  2475. this.videoFormats.forEach(ext => {
  2476. this.extensionMap.set(ext.slice(1), 'video'); // .mp4 -> mp4
  2477. });
  2478.  
  2479. // Add audio extensions to Map
  2480. this.audioFormats.forEach(ext => {
  2481. this.extensionMap.set(ext.slice(1), 'audio'); // .mp3 -> mp3
  2482. });
  2483.  
  2484. this.domElements = {};
  2485.  
  2486. this.state = {
  2487. isGenerating: false,
  2488. isPaused: false,
  2489. selectedFormat: 'm3u',
  2490. includeVideo: false,
  2491. includeAudio: false,
  2492. maxEntries: 1000000,
  2493. timeoutMs: 5000,
  2494. retryCount: 2,
  2495. maxDepth: 0,
  2496. maxSeenUrls: 5000,
  2497. stats: { ...this.initialStats }
  2498. };
  2499.  
  2500. this.sortOptions = { numeric: true, sensitivity: 'base' };
  2501.  
  2502. this.entries = [];
  2503. this.seenUrls = new Set();
  2504. this.toastQueue = [];
  2505. this.isProcessingToast = false;
  2506.  
  2507. this.icons = {
  2508. video: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2509. <polygon points="23 7 16 12 23 17 23 7"/>
  2510. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  2511. </svg>`,
  2512. audio: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2513. <path d="M9 18V5l12-2v13"/>
  2514. <circle cx="6" cy="18" r="3"/>
  2515. <circle cx="18" cy="16" r="3"/>
  2516. </svg>`,
  2517. folder: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2518. <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
  2519. </svg>`,
  2520. info: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2521. <circle cx="12" cy="12" r="10"/>
  2522. <line x1="12" y1="16" x2="12" y2="12"/>
  2523. <line x1="12" y1="8" x2="12.01" y2="8"/>
  2524. </svg>`,
  2525. file: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2526. <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
  2527. <polyline points="13 2 13 9 20 9"/>
  2528. </svg>`,
  2529. download: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2530. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  2531. <polyline points="7 10 12 15 17 10"/>
  2532. <line x1="12" y1="15" x2="12" y2="3"/>
  2533. </svg>`,
  2534. close: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2535. <line x1="18" y1="6" x2="6" y2="18"/>
  2536. <line x1="6" y1="6" x2="18" y2="18"/>
  2537. </svg>`,
  2538. pause: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2539. <rect x="6" y="4" width="4" height="16"/>
  2540. <rect x="14" y="4" width="4" height="16"/>
  2541. </svg>`,
  2542. resume: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2543. <polygon points="5 3 19 12 5 21 5 3"/>
  2544. </svg>`,
  2545. cancel: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2546. <circle cx="12" cy="12" r="10"/>
  2547. <line x1="15" y1="9" x2="9" y2="15"/>
  2548. <line x1="9" y1="9" x2="15" y2="15"/>
  2549. </svg>`,
  2550. success: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2551. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
  2552. <polyline points="22 4 12 14.01 9 11.01"/>
  2553. </svg>`,
  2554. error: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2555. <circle cx="12" cy="12" r="10"/>
  2556. <line x1="12" y1="8" x2="12" y2="12"/>
  2557. <line x1="12" y1="16" x2="12.01" y2="16"/>
  2558. </svg>`,
  2559. warning: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2560. <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
  2561. <line x1="12" y1="9" x2="12" y2="13"/>
  2562. <line x1="12" y1="17" x2="12.01" y2="17"/>
  2563. </svg>`,
  2564. github: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
  2565. <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
  2566. </svg>`,
  2567. twitter: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
  2568. <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
  2569. </svg>`,
  2570. chevronDown: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2571. <polyline points="6 9 12 15 18 9"/>
  2572. </svg>`,
  2573. layers: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2574. <polygon points="12 2 2 7 12 12 22 7 12 2"/>
  2575. <polyline points="2 17 12 22 22 17"/>
  2576. <polyline points="2 12 12 17 22 12"/>
  2577. </svg>`,
  2578. logToggle: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2579. <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
  2580. </svg>`
  2581. };
  2582.  
  2583. this.templates = {
  2584. toggleButton: (id, title, icon, checked = false) => `
  2585. <div class="M3Unator-toggle-container">
  2586. <label>
  2587. <input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
  2588. <span title="${title}">${icon}</span>
  2589. </label>
  2590. </div>
  2591. `,
  2592. controlButton: (type, icon, text) => `
  2593. <button class="M3Unator-control-btn ${type}">
  2594. ${icon}
  2595. <span>${text}</span>
  2596. </button>
  2597. `,
  2598. statsItem: (icon, id, title, className = '') => `
  2599. <span class="M3Unator-stat ${className}" title="${title}">
  2600. ${icon}
  2601. <span id="${id}">0</span>
  2602. </span>
  2603. `
  2604. };
  2605.  
  2606. this.baseStyles = `
  2607. .M3Unator-btn-base {
  2608. border: none;
  2609. border-radius: 8px;
  2610. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  2611. font-weight: 600;
  2612. cursor: pointer;
  2613. transition: all 0.2s ease;
  2614. display: flex;
  2615. align-items: center;
  2616. justify-content: center;
  2617. gap: 0.5rem;
  2618. }
  2619.  
  2620. .M3Unator-toggle-base {
  2621. position: relative;
  2622. display: flex;
  2623. align-items: center;
  2624. gap: 0.5rem;
  2625. transition: all 0.2s ease;
  2626. }
  2627.  
  2628. .M3Unator-control-base {
  2629. padding: 0.75rem 1.5rem;
  2630. border-radius: 12px;
  2631. font-weight: 600;
  2632. font-size: 0.95rem;
  2633. min-width: 160px;
  2634. background: rgba(30, 30, 46, 0.6);
  2635. backdrop-filter: blur(8px);
  2636. }
  2637.  
  2638. .M3Unator-stat-base {
  2639. display: inline-flex;
  2640. align-items: center;
  2641. gap: 0.382rem;
  2642. font-size: 0.875rem;
  2643. cursor: help;
  2644. min-width: 52px;
  2645. padding: 0 0.382rem;
  2646. }
  2647.  
  2648. .M3Unator-icon-base {
  2649. display: flex;
  2650. align-items: center;
  2651. justify-content: center;
  2652. width: 24px;
  2653. height: 24px;
  2654. transition: all 0.2s ease;
  2655. }
  2656. `;
  2657.  
  2658. GM_addStyle(this.baseStyles);
  2659.  
  2660. this.updateActivityIndicator = (status) => {
  2661. const indicator = this.domElements.activityIndicator;
  2662. if (!indicator) return;
  2663. // Remove all classes first
  2664. indicator.classList.remove('active', 'paused', 'cancelled', 'completed');
  2665. // Status check
  2666. if (this.state.isGenerating) {
  2667. if (this.state.isPaused) {
  2668. indicator.classList.add('paused');
  2669. } else {
  2670. indicator.classList.add('active');
  2671. }
  2672. } else if (status === 'cancelled') {
  2673. indicator.classList.add('cancelled');
  2674. } else if (status === 'completed') {
  2675. indicator.classList.add('completed');
  2676. }
  2677. };
  2678.  
  2679. this.logCache = new LogCache(100);
  2680. }
  2681.  
  2682. createComponent(type, props) {
  2683. switch (type) {
  2684. case 'toggle':
  2685. return this.templates.toggleButton(
  2686. props.id,
  2687. props.title,
  2688. props.icon,
  2689. props.checked
  2690. );
  2691. case 'control':
  2692. return this.templates.controlButton(
  2693. props.type,
  2694. props.icon,
  2695. props.text
  2696. );
  2697. case 'stats':
  2698. return `
  2699. <span class="M3Unator-stat ${props.class}" title="${props.title}">
  2700. ${props.icon}
  2701. <span id="${props.id}">0</span>
  2702. </span>
  2703. `;
  2704. default:
  2705. return '';
  2706. }
  2707. }
  2708.  
  2709. async init() {
  2710. const container = document.createElement('div');
  2711. container.className = 'M3Unator-container';
  2712.  
  2713. const toggleButtons = [
  2714. {
  2715. id: 'includeVideo',
  2716. title: 'Video (.mp4, .mkv)',
  2717. icon: this.icons.video,
  2718. checked: true
  2719. },
  2720. {
  2721. id: 'includeAudio',
  2722. title: 'Audio (.mp3, .m4a)',
  2723. icon: this.icons.audio,
  2724. checked: true
  2725. },
  2726. {
  2727. id: 'recursiveSearch',
  2728. title: 'Scan Subdirectories',
  2729. icon: this.icons.folder,
  2730. checked: true
  2731. }
  2732. ].map(props => this.createComponent('toggle', props)).join('');
  2733.  
  2734. const controlButtons = [
  2735. {
  2736. type: 'pause',
  2737. icon: this.icons.pause,
  2738. text: 'Pause'
  2739. },
  2740. {
  2741. type: 'resume',
  2742. icon: this.icons.resume,
  2743. text: 'Resume'
  2744. },
  2745. {
  2746. type: 'cancel',
  2747. icon: this.icons.cancel,
  2748. text: 'Cancel'
  2749. }
  2750. ].map(props => this.createComponent('control', props)).join('');
  2751.  
  2752. const statsItems = [
  2753. {
  2754. icon: this.icons.file,
  2755. id: 'totalFiles',
  2756. title: 'Total Files',
  2757. class: ''
  2758. },
  2759. {
  2760. icon: this.icons.video,
  2761. id: 'videoFiles',
  2762. title: 'Video (.mp4, .mkv)',
  2763. class: 'video'
  2764. },
  2765. {
  2766. icon: this.icons.audio,
  2767. id: 'audioFiles',
  2768. title: 'Audio (.mp3, .m4a)',
  2769. class: 'audio'
  2770. },
  2771. {
  2772. icon: this.icons.folder,
  2773. id: 'directories',
  2774. title: 'Subdirectories',
  2775. class: 'dir'
  2776. },
  2777. {
  2778. icon: this.icons.layers,
  2779. id: 'depthLevel',
  2780. title: 'Depth Level',
  2781. class: 'depth'
  2782. },
  2783. {
  2784. icon: this.icons.error,
  2785. id: 'errors',
  2786. title: 'Error',
  2787. class: 'error'
  2788. }
  2789. ].map(props => this.createComponent('stats', props)).join('');
  2790.  
  2791. container.innerHTML = `
  2792. <div class="M3Unator-popup">
  2793. <div class="M3Unator-header">
  2794. <h3 class="M3Unator-title">
  2795. ${this.icons.video}
  2796. <span>M3Unator</span>
  2797. </h3>
  2798. <div style="display: flex; align-items: center;">
  2799. <div class="M3Unator-social">
  2800. <a class="info-link">
  2801. ${this.icons.info}
  2802. </a>
  2803. <a href="https://github.com/hasanbeder/M3Unator" target="_blank" rel="noopener noreferrer" class="github-icon">
  2804. ${this.icons.github}
  2805. </a>
  2806. <a href="https://x.com/hasanbeder" target="_blank" rel="noopener noreferrer">
  2807. ${this.icons.twitter}
  2808. </a>
  2809. </div>
  2810. <button class="M3Unator-close">${this.icons.close}</button>
  2811. </div>
  2812. </div>
  2813. <div class="info-modal">
  2814. <div class="info-modal-content">
  2815. <div class="info-modal-header">
  2816. <h3>About M3Unator</h3>
  2817. <span class="info-close">${this.icons.close}</span>
  2818. </div>
  2819. <div class="info-modal-body">
  2820. <p><strong>M3Unator v1.0.2</strong> - The Ultimate Web Directory Playlist Creator</p>
  2821. <p>Create M3U/M3U8 playlists effortlessly from any web directory. Experience ultrafast scanning and intelligent media detection.</p>
  2822. <h4>Key Features:</h4>
  2823. <ul>
  2824. <li>⚡ Ultrafast directory scanning with parallel processing</li>
  2825. <li>🎥 Comprehensive media support (MP4, MKV, MP3, FLAC, etc.)</li>
  2826. <li>🔍 Smart recursive directory scanning</li>
  2827. <li>🛡️ Enhanced error handling and stability</li>
  2828. <li>🌙 Modern dark theme interface</li>
  2829. </ul>
  2830. <p>For updates and more information, visit the <a href="https://github.com/hasanbeder/M3Unator" target="_blank">GitHub repository</a>.</p>
  2831. </div>
  2832. </div>
  2833. </div>
  2834. <div class="M3Unator-content">
  2835. <div class="M3Unator-input-row">
  2836. <div class="M3Unator-input-group">
  2837. <input type="text"
  2838. id="playlistName"
  2839. class="M3Unator-input"
  2840. placeholder="Playlist Name"
  2841. required
  2842. spellcheck="false"
  2843. autocomplete="off"
  2844. autocorrect="off"
  2845. autocapitalize="off">
  2846.  
  2847. <div class="M3Unator-dropdown">
  2848. <button type="button" class="M3Unator-dropdown-button">
  2849. <span>.m3u</span>
  2850. ${this.icons.chevronDown}
  2851. </button>
  2852. <div class="M3Unator-dropdown-menu">
  2853. <div class="M3Unator-dropdown-item selected" data-value="m3u">.m3u</div>
  2854. <div class="M3Unator-dropdown-divider"></div>
  2855. <div class="M3Unator-dropdown-item" data-value="m3u8">.m3u8</div>
  2856. </div>
  2857. </div>
  2858. </div>
  2859. </div>
  2860.  
  2861. <div class="M3Unator-toggle-group">
  2862. ${toggleButtons}
  2863. </div>
  2864.  
  2865. <div class="M3Unator-depth-controls">
  2866. <div class="M3Unator-radio-group">
  2867. <label class="M3Unator-radio">
  2868. <input type="radio" name="depthType" value="current" id="currentDepth">
  2869. <span class="radio-mark"></span>
  2870. <span class="radio-label">Current directory</span>
  2871. </label>
  2872. <label class="M3Unator-radio">
  2873. <input type="radio" name="depthType" value="custom" id="customDepth">
  2874. <span class="radio-mark"></span>
  2875. <span class="radio-label">Custom depth:</span>
  2876. <input type="number"
  2877. id="maxDepth"
  2878. value="1"
  2879. min="1"
  2880. max="99"
  2881. class="M3Unator-depth-input"
  2882. title="Subdirectory scan depth"
  2883. style="width: 64px;"
  2884. inputmode="numeric"
  2885. pattern="[0-9]*">
  2886. </label>
  2887. </div>
  2888. </div>
  2889.  
  2890. <button class="M3Unator-button" id="generateBtn">
  2891. ${this.icons.download}
  2892. <span>Create Playlist</span>
  2893. </button>
  2894.  
  2895. <div class="M3Unator-controls">
  2896. ${controlButtons}
  2897. </div>
  2898.  
  2899. <div class="M3Unator-stats-bar">
  2900. <div class="M3Unator-stats">
  2901. ${statsItems}
  2902. </div>
  2903. </div>
  2904.  
  2905. <div class="M3Unator-log-container">
  2906. <button class="M3Unator-log-toggle">
  2907. <div class="toggle-text">
  2908. ${this.icons.logToggle}
  2909. <span>Recent Activity</span>
  2910. </div>
  2911. <div class="M3Unator-activity-indicator"></div>
  2912. </button>
  2913. <div id="scanLog" class="M3Unator-log collapsed"></div>
  2914. </div>
  2915. </div>
  2916.  
  2917. <style>
  2918. .M3Unator-container {
  2919. max-width: 400px;
  2920. width: 100%;
  2921. background: transparent;
  2922. backdrop-filter: none;
  2923. }
  2924.  
  2925. .M3Unator-popup {
  2926. background: #1e1e2e;
  2927. border-radius: 12px;
  2928. box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
  2929. border: 1px solid rgba(69, 71, 90, 0.6);
  2930. }
  2931.  
  2932. .info-modal {
  2933. display: none;
  2934. position: fixed;
  2935. top: 0;
  2936. left: 0;
  2937. width: 100%;
  2938. height: 100%;
  2939. background: rgba(0, 0, 0, 0.75);
  2940. z-index: 99999;
  2941. }
  2942.  
  2943. .info-content {
  2944. position: absolute;
  2945. top: 50%;
  2946. left: 50%;
  2947. transform: translate(-50%, -50%);
  2948. background: #1e1e2e;
  2949. padding: 2rem;
  2950. border-radius: 12px;
  2951. max-width: 600px;
  2952. width: 90%;
  2953. max-height: 80vh;
  2954. overflow-y: auto;
  2955. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  2956. border: 1px solid rgba(69, 71, 90, 0.6);
  2957. }
  2958.  
  2959. .M3Unator-content {
  2960. padding: 0.75rem;
  2961. display: flex;
  2962. flex-direction: column;
  2963. gap: 0.75rem;
  2964. max-width: 100%;
  2965. overflow: hidden;
  2966. }
  2967.  
  2968. .M3Unator-input-row {
  2969. display: flex;
  2970. width: 100%;
  2971. position: relative;
  2972. max-width: 100%;
  2973. overflow: visible;
  2974. }
  2975.  
  2976. .M3Unator-input-group {
  2977. flex: 1;
  2978. min-width: 0;
  2979. position: relative;
  2980. }
  2981.  
  2982. .M3Unator-input {
  2983. width: 100%;
  2984. min-width: 0;
  2985. padding-right: 80px;
  2986. box-sizing: border-box;
  2987. transition: all 0.2s ease;
  2988. background: rgba(30, 30, 46, 0.6);
  2989. border: 1px solid rgba(69, 71, 90, 0.6);
  2990. border-radius: 6px;
  2991. color: #f5c2e7;
  2992. padding: 8px 80px 8px 12px;
  2993. font-size: 14px;
  2994. }
  2995.  
  2996. .M3Unator-dropdown {
  2997. position: absolute;
  2998. right: 8px;
  2999. top: 50%;
  3000. transform: translateY(-50%);
  3001. width: 70px;
  3002. z-index: 9999;
  3003. display: none;
  3004. }
  3005.  
  3006. .M3Unator-dropdown.active {
  3007. display: block;
  3008. }
  3009.  
  3010. .M3Unator-dropdown-button {
  3011. width: 100%;
  3012. padding: 4px 8px;
  3013. border-radius: 6px;
  3014. background: rgba(30, 30, 46, 0.8);
  3015. border: 1px solid rgba(69, 71, 90, 0.6);
  3016. color: #f5c2e7;
  3017. cursor: pointer;
  3018. display: flex;
  3019. align-items: center;
  3020. justify-content: space-between;
  3021. gap: 4px;
  3022. transition: all 0.2s ease;
  3023. box-sizing: border-box;
  3024. font-size: 14px;
  3025. }
  3026.  
  3027. .M3Unator-dropdown-button:hover {
  3028. background: rgba(30, 30, 46, 0.9);
  3029. border-color: rgba(69, 71, 90, 0.8);
  3030. }
  3031.  
  3032. .M3Unator-dropdown-menu {
  3033. position: absolute;
  3034. top: 100%;
  3035. right: 0;
  3036. width: 100%;
  3037. margin-top: 4px;
  3038. background: rgba(30, 30, 46, 0.95);
  3039. border: 1px solid rgba(69, 71, 90, 0.6);
  3040. border-radius: 6px;
  3041. padding: 4px;
  3042. display: none;
  3043. box-sizing: border-box;
  3044. z-index: 9999;
  3045. }
  3046.  
  3047. .M3Unator-dropdown.active .M3Unator-dropdown-menu {
  3048. display: block;
  3049. }
  3050.  
  3051. .M3Unator-dropdown-item {
  3052. padding: 8px 12px;
  3053. cursor: pointer;
  3054. border-radius: 4px;
  3055. transition: all 0.2s ease;
  3056. text-align: center;
  3057. font-size: 14px;
  3058. font-family: monospace;
  3059. color: #cdd6f4;
  3060. }
  3061.  
  3062. .M3Unator-dropdown-divider {
  3063. height: 1px;
  3064. background: rgba(69, 71, 90, 0.6);
  3065. margin: 6px 0;
  3066. }
  3067.  
  3068. .M3Unator-dropdown-menu {
  3069. position: absolute;
  3070. top: 100%;
  3071. right: 0;
  3072. width: 100%;
  3073. margin-top: 4px;
  3074. background: #1e1e2e;
  3075. border: 1px solid rgba(69, 71, 90, 0.6);
  3076. border-radius: 6px;
  3077. padding: 6px;
  3078. display: none;
  3079. box-sizing: border-box;
  3080. z-index: 9999;
  3081. }
  3082.  
  3083. .M3Unator-dropdown-button {
  3084. width: 100%;
  3085. padding: 4px 8px;
  3086. border-radius: 6px;
  3087. background: #1e1e2e;
  3088. border: 1px solid rgba(69, 71, 90, 0.6);
  3089. color: #f5c2e7;
  3090. cursor: pointer;
  3091. display: flex;
  3092. align-items: center;
  3093. justify-content: space-between;
  3094. gap: 4px;
  3095. transition: all 0.2s ease;
  3096. box-sizing: border-box;
  3097. font-size: 14px;
  3098. font-family: monospace;
  3099. }
  3100.  
  3101. .M3Unator-dropdown-item:hover {
  3102. background: rgba(203, 166, 247, 0.1);
  3103. color: #f5c2e7;
  3104. }
  3105.  
  3106. .M3Unator-dropdown-item.selected {
  3107. background: rgba(203, 166, 247, 0.1);
  3108. color: #cba6f7;
  3109. }
  3110. </style>
  3111. </div>
  3112. `;
  3113.  
  3114. document.body.appendChild(container);
  3115. const launcher = document.createElement('button');
  3116. launcher.className = 'M3Unator-launcher';
  3117. launcher.innerHTML = `
  3118. ${this.icons.video}
  3119. <span>M3Unator</span>
  3120. `;
  3121. document.body.appendChild(launcher);
  3122. const popup = container.querySelector('.M3Unator-popup');
  3123. const header = container.querySelector('.M3Unator-header');
  3124. this.makeDraggable(popup, header);
  3125. const statsBar = container.querySelector('.M3Unator-stats-bar');
  3126. if (statsBar) {
  3127. statsBar.style.display = 'block';
  3128. }
  3129.  
  3130. this.domElements = {
  3131. container,
  3132. popup: container.querySelector('.M3Unator-popup'),
  3133. header: container.querySelector('.M3Unator-header'),
  3134. closeBtn: container.querySelector('.M3Unator-close'),
  3135. generateBtn: container.querySelector('#generateBtn'),
  3136. playlistInput: container.querySelector('#playlistName'),
  3137. includeVideo: container.querySelector('#includeVideo'),
  3138. includeAudio: container.querySelector('#includeAudio'),
  3139. recursiveSearch: container.querySelector('#recursiveSearch'),
  3140. controls: container.querySelector('.M3Unator-controls'),
  3141. scanLog: container.querySelector('#scanLog'),
  3142. statsBar: container.querySelector('.M3Unator-stats-bar'),
  3143. dropdown: container.querySelector('.M3Unator-dropdown'),
  3144. launcher,
  3145. stats: {
  3146. totalFiles: container.querySelector('#totalFiles'),
  3147. videoFiles: container.querySelector('#videoFiles'),
  3148. audioFiles: container.querySelector('#audioFiles'),
  3149. directories: container.querySelector('#directories'),
  3150. depthLevel: container.querySelector('#depthLevel'),
  3151. errors: container.querySelector('#errors')
  3152. },
  3153. depthControls: container.querySelector('.M3Unator-depth-controls'),
  3154. currentDepth: container.querySelector('#currentDepth'),
  3155. customDepth: container.querySelector('#customDepth'),
  3156. maxDepth: container.querySelector('#maxDepth'),
  3157. logToggle: container.querySelector('.M3Unator-log-toggle'),
  3158. logCounter: container.querySelector('.M3Unator-log-counter'),
  3159. activityIndicator: container.querySelector('.M3Unator-activity-indicator'),
  3160. };
  3161.  
  3162. launcher.onclick = () => {
  3163. this.domElements.container.setAttribute('data-visible', 'true');
  3164. const overlay = document.createElement('div');
  3165. overlay.className = 'M3Unator-overlay';
  3166. document.body.appendChild(overlay);
  3167. const popup = this.domElements.popup;
  3168. const rect = popup.getBoundingClientRect();
  3169. const centerX = (window.innerWidth - rect.width) / 2;
  3170. const centerY = (window.innerHeight - rect.height) / 2;
  3171. popup.style.left = `${centerX}px`;
  3172. popup.style.top = `${centerY}px`;
  3173. };
  3174.  
  3175. document.querySelector('.M3Unator-close').onclick = () => {
  3176. if (this.state.isGenerating) {
  3177. this.state.isGenerating = false;
  3178. this.state.isPaused = false;
  3179. this.reset({ isCancelled: true, enableToggles: true });
  3180. this.showToast('Scan cancelled', 'warning');
  3181. }
  3182. this.domElements.container.removeAttribute('data-visible');
  3183. const overlay = document.querySelector('.M3Unator-overlay');
  3184. if (overlay) overlay.remove();
  3185. };
  3186.  
  3187. this.setupPopupHandlers();
  3188.  
  3189. this.updateCounter(0);
  3190.  
  3191. this.domElements.logToggle.addEventListener('click', () => {
  3192. const log = this.domElements.scanLog;
  3193. const toggle = this.domElements.logToggle;
  3194. if (log.classList.contains('expanded')) {
  3195. log.classList.remove('expanded');
  3196. toggle.classList.remove('active');
  3197. } else {
  3198. log.classList.add('expanded');
  3199. toggle.classList.add('active');
  3200. log.scrollTop = log.scrollHeight;
  3201. }
  3202. });
  3203.  
  3204. this.domElements.scanLog.classList.remove('expanded');
  3205. this.domElements.logToggle.classList.remove('active');
  3206.  
  3207. this.logCount = 0;
  3208. }
  3209.  
  3210. updateStyles() {
  3211. GM_addStyle(`
  3212. .M3Unator-toggle-container {
  3213. @extend .M3Unator-toggle-base;
  3214. }
  3215.  
  3216. .M3Unator-control-btn {
  3217. @extend .M3Unator-control-base;
  3218. }
  3219.  
  3220. .M3Unator-stat {
  3221. @extend .M3Unator-stat-base;
  3222. }
  3223.  
  3224. .M3Unator-toggle-container span {
  3225. @extend .M3Unator-icon-base;
  3226. background: #1e1e2e;
  3227. border: 2px solid #45475a;
  3228. border-radius: 16px;
  3229. }
  3230.  
  3231. .M3Unator-control-btn.pause {
  3232. border-color: #fab387;
  3233. color: #fab387;
  3234. }
  3235.  
  3236. .M3Unator-control-btn.resume {
  3237. border-color: #94e2d5;
  3238. color: #94e2d5;
  3239. }
  3240.  
  3241. .M3Unator-control-btn.cancel {
  3242. border-color: #f38ba8;
  3243. color: #f38ba8;
  3244. }
  3245.  
  3246. `);
  3247. }
  3248.  
  3249. makeDraggable(element, handle) {
  3250. let isDragging = false;
  3251. let currentX;
  3252. let currentY;
  3253. let initialX;
  3254. let initialY;
  3255. let xOffset = 0;
  3256. let yOffset = 0;
  3257.  
  3258. const centerWindow = () => {
  3259. const rect = element.getBoundingClientRect();
  3260. const centerX = (window.innerWidth - rect.width) / 2;
  3261. const centerY = (window.innerHeight - rect.height) / 2;
  3262. element.style.left = `${centerX}px`;
  3263. element.style.top = `${centerY}px`;
  3264. xOffset = centerX;
  3265. yOffset = centerY;
  3266. element.style.transform = 'none';
  3267. };
  3268.  
  3269. centerWindow();
  3270.  
  3271. const getPosition = (e) => {
  3272. return {
  3273. x: e.type.includes('touch') ? e.touches[0].clientX : e.clientX,
  3274. y: e.type.includes('touch') ? e.touches[0].clientY : e.clientY
  3275. };
  3276. };
  3277.  
  3278. const dragStart = (e) => {
  3279. if (e.target === handle || handle.contains(e.target)) {
  3280. e.preventDefault();
  3281. const pos = getPosition(e);
  3282. isDragging = true;
  3283. const rect = element.getBoundingClientRect();
  3284. xOffset = rect.left;
  3285. yOffset = rect.top;
  3286. initialX = pos.x - xOffset;
  3287. initialY = pos.y - yOffset;
  3288.  
  3289. handle.style.cursor = 'grabbing';
  3290. }
  3291. };
  3292.  
  3293. const drag = (e) => {
  3294. if (isDragging) {
  3295. e.preventDefault();
  3296. const pos = getPosition(e);
  3297.  
  3298. currentX = pos.x - initialX;
  3299. currentY = pos.y - initialY;
  3300.  
  3301. const rect = element.getBoundingClientRect();
  3302. const maxX = window.innerWidth - rect.width;
  3303. const maxY = window.innerHeight - rect.height;
  3304.  
  3305. currentX = Math.min(Math.max(0, currentX), maxX);
  3306. currentY = Math.min(Math.max(0, currentY), maxY);
  3307.  
  3308. element.style.left = `${currentX}px`;
  3309. element.style.top = `${currentY}px`;
  3310. xOffset = currentX;
  3311. yOffset = currentY;
  3312. }
  3313. };
  3314.  
  3315. const dragEnd = () => {
  3316. if (isDragging) {
  3317. isDragging = false;
  3318. handle.style.cursor = 'grab';
  3319. }
  3320. };
  3321.  
  3322. handle.addEventListener('mousedown', dragStart);
  3323. document.addEventListener('mousemove', drag);
  3324. document.addEventListener('mouseup', dragEnd);
  3325.  
  3326. handle.addEventListener('touchstart', dragStart, { passive: false });
  3327. document.addEventListener('touchmove', drag, { passive: false });
  3328. document.addEventListener('touchend', dragEnd);
  3329.  
  3330. window.addEventListener('resize', () => {
  3331. if (!isDragging) {
  3332. centerWindow();
  3333. }
  3334. });
  3335.  
  3336. handle.style.cursor = 'grab';
  3337. handle.style.userSelect = 'none';
  3338. handle.style.touchAction = 'none';
  3339.  
  3340. element.style.position = 'fixed';
  3341. element.style.margin = '0';
  3342. element.style.touchAction = 'none';
  3343. element.style.transition = 'none';
  3344. }
  3345.  
  3346. showToast(message, type = 'success', duration = 3000) {
  3347. let toastContainer = document.querySelector('.M3Unator-toast-container');
  3348. if (!toastContainer) {
  3349. toastContainer = document.createElement('div');
  3350. toastContainer.className = 'M3Unator-toast-container';
  3351. document.body.appendChild(toastContainer);
  3352. }
  3353.  
  3354. // Remove previous toasts
  3355. const existingToasts = toastContainer.querySelectorAll('.M3Unator-toast');
  3356. existingToasts.forEach(toast => {
  3357. toast.classList.add('removing');
  3358. setTimeout(() => toast.remove(), 300);
  3359. });
  3360.  
  3361. const toast = document.createElement('div');
  3362. toast.className = `M3Unator-toast ${type}`;
  3363. const icon = this.icons[type] || this.icons.info;
  3364. toast.innerHTML = `${icon}<span>${message}</span>`;
  3365. toastContainer.appendChild(toast);
  3366. // Force a reflow to ensure the animation plays
  3367. void toast.offsetWidth;
  3368. // Add show class to trigger animation
  3369. requestAnimationFrame(() => {
  3370. toast.classList.add('show');
  3371. });
  3372.  
  3373. setTimeout(() => {
  3374. toast.classList.add('removing');
  3375. toast.classList.remove('show');
  3376. setTimeout(() => {
  3377. if (toast.parentNode === toastContainer) {
  3378. toast.remove();
  3379. }
  3380. if (toastContainer.children.length === 0) {
  3381. toastContainer.remove();
  3382. }
  3383. }, 300);
  3384. }, duration);
  3385. }
  3386.  
  3387. setupPopupHandlers() {
  3388. const generateBtn = this.domElements.generateBtn;
  3389. const playlistInput = this.domElements.playlistInput;
  3390. const includeVideo = this.domElements.includeVideo;
  3391. const includeAudio = this.domElements.includeAudio;
  3392. const recursiveSearch = this.domElements.recursiveSearch;
  3393. const controls = this.domElements.controls;
  3394.  
  3395. const dropdown = this.domElements.dropdown;
  3396. const dropdownButton = dropdown.querySelector('.M3Unator-dropdown-button');
  3397. const dropdownItems = dropdown.querySelectorAll('.M3Unator-dropdown-item');
  3398.  
  3399. const controlButtons = controls.querySelectorAll('.M3Unator-control-btn');
  3400. const pauseBtn = controlButtons[0];
  3401. const resumeBtn = controlButtons[1];
  3402. const cancelBtn = controlButtons[2];
  3403.  
  3404. dropdownButton.addEventListener('click', () => {
  3405. dropdown.classList.toggle('active');
  3406. });
  3407.  
  3408. document.addEventListener('click', (e) => {
  3409. if (!dropdown.contains(e.target)) {
  3410. dropdown.classList.remove('active');
  3411. }
  3412. });
  3413.  
  3414. dropdownItems.forEach(item => {
  3415. item.addEventListener('click', () => {
  3416. dropdownItems.forEach(i => i.classList.remove('selected'));
  3417. item.classList.add('selected');
  3418. dropdownButton.querySelector('span').textContent = item.textContent;
  3419. this.state.selectedFormat = item.dataset.value;
  3420. dropdown.classList.remove('active');
  3421. });
  3422. });
  3423.  
  3424. recursiveSearch.checked = true;
  3425. this.state.recursiveSearch = true;
  3426. includeVideo.checked = true;
  3427. includeAudio.checked = true;
  3428. this.state.includeVideo = true;
  3429. this.state.includeAudio = true;
  3430.  
  3431. includeVideo.addEventListener('change', (e) => {
  3432. this.state.includeVideo = e.target.checked;
  3433. this.addLogEntry(
  3434. e.target.checked ?
  3435. 'Video files will be included' :
  3436. 'Video files will not be included',
  3437. 'info'
  3438. );
  3439. });
  3440.  
  3441. includeAudio.addEventListener('change', (e) => {
  3442. this.state.includeAudio = e.target.checked;
  3443. this.addLogEntry(
  3444. e.target.checked ?
  3445. 'Audio files will be included' :
  3446. 'Audio files will not be included',
  3447. 'info'
  3448. );
  3449. });
  3450.  
  3451. const currentDepth = this.domElements.currentDepth;
  3452. const customDepth = this.domElements.customDepth;
  3453. const maxDepth = this.domElements.maxDepth;
  3454. const depthControls = this.domElements.depthControls;
  3455.  
  3456. depthControls.style.display = 'none';
  3457. depthControls.classList.remove('active');
  3458. this.state.maxDepth = -1;
  3459.  
  3460. currentDepth.checked = true;
  3461. customDepth.checked = false;
  3462. maxDepth.disabled = true;
  3463. maxDepth.value = '1';
  3464.  
  3465. recursiveSearch.addEventListener('change', (e) => {
  3466. if (!e.target.checked) {
  3467. depthControls.style.display = 'block';
  3468. depthControls.classList.add('active');
  3469. currentDepth.checked = true;
  3470. customDepth.checked = false;
  3471. maxDepth.disabled = true;
  3472. this.state.maxDepth = 0;
  3473. this.addLogEntry('Directory scanning disabled, only current directory will be scanned', 'info');
  3474. } else {
  3475. depthControls.style.display = 'none';
  3476. depthControls.classList.remove('active');
  3477. this.state.maxDepth = -1;
  3478. this.state.recursiveSearch = true;
  3479. this.addLogEntry('Directory scanning active, all directories will be scanned', 'info');
  3480. }
  3481. });
  3482.  
  3483. this.domElements.currentDepth.addEventListener('change', (e) => {
  3484. if (e.target.checked && !recursiveSearch.checked) {
  3485. this.state.maxDepth = 0;
  3486. this.domElements.maxDepth.disabled = true;
  3487. this.addLogEntry('Only current directory will be scanned', 'info');
  3488. }
  3489. });
  3490.  
  3491. this.domElements.customDepth.addEventListener('change', (e) => {
  3492. if (e.target.checked && !recursiveSearch.checked) {
  3493. const depthValue = parseInt(this.domElements.maxDepth.value) || 1;
  3494. this.state.maxDepth = depthValue;
  3495. this.domElements.maxDepth.disabled = false;
  3496. this.addLogEntry(
  3497. `Directory scanning depth: ${depthValue} ` +
  3498. `(current directory + ${depthValue} sublevels)`,
  3499. 'info'
  3500. );
  3501. }
  3502. });
  3503.  
  3504. this.domElements.maxDepth.addEventListener('input', (e) => {
  3505. if (this.domElements.customDepth.checked && !recursiveSearch.checked) {
  3506. const value = Math.min(99, Math.max(1, parseInt(e.target.value) || 1));
  3507. e.target.value = value;
  3508. this.state.maxDepth = value;
  3509. this.addLogEntry(
  3510. `Directory scanning depth updated: ${value} ` +
  3511. `(current directory + ${value} sublevels)`,
  3512. 'info'
  3513. );
  3514. }
  3515. });
  3516.  
  3517. pauseBtn.addEventListener('click', () => {
  3518. this.state.isPaused = true;
  3519. this.updateActivityIndicator('paused');
  3520. pauseBtn.style.display = 'none';
  3521. resumeBtn.style.display = 'flex';
  3522. generateBtn.innerHTML = `
  3523. <div class="M3Unator-spinner" style="animation-play-state: paused;"></div>
  3524. <span>Scan paused</span>
  3525. `;
  3526. this.showToast('Scan paused', 'warning');
  3527. this.addLogEntry('Scan paused...', 'warning');
  3528. });
  3529.  
  3530. resumeBtn.addEventListener('click', () => {
  3531. this.state.isPaused = false;
  3532. this.updateActivityIndicator('active');
  3533. resumeBtn.style.display = 'none';
  3534. pauseBtn.style.display = 'flex';
  3535. generateBtn.innerHTML = `
  3536. <div class="M3Unator-spinner"></div>
  3537. <span>Creating...</span>
  3538. `;
  3539. this.showToast('Scan resumed', 'success');
  3540. this.addLogEntry('Scan in progress...', 'success');
  3541. });
  3542.  
  3543. cancelBtn.addEventListener('click', () => {
  3544. this.state.isGenerating = false;
  3545. this.state.isPaused = false;
  3546. this.updateActivityIndicator('cancelled');
  3547. setTimeout(() => {
  3548. this.reset({ isCancelled: true, enableToggles: true });
  3549. this.showToast('Scan cancelled', 'warning');
  3550. }, 100);
  3551. });
  3552.  
  3553. generateBtn.addEventListener('click', async () => {
  3554. const playlistName = this.sanitizeInput(playlistInput.value.trim());
  3555.  
  3556. if (!playlistName) {
  3557. this.showToast('Please enter a valid playlist name', 'warning');
  3558. playlistInput.focus();
  3559. return;
  3560. }
  3561.  
  3562. if (!this.state.includeVideo && !this.state.includeAudio) {
  3563. this.showToast('Please select at least one media type', 'warning');
  3564. return;
  3565. }
  3566.  
  3567. try {
  3568. this.entries = [];
  3569. this.seenUrls.clear();
  3570. this.logCount = 0;
  3571. if (this.domElements.scanLog) {
  3572. this.domElements.scanLog.innerHTML = '';
  3573. }
  3574. this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
  3575.  
  3576. this.state.isGenerating = true;
  3577. this.state.isPaused = false;
  3578. this.updateActivityIndicator('active');
  3579. this.showToast('Scan started', 'success');
  3580. generateBtn.disabled = true;
  3581. generateBtn.innerHTML = `
  3582. <div class="M3Unator-spinner"></div>
  3583. <span>Creating...</span>
  3584. `;
  3585.  
  3586. this.domElements.includeVideo.disabled = true;
  3587. this.domElements.includeAudio.disabled = true;
  3588. this.domElements.recursiveSearch.disabled = true;
  3589. this.domElements.currentDepth.disabled = true;
  3590. this.domElements.customDepth.disabled = true;
  3591. this.domElements.maxDepth.disabled = true;
  3592.  
  3593. controls.style.display = 'flex';
  3594. controls.classList.add('active');
  3595. if (pauseBtn) {
  3596. pauseBtn.style.display = 'flex';
  3597. resumeBtn.style.display = 'none';
  3598. cancelBtn.style.display = 'flex';
  3599. }
  3600.  
  3601. this.domElements.statsBar.style.display = 'block';
  3602. this.domElements.statsBar.classList.add('active');
  3603.  
  3604. const entries = await this.scanDirectory(window.location.href, '', 0);
  3605.  
  3606. if (!this.state.isGenerating) {
  3607. return;
  3608. }
  3609.  
  3610. if (entries.length === 0) {
  3611. this.state.isGenerating = false;
  3612. this.updateActivityIndicator('cancelled');
  3613. this.showToast('No media files found', 'error');
  3614. this.reset({ isCancelled: true });
  3615. return;
  3616. }
  3617.  
  3618. this.addLogEntry(`Total ${entries.length} files found.`, 'success');
  3619. this.updateCounter(entries.length);
  3620.  
  3621. const content = this.createPlaylist(entries);
  3622. const fileName = `${playlistName}.${this.state.selectedFormat}`;
  3623.  
  3624. const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
  3625. const url = URL.createObjectURL(blob);
  3626. const a = document.createElement('a');
  3627. a.href = url;
  3628. a.download = fileName;
  3629. document.body.appendChild(a);
  3630. a.click();
  3631. document.body.removeChild(a);
  3632. URL.revokeObjectURL(url);
  3633.  
  3634. this.showToast(`Playlist "${fileName}" created successfully`, 'success');
  3635. this.updateActivityIndicator('completed');
  3636. this.reset({ keepLogs: true, keepUI: true, enableToggles: true });
  3637.  
  3638. } catch (error) {
  3639. this.state.isGenerating = false;
  3640. this.state.isPaused = false;
  3641. this.updateActivityIndicator('cancelled');
  3642. console.error('Error creating playlist:', error);
  3643. this.addLogEntry(`Error: ${error.message}`, 'error');
  3644. this.showToast('Error creating playlist', 'error');
  3645. this.reset({ isCancelled: true });
  3646. }
  3647. });
  3648. }
  3649.  
  3650. reset(options = {}) {
  3651. const {
  3652. isCancelled = false,
  3653. uiOnly = false,
  3654. keepLogs = false,
  3655. keepUI = false,
  3656. enableToggles = false,
  3657. wasGenerating = this.state.isGenerating
  3658. } = options;
  3659.  
  3660. // Save current state before updating
  3661. const wasPaused = this.state.isPaused;
  3662. this.state.isGenerating = false;
  3663. this.state.isPaused = false;
  3664. // Update activity indicator
  3665. if (isCancelled || wasPaused) {
  3666. this.updateActivityIndicator('cancelled');
  3667. } else if (wasGenerating) {
  3668. this.updateActivityIndicator('completed');
  3669. } else {
  3670. this.updateActivityIndicator(null);
  3671. }
  3672. if (!uiOnly) {
  3673. this.entries = [];
  3674. this.seenUrls.clear();
  3675. if (!keepLogs) {
  3676. this.logCount = 0;
  3677. if (this.domElements.scanLog) {
  3678. this.domElements.scanLog.innerHTML = '';
  3679. }
  3680. if (this.domElements.logCounter) {
  3681. this.domElements.logCounter.textContent = '0';
  3682. }
  3683. }
  3684.  
  3685. if (wasGenerating && !isCancelled) {
  3686. const stats = this.domElements.stats;
  3687. const summary = [
  3688. `Scan completed:`,
  3689. `• Video files: ${stats.videoFiles.textContent}`,
  3690. `• Audio files: ${stats.audioFiles.textContent}`,
  3691. `• Scanned directories: ${stats.directories.textContent}`,
  3692. `• Maximum depth: ${stats.depthLevel.textContent}`,
  3693. stats.errors.textContent > 0 ? `• Errors: ${stats.errors.textContent} (${this.state.stats.errors.skipped} skipped)` : null
  3694. ].filter(Boolean).join('\n');
  3695.  
  3696. this.addLogEntry(summary, 'final');
  3697. }
  3698. }
  3699. const elements = this.domElements;
  3700. if (elements.generateBtn) {
  3701. elements.generateBtn.disabled = false;
  3702. elements.generateBtn.innerHTML = `${this.icons.download}<span>Create Playlist</span>`;
  3703. }
  3704.  
  3705. if (elements.controls) {
  3706. elements.controls.style.display = 'none';
  3707. elements.controls.classList.remove('active');
  3708. const pauseBtn = elements.controls.querySelector('.M3Unator-control-btn.pause');
  3709. const resumeBtn = elements.controls.querySelector('.M3Unator-control-btn.resume');
  3710. const cancelBtn = elements.controls.querySelector('.M3Unator-control-btn.cancel');
  3711. if (pauseBtn) pauseBtn.style.display = 'none';
  3712. if (resumeBtn) resumeBtn.style.display = 'none';
  3713. if (cancelBtn) cancelBtn.style.display = 'none';
  3714. }
  3715.  
  3716. if (enableToggles) {
  3717. if (elements.includeVideo) elements.includeVideo.disabled = false;
  3718. if (elements.includeAudio) elements.includeAudio.disabled = false;
  3719. if (elements.recursiveSearch) elements.recursiveSearch.disabled = false;
  3720. if (elements.currentDepth) elements.currentDepth.disabled = false;
  3721. if (elements.customDepth) elements.customDepth.disabled = false;
  3722. if (elements.maxDepth) elements.maxDepth.disabled = elements.customDepth ? !elements.customDepth.checked : true;
  3723. }
  3724. if (uiOnly) return;
  3725.  
  3726. if (isCancelled) {
  3727. this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
  3728.  
  3729. if (!keepLogs) {
  3730. if (elements.scanLog) {
  3731. elements.scanLog.innerHTML = '';
  3732. elements.scanLog.classList.add('collapsed');
  3733. }
  3734.  
  3735. if (elements.logToggle) {
  3736. elements.logToggle.classList.remove('active');
  3737. }
  3738.  
  3739. if (this.domElements.stats) {
  3740. Object.entries(this.domElements.stats).forEach(([key, element]) => {
  3741. if (element) {
  3742. element.textContent = '0';
  3743. const statContainer = element.closest('.M3Unator-stat');
  3744. if (statContainer) {
  3745. statContainer.style.opacity = '0.5';
  3746. if (key === 'depthLevel') {
  3747. statContainer.dataset.progress = '';
  3748. statContainer.title = 'Depth Level: 0';
  3749. }
  3750. }
  3751. }
  3752. });
  3753. }
  3754. }
  3755. }
  3756.  
  3757. if (elements.recursiveSearch) {
  3758. elements.recursiveSearch.checked = true;
  3759. this.state.recursiveSearch = true;
  3760. this.state.maxDepth = -1;
  3761. }
  3762.  
  3763. if (elements.currentDepth) {
  3764. elements.currentDepth.checked = false;
  3765. }
  3766.  
  3767. if (elements.customDepth) {
  3768. elements.customDepth.checked = false;
  3769. }
  3770.  
  3771. if (elements.maxDepth) {
  3772. elements.maxDepth.disabled = true;
  3773. elements.maxDepth.value = '1';
  3774. }
  3775.  
  3776. if (elements.depthControls) {
  3777. elements.depthControls.classList.remove('active');
  3778. }
  3779. }
  3780.  
  3781. handleError(error, context = '') {
  3782. let userMessage = 'An error occurred';
  3783. let logMessage = error.message;
  3784. let type = 'error';
  3785.  
  3786. switch (true) {
  3787. case error.name === 'AbortError':
  3788. userMessage = 'Server not responding, operation timed out';
  3789. logMessage = `Timeout: ${context}`;
  3790. type = 'warning';
  3791. break;
  3792.  
  3793. case error.message.includes('HTTP error'):
  3794. const status = error.message.match(/\d+/)?.[0];
  3795. switch (status) {
  3796. case '403':
  3797. userMessage = 'Access denied to this directory';
  3798. break;
  3799. case '404':
  3800. userMessage = 'Directory or file not found';
  3801. break;
  3802. case '429':
  3803. userMessage = 'Too many requests, please wait a while';
  3804. break;
  3805. case '500':
  3806. case '502':
  3807. case '503':
  3808. userMessage = 'Server is currently unable to respond, please try again later';
  3809. break;
  3810. default:
  3811. userMessage = 'Error communicating with server';
  3812. }
  3813. logMessage = `${error.message} (${context})`;
  3814. break;
  3815.  
  3816. case error.message.includes('decode'):
  3817. userMessage = 'Filename or path could not be read';
  3818. logMessage = `Decode error: ${context} - ${error.message}`;
  3819. type = 'warning';
  3820. break;
  3821.  
  3822. case error.message.includes('NetworkError'):
  3823. userMessage = 'Network connection error, please check your connection';
  3824. logMessage = `Network error: ${context}`;
  3825. break;
  3826.  
  3827. case error.message.includes('SecurityError'):
  3828. userMessage = 'Operation not allowed due to security restrictions';
  3829. logMessage = `Security error: ${context}`;
  3830. break;
  3831.  
  3832. default:
  3833. userMessage = 'Unexpected error occurred';
  3834. logMessage = `${error.name}: ${error.message} (${context})`;
  3835. }
  3836.  
  3837. console.error(`[${context}]`, error);
  3838.  
  3839. this.showToast(userMessage, type);
  3840.  
  3841. this.addLogEntry(logMessage, type);
  3842.  
  3843. this.state.stats.errors.total++;
  3844. }
  3845.  
  3846. async fetchWithRetry(url, options = {}, retries = 3) {
  3847. let lastError;
  3848. for (let i = 0; i < retries; i++) {
  3849. try {
  3850. const response = await fetch(url, {
  3851. ...options,
  3852. headers: {
  3853. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
  3854. 'Accept-Language': 'en-US,en;q=0.9',
  3855. 'Connection': 'keep-alive',
  3856. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
  3857. },
  3858. timeout: options.timeout || 30000,
  3859. signal: options.signal
  3860. });
  3861.  
  3862. if (!response.ok) {
  3863. throw new Error(`HTTP error! Status: ${response.status}`);
  3864. }
  3865.  
  3866. const text = await response.text();
  3867. return {
  3868. decodedText: text,
  3869. status: response.status,
  3870. ok: response.ok
  3871. };
  3872. } catch (error) {
  3873. lastError = error;
  3874. if (error.name === 'AbortError' || error.message.includes('404')) {
  3875. throw error; // Throw these errors immediately
  3876. }
  3877. if (i < retries - 1) {
  3878. await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
  3879. continue;
  3880. }
  3881. }
  3882. }
  3883. throw lastError;
  3884. }
  3885.  
  3886. sanitizeInput(input) {
  3887. if (!input || typeof input !== 'string') {
  3888. return '';
  3889. }
  3890.  
  3891. const sanitized = input
  3892. .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '')
  3893. .trim()
  3894. .replace(/[\x00-\x1F\x7F]/g, '')
  3895. .replace(/[\u200B-\u200D\uFEFF]/g, '')
  3896. .replace(/[^\w\s\-_.()[\]{}#@!$%^&+=]/g, '');
  3897.  
  3898. if (!sanitized) {
  3899. return 'playlist';
  3900. }
  3901.  
  3902. if (sanitized.length > 255) {
  3903. return sanitized.slice(0, 255);
  3904. }
  3905.  
  3906. return sanitized;
  3907. }
  3908.  
  3909. decodeString(str, type = 'both') {
  3910. if (!str) return str;
  3911. try {
  3912. let decoded = str;
  3913. if (type === 'html' || type === 'both') {
  3914. decoded = decoded.replace(/&amp;/g, '&')
  3915. .replace(/&lt;/g, '<')
  3916. .replace(/&gt;/g, '>')
  3917. .replace(/&quot;/g, '"')
  3918. .replace(/&#039;/g, "'")
  3919. .replace(/&#x27;/g, "'")
  3920. .replace(/&#x2F;/g, "/");
  3921. }
  3922.  
  3923. if (type === 'url' || type === 'both') {
  3924. try {
  3925. decoded = decodeURIComponent(decoded);
  3926. } catch (e) {
  3927. decoded = decoded.replace(/%([0-9A-F]{2})/gi, (match, hex) => {
  3928. try {
  3929. return String.fromCharCode(parseInt(hex, 16));
  3930. } catch {
  3931. return match;
  3932. }
  3933. });
  3934. }
  3935. }
  3936. return decoded;
  3937. } catch (error) {
  3938. console.warn('Decode error:', error);
  3939. return str;
  3940. }
  3941. }
  3942.  
  3943. extractFileInfo(path) {
  3944. try {
  3945. const decodedPath = this.decodeString(path);
  3946. const parts = decodedPath.split('/');
  3947. const fileName = parts.pop() || '';
  3948. const dirPath = parts.join('/');
  3949. return {
  3950. fileName,
  3951. dirPath,
  3952. original: {
  3953. fileName: path.split('/').pop() || '',
  3954. dirPath: path.split('/').slice(0, -1).join('/')
  3955. }
  3956. };
  3957. } catch (error) {
  3958. this.handleError(error, `Path decode error: ${path}`);
  3959. const parts = path.split('/');
  3960. return {
  3961. fileName: parts.pop() || '',
  3962. dirPath: parts.join('/'),
  3963. original: {
  3964. fileName: parts.pop() || '',
  3965. dirPath: parts.join('/')
  3966. }
  3967. };
  3968. }
  3969. }
  3970.  
  3971. normalizeUrl(url) {
  3972. let normalized = url.replace(/([^:]\/)\/+/g, "$1");
  3973. return normalized.endsWith('/') ? normalized : normalized + '/';
  3974. }
  3975.  
  3976. isMediaFile(fileName, type) {
  3977. const lowerFileName = fileName.toLowerCase();
  3978. return type === 'video'
  3979. ? this.videoFormats.some(ext => lowerFileName.endsWith(ext))
  3980. : this.audioFormats.some(ext => lowerFileName.endsWith(ext));
  3981. }
  3982.  
  3983. resetCurrentStats() {
  3984. this.state.stats.files.video.current = 0;
  3985. this.state.stats.files.audio.current = 0;
  3986. }
  3987.  
  3988. updateFileStats(type) {
  3989. this.state.stats.files[type].total++;
  3990. this.state.stats.files[type].current++;
  3991. }
  3992.  
  3993. getCurrentStatsText() {
  3994. const { video, audio } = this.state.stats.files;
  3995. const details = [];
  3996.  
  3997. if (video.current > 0) details.push(`${video.current} video`);
  3998. if (audio.current > 0) details.push(`${audio.current} audio`);
  3999.  
  4000. return details.join(' and ');
  4001. }
  4002.  
  4003. async scanDirectory(url, currentPath = '', depth = 0) {
  4004. try {
  4005. this.resetCurrentStats();
  4006.  
  4007. if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) {
  4008. return this.entries;
  4009. }
  4010.  
  4011. while (this.state.isPaused && this.state.isGenerating) {
  4012. await new Promise(resolve => setTimeout(resolve, 100));
  4013. }
  4014.  
  4015. // Encode special characters properly
  4016. const normalizedUrl = this.normalizeUrl(url).replace(/#/g, '%23')
  4017. .replace(/\s+/g, '%20')
  4018. .replace(/\[/g, '%5B')
  4019. .replace(/\]/g, '%5D')
  4020. .replace(/'/g, '%27')
  4021. .replace(/"/g, '%22');
  4022.  
  4023. if (depth > this.state.stats.directories.depth) {
  4024. this.state.stats.directories.depth = depth;
  4025. }
  4026.  
  4027. this.state.stats.directories.total++;
  4028.  
  4029. this.addLogEntry(`Scanning directory (level ${depth}): ${decodeURIComponent(normalizedUrl)}`);
  4030.  
  4031. if (this.seenUrls.has(normalizedUrl)) {
  4032. this.addLogEntry(`This directory was previously scanned: ${decodeURIComponent(normalizedUrl)}`);
  4033. return this.entries;
  4034. }
  4035.  
  4036. this.seenUrls.add(normalizedUrl);
  4037. if (this.seenUrls.size > this.state.maxSeenUrls) {
  4038. const keepCount = Math.floor(this.state.maxSeenUrls * 0.75);
  4039. const urlsArray = Array.from(this.seenUrls);
  4040. const keepUrls = urlsArray.slice(-keepCount);
  4041. this.seenUrls = new Set(keepUrls);
  4042. this.addLogEntry(
  4043. `Cache cleared (${urlsArray.length} -> ${keepUrls.length})`,
  4044. 'info'
  4045. );
  4046. }
  4047.  
  4048. let response;
  4049. try {
  4050. response = await this.fetchWithRetry(normalizedUrl, {
  4051. signal: null, // Remove cancel signal
  4052. timeout: 30000 // 30 second timeout
  4053. });
  4054. } catch (error) {
  4055. if (error.name === 'AbortError') {
  4056. this.addLogEntry(`Request cancelled: ${decodeURIComponent(normalizedUrl)}`, 'warning');
  4057. } else if (error.message.includes('404')) {
  4058. this.addLogEntry(`Directory not found: ${decodeURIComponent(normalizedUrl)}`, 'warning');
  4059. } else {
  4060. this.addLogEntry(`Connection error: ${error.message}`, 'error');
  4061. }
  4062. return this.entries;
  4063. }
  4064.  
  4065. const html = response.decodedText;
  4066. const parser = new DOMParser();
  4067. const doc = parser.parseFromString(html, 'text/html');
  4068. const isLiteSpeed = doc.querySelector('div#table-list') !== null;
  4069. let hrefs = [];
  4070. if (isLiteSpeed) {
  4071. const rows = doc.querySelectorAll('#table-content tr');
  4072. rows.forEach(row => {
  4073. const linkElement = row.querySelector('a');
  4074. if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
  4075. const href = linkElement.getAttribute('href');
  4076. if (href) hrefs.push(href);
  4077. }
  4078. });
  4079. } else {
  4080. const hrefRegex = /href="([^"]+)"/gi;
  4081. const matches = html.matchAll(hrefRegex);
  4082. hrefs = Array.from(matches, m => m[1]).filter(href =>
  4083. href &&
  4084. !href.startsWith('?') &&
  4085. !href.startsWith('/') &&
  4086. href !== '../' &&
  4087. !href.includes('Parent Directory')
  4088. );
  4089. }
  4090.  
  4091. // Separate directories and files
  4092. const directories = [];
  4093. const files = [];
  4094.  
  4095. for (const href of hrefs) {
  4096. if (href.endsWith('/')) {
  4097. directories.push(href);
  4098. } else {
  4099. files.push(href);
  4100. }
  4101. }
  4102.  
  4103. // Process files in batches
  4104. const batchSize = 100; // Increased from 50 to 100
  4105. for (let i = 0; i < files.length; i += batchSize) {
  4106. if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) break;
  4107.  
  4108. const batch = files.slice(i, i + batchSize);
  4109. const batchProgress = {
  4110. total: batch.length,
  4111. processed: 0,
  4112. success: 0,
  4113. errors: 0
  4114. };
  4115.  
  4116. // Use Set for better performance
  4117. const processedUrls = new Set();
  4118.  
  4119. await Promise.all(batch.map(async href => {
  4120. try {
  4121. // Skip if URL was previously processed
  4122. const fullUrl = new URL(href, normalizedUrl).toString();
  4123. if (processedUrls.has(fullUrl)) return;
  4124. processedUrls.add(fullUrl);
  4125.  
  4126. const decodedHref = this.decodeString(href);
  4127. const { fileName } = this.extractFileInfo(decodedHref);
  4128. const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
  4129.  
  4130. this.state.stats.totalFiles = (this.state.stats.totalFiles || 0) + 1;
  4131.  
  4132. // Check file type using Map
  4133. const mediaType = this.isMediaFileOptimized(fileName);
  4134. if (mediaType && ((mediaType === 'video' && this.state.includeVideo) ||
  4135. (mediaType === 'audio' && this.state.includeAudio))) {
  4136. if (mediaType === 'video') {
  4137. this.updateFileStats('video');
  4138. } else {
  4139. this.updateFileStats('audio');
  4140. }
  4141.  
  4142. this.entries.push({
  4143. title: fullPath,
  4144. url: fullUrl
  4145. });
  4146. batchProgress.success++;
  4147. }
  4148. } catch (error) {
  4149. console.error('URL processing error:', error);
  4150. this.state.stats.errors.total++;
  4151. batchProgress.errors++;
  4152. } finally {
  4153. batchProgress.processed++;
  4154. // Update progress every 20 operations (instead of 10)
  4155. if (batchProgress.processed % 20 === 0 || batchProgress.processed === batchProgress.total) {
  4156. const progress = Math.floor((batchProgress.processed / batchProgress.total) * 100);
  4157. this.addLogEntry(
  4158. `Batch Processing: ${progress}% (${batchProgress.processed}/${batchProgress.total}, ` +
  4159. `Success: ${batchProgress.success}, Error: ${batchProgress.errors})`,
  4160. 'info'
  4161. );
  4162. }
  4163. }
  4164. }));
  4165.  
  4166. // Increase interval for memory cleanup
  4167. if (i > 0 && i % (batchSize * 20) === 0) {
  4168. global.gc && global.gc();
  4169. }
  4170. }
  4171.  
  4172. // Scan directories in parallel
  4173. const shouldScanSubdir =
  4174. this.state.maxDepth === -1 ||
  4175. (this.state.maxDepth > 0 && depth < this.state.maxDepth);
  4176.  
  4177. if (shouldScanSubdir && directories.length > 0) {
  4178. const parallelLimit = 15; // Parallel limit increased to 15
  4179. const queue = [...directories];
  4180. const activeRequests = new Set();
  4181. while (queue.length > 0 || activeRequests.size > 0) {
  4182. // Start new request if there are items in queue and active request limit is not reached
  4183. while (queue.length > 0 && activeRequests.size < parallelLimit) {
  4184. const dir = queue.shift();
  4185. try {
  4186. const decodedDir = this.decodeString(dir);
  4187. const fullUrl = new URL(decodedDir, normalizedUrl).toString();
  4188. const { fileName } = this.extractFileInfo(decodedDir);
  4189. const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
  4190.  
  4191. const promise = this.scanDirectory(fullUrl, fullPath, depth + 1)
  4192. .finally(() => {
  4193. activeRequests.delete(promise);
  4194. });
  4195.  
  4196. activeRequests.add(promise);
  4197. this.addLogEntry(`Entering subdirectory: ${fullPath}`);
  4198. } catch (error) {
  4199. console.error('Directory scanning error:', error);
  4200. this.state.stats.errors.total++;
  4201. }
  4202. }
  4203.  
  4204. // Wait for one of the active requests to complete
  4205. if (activeRequests.size > 0) {
  4206. await Promise.race(activeRequests);
  4207. }
  4208. }
  4209. }
  4210.  
  4211. this.updateCounter(this.state.stats.totalFiles);
  4212. return this.entries;
  4213.  
  4214. } catch (error) {
  4215. this.state.stats.errors.total++;
  4216. this.addLogEntry(`Scan error (${currentPath || url}): ${error.message}`, 'error');
  4217. return this.entries;
  4218. }
  4219. }
  4220.  
  4221. createPlaylist(entries) {
  4222. let content = '#EXTM3U\n';
  4223. const decodedEntries = entries.map(entry => {
  4224. try {
  4225. let title = this.decodeString(entry.title);
  4226. const depth = (title.match(/\//g) || []).length;
  4227. const isVideo = this.videoFormats.some(ext => title.toLowerCase().endsWith(ext));
  4228. const isAudio = this.audioFormats.some(ext => title.toLowerCase().endsWith(ext));
  4229. return {
  4230. ...entry,
  4231. decodedTitle: title,
  4232. depth: depth,
  4233. isVideo: isVideo,
  4234. isAudio: isAudio
  4235. };
  4236. } catch (error) {
  4237. return {
  4238. ...entry,
  4239. decodedTitle: entry.title,
  4240. depth: 0,
  4241. isVideo: false,
  4242. isAudio: false
  4243. };
  4244. }
  4245. });
  4246.  
  4247. const videoEntries = decodedEntries.filter(entry => entry.isVideo);
  4248. const audioEntries = decodedEntries.filter(entry => entry.isAudio);
  4249.  
  4250. const apacheSort = (a, b) => {
  4251. if (a.depth !== b.depth) {
  4252. return a.depth - b.depth;
  4253. }
  4254.  
  4255. const aStartsWithNumber = /^\d/.test(a.decodedTitle);
  4256. const bStartsWithNumber = /^\d/.test(b.decodedTitle);
  4257. if (aStartsWithNumber !== bStartsWithNumber) {
  4258. return aStartsWithNumber ? -1 : 1;
  4259. }
  4260.  
  4261. return a.decodedTitle.localeCompare(b.decodedTitle, undefined, {
  4262. numeric: true,
  4263. sensitivity: 'base'
  4264. });
  4265. };
  4266.  
  4267. const sortedVideoEntries = videoEntries.sort(apacheSort);
  4268. const sortedAudioEntries = audioEntries.sort(apacheSort);
  4269.  
  4270. const sortedEntries = [...sortedVideoEntries, ...sortedAudioEntries];
  4271.  
  4272. sortedEntries.forEach(entry => {
  4273. content += `#EXTINF:-1,${entry.decodedTitle}\n${entry.url}\n`;
  4274. });
  4275.  
  4276. return content;
  4277. }
  4278.  
  4279. addLogEntry(message, type = '') {
  4280. if ((this.state.isPaused || !this.state.isGenerating) && type !== 'final') {
  4281. return;
  4282. }
  4283.  
  4284. let decodedMessage = message;
  4285. try {
  4286. if (message.includes('http')) {
  4287. const urlRegex = /(https?:\/\/[^\s]+)/g;
  4288. decodedMessage = message.replace(urlRegex, (url) => {
  4289. try {
  4290. return decodeURIComponent(url);
  4291. } catch (e) {
  4292. return url;
  4293. }
  4294. });
  4295. }
  4296. } catch (error) {
  4297. console.warn('Decode error:', error);
  4298. }
  4299.  
  4300. // Add only to cache
  4301. this.logCache.add(decodedMessage, type);
  4302.  
  4303. // Update UI every 10 logs
  4304. if (this.logCache.stats.totalLogs % 10 === 0) {
  4305. this.updateLogUI();
  4306. }
  4307. }
  4308.  
  4309. updateLogUI() {
  4310. const scanLog = this.domElements.scanLog;
  4311. if (!scanLog) return;
  4312.  
  4313. // Add throttle for performance
  4314. if (this._updateLogUITimeout) {
  4315. clearTimeout(this._updateLogUITimeout);
  4316. }
  4317.  
  4318. this._updateLogUITimeout = setTimeout(() => {
  4319. requestAnimationFrame(() => {
  4320. const wasAtBottom = Math.abs(scanLog.scrollHeight - scanLog.clientHeight - scanLog.scrollTop) < 50;
  4321. // Use fragment to minimize DOM manipulation
  4322. const fragment = document.createDocumentFragment();
  4323. // Show last 50 logs (instead of 100)
  4324. const recentLogs = this.logCache.logs.slice(-50);
  4325. recentLogs.forEach(log => {
  4326. const div = document.createElement('div');
  4327. div.className = `M3Unator-log-entry ${log.type}`;
  4328. div.innerHTML = `
  4329. <span class="M3Unator-log-time">${log.timestamp}</span>
  4330. <span class="M3Unator-log-content">${log.message}</span>
  4331. `;
  4332. fragment.appendChild(div);
  4333. });
  4334.  
  4335. scanLog.innerHTML = '';
  4336. scanLog.appendChild(fragment);
  4337.  
  4338. if (wasAtBottom) {
  4339. scanLog.scrollTop = scanLog.scrollHeight;
  4340. }
  4341. });
  4342. }, 100); // 100ms throttle
  4343. }
  4344.  
  4345. generateScanReport() {
  4346. const stats = this.state.stats;
  4347. const logCache = this.logCache;
  4348. const summary = [
  4349. `📊 Scan Summary`,
  4350. `───────────────`,
  4351. `📁 Total Files: ${stats.totalFiles}`,
  4352. `🎥 Video Files: ${stats.files.video.total}`,
  4353. `🎵 Audio Files: ${stats.files.audio.total}`,
  4354. `📂 Directories: ${stats.directories.total}`,
  4355. `↕️ Maximum Depth: ${stats.directories.depth}`,
  4356. stats.errors.total > 0 ?
  4357. `⚠️ Errors: ${stats.errors.total} (${stats.errors.skipped} skipped)` :
  4358. null,
  4359. ``,
  4360. `📝 Log Statistics`,
  4361. `───────────────`,
  4362. `Total Logs: ${logCache.stats.totalLogs}`,
  4363. logCache.stats.skippedLogs > 0 ?
  4364. `Skipped Logs: ${logCache.stats.skippedLogs}` :
  4365. null,
  4366. ``,
  4367. `🔍 Last ${logCache.maxSize} Log Entries`,
  4368. `───────────────`,
  4369. ...logCache.logs.map(log =>
  4370. `[${log.timestamp}] ${log.type === 'error' ? '❌' :
  4371. log.type === 'warning' ? '⚠️' :
  4372. log.type === 'success' ? '✅' : 'ℹ️'} ${log.message}`)
  4373. ].filter(Boolean).join('\n');
  4374.  
  4375. return summary;
  4376. }
  4377.  
  4378. updateCounter(count) {
  4379. if (!this.domElements.stats || !this.domElements.statsBar) {
  4380. return;
  4381. }
  4382.  
  4383. const stats = this.state.stats;
  4384. const elements = this.domElements.stats;
  4385. const statsBar = this.domElements.statsBar;
  4386.  
  4387. statsBar.style.display = 'block';
  4388. const updates = {
  4389. 'totalFiles': count,
  4390. 'videoFiles': stats.files.video.total,
  4391. 'audioFiles': stats.files.audio.total,
  4392. 'directories': stats.directories.total,
  4393. 'depthLevel': stats.directories.depth,
  4394. 'errors': stats.errors.total
  4395. };
  4396.  
  4397. Object.entries(updates).forEach(([key, value]) => {
  4398. const element = elements[key];
  4399. if (element) {
  4400. element.textContent = value;
  4401. const statContainer = element.closest('.M3Unator-stat');
  4402. if (statContainer) {
  4403. statContainer.style.opacity = value > 0 ? '1' : '0.5';
  4404. if (key === 'depthLevel') {
  4405. const maxDepth = this.state.maxDepth || 0;
  4406. if (maxDepth > 0) {
  4407. const progress = (value / maxDepth) * 100;
  4408. statContainer.dataset.progress =
  4409. progress >= 100 ? 'high' :
  4410. progress >= 75 ? 'medium' :
  4411. progress >= 50 ? 'low' : '';
  4412. statContainer.title = `Depth Level: ${value}/${maxDepth}`;
  4413. } else {
  4414. statContainer.dataset.progress = '';
  4415. statContainer.title = `Depth Level: ${value}`;
  4416. }
  4417. }
  4418. }
  4419. }
  4420. });
  4421. }
  4422.  
  4423. // Add new method for file type checking
  4424. isMediaFileOptimized(fileName) {
  4425. const extension = fileName.toLowerCase().split('.').pop();
  4426. return this.extensionMap.get(extension);
  4427. }
  4428. }
  4429.  
  4430. const generator = new PlaylistGenerator();
  4431. generator.init();
  4432.  
  4433. // Event listeners for info modal
  4434. document.querySelector('.info-link').addEventListener('click', () => {
  4435. document.querySelector('.info-modal').style.display = 'block';
  4436. document.body.classList.add('modal-open');
  4437. });
  4438.  
  4439. document.querySelector('.info-close').addEventListener('click', () => {
  4440. document.querySelector('.info-modal').style.display = 'none';
  4441. document.body.classList.remove('modal-open');
  4442. });
  4443.  
  4444. window.addEventListener('click', (event) => {
  4445. const modal = document.querySelector('.info-modal');
  4446. if (event.target === modal) {
  4447. modal.style.display = 'none';
  4448. document.body.classList.remove('modal-open');
  4449. }
  4450. });
  4451.  
  4452. // Event listener for playlist name input
  4453. generator.domElements.playlistInput.addEventListener('input', (e) => {
  4454. const dropdown = e.target.parentElement.querySelector('.M3Unator-dropdown');
  4455. if (e.target.value.trim()) {
  4456. dropdown.style.display = 'block';
  4457. } else {
  4458. dropdown.style.display = 'none';
  4459. }
  4460. });
  4461. })();