M3Unator - Web Directory Playlist Creator

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

目前为 2024-12-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name M3Unator - Web Directory Playlist Creator
  3. // @namespace https://github.com/hasanbeder/M3Unator
  4. // @version 1.0.0
  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')) {
  21. console.log('This page is not an Index page, M3Unator disabled.');
  22. return;
  23. }
  24.  
  25. GM_addStyle(`
  26. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
  27.  
  28. [class^="M3Unator"] {
  29. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  30. }
  31.  
  32. .M3Unator-title {
  33. font-weight: 700;
  34. letter-spacing: -0.02em;
  35. }
  36.  
  37. .M3Unator-input-group label {
  38. font-weight: 500;
  39. letter-spacing: -0.01em;
  40. }
  41.  
  42. .M3Unator-input {
  43. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  44. font-size: 0.9375rem;
  45. letter-spacing: -0.01em;
  46. }
  47.  
  48. .M3Unator-button {
  49. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  50. font-weight: 600;
  51. letter-spacing: -0.01em;
  52. }
  53.  
  54. .M3Unator-control-btn {
  55. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  56. font-weight: 500;
  57. letter-spacing: -0.01em;
  58. }
  59.  
  60. .M3Unator-log {
  61. font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
  62. font-size: 0.8125rem;
  63. letter-spacing: -0.01em;
  64. line-height: 1.5;
  65. }
  66.  
  67. .M3Unator-log-counter {
  68. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  69. font-weight: 600;
  70. letter-spacing: -0.01em;
  71. }
  72.  
  73. .M3Unator-container {
  74. position: fixed;
  75. inset: 0;
  76. background: rgba(0, 0, 0, 0.75);
  77. backdrop-filter: blur(8px);
  78. display: none;
  79. place-items: center;
  80. padding: 1rem;
  81. z-index: 9999;
  82. }
  83.  
  84. .M3Unator-popup {
  85. background: #11111b;
  86. color: #cdd6f4;
  87. width: 100%;
  88. max-width: 480px;
  89. border-radius: 12px;
  90. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  91. overflow: hidden;
  92. animation: slideUp 0.3s ease;
  93. position: absolute;
  94. }
  95.  
  96. .M3Unator-header {
  97. padding: 1.25rem 1.618rem;
  98. background: #1e1e2e;
  99. color: #cdd6f4;
  100. display: flex;
  101. align-items: center;
  102. justify-content: space-between;
  103. cursor: move;
  104. user-select: none;
  105. border-bottom: 1px solid #313244;
  106. }
  107.  
  108. .M3Unator-title {
  109. display: flex;
  110. align-items: center;
  111. gap: 0.75rem;
  112. margin: 0;
  113. font-size: 1.25rem;
  114. font-weight: 600;
  115. line-height: 1;
  116. }
  117.  
  118. .M3Unator-title svg {
  119. width: 24px;
  120. height: 24px;
  121. color: #f5c2e7;
  122. filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
  123. flex-shrink: 0;
  124. display: flex;
  125. align-items: center;
  126. justify-content: center;
  127. margin-top: 1px;
  128. }
  129.  
  130. .M3Unator-title span {
  131. display: flex;
  132. align-items: center;
  133. line-height: 24px;
  134. background: linear-gradient(90deg,
  135. #f5c2e7,
  136. #cba6f7,
  137. #89b4fa,
  138. #a6e3a1,
  139. #f5c2e7
  140. );
  141. background-size: 300% auto;
  142. -webkit-background-clip: text;
  143. background-clip: text;
  144. -webkit-text-fill-color: transparent;
  145. animation: gradient 3s linear infinite;
  146. }
  147.  
  148. .M3Unator-close {
  149. background: rgba(203, 166, 247, 0.1);
  150. border: none;
  151. color: #cba6f7;
  152. width: 32px;
  153. height: 32px;
  154. border-radius: 8px;
  155. display: grid;
  156. place-items: center;
  157. cursor: pointer;
  158. transition: all 0.2s ease;
  159. }
  160.  
  161. .M3Unator-close:hover {
  162. background: rgba(203, 166, 247, 0.2);
  163. transform: rotate(360deg);
  164. }
  165.  
  166. .M3Unator-close svg {
  167. width: 18px;
  168. height: 18px;
  169. }
  170.  
  171. .M3Unator-content {
  172. padding: 0.75rem;
  173. display: flex;
  174. flex-direction: column;
  175. gap: 0.75rem;
  176. }
  177.  
  178. .M3Unator-input-group {
  179. margin-bottom: 0;
  180. }
  181.  
  182. .M3Unator-input-group label {
  183. display: block;
  184. margin-bottom: 0.5rem;
  185. font-weight: 500;
  186. color: #bac2de;
  187. }
  188.  
  189. .M3Unator-input {
  190. width: 100%;
  191. padding: 0.618rem;
  192. border: 1px solid #313244;
  193. border-radius: 8px;
  194. background-color: #1e1e2e !important;
  195. color: #cdd6f4 !important;
  196. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  197. font-size: 0.9375rem;
  198. letter-spacing: -0.01em;
  199. transition: all 0.2s ease;
  200. -webkit-text-fill-color: #cdd6f4 !important;
  201. text-decoration: none !important;
  202. -webkit-text-decoration: none !important;
  203. }
  204.  
  205. .M3Unator-input:focus {
  206. outline: none;
  207. border-color: #f5c2e7;
  208. box-shadow: 0 0 0 3px rgba(245, 194, 231, 0.1);
  209. background-color: #1e1e2e !important;
  210. color: #cdd6f4 !important;
  211. -webkit-text-fill-color: #cdd6f4 !important;
  212. text-decoration: none !important;
  213. -webkit-text-decoration: none !important;
  214. }
  215.  
  216. .M3Unator-input::placeholder {
  217. color: #6c7086;
  218. opacity: 1;
  219. }
  220.  
  221. .M3Unator-toggle-container {
  222. position: relative;
  223. display: flex;
  224. align-items: center;
  225. justify-content: center;
  226. }
  227.  
  228. .M3Unator-toggle-container input[type="checkbox"] {
  229. display: none;
  230. }
  231.  
  232. .M3Unator-toggle-container span {
  233. width: 48px;
  234. height: 48px;
  235. background: #1e1e2e;
  236. border: 2px solid #45475a;
  237. border-radius: 12px;
  238. display: inline-flex;
  239. align-items: center;
  240. justify-content: center;
  241. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  242. position: relative;
  243. }
  244.  
  245. .M3Unator-toggle-container svg {
  246. width: 24px;
  247. height: 24px;
  248. opacity: 0.7;
  249. transition: all 0.3s ease;
  250. position: absolute;
  251. top: 50%;
  252. left: 50%;
  253. transform: translate(-50%, -50%);
  254. }
  255.  
  256. .M3Unator-toggle-container input[type="checkbox"]:checked + span {
  257. background: rgba(203, 166, 247, 0.1);
  258. border-color: #cba6f7;
  259. box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
  260. }
  261.  
  262. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  263. opacity: 1;
  264. color: #cba6f7;
  265. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
  266. }
  267.  
  268. .M3Unator-toggle-container span:hover {
  269. background: #313244;
  270. transform: translateY(-2px);
  271. }
  272.  
  273. .M3Unator-toggle-container span:active {
  274. transform: translateY(1px);
  275. }
  276.  
  277. .M3Unator-toggle-container input[type="checkbox"]:checked + span:hover {
  278. background: rgba(203, 166, 247, 0.2);
  279. }
  280.  
  281. .M3Unator-toggle-container span:active {
  282. transform: translateY(1px);
  283. }
  284.  
  285. .M3Unator-toggle-container svg {
  286. width: 24px;
  287. height: 24px;
  288. opacity: 0.8;
  289. transition: all 0.2s ease;
  290. }
  291.  
  292. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  293. opacity: 1;
  294. color: #cba6f7;
  295. }
  296.  
  297. .M3Unator-toggle-group {
  298. display: flex;
  299. gap: 0.75rem;
  300. margin: 0.75rem 0;
  301. justify-content: center;
  302. background: rgba(30, 30, 46, 0.4);
  303. padding: 0.75rem;
  304. border-radius: 12px;
  305. backdrop-filter: blur(8px);
  306. }
  307.  
  308. [title]:hover::after {
  309. content: attr(title);
  310. position: absolute;
  311. bottom: calc(100% + 5px);
  312. left: 50%;
  313. transform: translateX(-50%);
  314. padding: 0.5rem 0.75rem;
  315. background: rgba(30, 30, 46, 0.95);
  316. color: #cdd6f4;
  317. font-size: 0.875rem;
  318. white-space: nowrap;
  319. border-radius: 6px;
  320. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  321. z-index: 1000;
  322. border: 1px solid #313244;
  323. text-align: center;
  324. backdrop-filter: blur(8px);
  325. pointer-events: none;
  326. }
  327.  
  328. .M3Unator-button {
  329. width: 100%;
  330. padding: 0.75rem;
  331. background: #f5c2e7;
  332. color: #11111b;
  333. border: none;
  334. border-radius: 8px;
  335. font-weight: 600;
  336. cursor: pointer;
  337. transition: all 0.2s ease;
  338. display: flex;
  339. align-items: center;
  340. justify-content: center;
  341. gap: 0.5rem;
  342. }
  343.  
  344. .M3Unator-button:hover {
  345. background: #f5c2e7;
  346. transform: translateY(-1px);
  347. box-shadow: 0 4px 12px rgba(245, 194, 231, 0.2);
  348. }
  349.  
  350. .M3Unator-button:active {
  351. transform: translateY(0);
  352. }
  353.  
  354. .M3Unator-button:disabled {
  355. opacity: 0.5;
  356. cursor: not-allowed;
  357. }
  358.  
  359. .M3Unator-launcher {
  360. position: fixed;
  361. top: 1rem;
  362. right: 1.618rem;
  363. height: 48px;
  364. padding: 0 1.25rem;
  365. border-radius: 12px;
  366. background: rgba(30, 30, 46, 0.95);
  367. border: 2px solid #313244;
  368. cursor: pointer;
  369. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  370. display: flex;
  371. align-items: center;
  372. gap: 0.75rem;
  373. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  374. z-index: 9998;
  375. backdrop-filter: blur(12px);
  376. }
  377.  
  378. .M3Unator-launcher:hover {
  379. background: rgba(30, 30, 46, 0.98);
  380. border-color: #45475a;
  381. transform: translateY(-2px);
  382. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
  383. }
  384.  
  385. .M3Unator-launcher svg {
  386. width: 24px;
  387. height: 24px;
  388. color: #f5c2e7;
  389. filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
  390. }
  391.  
  392. .M3Unator-launcher span {
  393. font-weight: 600;
  394. font-size: 0.95rem;
  395. background: linear-gradient(90deg,
  396. #f5c2e7,
  397. #cba6f7,
  398. #89b4fa,
  399. #a6e3a1,
  400. #f5c2e7
  401. );
  402. background-size: 300% auto;
  403. -webkit-background-clip: text;
  404. background-clip: text;
  405. -webkit-text-fill-color: transparent;
  406. animation: gradient 3s linear infinite;
  407. }
  408.  
  409. @keyframes gradient {
  410. 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
  411. 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
  412. 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
  413. }
  414.  
  415. .M3Unator-dropdown {
  416. position: relative;
  417. width: 100%;
  418. }
  419.  
  420. .M3Unator-dropdown-button {
  421. width: 100%;
  422. padding: 0.618rem;
  423. background: #1e1e2e;
  424. border: 1px solid #313244;
  425. border-radius: 8px;
  426. color: #cdd6f4;
  427. font-size: 0.875rem;
  428. text-align: left;
  429. cursor: pointer;
  430. transition: all 0.2s ease;
  431. display: flex;
  432. align-items: center;
  433. justify-content: space-between;
  434. }
  435.  
  436. .M3Unator-dropdown-button:hover {
  437. border-color: #45475a;
  438. background: rgba(30, 30, 46, 0.8);
  439. }
  440.  
  441. .M3Unator-dropdown-button svg {
  442. width: 16px;
  443. height: 16px;
  444. transition: transform 0.2s ease;
  445. }
  446.  
  447. .M3Unator-dropdown.active .M3Unator-dropdown-button {
  448. border-color: #cba6f7;
  449. border-radius: 8px 8px 0 0;
  450. }
  451.  
  452. .M3Unator-dropdown.active .M3Unator-dropdown-button svg {
  453. transform: rotate(180deg);
  454. }
  455.  
  456. .M3Unator-dropdown-menu {
  457. position: absolute;
  458. top: 100%;
  459. left: 0;
  460. right: 0;
  461. background: #1e1e2e;
  462. border: 1px solid #cba6f7;
  463. border-top: none;
  464. border-radius: 0 0 8px 8px;
  465. overflow: hidden;
  466. z-index: 1000;
  467. display: none;
  468. animation: dropdownSlide 0.2s ease;
  469. user-select: none;
  470. }
  471.  
  472. .M3Unator-dropdown.active .M3Unator-dropdown-menu {
  473. display: block;
  474. }
  475.  
  476. .M3Unator-dropdown-item {
  477. padding: 0.618rem;
  478. color: #cdd6f4;
  479. cursor: pointer;
  480. transition: all 0.2s ease;
  481. user-select: none;
  482. }
  483.  
  484. .M3Unator-dropdown-item:hover {
  485. background: rgba(203, 166, 247, 0.1);
  486. }
  487.  
  488. .M3Unator-dropdown-item.selected {
  489. background: rgba(203, 166, 247, 0.1);
  490. color: #cba6f7;
  491. }
  492.  
  493. @keyframes slideUp {
  494. from { transform: translateY(20px); opacity: 0; }
  495. to { transform: translateY(0); opacity: 1; }
  496. }
  497.  
  498. .M3Unator-log {
  499. margin-top: 0.75rem;
  500. max-height: calc(100vh - 70vh);
  501. font-size: 0.8125rem;
  502. line-height: 1.4;
  503. }
  504.  
  505. .M3Unator-log:empty {
  506. display: none;
  507. }
  508.  
  509. .M3Unator-log-entry {
  510. padding: 0.25rem 0.5rem;
  511. border-bottom: 1px solid #313244;
  512. }
  513.  
  514. .M3Unator-log-entry:last-child {
  515. border-bottom: none;
  516. }
  517.  
  518. .M3Unator-log-entry.success {
  519. color: #94e2d5;
  520. }
  521.  
  522. .M3Unator-log-entry.error {
  523. color: #f38ba8;
  524. }
  525.  
  526. .M3Unator-log-entry.warning {
  527. color: #fab387;
  528. }
  529.  
  530. .M3Unator-log-counter {
  531. display: inline-flex;
  532. align-items: center;
  533. justify-content: center;
  534. background: rgba(245, 194, 231, 0.1);
  535. color: #f5c2e7;
  536. padding: 0.25rem 0.75rem;
  537. border-radius: 8px;
  538. font-size: 0.875rem;
  539. font-weight: 500;
  540. margin-left: 0.75rem;
  541. min-width: 3rem;
  542. text-align: center;
  543. }
  544.  
  545. @keyframes gradient {
  546. 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
  547. 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
  548. 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
  549. }
  550.  
  551. .M3Unator-title span.text {
  552. display: inline-block;
  553. position: relative;
  554. padding: 0 0.25rem;
  555. }
  556.  
  557. .M3Unator-title.scanning span.text {
  558. background: linear-gradient(90deg,
  559. #f5c2e7,
  560. #cba6f7,
  561. #89b4fa,
  562. #a6e3a1,
  563. #f5c2e7
  564. );
  565. background-size: 300% auto;
  566. -webkit-background-clip: text;
  567. background-clip: text;
  568. -webkit-text-fill-color: transparent;
  569. animation: gradient 3s linear infinite;
  570. font-weight: 700;
  571. letter-spacing: 0.5px;
  572. }
  573.  
  574. .M3Unator-title.scanning span.text::after {
  575. content: '';
  576. position: absolute;
  577. bottom: -2px;
  578. left: 0;
  579. width: 100%;
  580. height: 2px;
  581. background: inherit;
  582. animation: gradient 3s linear infinite;
  583. }
  584.  
  585. .M3Unator-title.scanning svg {
  586. animation: morphAnimation 2s ease-in-out infinite;
  587. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.5));
  588. }
  589.  
  590. @keyframes morphAnimation {
  591. 0% {
  592. transform: scale(1);
  593. opacity: 1;
  594. }
  595. 50% {
  596. transform: scale(1.2);
  597. opacity: 0.7;
  598. }
  599. 100% {
  600. transform: scale(1);
  601. opacity: 1;
  602. }
  603. }
  604.  
  605. .M3Unator-controls {
  606. display: none;
  607. gap: 0.75rem;
  608. margin: 0.75rem 0;
  609. justify-content: center;
  610. }
  611.  
  612. .M3Unator-controls.active {
  613. display: flex;
  614. }
  615.  
  616. .M3Unator-control-btn {
  617. display: none;
  618. padding: 0.75rem 1.5rem;
  619. border-radius: 12px;
  620. font-weight: 600;
  621. font-size: 0.95rem;
  622. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  623. align-items: center;
  624. gap: 0.75rem;
  625. min-width: 160px;
  626. justify-content: center;
  627. background: rgba(30, 30, 46, 0.6);
  628. backdrop-filter: blur(8px);
  629. width: 160px;
  630. }
  631.  
  632. .M3Unator-control-btn:hover {
  633. background: #313244;
  634. transform: translateY(-1px);
  635. }
  636.  
  637. .M3Unator-control-btn:active {
  638. transform: translateY(1px);
  639. }
  640.  
  641. .M3Unator-control-btn.pause {
  642. border-color: #fab387;
  643. color: #fab387;
  644. }
  645.  
  646. .M3Unator-control-btn.pause:hover {
  647. background: rgba(250, 179, 135, 0.1);
  648. }
  649.  
  650. .M3Unator-control-btn.resume {
  651. border-color: #94e2d5;
  652. color: #94e2d5;
  653. }
  654.  
  655. .M3Unator-control-btn.resume:hover {
  656. background: rgba(148, 226, 213, 0.1);
  657. }
  658.  
  659. .M3Unator-control-btn.cancel {
  660. border-color: #f38ba8;
  661. color: #f38ba8;
  662. }
  663.  
  664. .M3Unator-control-btn.cancel:hover {
  665. background: rgba(243, 139, 168, 0.1);
  666. }
  667.  
  668. .M3Unator-control-btn svg {
  669. width: 14px;
  670. height: 14px;
  671. }
  672.  
  673. .M3Unator-button {
  674. width: 100%;
  675. padding: 0 1rem;
  676. background: #f5c2e7;
  677. color: #11111b;
  678. border: none;
  679. border-radius: 6px;
  680. font-weight: 600;
  681. font-size: 0.875rem;
  682. cursor: pointer;
  683. transition: all 0.2s ease;
  684. display: flex;
  685. align-items: center;
  686. justify-content: center;
  687. gap: 0.375rem;
  688. height: 48px;
  689. min-height: 48px;
  690. line-height: 1;
  691. }
  692.  
  693. .M3Unator-spinner {
  694. width: 20px;
  695. height: 20px;
  696. border: 2px solid rgba(17, 17, 27, 0.3);
  697. border-radius: 50%;
  698. border-top-color: #11111b;
  699. animation: spin 0.6s linear infinite;
  700. margin-right: 0;
  701. flex-shrink: 0;
  702. }
  703.  
  704. .M3Unator-toast-container {
  705. position: fixed;
  706. bottom: 24px;
  707. right: 24px;
  708. z-index: 100002;
  709. }
  710.  
  711. .M3Unator-toast {
  712. display: flex;
  713. align-items: center;
  714. gap: 8px;
  715. padding: 12px 16px;
  716. border-radius: 12px;
  717. margin-top: 8px;
  718. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  719. font-size: 14px;
  720. font-weight: 500;
  721. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  722. animation: toastIn 0.3s ease;
  723. background: #1e1e2e;
  724. border: 2px solid;
  725. }
  726.  
  727. .M3Unator-toast.success {
  728. color: #a6e3a1;
  729. border-color: #a6e3a1;
  730. }
  731.  
  732. .M3Unator-toast.error {
  733. color: #f38ba8;
  734. border-color: #f38ba8;
  735. }
  736.  
  737. .M3Unator-toast.warning {
  738. color: #fab387;
  739. border-color: #fab387;
  740. }
  741.  
  742. .M3Unator-toast.removing {
  743. animation: toastOut 0.3s ease forwards;
  744. }
  745.  
  746. @keyframes toastIn {
  747. from {
  748. transform: translateX(100%);
  749. opacity: 0;
  750. }
  751. to {
  752. transform: translateX(0);
  753. opacity: 1;
  754. }
  755. }
  756.  
  757. @keyframes toastOut {
  758. from {
  759. transform: translateX(0);
  760. opacity: 1;
  761. }
  762. to {
  763. transform: translateX(100%);
  764. opacity: 0;
  765. }
  766. }
  767.  
  768. .M3Unator-toast svg {
  769. width: 20px;
  770. height: 20px;
  771. flex-shrink: 0;
  772. }
  773.  
  774. .M3Unator-input-row {
  775. display: flex;
  776. gap: 0.75rem;
  777. margin-bottom: 0.75rem;
  778. }
  779.  
  780. .M3Unator-input-row .M3Unator-input-group {
  781. margin-bottom: 0;
  782. }
  783.  
  784. .M3Unator-input-row .M3Unator-input-group:first-child {
  785. flex: 2;
  786. }
  787.  
  788. .M3Unator-input-row .M3Unator-input-group:last-child {
  789. flex: 1;
  790. }
  791.  
  792. .M3Unator-social {
  793. display: flex;
  794. gap: 8px;
  795. margin-right: 8px;
  796. }
  797.  
  798. .M3Unator-social a {
  799. width: 32px;
  800. height: 32px;
  801. border-radius: 8px;
  802. display: grid;
  803. place-items: center;
  804. color: #cdd6f4;
  805. background: rgba(205, 214, 244, 0.1);
  806. transition: all 0.2s ease;
  807. }
  808.  
  809. .M3Unator-social a:hover {
  810. background: rgba(205, 214, 244, 0.2);
  811. transform: rotate(360deg);
  812. }
  813.  
  814. .M3Unator-social svg {
  815. width: 18px;
  816. height: 18px;
  817. }
  818.  
  819. .M3Unator-advanced-settings {
  820. margin-top: 1rem;
  821. padding: 1rem;
  822. background: rgba(30, 30, 46, 0.5);
  823. border: 1px solid #313244;
  824. border-radius: 8px;
  825. display: none;
  826. }
  827.  
  828. .M3Unator-advanced-settings.active {
  829. display: block;
  830. animation: fadeIn 0.3s ease;
  831. }
  832.  
  833. .M3Unator-advanced-toggle {
  834. width: 100%;
  835. padding: 0.75rem;
  836. background: #1e1e2e;
  837. border: 1px solid #313244;
  838. border-radius: 8px;
  839. color: #cdd6f4;
  840. font-size: 0.875rem;
  841. font-weight: 500;
  842. cursor: pointer;
  843. display: flex;
  844. align-items: center;
  845. justify-content: space-between;
  846. transition: all 0.2s ease;
  847. }
  848.  
  849. .M3Unator-advanced-toggle:hover {
  850. background: #313244;
  851. }
  852.  
  853. .M3Unator-advanced-toggle svg {
  854. width: 16px;
  855. height: 16px;
  856. transition: transform 0.2s ease;
  857. }
  858.  
  859. .M3Unator-advanced-toggle.active svg {
  860. transform: rotate(180deg);
  861. }
  862.  
  863. .M3Unator-depth-slider {
  864. -webkit-appearance: none;
  865. width: 100%;
  866. height: 4px;
  867. border-radius: 2px;
  868. background: #313244;
  869. outline: none;
  870. margin: 1rem 0;
  871. }
  872.  
  873. .M3Unator-depth-slider::-webkit-slider-thumb {
  874. -webkit-appearance: none;
  875. appearance: none;
  876. width: 16px;
  877. height: 16px;
  878. border-radius: 50%;
  879. background: #cba6f7;
  880. cursor: pointer;
  881. transition: all 0.2s ease;
  882. }
  883.  
  884. .M3Unator-depth-slider::-webkit-slider-thumb:hover {
  885. transform: scale(1.2);
  886. }
  887.  
  888. .M3Unator-depth-value {
  889. text-align: center;
  890. font-size: 0.875rem;
  891. color: #cdd6f4;
  892. margin-top: 0.5rem;
  893. }
  894.  
  895. @keyframes fadeIn {
  896. from { opacity: 0; transform: translateY(-10px); }
  897. to { opacity: 1; transform: translateY(0); }
  898. }
  899.  
  900. .M3Unator-depth-settings {
  901. margin-top: 0.75rem;
  902. margin-left: 1.75rem;
  903. padding: 0.75rem;
  904. background: rgba(30, 30, 46, 0.3);
  905. border-left: 2px solid #cba6f7;
  906. border-radius: 0 8px 8px 0;
  907. display: none;
  908. animation: slideDown 0.3s ease;
  909. }
  910.  
  911. .M3Unator-depth-settings.active {
  912. display: block;
  913. }
  914.  
  915. .M3Unator-depth-input {
  916. position: relative;
  917. display: flex;
  918. align-items: center;
  919. gap: 0.75rem;
  920. margin-top: 0.5rem;
  921. }
  922.  
  923. .M3Unator-depth-input input[type="number"] {
  924. width: 64px;
  925. padding: 0.25rem 0.375rem;
  926. border: 1px solid #45475a;
  927. border-radius: 4px;
  928. background: rgba(30, 30, 46, 0.8);
  929. color: #cdd6f4;
  930. font-size: 0.875rem;
  931. text-align: center;
  932. margin: 0 0 0 0.5rem;
  933. }
  934.  
  935. .M3Unator-depth-input input[type="number"]:focus {
  936. outline: none;
  937. border-color: #cba6f7;
  938. box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
  939. }
  940.  
  941. .M3Unator-depth-input input[type="number"]::-webkit-inner-spin-button {
  942. opacity: 1;
  943. background: #313244;
  944. border-left: 1px solid #45475a;
  945. border-radius: 0 4px 4px 0;
  946. cursor: pointer;
  947. }
  948.  
  949. .M3Unator-depth-toggle {
  950. display: flex;
  951. align-items: center;
  952. gap: 0.5rem;
  953. padding: 0.5rem;
  954. background: #1e1e2e;
  955. border: 1px solid #45475a;
  956. border-radius: 6px;
  957. color: #cdd6f4;
  958. font-size: 0.875rem;
  959. cursor: pointer;
  960. transition: all 0.2s ease;
  961. }
  962.  
  963. .M3Unator-depth-toggle:hover {
  964. background: #313244;
  965. border-color: #cba6f7;
  966. }
  967.  
  968. .M3Unator-depth-toggle.active {
  969. background: rgba(203, 166, 247, 0.1);
  970. border-color: #cba6f7;
  971. color: #cba6f7;
  972. }
  973.  
  974. @keyframes slideDown {
  975. from { opacity: 0; transform: translateY(-10px); }
  976. to { opacity: 1; transform: translateY(0); }
  977. }
  978.  
  979. .M3Unator-stats-bar {
  980. margin: 0.75rem 0;
  981. padding: 0.5rem;
  982. background: rgba(30, 30, 46, 0.5);
  983. border: 1px solid #313244;
  984. border-radius: 8px;
  985. display: none;
  986. }
  987.  
  988. .M3Unator-stats-bar.active {
  989. display: block;
  990. }
  991.  
  992. .M3Unator-stats {
  993. display: flex;
  994. align-items: center;
  995. justify-content: space-around;
  996. gap: 0.382rem;
  997. padding: 0.25rem;
  998. }
  999.  
  1000. .M3Unator-stat {
  1001. display: inline-flex;
  1002. align-items: center;
  1003. gap: 0.25rem;
  1004. font-size: 0.75rem;
  1005. color: #cdd6f4;
  1006. cursor: help;
  1007. min-width: 40px;
  1008. justify-content: flex-start;
  1009. padding: 0 0.25rem;
  1010. position: relative;
  1011. }
  1012.  
  1013. .M3Unator-stat span {
  1014. min-width: 16px;
  1015. text-align: right;
  1016. font-variant-numeric: tabular-nums;
  1017. font-size: 0.7rem;
  1018. font-weight: 500;
  1019. }
  1020.  
  1021. .M3Unator-stat svg {
  1022. opacity: 0.8;
  1023. flex-shrink: 0;
  1024. width: 14px;
  1025. height: 14px;
  1026. }
  1027.  
  1028. .M3Unator-stat.video {
  1029. color: #94e2d5;
  1030. }
  1031.  
  1032. .M3Unator-stat.audio {
  1033. color: #89b4fa;
  1034. }
  1035.  
  1036. .M3Unator-stat.dir {
  1037. color: #cba6f7;
  1038. }
  1039.  
  1040. .M3Unator-stat.error {
  1041. color: #f38ba8;
  1042. }
  1043.  
  1044. .M3Unator-stat.depth {
  1045. color: #a6e3a1;
  1046. transition: color 0.3s ease;
  1047. }
  1048.  
  1049. .M3Unator-stat.depth[data-progress="high"] {
  1050. color: #f38ba8;
  1051. }
  1052.  
  1053. .M3Unator-stat.depth[data-progress="medium"] {
  1054. color: #fab387;
  1055. }
  1056.  
  1057. .M3Unator-stat.depth[data-progress="low"] {
  1058. color: #f9e2af;
  1059. }
  1060.  
  1061. .M3Unator-stat:hover::after {
  1062. content: attr(title);
  1063. position: absolute;
  1064. bottom: calc(100% + 5px);
  1065. left: 50%;
  1066. transform: translateX(-50%);
  1067. padding: 0.5rem 0.75rem;
  1068. background: rgba(30, 30, 46, 0.95);
  1069. color: #cdd6f4;
  1070. font-size: 0.875rem;
  1071. white-space: nowrap;
  1072. border-radius: 6px;
  1073. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  1074. z-index: 1000;
  1075. border: 1px solid #313244;
  1076. text-align: center;
  1077. backdrop-filter: blur(8px);
  1078. pointer-events: none;
  1079. }
  1080.  
  1081. .M3Unator-spinner {
  1082. width: 20px;
  1083. height: 20px;
  1084. border: 2px solid rgba(17, 17, 27, 0.3);
  1085. border-radius: 50%;
  1086. border-top-color: #11111b;
  1087. animation: spin 0.6s linear infinite;
  1088. margin-right: 0;
  1089. flex-shrink: 0;
  1090. }
  1091.  
  1092. @keyframes spin {
  1093. to { transform: rotate(360deg); }
  1094. }
  1095.  
  1096. .M3Unator-toast {
  1097. animation: toastSlideUp 0.2s ease forwards;
  1098. }
  1099.  
  1100. .M3Unator-toast.removing {
  1101. animation: toastSlideDown 0.2s ease forwards;
  1102. }
  1103.  
  1104. .M3Unator-popup {
  1105. animation: slideUp 0.2s ease;
  1106. }
  1107.  
  1108. .M3Unator-stats-bar {
  1109. animation: fadeIn 0.2s ease;
  1110. }
  1111.  
  1112. .M3Unator-log {
  1113. transition: max-height 0.3s ease;
  1114. }
  1115.  
  1116. .M3Unator-log.collapsed {
  1117. max-height: 0;
  1118. overflow: hidden;
  1119. }
  1120.  
  1121. .M3Unator-log-toggle {
  1122. width: 100%;
  1123. padding: 0.75rem;
  1124. background: #1e1e2e;
  1125. border: none;
  1126. color: #cdd6f4;
  1127. display: flex;
  1128. align-items: center;
  1129. justify-content: center;
  1130. gap: 0.75rem;
  1131. cursor: pointer;
  1132. transition: all 0.2s ease;
  1133. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  1134. }
  1135.  
  1136. .M3Unator-log-toggle:hover {
  1137. background: rgba(30, 30, 46, 0.8);
  1138. }
  1139.  
  1140. .M3Unator-log-toggle svg {
  1141. width: 20px;
  1142. height: 20px;
  1143. flex-shrink: 0;
  1144. }
  1145.  
  1146. .M3Unator-log-toggle.active svg {
  1147. transform: rotate(180deg);
  1148. }
  1149.  
  1150. .M3Unator-toggle-container span svg .infinity-icon {
  1151. opacity: 0.5;
  1152. transition: opacity 0.2s ease;
  1153. transform: scale(0.6) translateY(4px);
  1154. transform-origin: center;
  1155. stroke-width: 1.5;
  1156. }
  1157.  
  1158. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg .infinity-icon {
  1159. opacity: 1;
  1160. }
  1161.  
  1162. .M3Unator-depth-controls {
  1163. background: rgba(30, 30, 46, 0.4);
  1164. backdrop-filter: blur(8px);
  1165. border: 1px solid #313244;
  1166. border-radius: 8px;
  1167. padding: 0.618rem;
  1168. margin-top: 1rem;
  1169. display: none;
  1170. }
  1171.  
  1172. .M3Unator-depth-controls.active {
  1173. display: block;
  1174. }
  1175.  
  1176. .M3Unator-radio-group {
  1177. display: flex;
  1178. gap: 0.75rem;
  1179. justify-content: center;
  1180. background: rgba(30, 30, 46, 0.6);
  1181. padding: 0.5rem;
  1182. border-radius: 6px;
  1183. }
  1184.  
  1185. .M3Unator-radio {
  1186. display: flex;
  1187. align-items: center;
  1188. gap: 0.5rem;
  1189. cursor: pointer;
  1190. padding: 0.5rem;
  1191. border-radius: 4px;
  1192. transition: all 0.2s ease;
  1193. background: transparent;
  1194. border: 1px solid transparent;
  1195. }
  1196.  
  1197. .M3Unator-radio:hover {
  1198. background: rgba(203, 166, 247, 0.1);
  1199. }
  1200.  
  1201. .M3Unator-radio input[type="radio"] {
  1202. display: none;
  1203. }
  1204.  
  1205. .M3Unator-radio .radio-mark {
  1206. width: 16px;
  1207. height: 16px;
  1208. border: 1.5px solid #45475a;
  1209. border-radius: 50%;
  1210. display: flex;
  1211. align-items: center;
  1212. justify-content: center;
  1213. transition: all 0.2s ease;
  1214. flex-shrink: 0;
  1215. background: rgba(30, 30, 46, 0.6);
  1216. position: relative;
  1217. }
  1218.  
  1219. .M3Unator-radio input[type="radio"]:checked + .radio-mark {
  1220. border-color: #cba6f7;
  1221. background: rgba(203, 166, 247, 0.1);
  1222. }
  1223.  
  1224. .M3Unator-radio input[type="radio"]:checked + .radio-mark::after {
  1225. content: '';
  1226. width: 8px;
  1227. height: 8px;
  1228. border-radius: 50%;
  1229. background: #cba6f7;
  1230. position: absolute;
  1231. }
  1232.  
  1233. .M3Unator-radio .radio-label {
  1234. color: #cdd6f4;
  1235. font-size: 0.875rem;
  1236. user-select: none;
  1237. display: flex;
  1238. align-items: center;
  1239. gap: 0.5rem;
  1240. }
  1241.  
  1242. .M3Unator-depth-input {
  1243. width: 64px;
  1244. padding: 0.25rem 0.375rem;
  1245. border: 1px solid #45475a;
  1246. border-radius: 4px;
  1247. background: rgba(30, 30, 46, 0.8);
  1248. color: #cdd6f4;
  1249. font-size: 0.875rem;
  1250. text-align: center;
  1251. transition: all 0.2s ease;
  1252. -moz-appearance: textfield;
  1253. margin-top: -1px;
  1254. display: inline-flex;
  1255. align-items: center;
  1256. height: 28px;
  1257. }
  1258.  
  1259. .M3Unator-depth-input::-webkit-outer-spin-button,
  1260. .M3Unator-depth-input::-webkit-inner-spin-button {
  1261. -webkit-appearance: inner-spin-button;
  1262. opacity: 1;
  1263. background: #313244;
  1264. border-left: 1px solid #45475a;
  1265. border-radius: 0 4px 4px 0;
  1266. cursor: pointer;
  1267. height: 100%;
  1268. position: absolute;
  1269. right: 0;
  1270. top: 0;
  1271. }
  1272.  
  1273. .M3Unator-depth-input:focus {
  1274. outline: none;
  1275. border-color: #cba6f7;
  1276. box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
  1277. }
  1278.  
  1279. .M3Unator-depth-input:disabled {
  1280. opacity: 0.5;
  1281. cursor: not-allowed;
  1282. background: rgba(30, 30, 46, 0.4);
  1283. }
  1284.  
  1285. .M3Unator-radio .radio-label {
  1286. display: flex;
  1287. align-items: center;
  1288. gap: 0.5rem;
  1289. color: #cdd6f4;
  1290. font-size: 0.875rem;
  1291. user-select: none;
  1292. }
  1293.  
  1294. .M3Unator-url-container {
  1295. display: flex;
  1296. align-items: center;
  1297. background: rgba(30, 30, 46, 0.6);
  1298. border: 1px solid #313244;
  1299. border-radius: 6px;
  1300. padding: 0.618rem;
  1301. margin-bottom: 1rem;
  1302. transition: all 0.2s ease;
  1303. }
  1304.  
  1305. .M3Unator-url-container:hover {
  1306. border-color: #45475a;
  1307. }
  1308.  
  1309. .M3Unator-url-icon {
  1310. color: #6c7086;
  1311. margin-right: 0.618rem;
  1312. flex-shrink: 0;
  1313. }
  1314.  
  1315. .M3Unator-url-input {
  1316. flex: 1;
  1317. background: transparent;
  1318. border: none;
  1319. color: #cdd6f4;
  1320. font-size: 0.875rem;
  1321. padding: 0;
  1322. margin: 0;
  1323. width: 100%;
  1324. }
  1325.  
  1326. .M3Unator-url-input:focus {
  1327. outline: none;
  1328. }
  1329.  
  1330. .M3Unator-url-copy {
  1331. background: transparent;
  1332. border: none;
  1333. color: #6c7086;
  1334. padding: 0.382rem;
  1335. margin-left: 0.618rem;
  1336. cursor: pointer;
  1337. border-radius: 4px;
  1338. transition: all 0.2s ease;
  1339. display: flex;
  1340. align-items: center;
  1341. justify-content: center;
  1342. }
  1343.  
  1344. .M3Unator-url-copy:hover {
  1345. color: #cdd6f4;
  1346. background: rgba(205, 214, 244, 0.1);
  1347. }
  1348.  
  1349. .M3Unator-url-copy.copied {
  1350. color: #a6e3a1;
  1351. animation: copyPulse 0.3s ease;
  1352. }
  1353.  
  1354. @keyframes copyPulse {
  1355. 0% { transform: scale(1); }
  1356. 50% { transform: scale(1.2); }
  1357. 100% { transform: scale(1); }
  1358. }
  1359.  
  1360. .M3Unator-toggle-group {
  1361. display: flex;
  1362. gap: 1.25rem;
  1363. margin: 1.5rem 0;
  1364. justify-content: center;
  1365. background: rgba(30, 30, 46, 0.4);
  1366. padding: 1.25rem;
  1367. border-radius: 16px;
  1368. backdrop-filter: blur(8px);
  1369. }
  1370.  
  1371. .M3Unator-toggle-container {
  1372. position: relative;
  1373. }
  1374.  
  1375. .M3Unator-toggle-container span {
  1376. width: 64px;
  1377. height: 64px;
  1378. background: #1e1e2e;
  1379. border: 2px solid #45475a;
  1380. border-radius: 16px;
  1381. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1382. display: flex;
  1383. align-items: center;
  1384. justify-content: center;
  1385. }
  1386.  
  1387. .M3Unator-toggle-container input[type="checkbox"]:checked + span {
  1388. background: rgba(203, 166, 247, 0.1);
  1389. border-color: #cba6f7;
  1390. box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
  1391. transform: translateY(-2px);
  1392. }
  1393.  
  1394. .M3Unator-toggle-container span:hover {
  1395. background: #313244;
  1396. transform: translateY(-2px);
  1397. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
  1398. }
  1399.  
  1400. .M3Unator-toggle-container svg {
  1401. width: 32px;
  1402. height: 32px;
  1403. opacity: 0.7;
  1404. transition: all 0.3s ease;
  1405. }
  1406.  
  1407. .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
  1408. opacity: 1;
  1409. color: #cba6f7;
  1410. filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
  1411. }
  1412.  
  1413. .M3Unator-progress {
  1414. background: rgba(30, 30, 46, 0.6);
  1415. border-radius: 12px;
  1416. padding: 1rem;
  1417. margin: 1rem 0;
  1418. backdrop-filter: blur(8px);
  1419. border: 1px solid rgba(203, 166, 247, 0.2);
  1420. }
  1421.  
  1422. .M3Unator-progress-text {
  1423. color: #f5c2e7;
  1424. font-weight: 600;
  1425. text-align: center;
  1426. margin-bottom: 0.5rem;
  1427. font-size: 1.1rem;
  1428. }
  1429.  
  1430. .M3Unator-progress-spinner {
  1431. width: 24px;
  1432. height: 24px;
  1433. border: 3px solid rgba(245, 194, 231, 0.1);
  1434. border-top-color: #f5c2e7;
  1435. border-radius: 50%;
  1436. animation: spin 1s linear infinite;
  1437. margin: 0 auto;
  1438. }
  1439.  
  1440. .M3Unator-controls {
  1441. display: flex;
  1442. gap: 0.75rem;
  1443. margin: 0.75rem 0;
  1444. justify-content: center;
  1445. }
  1446.  
  1447. .M3Unator-control-btn {
  1448. display: none;
  1449. padding: 0.75rem 1.5rem;
  1450. border-radius: 12px;
  1451. font-weight: 600;
  1452. font-size: 0.95rem;
  1453. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  1454. align-items: center;
  1455. gap: 0.75rem;
  1456. min-width: 160px;
  1457. justify-content: center;
  1458. background: rgba(30, 30, 46, 0.6);
  1459. backdrop-filter: blur(8px);
  1460. width: 160px;
  1461. }
  1462.  
  1463. .M3Unator-control-btn.pause {
  1464. background: rgba(250, 179, 135, 0.1);
  1465. border: 2px solid #fab387;
  1466. color: #fab387;
  1467. }
  1468.  
  1469. .M3Unator-control-btn.resume {
  1470. background: rgba(148, 226, 213, 0.1);
  1471. border: 2px solid #94e2d5;
  1472. color: #94e2d5;
  1473. }
  1474.  
  1475. .M3Unator-control-btn.cancel {
  1476. background: rgba(243, 139, 168, 0.1);
  1477. border: 2px solid #f38ba8;
  1478. color: #f38ba8;
  1479. }
  1480.  
  1481. .M3Unator-control-btn:hover {
  1482. transform: translateY(-2px);
  1483. box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
  1484. }
  1485.  
  1486. .M3Unator-control-btn svg {
  1487. width: 20px;
  1488. height: 20px;
  1489. }
  1490.  
  1491. .M3Unator-layers-icon {
  1492. width: 20px;
  1493. height: 20px;
  1494. margin-right: 0.5rem;
  1495. }
  1496.  
  1497. .M3Unator-input:-webkit-autofill,
  1498. .M3Unator-input:-webkit-autofill:hover,
  1499. .M3Unator-input:-webkit-autofill:focus,
  1500. .M3Unator-input:-webkit-autofill:active {
  1501. -webkit-text-fill-color: #cdd6f4 !important;
  1502. -webkit-box-shadow: 0 0 0 30px #1e1e2e inset !important;
  1503. box-shadow: 0 0 0 30px #1e1e2e inset !important;
  1504. background-color: #1e1e2e !important;
  1505. color: #cdd6f4 !important;
  1506. caret-color: #cdd6f4 !important;
  1507. transition: background-color 5000s ease-in-out 0s !important;
  1508. text-decoration: none !important;
  1509. -webkit-text-decoration: none !important;
  1510. }
  1511.  
  1512. .M3Unator-input:-moz-autofill,
  1513. .M3Unator-input:-moz-autofill-preview {
  1514. background-color: #1e1e2e !important;
  1515. color: #cdd6f4 !important;
  1516. text-decoration: none !important;
  1517. }
  1518.  
  1519. .M3Unator-input:-ms-input-placeholder {
  1520. background-color: #1e1e2e !important;
  1521. color: #cdd6f4 !important;
  1522. text-decoration: none !important;
  1523. }
  1524.  
  1525. .M3Unator-log-container {
  1526. margin: 0;
  1527. }
  1528.  
  1529. .M3Unator-log-toggle {
  1530. width: 100%;
  1531. padding: 0.5rem 0.75rem;
  1532. background: rgba(203, 166, 247, 0.05);
  1533. border: none;
  1534. color: #cdd6f4;
  1535. display: flex;
  1536. align-items: center;
  1537. justify-content: space-between;
  1538. cursor: pointer;
  1539. transition: all 0.2s ease;
  1540. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  1541. font-size: 0.875rem;
  1542. font-weight: 500;
  1543. border-radius: 6px;
  1544. }
  1545.  
  1546. .M3Unator-log-toggle:hover {
  1547. background: rgba(203, 166, 247, 0.1);
  1548. }
  1549.  
  1550. .M3Unator-log-toggle svg {
  1551. width: 18px;
  1552. height: 18px;
  1553. color: #cba6f7;
  1554. opacity: 0.8;
  1555. transition: all 0.2s ease;
  1556. margin-right: 0.5rem;
  1557. }
  1558.  
  1559. .M3Unator-log-toggle.active svg {
  1560. transform: rotate(180deg);
  1561. opacity: 1;
  1562. }
  1563.  
  1564. .M3Unator-log-counter {
  1565. background: rgba(203, 166, 247, 0.1);
  1566. color: #cba6f7;
  1567. padding: 0.25rem 0.75rem;
  1568. border-radius: 12px;
  1569. font-size: 0.75rem;
  1570. font-weight: 600;
  1571. min-width: 1.5rem;
  1572. text-align: center;
  1573. display: inline-flex;
  1574. align-items: center;
  1575. justify-content: center;
  1576. margin-left: auto;
  1577. }
  1578.  
  1579. .M3Unator-log-toggle:hover .M3Unator-log-counter {
  1580. background: rgba(203, 166, 247, 0.15);
  1581. }
  1582.  
  1583. .M3Unator-log-toggle .toggle-text {
  1584. display: flex;
  1585. align-items: center;
  1586. gap: 0.5rem;
  1587. }
  1588.  
  1589. .M3Unator-log {
  1590. height: 0;
  1591. max-height: 0;
  1592. overflow: hidden;
  1593. transition: all 0.3s ease;
  1594. background: #11111b;
  1595. padding: 0;
  1596. border-top: none;
  1597. margin: 0;
  1598. }
  1599.  
  1600. .M3Unator-log.expanded {
  1601. height: auto;
  1602. max-height: 300px;
  1603. padding: 0.75rem;
  1604. border-top: 1px solid #313244;
  1605. overflow-y: auto;
  1606. }
  1607.  
  1608. .M3Unator-log-entry {
  1609. padding: 0.25rem 0.5rem;
  1610. border-bottom: 1px solid rgba(49, 50, 68, 0.5);
  1611. font-size: 0.875rem;
  1612. }
  1613.  
  1614. .M3Unator-log-entry:last-child {
  1615. border-bottom: none;
  1616. }
  1617.  
  1618. .M3Unator-log-time {
  1619. color: #6c7086;
  1620. margin-right: 0.5rem;
  1621. }
  1622.  
  1623. .M3Unator-log-entry.success {
  1624. color: #94e2d5;
  1625. }
  1626.  
  1627. .M3Unator-log-entry.error {
  1628. color: #f38ba8;
  1629. }
  1630.  
  1631. .M3Unator-log-entry.warning {
  1632. color: #fab387;
  1633. }
  1634.  
  1635. .M3Unator-log-entry.info {
  1636. color: #89b4fa;
  1637. }
  1638.  
  1639. .M3Unator-log-entry.final {
  1640. color: #a6e3a1;
  1641. font-weight: 500;
  1642. }
  1643.  
  1644. .M3Unator-log {
  1645. margin-top: 0.75rem;
  1646. max-height: calc(100vh - 70vh);
  1647. font-size: 0.8125rem;
  1648. line-height: 1.4;
  1649. }
  1650.  
  1651. .M3Unator-log-entry {
  1652. padding: 0.25rem 0.5rem;
  1653. border-radius: 4px;
  1654. }
  1655.  
  1656. .M3Unator-log-toggle {
  1657. padding: 0.5rem 0.75rem;
  1658. height: 36px;
  1659. }
  1660.  
  1661. .M3Unator-log-counter {
  1662. padding: 0.125rem 0.375rem;
  1663. font-size: 0.75rem;
  1664. border-radius: 4px;
  1665. }
  1666.  
  1667. .M3Unator-log-time {
  1668. font-size: 0.75rem;
  1669. opacity: 0.7;
  1670. margin-right: 0.5rem;
  1671. }
  1672. `);
  1673.  
  1674. GM_addStyle(`
  1675. .M3Unator-popup {
  1676. position: fixed;
  1677. background: #11111b;
  1678. color: #cdd6f4;
  1679. width: 100%;
  1680. max-width: 480px;
  1681. border-radius: 12px;
  1682. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  1683. overflow: hidden;
  1684. animation: slideUp 0.3s ease;
  1685. z-index: 9999;
  1686. }
  1687.  
  1688. .M3Unator-header {
  1689. padding: 1rem 1.25rem;
  1690. background: #1e1e2e;
  1691. color: #cdd6f4;
  1692. display: flex;
  1693. align-items: center;
  1694. justify-content: space-between;
  1695. cursor: move;
  1696. user-select: none;
  1697. border-bottom: 1px solid #313244;
  1698. }
  1699.  
  1700. .M3Unator-container {
  1701. position: fixed;
  1702. inset: 0;
  1703. background: rgba(0, 0, 0, 0.75);
  1704. backdrop-filter: blur(8px);
  1705. display: none;
  1706. place-items: center;
  1707. z-index: 9999;
  1708. }
  1709. `);
  1710.  
  1711. GM_addStyle(`
  1712. /* Info Modal Styles */
  1713. .info-modal {
  1714. display: none;
  1715. position: fixed;
  1716. z-index: 100000;
  1717. left: 0;
  1718. top: 0;
  1719. width: 100%;
  1720. height: 100%;
  1721. background-color: rgba(30, 30, 46, 0.6);
  1722. backdrop-filter: blur(8px);
  1723. }
  1724.  
  1725. .info-modal-content {
  1726. background-color: #1e1e2e;
  1727. color: #cdd6f4;
  1728. margin: 15% auto;
  1729. padding: 24px;
  1730. border: 2px solid #45475a;
  1731. width: 90%;
  1732. max-width: 600px;
  1733. border-radius: 12px;
  1734. position: relative;
  1735. z-index: 100001;
  1736. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  1737. }
  1738.  
  1739. .info-close {
  1740. position: absolute;
  1741. right: 16px;
  1742. top: 16px;
  1743. width: 24px;
  1744. height: 24px;
  1745. display: flex;
  1746. align-items: center;
  1747. justify-content: center;
  1748. color: #6c7086;
  1749. cursor: pointer;
  1750. transition: color 0.2s ease;
  1751. background: none;
  1752. border: none;
  1753. padding: 0;
  1754. }
  1755.  
  1756. .info-close:hover {
  1757. color: #cba6f7;
  1758. }
  1759.  
  1760. .info-link {
  1761. cursor: pointer;
  1762. color: #6c7086;
  1763. transition: color 0.2s ease;
  1764. display: flex;
  1765. align-items: center;
  1766. justify-content: center;
  1767. }
  1768.  
  1769. .info-link:hover {
  1770. color: #cba6f7;
  1771. }
  1772.  
  1773. .info-section {
  1774. margin-bottom: 24px;
  1775. }
  1776.  
  1777. .info-section:last-child {
  1778. margin-bottom: 0;
  1779. }
  1780.  
  1781. .info-section h3 {
  1782. color: #cba6f7;
  1783. margin-bottom: 12px;
  1784. font-size: 1.1em;
  1785. font-weight: 600;
  1786. }
  1787.  
  1788. .info-section p {
  1789. color: #cdd6f4;
  1790. line-height: 1.6;
  1791. margin: 0;
  1792. }
  1793.  
  1794. .info-section strong {
  1795. color: #89b4fa;
  1796. font-weight: 600;
  1797. }
  1798. `);
  1799.  
  1800. class PlaylistGenerator {
  1801. constructor() {
  1802. this.initialStats = {
  1803. directories: {
  1804. total: 0,
  1805. depth: 0
  1806. },
  1807. files: {
  1808. video: {
  1809. total: 0,
  1810. current: 0
  1811. },
  1812. audio: {
  1813. total: 0,
  1814. current: 0
  1815. }
  1816. },
  1817. errors: {
  1818. total: 0,
  1819. skipped: 0
  1820. },
  1821. totalFiles: 0
  1822. };
  1823.  
  1824. this.domElements = {};
  1825.  
  1826. this.state = {
  1827. isGenerating: false,
  1828. isPaused: false,
  1829. selectedFormat: 'm3u',
  1830. includeVideo: false,
  1831. includeAudio: false,
  1832. maxEntries: 100000,
  1833. timeoutMs: 5000,
  1834. retryCount: 2,
  1835. maxDepth: 0,
  1836. maxSeenUrls: 5000,
  1837. stats: { ...this.initialStats }
  1838. };
  1839.  
  1840. this.sortOptions = { numeric: true, sensitivity: 'base' };
  1841.  
  1842. this.entries = [];
  1843. this.seenUrls = new Set();
  1844. this.toastQueue = [];
  1845. this.isProcessingToast = false;
  1846.  
  1847. this.videoFormats = [
  1848. '.mp4', '.mkv', '.avi', '.webm', '.mov', '.flv', '.wmv',
  1849. '.m4v', '.mpg', '.mpeg', '.3gp', '.vob', '.ts', '.mts',
  1850. '.m2ts', '.divx', '.xvid', '.asf', '.ogv', '.rm', '.rmvb',
  1851. '.wtv', '.qt', '.hevc', '.f4v', '.swf', '.vro', '.ogx',
  1852. '.drc', '.gifv', '.mxf', '.roq', '.nsv'
  1853. ];
  1854.  
  1855. this.audioFormats = [
  1856. '.mp3', '.m4a', '.wav', '.flac', '.aac', '.ogg', '.wma',
  1857. '.opus', '.aiff', '.ape', '.mka', '.ac3', '.dts', '.m4b',
  1858. '.m4p', '.m4r', '.mid', '.midi', '.mp2', '.mpa', '.mpc',
  1859. '.ra', '.tta', '.voc', '.vox', '.amr', '.awb', '.dsf',
  1860. '.dff', '.alac', '.wv', '.oga', '.sln', '.aif', '.pcm'
  1861. ];
  1862.  
  1863. this.icons = {
  1864. 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">
  1865. <polygon points="23 7 16 12 23 17 23 7"/>
  1866. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
  1867. </svg>`,
  1868. 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">
  1869. <path d="M9 18V5l12-2v13"/>
  1870. <circle cx="6" cy="18" r="3"/>
  1871. <circle cx="18" cy="16" r="3"/>
  1872. </svg>`,
  1873. 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">
  1874. <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"/>
  1875. </svg>`,
  1876. 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">
  1877. <circle cx="12" cy="12" r="10"/>
  1878. <line x1="12" y1="16" x2="12" y2="12"/>
  1879. <line x1="12" y1="8" x2="12.01" y2="8"/>
  1880. </svg>`,
  1881. 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">
  1882. <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
  1883. <polyline points="13 2 13 9 20 9"/>
  1884. </svg>`,
  1885. 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">
  1886. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1887. <polyline points="7 10 12 15 17 10"/>
  1888. <line x1="12" y1="15" x2="12" y2="3"/>
  1889. </svg>`,
  1890. 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">
  1891. <line x1="18" y1="6" x2="6" y2="18"/>
  1892. <line x1="6" y1="6" x2="18" y2="18"/>
  1893. </svg>`,
  1894. 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">
  1895. <rect x="6" y="4" width="4" height="16"/>
  1896. <rect x="14" y="4" width="4" height="16"/>
  1897. </svg>`,
  1898. 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">
  1899. <polygon points="5 3 19 12 5 21 5 3"/>
  1900. </svg>`,
  1901. 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">
  1902. <circle cx="12" cy="12" r="10"/>
  1903. <line x1="15" y1="9" x2="9" y2="15"/>
  1904. <line x1="9" y1="9" x2="15" y2="15"/>
  1905. </svg>`,
  1906. 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">
  1907. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
  1908. <polyline points="22 4 12 14.01 9 11.01"/>
  1909. </svg>`,
  1910. 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">
  1911. <circle cx="12" cy="12" r="10"/>
  1912. <line x1="12" y1="8" x2="12" y2="12"/>
  1913. <line x1="12" y1="16" x2="12.01" y2="16"/>
  1914. </svg>`,
  1915. 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">
  1916. <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"/>
  1917. <line x1="12" y1="9" x2="12" y2="13"/>
  1918. <line x1="12" y1="17" x2="12.01" y2="17"/>
  1919. </svg>`,
  1920. github: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
  1921. <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"/>
  1922. </svg>`,
  1923. twitter: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
  1924. <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"/>
  1925. </svg>`,
  1926. 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">
  1927. <polyline points="6 9 12 15 18 9"/>
  1928. </svg>`,
  1929. 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">
  1930. <polygon points="12 2 2 7 12 12 22 7 12 2"/>
  1931. <polyline points="2 17 12 22 22 17"/>
  1932. <polyline points="2 12 12 17 22 12"/>
  1933. </svg>`,
  1934. 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">
  1935. <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
  1936. </svg>`
  1937. };
  1938.  
  1939. this.templates = {
  1940. toggleButton: (id, title, icon, checked = false) => `
  1941. <div class="M3Unator-toggle-container">
  1942. <label>
  1943. <input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
  1944. <span title="${title}">${icon}</span>
  1945. </label>
  1946. </div>
  1947. `,
  1948. controlButton: (type, icon, text) => `
  1949. <button class="M3Unator-control-btn ${type}">
  1950. ${icon}
  1951. <span>${text}</span>
  1952. </button>
  1953. `,
  1954. statsItem: (icon, id, title) => `
  1955. <span class="M3Unator-stat ${props.class}" title="${props.title}">
  1956. ${icon}
  1957. <span id="${props.id}">0</span>
  1958. </span>
  1959. `
  1960. };
  1961.  
  1962. this.baseStyles = `
  1963. .M3Unator-btn-base {
  1964. border: none;
  1965. border-radius: 8px;
  1966. font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  1967. font-weight: 600;
  1968. cursor: pointer;
  1969. transition: all 0.2s ease;
  1970. display: flex;
  1971. align-items: center;
  1972. justify-content: center;
  1973. gap: 0.5rem;
  1974. }
  1975.  
  1976. .M3Unator-toggle-base {
  1977. position: relative;
  1978. display: flex;
  1979. align-items: center;
  1980. gap: 0.5rem;
  1981. transition: all 0.2s ease;
  1982. }
  1983.  
  1984. .M3Unator-control-base {
  1985. padding: 0.75rem 1.5rem;
  1986. border-radius: 12px;
  1987. font-weight: 600;
  1988. font-size: 0.95rem;
  1989. min-width: 160px;
  1990. background: rgba(30, 30, 46, 0.6);
  1991. backdrop-filter: blur(8px);
  1992. }
  1993.  
  1994. .M3Unator-stat-base {
  1995. display: inline-flex;
  1996. align-items: center;
  1997. gap: 0.382rem;
  1998. font-size: 0.875rem;
  1999. cursor: help;
  2000. min-width: 52px;
  2001. padding: 0 0.382rem;
  2002. }
  2003.  
  2004. .M3Unator-icon-base {
  2005. display: flex;
  2006. align-items: center;
  2007. justify-content: center;
  2008. width: 24px;
  2009. height: 24px;
  2010. transition: all 0.2s ease;
  2011. }
  2012. `;
  2013.  
  2014. GM_addStyle(this.baseStyles);
  2015. }
  2016.  
  2017. createComponent(type, props) {
  2018. switch (type) {
  2019. case 'toggle':
  2020. return this.templates.toggleButton(
  2021. props.id,
  2022. props.title,
  2023. props.icon,
  2024. props.checked
  2025. );
  2026. case 'control':
  2027. return this.templates.controlButton(
  2028. props.type,
  2029. props.icon,
  2030. props.text
  2031. );
  2032. case 'stats':
  2033. return `
  2034. <span class="M3Unator-stat ${props.class}" title="${props.title}">
  2035. ${props.icon}
  2036. <span id="${props.id}">0</span>
  2037. </span>
  2038. `;
  2039. default:
  2040. return '';
  2041. }
  2042. }
  2043.  
  2044. async init() {
  2045. const container = document.createElement('div');
  2046. container.className = 'M3Unator-container';
  2047.  
  2048. const toggleButtons = [
  2049. {
  2050. id: 'includeVideo',
  2051. title: 'Video (.mp4, .mkv)',
  2052. icon: this.icons.video,
  2053. checked: true
  2054. },
  2055. {
  2056. id: 'includeAudio',
  2057. title: 'Audio (.mp3, .m4a)',
  2058. icon: this.icons.audio,
  2059. checked: true
  2060. },
  2061. {
  2062. id: 'recursiveSearch',
  2063. title: 'Scan Subdirectories',
  2064. icon: this.icons.folder,
  2065. checked: true
  2066. }
  2067. ].map(props => this.createComponent('toggle', props)).join('');
  2068.  
  2069. const controlButtons = [
  2070. {
  2071. type: 'pause',
  2072. icon: this.icons.pause,
  2073. text: 'Pause'
  2074. },
  2075. {
  2076. type: 'resume',
  2077. icon: this.icons.resume,
  2078. text: 'Resume'
  2079. },
  2080. {
  2081. type: 'cancel',
  2082. icon: this.icons.cancel,
  2083. text: 'Cancel'
  2084. }
  2085. ].map(props => this.createComponent('control', props)).join('');
  2086.  
  2087. const statsItems = [
  2088. {
  2089. icon: this.icons.file,
  2090. id: 'totalFiles',
  2091. title: 'Total Files',
  2092. class: ''
  2093. },
  2094. {
  2095. icon: this.icons.video,
  2096. id: 'videoFiles',
  2097. title: 'Video (.mp4, .mkv)',
  2098. class: 'video'
  2099. },
  2100. {
  2101. icon: this.icons.audio,
  2102. id: 'audioFiles',
  2103. title: 'Audio (.mp3, .m4a)',
  2104. class: 'audio'
  2105. },
  2106. {
  2107. icon: this.icons.folder,
  2108. id: 'directories',
  2109. title: 'Subdirectories',
  2110. class: 'dir'
  2111. },
  2112. {
  2113. icon: this.icons.layers,
  2114. id: 'depthLevel',
  2115. title: 'Depth Level',
  2116. class: 'depth'
  2117. },
  2118. {
  2119. icon: this.icons.error,
  2120. id: 'errors',
  2121. title: 'Error',
  2122. class: 'error'
  2123. }
  2124. ].map(props => this.createComponent('stats', props)).join('');
  2125.  
  2126. container.innerHTML = `
  2127. <div class="M3Unator-popup">
  2128. <div class="M3Unator-header">
  2129. <h3 class="M3Unator-title">
  2130. ${this.icons.video}
  2131. <span>M3Unator</span>
  2132. </h3>
  2133. <div style="display: flex; align-items: center;">
  2134. <div class="M3Unator-social">
  2135. <a class="info-link">
  2136. ${this.icons.info}
  2137. </a>
  2138. <a href="https://github.com/hasanbeder/M3Unator" target="_blank" rel="noopener noreferrer" class="github-icon">
  2139. ${this.icons.github}
  2140. </a>
  2141. <a href="https://x.com/hasanbeder" target="_blank" rel="noopener noreferrer">
  2142. ${this.icons.twitter}
  2143. </a>
  2144. </div>
  2145. <button class="M3Unator-close">${this.icons.close}</button>
  2146. </div>
  2147. </div>
  2148. <div class="info-modal">
  2149. <div class="info-modal-content">
  2150. <button class="info-close">${this.icons.close}</button>
  2151. <div class="info-section">
  2152. <h3>About M3Unator</h3>
  2153. <p>M3Unator is your smart playlist creator that transforms any web directory into organized media playlists. Whether you have a collection of movies, TV shows, music, or mixed media, M3Unator helps you create perfect M3U playlists with just a few clicks.</p>
  2154. </div>
  2155. <div class="info-section">
  2156. <h3>Media Support</h3>
  2157. <p>
  2158. <strong>Video:</strong> All major formats including MP4, MKV, AVI, WebM, MOV, FLV, WMV, TS, and more<br>
  2159. <strong>Audio:</strong> Wide range of formats like MP3, WAV, FLAC, M4A, AAC, OGG, and others
  2160. </p>
  2161. </div>
  2162. <div class="info-section">
  2163. <h3>Smart Features</h3>
  2164. <p>
  2165. - Intelligent directory scanning with customizable depth<br>
  2166. - Automatic media type detection and filtering<br>
  2167. - Real-time progress tracking and detailed logs<br>
  2168. - Pause, resume, and cancel operations anytime<br>
  2169. - Smart file sorting and organization<br>
  2170. - Compatible with all major media players
  2171. </p>
  2172. </div>
  2173. </div>
  2174. </div>
  2175. <div class="M3Unator-content">
  2176. <div class="M3Unator-input-row">
  2177. <div class="M3Unator-input-group">
  2178. <input type="text"
  2179. id="playlistName"
  2180. class="M3Unator-input"
  2181. placeholder="Playlist Name"
  2182. required
  2183. spellcheck="false"
  2184. autocomplete="off"
  2185. autocorrect="off"
  2186. autocapitalize="off">
  2187. </div>
  2188.  
  2189. <div class="M3Unator-input-group">
  2190. <div class="M3Unator-dropdown">
  2191. <button type="button" class="M3Unator-dropdown-button">
  2192. <span>M3U</span>
  2193. ${this.icons.chevronDown}
  2194. </button>
  2195. <div class="M3Unator-dropdown-menu">
  2196. <div class="M3Unator-dropdown-item selected" data-value="m3u">M3U</div>
  2197. <div class="M3Unator-dropdown-item" data-value="m3u8">M3U8 (UTF-8)</div>
  2198. </div>
  2199. </div>
  2200. </div>
  2201. </div>
  2202.  
  2203. <div class="M3Unator-toggle-group">
  2204. ${toggleButtons}
  2205. </div>
  2206.  
  2207. <div class="M3Unator-depth-controls">
  2208. <div class="M3Unator-radio-group">
  2209. <label class="M3Unator-radio">
  2210. <input type="radio" name="depthType" value="current" id="currentDepth">
  2211. <span class="radio-mark"></span>
  2212. <span class="radio-label">Current directory</span>
  2213. </label>
  2214. <label class="M3Unator-radio">
  2215. <input type="radio" name="depthType" value="custom" id="customDepth">
  2216. <span class="radio-mark"></span>
  2217. <span class="radio-label">Custom depth:</span>
  2218. <input type="number"
  2219. id="maxDepth"
  2220. value="1"
  2221. min="1"
  2222. max="99"
  2223. class="M3Unator-depth-input"
  2224. title="Subdirectory scan depth"
  2225. style="width: 64px;"
  2226. inputmode="numeric"
  2227. pattern="[0-9]*">
  2228. </label>
  2229. </div>
  2230. </div>
  2231.  
  2232. <button class="M3Unator-button" id="generateBtn">
  2233. ${this.icons.download}
  2234. <span>Create Playlist</span>
  2235. </button>
  2236.  
  2237. <div class="M3Unator-controls">
  2238. ${controlButtons}
  2239. </div>
  2240.  
  2241. <div class="M3Unator-stats-bar">
  2242. <div class="M3Unator-stats">
  2243. ${statsItems}
  2244. </div>
  2245. </div>
  2246.  
  2247. <div class="M3Unator-log-container">
  2248. <button class="M3Unator-log-toggle">
  2249. <div class="toggle-text">
  2250. ${this.icons.logToggle}
  2251. <span>Log Messages</span>
  2252. </div>
  2253. <span class="M3Unator-log-counter">0</span>
  2254. </button>
  2255. <div id="scanLog" class="M3Unator-log collapsed"></div>
  2256. </div>
  2257. </div>
  2258.  
  2259. <style>
  2260. .M3Unator-content {
  2261. padding: 0.75rem;
  2262. display: flex;
  2263. flex-direction: column;
  2264. gap: 0.75rem;
  2265. }
  2266.  
  2267. .M3Unator-input-row {
  2268. display: flex;
  2269. gap: 0.75rem;
  2270. margin-bottom: 0;
  2271. }
  2272.  
  2273. .M3Unator-toggle-group {
  2274. margin: 0;
  2275. display: flex;
  2276. gap: 0.75rem;
  2277. justify-content: center;
  2278. background: rgba(30, 30, 46, 0.4);
  2279. padding: 0.75rem;
  2280. border-radius: 12px;
  2281. backdrop-filter: blur(8px);
  2282. }
  2283.  
  2284. .M3Unator-button {
  2285. margin: 0;
  2286. height: 42px;
  2287. }
  2288.  
  2289. .M3Unator-controls {
  2290. margin: 0;
  2291. display: none;
  2292. gap: 0.75rem;
  2293. justify-content: center;
  2294. }
  2295.  
  2296. .M3Unator-stats-bar {
  2297. margin: 0;
  2298. padding: 0.5rem;
  2299. background: rgba(30, 30, 46, 0.5);
  2300. border: 1px solid #313244;
  2301. border-radius: 8px;
  2302. display: none;
  2303. }
  2304.  
  2305. .M3Unator-log {
  2306. margin-top: 0.75rem;
  2307. max-height: calc(100vh - 70vh);
  2308. font-size: 0.8125rem;
  2309. line-height: 1.4;
  2310. }
  2311.  
  2312. .M3Unator-header {
  2313. padding: 0.75rem 1rem;
  2314. }
  2315.  
  2316. .M3Unator-toggle-container span {
  2317. width: 48px;
  2318. height: 48px;
  2319. }
  2320.  
  2321. .M3Unator-depth-controls {
  2322. padding: 0.75rem;
  2323. margin-top: 0.75rem;
  2324. }
  2325.  
  2326. .M3Unator-radio-group {
  2327. padding: 0.5rem;
  2328. }
  2329.  
  2330. .M3Unator-popup {
  2331. max-height: 85vh;
  2332. display: flex;
  2333. flex-direction: column;
  2334. }
  2335.  
  2336. .M3Unator-header {
  2337. flex-shrink: 0;
  2338. }
  2339.  
  2340. .M3Unator-content {
  2341. flex: 1;
  2342. overflow-y: auto;
  2343. scrollbar-width: thin;
  2344. scrollbar-color: #cba6f7 #1e1e2e;
  2345. }
  2346.  
  2347. .M3Unator-content::-webkit-scrollbar {
  2348. width: 8px;
  2349. }
  2350.  
  2351. .M3Unator-content::-webkit-scrollbar-track {
  2352. background: #1e1e2e;
  2353. border-radius: 4px;
  2354. }
  2355.  
  2356. .M3Unator-content::-webkit-scrollbar-thumb {
  2357. background: #cba6f7;
  2358. border-radius: 4px;
  2359. }
  2360.  
  2361. .M3Unator-content::-webkit-scrollbar-thumb:hover {
  2362. background: #f5c2e7;
  2363. }
  2364.  
  2365. .M3Unator-depth-controls {
  2366. padding: 0.75rem;
  2367. margin: 0;
  2368. background: rgba(30, 30, 46, 0.4);
  2369. border-radius: 12px;
  2370. backdrop-filter: blur(8px);
  2371. }
  2372.  
  2373. .M3Unator-radio-group {
  2374. padding: 0.5rem;
  2375. background: rgba(30, 30, 46, 0.6);
  2376. border-radius: 8px;
  2377. }
  2378. </style>
  2379. </div>
  2380. `;
  2381.  
  2382. document.body.appendChild(container);
  2383. const launcher = document.createElement('button');
  2384. launcher.className = 'M3Unator-launcher';
  2385. launcher.innerHTML = `
  2386. ${this.icons.video}
  2387. <span>M3Unator</span>
  2388. `;
  2389. document.body.appendChild(launcher);
  2390. const popup = container.querySelector('.M3Unator-popup');
  2391. const header = container.querySelector('.M3Unator-header');
  2392. this.makeDraggable(popup, header);
  2393. const statsBar = container.querySelector('.M3Unator-stats-bar');
  2394. if (statsBar) {
  2395. statsBar.style.display = 'block';
  2396. }
  2397.  
  2398. this.domElements = {
  2399. container,
  2400. popup: container.querySelector('.M3Unator-popup'),
  2401. header: container.querySelector('.M3Unator-header'),
  2402. closeBtn: container.querySelector('.M3Unator-close'),
  2403. generateBtn: container.querySelector('#generateBtn'),
  2404. playlistInput: container.querySelector('#playlistName'),
  2405. includeVideo: container.querySelector('#includeVideo'),
  2406. includeAudio: container.querySelector('#includeAudio'),
  2407. recursiveSearch: container.querySelector('#recursiveSearch'),
  2408. controls: container.querySelector('.M3Unator-controls'),
  2409. scanLog: container.querySelector('#scanLog'),
  2410. statsBar: container.querySelector('.M3Unator-stats-bar'),
  2411. dropdown: container.querySelector('.M3Unator-dropdown'),
  2412. launcher,
  2413. stats: {
  2414. totalFiles: container.querySelector('#totalFiles'),
  2415. videoFiles: container.querySelector('#videoFiles'),
  2416. audioFiles: container.querySelector('#audioFiles'),
  2417. directories: container.querySelector('#directories'),
  2418. depthLevel: container.querySelector('#depthLevel'),
  2419. errors: container.querySelector('#errors')
  2420. },
  2421. depthControls: container.querySelector('.M3Unator-depth-controls'),
  2422. currentDepth: container.querySelector('#currentDepth'),
  2423. customDepth: container.querySelector('#customDepth'),
  2424. maxDepth: container.querySelector('#maxDepth'),
  2425. logToggle: container.querySelector('.M3Unator-log-toggle'),
  2426. logCounter: container.querySelector('.M3Unator-log-counter'),
  2427. };
  2428.  
  2429. launcher.onclick = () => {
  2430. this.domElements.container.style.display = 'grid';
  2431. const popup = this.domElements.popup;
  2432. const rect = popup.getBoundingClientRect();
  2433. const centerX = (window.innerWidth - rect.width) / 2;
  2434. const centerY = (window.innerHeight - rect.height) / 2;
  2435. popup.style.left = `${centerX}px`;
  2436. popup.style.top = `${centerY}px`;
  2437. };
  2438.  
  2439. this.domElements.closeBtn.onclick = () => {
  2440. this.state.isGenerating = false;
  2441. this.state.isPaused = false;
  2442. this.reset({ isCancelled: true });
  2443. this.domElements.container.style.display = 'none';
  2444. this.showToast('Operation cancelled', 'warning');
  2445. };
  2446.  
  2447. this.setupPopupHandlers();
  2448.  
  2449. this.updateCounter(0);
  2450.  
  2451. this.domElements.logToggle.addEventListener('click', () => {
  2452. const log = this.domElements.scanLog;
  2453. const toggle = this.domElements.logToggle;
  2454. if (log.classList.contains('expanded')) {
  2455. log.classList.remove('expanded');
  2456. toggle.classList.remove('active');
  2457. } else {
  2458. log.classList.add('expanded');
  2459. toggle.classList.add('active');
  2460. log.scrollTop = log.scrollHeight;
  2461. }
  2462. });
  2463.  
  2464. this.domElements.scanLog.classList.remove('expanded');
  2465. this.domElements.logToggle.classList.remove('active');
  2466.  
  2467. this.logCount = 0;
  2468. }
  2469.  
  2470. updateStyles() {
  2471. GM_addStyle(`
  2472. .M3Unator-toggle-container {
  2473. @extend .M3Unator-toggle-base;
  2474. }
  2475.  
  2476. .M3Unator-control-btn {
  2477. @extend .M3Unator-control-base;
  2478. }
  2479.  
  2480. .M3Unator-stat {
  2481. @extend .M3Unator-stat-base;
  2482. }
  2483.  
  2484. .M3Unator-toggle-container span {
  2485. @extend .M3Unator-icon-base;
  2486. background: #1e1e2e;
  2487. border: 2px solid #45475a;
  2488. border-radius: 16px;
  2489. }
  2490.  
  2491. .M3Unator-control-btn.pause {
  2492. border-color: #fab387;
  2493. color: #fab387;
  2494. }
  2495.  
  2496. .M3Unator-control-btn.resume {
  2497. border-color: #94e2d5;
  2498. color: #94e2d5;
  2499. }
  2500.  
  2501. .M3Unator-control-btn.cancel {
  2502. border-color: #f38ba8;
  2503. color: #f38ba8;
  2504. }
  2505.  
  2506. `);
  2507. }
  2508.  
  2509. makeDraggable(element, handle) {
  2510. let isDragging = false;
  2511. let currentX;
  2512. let currentY;
  2513. let initialX;
  2514. let initialY;
  2515. let xOffset = 0;
  2516. let yOffset = 0;
  2517.  
  2518. const centerWindow = () => {
  2519. const rect = element.getBoundingClientRect();
  2520. const centerX = (window.innerWidth - rect.width) / 2;
  2521. const centerY = (window.innerHeight - rect.height) / 2;
  2522. element.style.left = `${centerX}px`;
  2523. element.style.top = `${centerY}px`;
  2524. xOffset = centerX;
  2525. yOffset = centerY;
  2526. element.style.transform = 'none';
  2527. };
  2528.  
  2529. centerWindow();
  2530.  
  2531. const getPosition = (e) => {
  2532. return {
  2533. x: e.type.includes('touch') ? e.touches[0].clientX : e.clientX,
  2534. y: e.type.includes('touch') ? e.touches[0].clientY : e.clientY
  2535. };
  2536. };
  2537.  
  2538. const dragStart = (e) => {
  2539. if (e.target === handle || handle.contains(e.target)) {
  2540. e.preventDefault();
  2541. const pos = getPosition(e);
  2542. isDragging = true;
  2543. const rect = element.getBoundingClientRect();
  2544. xOffset = rect.left;
  2545. yOffset = rect.top;
  2546. initialX = pos.x - xOffset;
  2547. initialY = pos.y - yOffset;
  2548.  
  2549. handle.style.cursor = 'grabbing';
  2550. }
  2551. };
  2552.  
  2553. const drag = (e) => {
  2554. if (isDragging) {
  2555. e.preventDefault();
  2556. const pos = getPosition(e);
  2557.  
  2558. currentX = pos.x - initialX;
  2559. currentY = pos.y - initialY;
  2560.  
  2561. const rect = element.getBoundingClientRect();
  2562. const maxX = window.innerWidth - rect.width;
  2563. const maxY = window.innerHeight - rect.height;
  2564.  
  2565. currentX = Math.min(Math.max(0, currentX), maxX);
  2566. currentY = Math.min(Math.max(0, currentY), maxY);
  2567.  
  2568. element.style.left = `${currentX}px`;
  2569. element.style.top = `${currentY}px`;
  2570. xOffset = currentX;
  2571. yOffset = currentY;
  2572. }
  2573. };
  2574.  
  2575. const dragEnd = () => {
  2576. if (isDragging) {
  2577. isDragging = false;
  2578. handle.style.cursor = 'grab';
  2579. }
  2580. };
  2581.  
  2582. handle.addEventListener('mousedown', dragStart);
  2583. document.addEventListener('mousemove', drag);
  2584. document.addEventListener('mouseup', dragEnd);
  2585.  
  2586. handle.addEventListener('touchstart', dragStart, { passive: false });
  2587. document.addEventListener('touchmove', drag, { passive: false });
  2588. document.addEventListener('touchend', dragEnd);
  2589.  
  2590. window.addEventListener('resize', () => {
  2591. if (!isDragging) {
  2592. centerWindow();
  2593. }
  2594. });
  2595.  
  2596. handle.style.cursor = 'grab';
  2597. handle.style.userSelect = 'none';
  2598. handle.style.touchAction = 'none';
  2599.  
  2600. element.style.position = 'fixed';
  2601. element.style.margin = '0';
  2602. element.style.touchAction = 'none';
  2603. element.style.transition = 'none';
  2604. }
  2605.  
  2606. showToast(message, type = 'success', duration = 3000) {
  2607. let toastContainer = document.querySelector('.M3Unator-toast-container');
  2608. if (!toastContainer) {
  2609. toastContainer = document.createElement('div');
  2610. toastContainer.className = 'M3Unator-toast-container';
  2611. document.body.appendChild(toastContainer);
  2612. }
  2613.  
  2614. while (toastContainer.firstChild) {
  2615. toastContainer.removeChild(toastContainer.firstChild);
  2616. }
  2617.  
  2618. const toast = document.createElement('div');
  2619. toast.className = `M3Unator-toast ${type}`;
  2620. toast.innerHTML = `${this.icons[type]}<span>${message}</span>`;
  2621.  
  2622. toastContainer.appendChild(toast);
  2623.  
  2624. setTimeout(() => {
  2625. toast.classList.add('removing');
  2626. setTimeout(() => {
  2627. if (toast.parentNode === toastContainer) {
  2628. toastContainer.removeChild(toast);
  2629. }
  2630. if (toastContainer.children.length === 0) {
  2631. document.body.removeChild(toastContainer);
  2632. }
  2633. }, 300);
  2634. }, duration);
  2635. }
  2636.  
  2637. setupPopupHandlers() {
  2638. const generateBtn = this.domElements.generateBtn;
  2639. const playlistInput = this.domElements.playlistInput;
  2640. const includeVideo = this.domElements.includeVideo;
  2641. const includeAudio = this.domElements.includeAudio;
  2642. const recursiveSearch = this.domElements.recursiveSearch;
  2643. const controls = this.domElements.controls;
  2644.  
  2645. const dropdown = this.domElements.dropdown;
  2646. const dropdownButton = dropdown.querySelector('.M3Unator-dropdown-button');
  2647. const dropdownItems = dropdown.querySelectorAll('.M3Unator-dropdown-item');
  2648.  
  2649. const controlButtons = controls.querySelectorAll('.M3Unator-control-btn');
  2650. const pauseBtn = controlButtons[0];
  2651. const resumeBtn = controlButtons[1];
  2652. const cancelBtn = controlButtons[2];
  2653.  
  2654. dropdownButton.addEventListener('click', () => {
  2655. dropdown.classList.toggle('active');
  2656. });
  2657.  
  2658. document.addEventListener('click', (e) => {
  2659. if (!dropdown.contains(e.target)) {
  2660. dropdown.classList.remove('active');
  2661. }
  2662. });
  2663.  
  2664. dropdownItems.forEach(item => {
  2665. item.addEventListener('click', () => {
  2666. dropdownItems.forEach(i => i.classList.remove('selected'));
  2667. item.classList.add('selected');
  2668. dropdownButton.querySelector('span').textContent = item.textContent;
  2669. this.state.selectedFormat = item.dataset.value;
  2670. dropdown.classList.remove('active');
  2671. });
  2672. });
  2673.  
  2674. recursiveSearch.checked = true;
  2675. this.state.recursiveSearch = true;
  2676. includeVideo.checked = true;
  2677. includeAudio.checked = true;
  2678. this.state.includeVideo = true;
  2679. this.state.includeAudio = true;
  2680.  
  2681. includeVideo.addEventListener('change', (e) => {
  2682. this.state.includeVideo = e.target.checked;
  2683. this.addLogEntry(
  2684. e.target.checked ?
  2685. 'Video files will be included' :
  2686. 'Video files will not be included',
  2687. 'info'
  2688. );
  2689. });
  2690.  
  2691. includeAudio.addEventListener('change', (e) => {
  2692. this.state.includeAudio = e.target.checked;
  2693. this.addLogEntry(
  2694. e.target.checked ?
  2695. 'Audio files will be included' :
  2696. 'Audio files will not be included',
  2697. 'info'
  2698. );
  2699. });
  2700.  
  2701. const currentDepth = this.domElements.currentDepth;
  2702. const customDepth = this.domElements.customDepth;
  2703. const maxDepth = this.domElements.maxDepth;
  2704. const depthControls = this.domElements.depthControls;
  2705.  
  2706. depthControls.style.display = 'none';
  2707. depthControls.classList.remove('active');
  2708. this.state.maxDepth = -1;
  2709.  
  2710. currentDepth.checked = true;
  2711. customDepth.checked = false;
  2712. maxDepth.disabled = true;
  2713. maxDepth.value = '1';
  2714.  
  2715. recursiveSearch.addEventListener('change', (e) => {
  2716. if (!e.target.checked) {
  2717. depthControls.style.display = 'block';
  2718. depthControls.classList.add('active');
  2719. currentDepth.checked = true;
  2720. customDepth.checked = false;
  2721. maxDepth.disabled = true;
  2722. this.state.maxDepth = 0;
  2723. this.addLogEntry('Directory scanning disabled, only current directory will be scanned', 'info');
  2724. } else {
  2725. depthControls.style.display = 'none';
  2726. depthControls.classList.remove('active');
  2727. this.state.maxDepth = -1;
  2728. this.state.recursiveSearch = true;
  2729. this.addLogEntry('Directory scanning active, all directories will be scanned', 'info');
  2730. }
  2731. });
  2732.  
  2733. this.domElements.currentDepth.addEventListener('change', (e) => {
  2734. if (e.target.checked && !recursiveSearch.checked) {
  2735. this.state.maxDepth = 0;
  2736. this.domElements.maxDepth.disabled = true;
  2737. this.addLogEntry('Only current directory will be scanned', 'info');
  2738. }
  2739. });
  2740.  
  2741. this.domElements.customDepth.addEventListener('change', (e) => {
  2742. if (e.target.checked && !recursiveSearch.checked) {
  2743. const depthValue = parseInt(this.domElements.maxDepth.value) || 1;
  2744. this.state.maxDepth = depthValue;
  2745. this.domElements.maxDepth.disabled = false;
  2746. this.addLogEntry(
  2747. `Directory scanning depth: ${depthValue} ` +
  2748. `(current directory + ${depthValue} sublevels)`,
  2749. 'info'
  2750. );
  2751. }
  2752. });
  2753.  
  2754. this.domElements.maxDepth.addEventListener('input', (e) => {
  2755. if (this.domElements.customDepth.checked && !recursiveSearch.checked) {
  2756. const value = Math.min(99, Math.max(1, parseInt(e.target.value) || 1));
  2757. e.target.value = value;
  2758. this.state.maxDepth = value;
  2759. this.addLogEntry(
  2760. `Directory scanning depth updated: ${value} ` +
  2761. `(current directory + ${value} sublevels)`,
  2762. 'info'
  2763. );
  2764. }
  2765. });
  2766.  
  2767. pauseBtn.addEventListener('click', () => {
  2768. this.state.isPaused = true;
  2769. pauseBtn.style.display = 'none';
  2770. resumeBtn.style.display = 'flex';
  2771. generateBtn.innerHTML = `
  2772. <div class="M3Unator-spinner" style="animation-play-state: paused;"></div>
  2773. <span>Scan paused</span>
  2774. `;
  2775. this.showToast('Scan paused', 'warning');
  2776. this.addLogEntry('Scan paused...', 'warning');
  2777. });
  2778.  
  2779. resumeBtn.addEventListener('click', () => {
  2780. this.state.isPaused = false;
  2781. resumeBtn.style.display = 'none';
  2782. pauseBtn.style.display = 'flex';
  2783. generateBtn.innerHTML = `
  2784. <div class="M3Unator-spinner"></div>
  2785. <span>Creating...</span>
  2786. `;
  2787. this.showToast('Scan in progress', 'success');
  2788. this.addLogEntry('Scan in progress...', 'success');
  2789. });
  2790.  
  2791. cancelBtn.addEventListener('click', () => {
  2792. this.state.isGenerating = false;
  2793. this.state.isPaused = false;
  2794. setTimeout(() => {
  2795. this.reset({ isCancelled: true, enableToggles: true });
  2796. this.showToast('Scan cancelled', 'warning');
  2797. }, 100);
  2798. });
  2799.  
  2800. generateBtn.addEventListener('click', async () => {
  2801. const playlistName = this.sanitizeInput(playlistInput.value.trim());
  2802.  
  2803. if (!playlistName) {
  2804. this.showToast('Please enter a valid playlist name', 'warning');
  2805. return;
  2806. }
  2807.  
  2808. if (!this.state.includeVideo && !this.state.includeAudio) {
  2809. this.showToast('Please select at least one media type', 'warning');
  2810. return;
  2811. }
  2812.  
  2813. try {
  2814. this.entries = [];
  2815. this.seenUrls.clear();
  2816. this.logCount = 0;
  2817. if (this.domElements.scanLog) {
  2818. this.domElements.scanLog.innerHTML = '';
  2819. }
  2820. if (this.domElements.logCounter) {
  2821. this.domElements.logCounter.textContent = '0';
  2822. }
  2823. this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
  2824.  
  2825. this.state.isGenerating = true;
  2826. generateBtn.disabled = true;
  2827. generateBtn.innerHTML = `
  2828. <div class="M3Unator-spinner"></div>
  2829. <span>Creating...</span>
  2830. `;
  2831.  
  2832. this.domElements.includeVideo.disabled = true;
  2833. this.domElements.includeAudio.disabled = true;
  2834. this.domElements.recursiveSearch.disabled = true;
  2835. this.domElements.currentDepth.disabled = true;
  2836. this.domElements.customDepth.disabled = true;
  2837. this.domElements.maxDepth.disabled = true;
  2838.  
  2839. controls.style.display = 'flex';
  2840. controls.classList.add('active');
  2841. if (pauseBtn) {
  2842. pauseBtn.style.display = 'flex';
  2843. resumeBtn.style.display = 'none';
  2844. cancelBtn.style.display = 'flex';
  2845. }
  2846.  
  2847. this.domElements.statsBar.style.display = 'block';
  2848. this.domElements.statsBar.classList.add('active');
  2849.  
  2850. const entries = await this.scanDirectory(window.location.href, '', 0);
  2851.  
  2852. if (!this.state.isGenerating) {
  2853. return;
  2854. }
  2855.  
  2856. if (entries.length === 0) {
  2857. this.showToast('No media files found', 'error');
  2858. this.reset({ isCancelled: true });
  2859. return;
  2860. }
  2861.  
  2862. this.addLogEntry(`Total ${entries.length} files found.`, 'success');
  2863. this.updateCounter(entries.length);
  2864.  
  2865. const content = this.createPlaylist(entries);
  2866. const fileName = `${playlistName}.${this.state.selectedFormat}`;
  2867.  
  2868. const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
  2869. const url = URL.createObjectURL(blob);
  2870. const a = document.createElement('a');
  2871. a.href = url;
  2872. a.download = fileName;
  2873. document.body.appendChild(a);
  2874. a.click();
  2875. document.body.removeChild(a);
  2876. URL.revokeObjectURL(url);
  2877.  
  2878. this.reset({ keepLogs: true, keepUI: true, enableToggles: true });
  2879.  
  2880. } catch (error) {
  2881. console.error('Error creating playlist:', error);
  2882. this.addLogEntry(`Error: ${error.message}`, 'error');
  2883. this.showToast('Error creating playlist', 'error');
  2884. this.reset({ isCancelled: true });
  2885. }
  2886. });
  2887. }
  2888.  
  2889. reset(options = {}) {
  2890. const {
  2891. isCancelled = false,
  2892. uiOnly = false,
  2893. keepLogs = false,
  2894. keepUI = false,
  2895. enableToggles = false,
  2896. wasGenerating = this.state.isGenerating
  2897. } = options;
  2898.  
  2899. this.state.isGenerating = false;
  2900. this.state.isPaused = false;
  2901. if (!uiOnly) {
  2902. this.entries = [];
  2903. this.seenUrls.clear();
  2904. if (!keepLogs) {
  2905. this.logCount = 0;
  2906. if (this.domElements.scanLog) {
  2907. this.domElements.scanLog.innerHTML = '';
  2908. }
  2909. if (this.domElements.logCounter) {
  2910. this.domElements.logCounter.textContent = '0';
  2911. }
  2912. }
  2913.  
  2914. if (wasGenerating && !isCancelled) {
  2915. const stats = this.domElements.stats;
  2916. const summary = [
  2917. `Scan completed:`,
  2918. `• Video files: ${stats.videoFiles.textContent}`,
  2919. `• Audio files: ${stats.audioFiles.textContent}`,
  2920. `• Scanned directories: ${stats.directories.textContent}`,
  2921. `• Maximum depth: ${stats.depthLevel.textContent}`,
  2922. stats.errors.textContent > 0 ? `• Errors: ${stats.errors.textContent} (${this.state.stats.errors.skipped} skipped)` : null
  2923. ].filter(Boolean).join('\n');
  2924.  
  2925. this.addLogEntry(summary, 'final');
  2926. }
  2927. }
  2928. const elements = this.domElements;
  2929. if (elements.generateBtn) {
  2930. elements.generateBtn.disabled = false;
  2931. elements.generateBtn.innerHTML = `${this.icons.download}<span>Create Playlist</span>`;
  2932. }
  2933.  
  2934. if (elements.controls) {
  2935. elements.controls.style.display = 'none';
  2936. elements.controls.classList.remove('active');
  2937. const pauseBtn = elements.controls.querySelector('.M3Unator-control-btn.pause');
  2938. const resumeBtn = elements.controls.querySelector('.M3Unator-control-btn.resume');
  2939. const cancelBtn = elements.controls.querySelector('.M3Unator-control-btn.cancel');
  2940. if (pauseBtn) pauseBtn.style.display = 'none';
  2941. if (resumeBtn) resumeBtn.style.display = 'none';
  2942. if (cancelBtn) cancelBtn.style.display = 'none';
  2943. }
  2944.  
  2945. if (enableToggles) {
  2946. if (elements.includeVideo) elements.includeVideo.disabled = false;
  2947. if (elements.includeAudio) elements.includeAudio.disabled = false;
  2948. if (elements.recursiveSearch) elements.recursiveSearch.disabled = false;
  2949. if (elements.currentDepth) elements.currentDepth.disabled = false;
  2950. if (elements.customDepth) elements.customDepth.disabled = false;
  2951. if (elements.maxDepth) elements.maxDepth.disabled = elements.customDepth ? !elements.customDepth.checked : true;
  2952. }
  2953. if (uiOnly) return;
  2954.  
  2955. if (isCancelled) {
  2956. this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
  2957.  
  2958. if (!keepLogs) {
  2959. if (elements.scanLog) {
  2960. elements.scanLog.innerHTML = '';
  2961. elements.scanLog.classList.add('collapsed');
  2962. }
  2963.  
  2964. if (elements.logCounter) {
  2965. elements.logCounter.textContent = '0';
  2966. }
  2967.  
  2968. if (elements.logToggle) {
  2969. elements.logToggle.classList.remove('active');
  2970. }
  2971.  
  2972. if (this.domElements.stats) {
  2973. Object.entries(this.domElements.stats).forEach(([key, element]) => {
  2974. if (element) {
  2975. element.textContent = '0';
  2976. const statContainer = element.closest('.M3Unator-stat');
  2977. if (statContainer) {
  2978. statContainer.style.opacity = '0.5';
  2979. if (key === 'depthLevel') {
  2980. statContainer.dataset.progress = '';
  2981. statContainer.title = 'Depth Level: 0';
  2982. }
  2983. }
  2984. }
  2985. });
  2986. }
  2987. }
  2988. }
  2989.  
  2990. if (elements.recursiveSearch) {
  2991. elements.recursiveSearch.checked = true;
  2992. this.state.recursiveSearch = true;
  2993. this.state.maxDepth = -1;
  2994. }
  2995.  
  2996. if (elements.currentDepth) {
  2997. elements.currentDepth.checked = false;
  2998. }
  2999.  
  3000. if (elements.customDepth) {
  3001. elements.customDepth.checked = false;
  3002. }
  3003.  
  3004. if (elements.maxDepth) {
  3005. elements.maxDepth.disabled = true;
  3006. elements.maxDepth.value = '1';
  3007. }
  3008.  
  3009. if (elements.depthControls) {
  3010. elements.depthControls.classList.remove('active');
  3011. }
  3012. }
  3013.  
  3014. handleError(error, context = '') {
  3015. let userMessage = 'An error occurred';
  3016. let logMessage = error.message;
  3017. let type = 'error';
  3018.  
  3019. switch (true) {
  3020. case error.name === 'AbortError':
  3021. userMessage = 'Server not responding, operation timed out';
  3022. logMessage = `Timeout: ${context}`;
  3023. type = 'warning';
  3024. break;
  3025.  
  3026. case error.message.includes('HTTP error'):
  3027. const status = error.message.match(/\d+/)?.[0];
  3028. switch (status) {
  3029. case '403':
  3030. userMessage = 'Access denied to this directory';
  3031. break;
  3032. case '404':
  3033. userMessage = 'Directory or file not found';
  3034. break;
  3035. case '429':
  3036. userMessage = 'Too many requests, please wait a while';
  3037. break;
  3038. case '500':
  3039. case '502':
  3040. case '503':
  3041. userMessage = 'Server is currently unable to respond, please try again later';
  3042. break;
  3043. default:
  3044. userMessage = 'Error communicating with server';
  3045. }
  3046. logMessage = `${error.message} (${context})`;
  3047. break;
  3048.  
  3049. case error.message.includes('decode'):
  3050. userMessage = 'Filename or path could not be read';
  3051. logMessage = `Decode error: ${context} - ${error.message}`;
  3052. type = 'warning';
  3053. break;
  3054.  
  3055. case error.message.includes('NetworkError'):
  3056. userMessage = 'Network connection error, please check your connection';
  3057. logMessage = `Network error: ${context}`;
  3058. break;
  3059.  
  3060. case error.message.includes('SecurityError'):
  3061. userMessage = 'Operation not allowed due to security restrictions';
  3062. logMessage = `Security error: ${context}`;
  3063. break;
  3064.  
  3065. default:
  3066. userMessage = 'Unexpected error occurred';
  3067. logMessage = `${error.name}: ${error.message} (${context})`;
  3068. }
  3069.  
  3070. console.error(`[${context}]`, error);
  3071.  
  3072. this.showToast(userMessage, type);
  3073.  
  3074. this.addLogEntry(logMessage, type);
  3075.  
  3076. this.state.stats.errors.total++;
  3077. }
  3078.  
  3079. async fetchWithRetry(url, options = {}) {
  3080. let response;
  3081. let retryCount = 0;
  3082. const maxRetries = this.state.retryCount;
  3083. const baseTimeout = 1000;
  3084. const maxTimeout = 10000;
  3085.  
  3086. while (retryCount <= maxRetries) {
  3087. try {
  3088. const controller = new AbortController();
  3089. const currentTimeout = Math.min(
  3090. maxTimeout,
  3091. baseTimeout * Math.pow(2, retryCount) * (0.5 + Math.random())
  3092. );
  3093. const timeoutId = setTimeout(() => controller.abort(), currentTimeout);
  3094.  
  3095. response = await fetch(url, {
  3096. ...options,
  3097. signal: controller.signal,
  3098. headers: {
  3099. 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  3100. 'Accept-Charset': 'utf-8',
  3101. 'Accept-Language': 'en-US,en;q=0.9,tr-TR;q=0.8,tr;q=0.7'
  3102. }
  3103. });
  3104.  
  3105. clearTimeout(timeoutId);
  3106.  
  3107. if (response.ok) {
  3108. const contentType = response.headers.get('content-type');
  3109. const charset = contentType && contentType.includes('charset=')
  3110. ? contentType.split('charset=')[1].toLowerCase()
  3111. : 'utf-8';
  3112.  
  3113. const buffer = await response.arrayBuffer();
  3114. const decoder = new TextDecoder(charset);
  3115. response.decodedText = decoder.decode(buffer);
  3116.  
  3117. return response;
  3118. }
  3119.  
  3120. throw new Error(`HTTP error! Status: ${response.status}`);
  3121. } catch (error) {
  3122. retryCount++;
  3123. this.handleError(error, `URL: ${url}`);
  3124.  
  3125. if (retryCount > maxRetries) {
  3126. this.addLogEntry(
  3127. `Maximum number of attempts reached (${maxRetries}), skipping directory: ${url}`,
  3128. 'error'
  3129. );
  3130. this.state.stats.errors.skipped++;
  3131. throw error;
  3132. }
  3133.  
  3134. const backoffDelay = Math.min(
  3135. maxTimeout,
  3136. baseTimeout * Math.pow(2, retryCount - 1) * (0.5 + Math.random())
  3137. );
  3138.  
  3139. this.addLogEntry(
  3140. `Retrying (${retryCount}/${maxRetries}), waiting for ${(backoffDelay/1000).toFixed(1)} seconds...`,
  3141. 'warning'
  3142. );
  3143.  
  3144. await new Promise(resolve => setTimeout(resolve, backoffDelay));
  3145. }
  3146. }
  3147. }
  3148.  
  3149. sanitizeInput(input) {
  3150. if (!input || typeof input !== 'string') {
  3151. return '';
  3152. }
  3153.  
  3154. const sanitized = input
  3155. .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '')
  3156. .trim()
  3157. .replace(/[\x00-\x1F\x7F]/g, '')
  3158. .replace(/[\u200B-\u200D\uFEFF]/g, '')
  3159. .replace(/[^\w\s\-_.()[\]{}#@!$%^&+=]/g, '');
  3160.  
  3161. if (!sanitized) {
  3162. return 'playlist';
  3163. }
  3164.  
  3165. if (sanitized.length > 255) {
  3166. return sanitized.slice(0, 255);
  3167. }
  3168.  
  3169. return sanitized;
  3170. }
  3171.  
  3172. decodeString(str, type = 'both') {
  3173. if (!str) return str;
  3174. try {
  3175. let decoded = str;
  3176. if (type === 'html' || type === 'both') {
  3177. decoded = decoded.replace(/&amp;/g, '&')
  3178. .replace(/&lt;/g, '<')
  3179. .replace(/&gt;/g, '>')
  3180. .replace(/&quot;/g, '"')
  3181. .replace(/&#039;/g, "'")
  3182. .replace(/&#x27;/g, "'")
  3183. .replace(/&#x2F;/g, "/");
  3184. }
  3185.  
  3186. if (type === 'url' || type === 'both') {
  3187. try {
  3188. decoded = decodeURIComponent(decoded);
  3189. } catch (e) {
  3190. decoded = decoded.replace(/%([0-9A-F]{2})/gi, (match, hex) => {
  3191. try {
  3192. return String.fromCharCode(parseInt(hex, 16));
  3193. } catch {
  3194. return match;
  3195. }
  3196. });
  3197. }
  3198. }
  3199. return decoded;
  3200. } catch (error) {
  3201. console.warn('Decode error:', error);
  3202. return str;
  3203. }
  3204. }
  3205.  
  3206. extractFileInfo(path) {
  3207. try {
  3208. const decodedPath = this.decodeString(path);
  3209. const parts = decodedPath.split('/');
  3210. const fileName = parts.pop() || '';
  3211. const dirPath = parts.join('/');
  3212. return {
  3213. fileName,
  3214. dirPath,
  3215. original: {
  3216. fileName: path.split('/').pop() || '',
  3217. dirPath: path.split('/').slice(0, -1).join('/')
  3218. }
  3219. };
  3220. } catch (error) {
  3221. this.handleError(error, `Path decode error: ${path}`);
  3222. const parts = path.split('/');
  3223. return {
  3224. fileName: parts.pop() || '',
  3225. dirPath: parts.join('/'),
  3226. original: {
  3227. fileName: parts.pop() || '',
  3228. dirPath: parts.join('/')
  3229. }
  3230. };
  3231. }
  3232. }
  3233.  
  3234. normalizeUrl(url) {
  3235. let normalized = url.replace(/([^:]\/)\/+/g, "$1");
  3236. return normalized.endsWith('/') ? normalized : normalized + '/';
  3237. }
  3238.  
  3239. isMediaFile(fileName, type) {
  3240. const lowerFileName = fileName.toLowerCase();
  3241. return type === 'video'
  3242. ? this.videoFormats.some(ext => lowerFileName.endsWith(ext))
  3243. : this.audioFormats.some(ext => lowerFileName.endsWith(ext));
  3244. }
  3245.  
  3246. resetCurrentStats() {
  3247. this.state.stats.files.video.current = 0;
  3248. this.state.stats.files.audio.current = 0;
  3249. }
  3250.  
  3251. updateFileStats(type) {
  3252. this.state.stats.files[type].total++;
  3253. this.state.stats.files[type].current++;
  3254. }
  3255.  
  3256. getCurrentStatsText() {
  3257. const { video, audio } = this.state.stats.files;
  3258. const details = [];
  3259.  
  3260. if (video.current > 0) details.push(`${video.current} video`);
  3261. if (audio.current > 0) details.push(`${audio.current} audio`);
  3262.  
  3263. return details.join(' and ');
  3264. }
  3265.  
  3266. async scanDirectory(url, currentPath = '', depth = 0) {
  3267. try {
  3268. this.resetCurrentStats();
  3269.  
  3270. if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) {
  3271. return this.entries;
  3272. }
  3273.  
  3274. while (this.state.isPaused && this.state.isGenerating) {
  3275. await new Promise(resolve => setTimeout(resolve, 100));
  3276. }
  3277.  
  3278. const normalizedUrl = this.normalizeUrl(url);
  3279.  
  3280. if (depth > this.state.stats.directories.depth) {
  3281. this.state.stats.directories.depth = depth;
  3282. }
  3283.  
  3284. this.state.stats.directories.total++;
  3285.  
  3286. this.addLogEntry(`Scanning directory (${depth}. level): ${normalizedUrl}`);
  3287.  
  3288. if (this.seenUrls.has(normalizedUrl)) {
  3289. this.addLogEntry(`Directory already scanned: ${normalizedUrl}`);
  3290. return this.entries;
  3291. }
  3292.  
  3293. this.seenUrls.add(normalizedUrl);
  3294. if (this.seenUrls.size > this.state.maxSeenUrls) {
  3295. const keepCount = Math.floor(this.state.maxSeenUrls * 0.75);
  3296. const urlsArray = Array.from(this.seenUrls);
  3297. const keepUrls = urlsArray.slice(-keepCount);
  3298. this.seenUrls = new Set(keepUrls);
  3299. this.addLogEntry(
  3300. `Cache cleared (${urlsArray.length} -> ${keepUrls.length})`,
  3301. 'info'
  3302. );
  3303. }
  3304.  
  3305. let response;
  3306. try {
  3307. response = await this.fetchWithRetry(normalizedUrl);
  3308. } catch (error) {
  3309. return this.entries;
  3310. }
  3311.  
  3312. const html = response.decodedText;
  3313. const hrefRegex = /href="([^"]+)"/gi;
  3314. const matches = html.matchAll(hrefRegex);
  3315. const hrefs = Array.from(matches, m => m[1]).filter(href =>
  3316. href &&
  3317. !href.startsWith('?') &&
  3318. !href.startsWith('/') &&
  3319. href !== '../' &&
  3320. !href.includes('Parent Directory')
  3321. );
  3322.  
  3323. let totalFilesInCurrentDir = 0;
  3324.  
  3325. for (const href of hrefs) {
  3326. if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) break;
  3327.  
  3328. try {
  3329. const decodedHref = this.decodeString(href);
  3330. const fullUrl = new URL(decodedHref, normalizedUrl).toString();
  3331. const { fileName } = this.extractFileInfo(decodedHref);
  3332. const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
  3333.  
  3334. if (href.endsWith('/')) {
  3335. const shouldScanSubdir =
  3336. this.state.maxDepth === -1 ||
  3337. (this.state.maxDepth > 0 && depth < this.state.maxDepth);
  3338. if (shouldScanSubdir) {
  3339. this.addLogEntry(`Entering directory: ${fullPath}`);
  3340. await this.scanDirectory(fullUrl, fullPath, depth + 1);
  3341. }
  3342. } else {
  3343. totalFilesInCurrentDir++;
  3344. this.state.stats.totalFiles = (this.state.stats.totalFiles || 0) + 1;
  3345.  
  3346. const isVideo = this.isMediaFile(fileName, 'video');
  3347. const isAudio = this.isMediaFile(fileName, 'audio');
  3348.  
  3349. if ((isVideo && this.state.includeVideo) || (isAudio && this.state.includeAudio)) {
  3350. if (isVideo && this.state.includeVideo) {
  3351. this.updateFileStats('video');
  3352. }
  3353. if (isAudio && this.state.includeAudio) {
  3354. this.updateFileStats('audio');
  3355. }
  3356.  
  3357. this.entries.push({
  3358. title: fullPath,
  3359. url: fullUrl
  3360. });
  3361. }
  3362. }
  3363. this.updateCounter(this.state.stats.totalFiles);
  3364. } catch (error) {
  3365. console.error('Error processing URL:', error);
  3366. this.state.stats.errors.total++;
  3367. continue;
  3368. }
  3369. }
  3370.  
  3371. if (totalFilesInCurrentDir > 0) {
  3372. this.addLogEntry(
  3373. `"${currentPath || normalizedUrl}" directory contains ${totalFilesInCurrentDir} files`,
  3374. 'info'
  3375. );
  3376. }
  3377.  
  3378. const statsText = this.getCurrentStatsText();
  3379. if (statsText) {
  3380. this.addLogEntry(
  3381. `From these, ${statsText} files were added to the playlist`,
  3382. 'success'
  3383. );
  3384. }
  3385.  
  3386. return this.entries;
  3387. } catch (error) {
  3388. this.state.stats.errors.total++;
  3389. this.addLogEntry(`Scan error (${currentPath || url}): ${error.message}`, 'error');
  3390. return this.entries;
  3391. }
  3392. }
  3393.  
  3394. createPlaylist(entries) {
  3395. let content = '#EXTM3U\n';
  3396. const decodedEntries = entries.map(entry => {
  3397. try {
  3398. let title = this.decodeString(entry.title);
  3399. const depth = (title.match(/\//g) || []).length;
  3400. const isVideo = this.videoFormats.some(ext => title.toLowerCase().endsWith(ext));
  3401. const isAudio = this.audioFormats.some(ext => title.toLowerCase().endsWith(ext));
  3402. return {
  3403. ...entry,
  3404. decodedTitle: title,
  3405. depth: depth,
  3406. isVideo: isVideo,
  3407. isAudio: isAudio
  3408. };
  3409. } catch (error) {
  3410. return {
  3411. ...entry,
  3412. decodedTitle: entry.title,
  3413. depth: 0,
  3414. isVideo: false,
  3415. isAudio: false
  3416. };
  3417. }
  3418. });
  3419.  
  3420. const videoEntries = decodedEntries.filter(entry => entry.isVideo);
  3421. const audioEntries = decodedEntries.filter(entry => entry.isAudio);
  3422.  
  3423. const apacheSort = (a, b) => {
  3424. if (a.depth !== b.depth) {
  3425. return a.depth - b.depth;
  3426. }
  3427.  
  3428. const aStartsWithNumber = /^\d/.test(a.decodedTitle);
  3429. const bStartsWithNumber = /^\d/.test(b.decodedTitle);
  3430. if (aStartsWithNumber !== bStartsWithNumber) {
  3431. return aStartsWithNumber ? -1 : 1;
  3432. }
  3433.  
  3434. return a.decodedTitle.localeCompare(b.decodedTitle, undefined, {
  3435. numeric: true,
  3436. sensitivity: 'base'
  3437. });
  3438. };
  3439.  
  3440. const sortedVideoEntries = videoEntries.sort(apacheSort);
  3441. const sortedAudioEntries = audioEntries.sort(apacheSort);
  3442.  
  3443. const sortedEntries = [...sortedVideoEntries, ...sortedAudioEntries];
  3444.  
  3445. sortedEntries.forEach(entry => {
  3446. content += `#EXTINF:-1,${entry.decodedTitle}\n${entry.url}\n`;
  3447. });
  3448.  
  3449. return content;
  3450. }
  3451.  
  3452. addLogEntry(message, type = '') {
  3453.  
  3454. if ((this.state.isPaused || !this.state.isGenerating) && type !== 'final') {
  3455. return;
  3456. }
  3457.  
  3458. const scanLog = this.domElements.scanLog;
  3459. const logCounter = this.domElements.logCounter;
  3460. this.logCount++;
  3461. if (logCounter) {
  3462. logCounter.textContent = this.logCount;
  3463. }
  3464. let decodedMessage = message;
  3465. try {
  3466. if (message.includes('http')) {
  3467. const urlRegex = /(https?:\/\/[^\s]+)/g;
  3468. decodedMessage = message.replace(urlRegex, (url) => {
  3469. try {
  3470. return decodeURIComponent(url);
  3471. } catch (e) {
  3472. return url;
  3473. }
  3474. });
  3475. }
  3476. } catch (error) {
  3477. console.warn('Decode error:', error);
  3478. }
  3479. const entry = document.createElement('div');
  3480. entry.className = `M3Unator-log-entry ${type}`;
  3481.  
  3482. const timestamp = new Date().toLocaleTimeString();
  3483. entry.innerHTML = `<span class="M3Unator-log-time">[${timestamp}]</span> ${decodedMessage}`;
  3484. scanLog.appendChild(entry);
  3485. scanLog.scrollTop = scanLog.scrollHeight;
  3486. }
  3487.  
  3488. updateCounter(count) {
  3489. if (!this.domElements.stats || !this.domElements.statsBar) {
  3490. return;
  3491. }
  3492.  
  3493. const stats = this.state.stats;
  3494. const elements = this.domElements.stats;
  3495. const statsBar = this.domElements.statsBar;
  3496.  
  3497. statsBar.style.display = 'block';
  3498. const updates = {
  3499. 'totalFiles': count,
  3500. 'videoFiles': stats.files.video.total,
  3501. 'audioFiles': stats.files.audio.total,
  3502. 'directories': stats.directories.total,
  3503. 'depthLevel': stats.directories.depth,
  3504. 'errors': stats.errors.total
  3505. };
  3506.  
  3507. Object.entries(updates).forEach(([key, value]) => {
  3508. const element = elements[key];
  3509. if (element) {
  3510. element.textContent = value;
  3511. const statContainer = element.closest('.M3Unator-stat');
  3512. if (statContainer) {
  3513. statContainer.style.opacity = value > 0 ? '1' : '0.5';
  3514. if (key === 'depthLevel') {
  3515. const maxDepth = this.state.maxDepth || 0;
  3516. if (maxDepth > 0) {
  3517. const progress = (value / maxDepth) * 100;
  3518. statContainer.dataset.progress =
  3519. progress >= 100 ? 'high' :
  3520. progress >= 75 ? 'medium' :
  3521. progress >= 50 ? 'low' : '';
  3522. statContainer.title = `Depth Level: ${value}/${maxDepth}`;
  3523. } else {
  3524. statContainer.dataset.progress = '';
  3525. statContainer.title = `Depth Level: ${value}`;
  3526. }
  3527. }
  3528. }
  3529. }
  3530. });
  3531. }
  3532. }
  3533.  
  3534. const generator = new PlaylistGenerator();
  3535. generator.init();
  3536.  
  3537. // Event listeners for info modal
  3538. document.querySelector('.info-link').addEventListener('click', () => {
  3539. document.querySelector('.info-modal').style.display = 'block';
  3540. });
  3541.  
  3542. document.querySelector('.info-close').addEventListener('click', () => {
  3543. document.querySelector('.info-modal').style.display = 'none';
  3544. });
  3545.  
  3546. window.addEventListener('click', (event) => {
  3547. const modal = document.querySelector('.info-modal');
  3548. if (event.target === modal) {
  3549. modal.style.display = 'none';
  3550. }
  3551. });
  3552. })();