- // ==UserScript==
- // @name M3Unator - Web Directory Playlist Creator
- // @namespace https://github.com/hasanbeder/M3Unator
- // @version 1.0.2
- // @description Create M3U/M3U8 playlists from directory listing pages. Automatically finds video and audio files in web server indexes.
- // @author Hasan Beder
- // @license GPL-3.0
- // @match *://*/*
- // @grant GM_addStyle
- // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmNWMyZTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjIzIDcgMTYgMTIgMjMgMTcgMjMgNyIvPjxyZWN0IHg9IjEiIHk9IjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNCIgcng9IjIiIHJ5PSIyIi8+PC9zdmc+
- // @homepageURL https://github.com/hasanbeder/M3Unator
- // @supportURL https://github.com/hasanbeder/M3Unator/issues
- // @run-at document-end
- // @noframes true
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- if (!document.title.includes('Index of') && !document.querySelector('div#table-list')) {
- console.log('This page is not an Index page, M3Unator disabled.');
- return;
- }
-
- function parseLiteSpeedDirectory() {
- const links = [];
- const rows = document.querySelectorAll('#table-content tr');
-
- rows.forEach(row => {
- const linkElement = row.querySelector('a');
- if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
- const href = linkElement.getAttribute('href');
- if (href) {
- links.push(new URL(href, window.location.href).href);
- }
- }
- });
-
- return links;
- }
-
- // Add LiteSpeed support to the existing getDirectoryLinks function
- function getDirectoryLinks() {
- const links = [];
-
- // LiteSpeed directory listing
- if (document.querySelector('div#table-list')) {
- const rows = document.querySelectorAll('#table-content tr');
- rows.forEach(row => {
- const linkElement = row.querySelector('a');
- if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
- const href = linkElement.getAttribute('href');
- if (href) {
- links.push(new URL(href, window.location.href).href);
- }
- }
- });
- return links;
- }
-
- // Apache/Nginx style directory listing
- const anchors = document.querySelectorAll('a');
- anchors.forEach(anchor => {
- if (!anchor.textContent.includes('Parent Directory')) {
- const href = anchor.getAttribute('href');
- if (href && !href.startsWith('?') && !href.startsWith('/')) {
- links.push(new URL(href, window.location.href).href);
- }
- }
- });
-
- return links;
- }
-
- GM_addStyle(`
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
-
- [class^="M3Unator"] {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- }
-
- .M3Unator-title {
- font-weight: 700;
- letter-spacing: -0.02em;
- }
-
- .M3Unator-input-group label {
- font-weight: 500;
- letter-spacing: -0.01em;
- }
-
- .M3Unator-input {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-size: 0.9375rem;
- letter-spacing: -0.01em;
- }
-
- .M3Unator-button {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-weight: 600;
- letter-spacing: -0.01em;
- }
-
- .M3Unator-control-btn {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-weight: 500;
- letter-spacing: -0.01em;
- }
-
- .M3Unator-log {
- font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
- font-size: 0.8125rem;
- letter-spacing: -0.01em;
- line-height: 1.5;
- }
-
- .M3Unator-log-counter {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-weight: 600;
- letter-spacing: -0.01em;
- }
-
- .M3Unator-container {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.75);
- backdrop-filter: blur(8px);
- display: none;
- place-items: center;
- padding: 1rem;
- z-index: 9999;
- }
-
- .M3Unator-container[data-visible="true"] {
- display: grid;
- }
-
- .M3Unator-overlay {
- position: fixed;
- inset: 0;
- background: transparent;
- z-index: 9998;
- }
-
- body.modal-open {
- overflow: hidden;
- pointer-events: none; /* Prevent background clicks */
- }
-
- body.modal-open .M3Unator-container,
- body.modal-open .M3Unator-popup {
- pointer-events: all; /* Allow clicks on modal content */
- }
-
- .M3Unator-popup {
- background: #11111b;
- color: #cdd6f4;
- width: 100%;
- max-width: 480px;
- border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
- overflow: hidden;
- animation: slideUp 0.3s ease;
- position: absolute;
- }
-
- .M3Unator-header {
- padding: 1.25rem 1.618rem;
- background: #1e1e2e;
- color: #cdd6f4;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: move;
- user-select: none;
- border-bottom: 1px solid #313244;
- }
-
- .M3Unator-title {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin: 0;
- font-size: 1.25rem;
- font-weight: 600;
- line-height: 1;
- }
-
- .M3Unator-title svg {
- width: 24px;
- height: 24px;
- color: #f5c2e7;
- filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-top: 1px;
- }
-
- .M3Unator-title span {
- display: flex;
- align-items: center;
- line-height: 24px;
- background: linear-gradient(90deg,
- #f5c2e7,
- #cba6f7,
- #89b4fa,
- #a6e3a1,
- #f5c2e7
- );
- background-size: 300% auto;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: gradient 3s linear infinite;
- }
-
- .M3Unator-close {
- background: rgba(203, 166, 247, 0.1);
- border: none;
- color: #cba6f7;
- width: 32px;
- height: 32px;
- border-radius: 8px;
- display: grid;
- place-items: center;
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- .M3Unator-close:hover {
- background: rgba(203, 166, 247, 0.2);
- transform: rotate(360deg);
- }
-
- .M3Unator-close svg {
- width: 18px;
- height: 18px;
- }
-
- .M3Unator-content {
- padding: 0.75rem;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- }
-
- .M3Unator-input-group {
- margin-bottom: 0;
- }
-
- .M3Unator-input-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 500;
- color: #bac2de;
- }
-
- .M3Unator-input {
- width: 100%;
- height: 42px; /* Same height as Create Playlist button */
- padding: 0 12px;
- border: 1px solid #45475a;
- border-radius: 8px;
- background: #1e1e2e;
- color: #f5c2e7;
- font-size: 14px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- }
-
- .M3Unator-input:focus {
- outline: none;
- border-color: #f5c2e7;
- box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1);
- }
-
- .M3Unator-input::placeholder {
- color: #6c7086;
- opacity: 1;
- }
-
- .M3Unator-toggle-container {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .M3Unator-toggle-container input[type="checkbox"] {
- display: none;
- }
-
- .M3Unator-toggle-container span {
- width: 48px;
- height: 48px;
- background: #1e1e2e;
- border: 2px solid #45475a;
- border-radius: 12px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
- }
-
- .M3Unator-toggle-container svg {
- width: 24px;
- height: 24px;
- opacity: 0.7;
- transition: all 0.3s ease;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span {
- background: rgba(203, 166, 247, 0.1);
- border-color: #cba6f7;
- box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
- opacity: 1;
- color: #cba6f7;
- filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
- }
-
- .M3Unator-toggle-container span:hover {
- background: #313244;
- transform: translateY(-2px);
- }
-
- .M3Unator-toggle-container span:active {
- transform: translateY(1px);
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span:hover {
- background: rgba(203, 166, 247, 0.2);
- }
-
- .M3Unator-toggle-container span:active {
- transform: translateY(1px);
- }
-
- .M3Unator-toggle-container svg {
- width: 24px;
- height: 24px;
- opacity: 0.8;
- transition: all 0.2s ease;
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
- opacity: 1;
- color: #cba6f7;
- }
-
- .M3Unator-toggle-group {
- display: flex;
- gap: 0.75rem;
- margin: 0.75rem 0;
- justify-content: center;
- background: rgba(30, 30, 46, 0.4);
- padding: 0.75rem;
- border-radius: 12px;
- backdrop-filter: blur(8px);
- }
-
- [title]:hover::after {
- content: attr(title);
- position: absolute;
- bottom: calc(100% + 5px);
- left: 50%;
- transform: translateX(-50%);
- padding: 0.5rem 0.75rem;
- background: rgba(30, 30, 46, 0.95);
- color: #cdd6f4;
- font-size: 0.875rem;
- white-space: nowrap;
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- z-index: 1000;
- border: 1px solid #313244;
- text-align: center;
- backdrop-filter: blur(8px);
- pointer-events: none;
- }
-
- .M3Unator-button {
- width: 100%;
- height: 42px;
- padding: 0 16px;
- border: none;
- border-radius: 8px;
- background: #f5c2e7;
- color: #1e1e2e;
- font-weight: 600;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- }
-
- .M3Unator-button:hover {
- background: #f5c2e7;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(245, 194, 231, 0.2);
- }
-
- .M3Unator-button:active {
- transform: translateY(0);
- }
-
- .M3Unator-button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- .M3Unator-launcher {
- position: fixed;
- top: 1rem;
- right: 1.618rem;
- height: 48px;
- padding: 0 1.25rem;
- border-radius: 12px;
- background: rgba(30, 30, 46, 0.95);
- border: 2px solid #313244;
- cursor: pointer;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
- display: flex;
- align-items: center;
- gap: 0.75rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- z-index: 9998;
- backdrop-filter: blur(12px);
- }
-
- .M3Unator-launcher:hover {
- background: rgba(30, 30, 46, 0.98);
- border-color: #45475a;
- transform: translateY(-2px);
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
- }
-
- .M3Unator-launcher svg {
- width: 24px;
- height: 24px;
- color: #f5c2e7;
- filter: drop-shadow(0 0 8px rgba(245, 194, 231, 0.4));
- }
-
- .M3Unator-launcher span {
- font-weight: 600;
- font-size: 0.95rem;
- background: linear-gradient(90deg,
- #f5c2e7,
- #cba6f7,
- #89b4fa,
- #a6e3a1,
- #f5c2e7
- );
- background-size: 300% auto;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: gradient 3s linear infinite;
- }
-
- @keyframes gradient {
- 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
- 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
- 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
- }
-
- .M3Unator-dropdown {
- position: relative;
- width: 100%;
- }
-
- .M3Unator-dropdown-button {
- width: 100%;
- padding: 0.618rem;
- background: #1e1e2e;
- border: 1px solid #313244;
- border-radius: 8px;
- color: #cdd6f4;
- font-size: 0.875rem;
- text-align: left;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .M3Unator-dropdown-button:hover {
- border-color: #45475a;
- background: rgba(30, 30, 46, 0.8);
- }
-
- .M3Unator-dropdown-button svg {
- width: 16px;
- height: 16px;
- min-width: 16px;
- min-height: 16px;
- transition: transform 0.2s ease;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-button {
- border-color: #cba6f7;
- border-radius: 8px 8px 0 0;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-button svg {
- transform: rotate(180deg);
- }
-
- .M3Unator-dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- background: #1e1e2e;
- border: 1px solid #cba6f7;
- border-top: none;
- border-radius: 0 0 8px 8px;
- overflow: hidden;
- z-index: 1000;
- display: none;
- animation: dropdownSlide 0.2s ease;
- user-select: none;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-menu {
- display: block;
- }
-
- .M3Unator-dropdown-item {
- padding: 0.618rem;
- color: #cdd6f4;
- cursor: pointer;
- transition: all 0.2s ease;
- user-select: none;
- }
-
- .M3Unator-dropdown-item:hover {
- background: rgba(203, 166, 247, 0.1);
- }
-
- .M3Unator-dropdown-item.selected {
- background: rgba(203, 166, 247, 0.1);
- color: #cba6f7;
- }
-
- @keyframes slideUp {
- from { transform: translateY(20px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
- }
-
- .M3Unator-log {
- margin-top: 0.75rem;
- max-height: calc(100vh - 70vh);
- font-size: 0.8125rem;
- line-height: 1.4;
- }
-
- .M3Unator-log:empty {
- display: none;
- }
-
- .M3Unator-log-entry {
- padding: 0.25rem 0.5rem;
- border-bottom: 1px solid #313244;
- }
-
- .M3Unator-log-entry:last-child {
- border-bottom: none;
- }
-
- .M3Unator-log-entry.success {
- color: #94e2d5;
- }
-
- .M3Unator-log-entry.error {
- color: #f38ba8;
- }
-
- .M3Unator-log-entry.warning {
- color: #fab387;
- }
-
- .M3Unator-log-counter {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: rgba(245, 194, 231, 0.1);
- color: #f5c2e7;
- padding: 0.25rem 0.75rem;
- border-radius: 8px;
- font-size: 0.875rem;
- font-weight: 500;
- margin-left: 0.75rem;
- min-width: 3rem;
- text-align: center;
- }
-
- @keyframes gradient {
- 0% { background-position: 0% 50%; filter: hue-rotate(0deg); }
- 50% { background-position: 100% 50%; filter: hue-rotate(180deg); }
- 100% { background-position: 0% 50%; filter: hue-rotate(360deg); }
- }
-
- .M3Unator-title span.text {
- display: inline-block;
- position: relative;
- padding: 0 0.25rem;
- }
-
- .M3Unator-title.scanning span.text {
- background: linear-gradient(90deg,
- #f5c2e7,
- #cba6f7,
- #89b4fa,
- #a6e3a1,
- #f5c2e7
- );
- background-size: 300% auto;
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- animation: gradient 3s linear infinite;
- font-weight: 700;
- letter-spacing: 0.5px;
- }
-
- .M3Unator-title.scanning span.text::after {
- content: '';
- position: absolute;
- bottom: -2px;
- left: 0;
- width: 100%;
- height: 2px;
- background: inherit;
- animation: gradient 3s linear infinite;
- }
-
- .M3Unator-title.scanning svg {
- animation: morphAnimation 2s ease-in-out infinite;
- filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.5));
- }
-
- @keyframes morphAnimation {
- 0% {
- transform: scale(1);
- opacity: 1;
- }
- 50% {
- transform: scale(1.2);
- opacity: 0.7;
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
-
- .M3Unator-controls {
- display: none;
- gap: 0.75rem;
- margin: 0.75rem 0;
- justify-content: center;
- }
-
- .M3Unator-controls.active {
- display: flex;
- }
-
- .M3Unator-control-btn {
- display: none;
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- font-weight: 600;
- font-size: 0.95rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- align-items: center;
- gap: 0.75rem;
- min-width: 160px;
- justify-content: center;
- background: rgba(30, 30, 46, 0.6);
- backdrop-filter: blur(8px);
- width: 160px;
- }
-
- .M3Unator-control-btn:hover {
- background: #313244;
- transform: translateY(-1px);
- }
-
- .M3Unator-control-btn:active {
- transform: translateY(1px);
- }
-
- .M3Unator-control-btn.pause {
- border-color: #fab387;
- color: #fab387;
- }
-
- .M3Unator-control-btn.pause:hover {
- background: rgba(250, 179, 135, 0.1);
- }
-
- .M3Unator-control-btn.resume {
- border-color: #94e2d5;
- color: #94e2d5;
- }
-
- .M3Unator-control-btn.resume:hover {
- background: rgba(148, 226, 213, 0.1);
- }
-
- .M3Unator-control-btn.cancel {
- border-color: #f38ba8;
- color: #f38ba8;
- }
-
- .M3Unator-control-btn.cancel:hover {
- background: rgba(243, 139, 168, 0.1);
- }
-
- .M3Unator-control-btn svg {
- width: 14px;
- height: 14px;
- }
-
- .M3Unator-button {
- width: 100%;
- padding: 0 1rem;
- background: #f5c2e7;
- color: #11111b;
- border: none;
- border-radius: 6px;
- font-weight: 600;
- font-size: 0.875rem;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.375rem;
- height: 48px;
- min-height: 48px;
- line-height: 1;
- }
-
- .M3Unator-spinner {
- width: 20px;
- height: 20px;
- border: 2px solid rgba(17, 17, 27, 0.3);
- border-radius: 50%;
- border-top-color: #11111b;
- animation: spin 0.6s linear infinite;
- margin-right: 0;
- flex-shrink: 0;
- }
-
- .M3Unator-toast-container {
- position: fixed;
- bottom: 24px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 999999;
- pointer-events: none;
- display: flex;
- flex-direction: column;
- align-items: center;
- width: auto;
- }
-
- .M3Unator-toast {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 24px;
- border-radius: 12px;
- margin-bottom: 12px;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-size: 14px;
- font-weight: 500;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
- background: rgba(17, 17, 27, 0.95);
- border: 2px solid;
- pointer-events: all;
- min-width: 300px;
- max-width: 500px;
- backdrop-filter: blur(16px);
- will-change: transform, opacity;
- animation: none;
- transform-origin: center bottom;
- }
-
- .M3Unator-toast.show {
- animation: toastBounceIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
- }
-
- .M3Unator-toast.removing {
- animation: toastBounceOut 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
- }
-
- @keyframes toastBounceIn {
- 0% {
- opacity: 0;
- transform: scale(0.3) translateY(2000px);
- }
- 60% {
- opacity: 1;
- transform: scale(1.1) translateY(-20px);
- }
- 75% {
- transform: scale(0.95) translateY(10px);
- }
- 90% {
- transform: scale(1.02) translateY(-5px);
- }
- 100% {
- transform: scale(1) translateY(0);
- }
- }
-
- @keyframes toastBounceOut {
- 0% {
- transform: scale(1) translateY(0);
- opacity: 1;
- }
- 20% {
- transform: scale(1.1) translateY(-20px);
- opacity: 0.8;
- }
- 100% {
- transform: scale(0.3) translateY(2000px);
- opacity: 0;
- }
- }
-
- .M3Unator-toast svg {
- width: 20px;
- height: 20px;
- flex-shrink: 0;
- filter: drop-shadow(0 0 4px currentColor);
- animation: iconPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
- opacity: 0;
- transform: scale(0.5);
- }
-
- @keyframes iconPop {
- 0% {
- opacity: 0;
- transform: scale(0.5) rotate(-180deg);
- }
- 100% {
- opacity: 1;
- transform: scale(1) rotate(0deg);
- }
- }
-
- .M3Unator-toast span {
- opacity: 0;
- transform: translateX(-10px);
- animation: textSlide 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
- animation-delay: 0.15s;
- }
-
- @keyframes textSlide {
- 0% {
- opacity: 0;
- transform: translateX(-10px);
- }
- 100% {
- opacity: 1;
- transform: translateX(0);
- }
- }
-
- .M3Unator-input-row {
- display: flex;
- gap: 0.75rem;
- margin-bottom: 0.75rem;
- }
-
- .M3Unator-input-row .M3Unator-input-group {
- margin-bottom: 0;
- }
-
- .M3Unator-input-row .M3Unator-input-group:first-child {
- flex: 2;
- }
-
- .M3Unator-input-row .M3Unator-input-group:last-child {
- flex: 1;
- }
-
- .M3Unator-social {
- display: flex;
- gap: 8px;
- margin-right: 8px;
- }
-
- .M3Unator-social a {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- display: grid;
- place-items: center;
- color: #cdd6f4;
- background: rgba(205, 214, 244, 0.1);
- transition: all 0.2s ease;
- }
-
- .M3Unator-social a:hover {
- background: rgba(205, 214, 244, 0.2);
- transform: rotate(360deg);
- }
-
- .M3Unator-social svg {
- width: 18px;
- height: 18px;
- }
-
- .M3Unator-advanced-settings {
- margin-top: 1rem;
- padding: 1rem;
- background: rgba(30, 30, 46, 0.5);
- border: 1px solid #313244;
- border-radius: 8px;
- display: none;
- }
-
- .M3Unator-advanced-settings.active {
- display: block;
- animation: fadeIn 0.3s ease;
- }
-
- .M3Unator-advanced-toggle {
- width: 100%;
- padding: 0.75rem;
- background: #1e1e2e;
- border: 1px solid #313244;
- border-radius: 8px;
- color: #cdd6f4;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- transition: all 0.2s ease;
- }
-
- .M3Unator-advanced-toggle:hover {
- background: #313244;
- }
-
- .M3Unator-advanced-toggle svg {
- width: 16px;
- height: 16px;
- transition: transform 0.2s ease;
- }
-
- .M3Unator-advanced-toggle.active svg {
- transform: rotate(180deg);
- }
-
- .M3Unator-depth-slider {
- -webkit-appearance: none;
- width: 100%;
- height: 4px;
- border-radius: 2px;
- background: #313244;
- outline: none;
- margin: 1rem 0;
- }
-
- .M3Unator-depth-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: #cba6f7;
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- .M3Unator-depth-slider::-webkit-slider-thumb:hover {
- transform: scale(1.2);
- }
-
- .M3Unator-depth-value {
- text-align: center;
- font-size: 0.875rem;
- color: #cdd6f4;
- margin-top: 0.5rem;
- }
-
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(-10px); }
- to { opacity: 1; transform: translateY(0); }
- }
-
- .M3Unator-depth-settings {
- margin-top: 0.75rem;
- margin-left: 1.75rem;
- padding: 0.75rem;
- background: rgba(30, 30, 46, 0.3);
- border-left: 2px solid #cba6f7;
- border-radius: 0 8px 8px 0;
- display: none;
- animation: slideDown 0.3s ease;
- }
-
- .M3Unator-depth-settings.active {
- display: block;
- }
-
- .M3Unator-depth-input {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin-top: 0.5rem;
- }
-
- .M3Unator-depth-input input[type="number"] {
- width: 64px;
- padding: 0.25rem 0.375rem;
- border: 1px solid #45475a;
- border-radius: 4px;
- background: rgba(30, 30, 46, 0.8);
- color: #cdd6f4;
- font-size: 0.875rem;
- text-align: center;
- margin: 0 0 0 0.5rem;
- }
-
- .M3Unator-depth-input input[type="number"]:focus {
- outline: none;
- border-color: #cba6f7;
- box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
- }
-
- .M3Unator-depth-input input[type="number"]::-webkit-inner-spin-button {
- opacity: 1;
- background: #313244;
- border-left: 1px solid #45475a;
- border-radius: 0 4px 4px 0;
- cursor: pointer;
- }
-
- .M3Unator-depth-toggle {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem;
- background: #1e1e2e;
- border: 1px solid #45475a;
- border-radius: 6px;
- color: #cdd6f4;
- font-size: 0.875rem;
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- .M3Unator-depth-toggle:hover {
- background: #313244;
- border-color: #cba6f7;
- }
-
- .M3Unator-depth-toggle.active {
- background: rgba(203, 166, 247, 0.1);
- border-color: #cba6f7;
- color: #cba6f7;
- }
-
- @keyframes slideDown {
- from { opacity: 0; transform: translateY(-10px); }
- to { opacity: 1; transform: translateY(0); }
- }
-
- .M3Unator-stats-bar {
- margin: 0.75rem 0;
- padding: 0.5rem;
- background: rgba(30, 30, 46, 0.5);
- border: 1px solid #313244;
- border-radius: 8px;
- display: none;
- }
-
- .M3Unator-stats-bar.active {
- display: block;
- }
-
- .M3Unator-stats {
- display: flex;
- align-items: center;
- justify-content: space-around;
- gap: 0.382rem;
- padding: 0.25rem;
- }
-
- .M3Unator-stat {
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- font-size: 0.75rem;
- color: #cdd6f4;
- cursor: help;
- min-width: 40px;
- justify-content: flex-start;
- padding: 0 0.25rem;
- position: relative;
- }
-
- .M3Unator-stat span {
- min-width: 16px;
- text-align: right;
- font-variant-numeric: tabular-nums;
- font-size: 0.7rem;
- font-weight: 500;
- }
-
- .M3Unator-stat svg {
- opacity: 0.8;
- flex-shrink: 0;
- width: 14px;
- height: 14px;
- }
-
- .M3Unator-stat.video {
- color: #94e2d5;
- }
-
- .M3Unator-stat.audio {
- color: #89b4fa;
- }
-
- .M3Unator-stat.dir {
- color: #cba6f7;
- }
-
- .M3Unator-stat.error {
- color: #f38ba8;
- }
-
- .M3Unator-stat.depth {
- color: #a6e3a1;
- transition: color 0.3s ease;
- }
-
- .M3Unator-stat.depth[data-progress="high"] {
- color: #f38ba8;
- }
-
- .M3Unator-stat.depth[data-progress="medium"] {
- color: #fab387;
- }
-
- .M3Unator-stat.depth[data-progress="low"] {
- color: #f9e2af;
- }
-
- .M3Unator-stat:hover::after {
- content: attr(title);
- position: absolute;
- bottom: calc(100% + 5px);
- left: 50%;
- transform: translateX(-50%);
- padding: 0.5rem 0.75rem;
- background: rgba(30, 30, 46, 0.95);
- color: #cdd6f4;
- font-size: 0.875rem;
- white-space: nowrap;
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- z-index: 1000;
- border: 1px solid #313244;
- text-align: center;
- backdrop-filter: blur(8px);
- pointer-events: none;
- }
-
- .M3Unator-spinner {
- width: 20px;
- height: 20px;
- border: 2px solid rgba(17, 17, 27, 0.3);
- border-radius: 50%;
- border-top-color: #11111b;
- animation: spin 0.6s linear infinite;
- margin-right: 0;
- flex-shrink: 0;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- .M3Unator-toast {
- animation: toastSlideUp 0.2s ease forwards;
- }
-
- .M3Unator-toast.removing {
- animation: toastSlideDown 0.2s ease forwards;
- }
-
- .M3Unator-popup {
- animation: slideUp 0.2s ease;
- }
-
- .M3Unator-stats-bar {
- animation: fadeIn 0.2s ease;
- }
-
- .M3Unator-log {
- transition: max-height 0.3s ease;
- }
-
- .M3Unator-log.collapsed {
- max-height: 0;
- overflow: hidden;
- }
-
- .M3Unator-log-toggle {
- width: 100%;
- padding: 0.5rem 0.75rem;
- background: rgba(203, 166, 247, 0.05);
- border: none;
- color: #cdd6f4;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
- transition: all 0.2s ease;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-size: 0.875rem;
- font-weight: 500;
- border-radius: 6px;
- }
-
- .M3Unator-log-toggle:hover {
- background: rgba(203, 166, 247, 0.1);
- }
-
- .M3Unator-activity-indicator {
- width: 14px;
- height: 14px;
- border-radius: 50%;
- background: #45475a; /* Darker gray */
- margin-left: auto;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- .M3Unator-activity-indicator.active {
- background: #89dceb; /* Brighter blue */
- box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2);
- animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- .M3Unator-activity-indicator.paused {
- background: #f9e2af; /* More visible yellow */
- box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2);
- }
-
- .M3Unator-activity-indicator.error {
- background: #f38ba8; /* Current red is good */
- box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2);
- }
-
- .M3Unator-activity-indicator.completed {
- background: #94e2d5; /* Brighter green-turquoise */
- box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2);
- }
-
- @keyframes pulseActive {
- 0% {
- box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 8px rgba(137, 220, 235, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(137, 220, 235, 0);
- }
- }
-
- @keyframes pulsePaused {
- 0% {
- box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 8px rgba(249, 226, 175, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(249, 226, 175, 0);
- }
- }
-
- @keyframes completeScale {
- 0% {
- transform: scale(0.8);
- opacity: 0.5;
- }
- 50% {
- transform: scale(1.2);
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
-
- .M3Unator-toggle-container span svg .infinity-icon {
- opacity: 0.5;
- transition: opacity 0.2s ease;
- transform: scale(0.6) translateY(4px);
- transform-origin: center;
- stroke-width: 1.5;
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span svg .infinity-icon {
- opacity: 1;
- }
-
- .M3Unator-depth-controls {
- background: rgba(30, 30, 46, 0.4);
- backdrop-filter: blur(8px);
- border: 1px solid #313244;
- border-radius: 8px;
- padding: 0.618rem;
- margin-top: 1rem;
- display: none;
- }
-
- .M3Unator-depth-controls.active {
- display: block;
- }
-
- .M3Unator-radio-group {
- display: flex;
- gap: 0.75rem;
- justify-content: center;
- background: rgba(30, 30, 46, 0.6);
- padding: 0.5rem;
- border-radius: 6px;
- }
-
- .M3Unator-radio {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- cursor: pointer;
- padding: 0.5rem;
- border-radius: 4px;
- transition: all 0.2s ease;
- background: transparent;
- border: 1px solid transparent;
- }
-
- .M3Unator-radio:hover {
- background: rgba(203, 166, 247, 0.1);
- }
-
- .M3Unator-radio input[type="radio"] {
- display: none;
- }
-
- .M3Unator-radio .radio-mark {
- width: 16px;
- height: 16px;
- border: 1.5px solid #45475a;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- flex-shrink: 0;
- background: rgba(30, 30, 46, 0.6);
- position: relative;
- }
-
- .M3Unator-radio input[type="radio"]:checked + .radio-mark {
- border-color: #cba6f7;
- background: rgba(203, 166, 247, 0.1);
- }
-
- .M3Unator-radio input[type="radio"]:checked + .radio-mark::after {
- content: '';
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #cba6f7;
- position: absolute;
- }
-
- .M3Unator-radio .radio-label {
- color: #cdd6f4;
- font-size: 0.875rem;
- user-select: none;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- .M3Unator-depth-input {
- width: 64px;
- padding: 0.25rem 0.375rem;
- border: 1px solid #45475a;
- border-radius: 4px;
- background: rgba(30, 30, 46, 0.8);
- color: #cdd6f4;
- font-size: 0.875rem;
- text-align: center;
- transition: all 0.2s ease;
- -moz-appearance: textfield;
- margin-top: -1px;
- display: inline-flex;
- align-items: center;
- height: 28px;
- }
-
- .M3Unator-depth-input::-webkit-outer-spin-button,
- .M3Unator-depth-input::-webkit-inner-spin-button {
- -webkit-appearance: inner-spin-button;
- opacity: 1;
- background: #313244;
- border-left: 1px solid #45475a;
- border-radius: 0 4px 4px 0;
- cursor: pointer;
- height: 100%;
- position: absolute;
- right: 0;
- top: 0;
- }
-
- .M3Unator-depth-input:focus {
- outline: none;
- border-color: #cba6f7;
- box-shadow: 0 0 0 2px rgba(203, 166, 247, 0.2);
- }
-
- .M3Unator-depth-input:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- background: rgba(30, 30, 46, 0.4);
- }
-
- .M3Unator-radio .radio-label {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- color: #cdd6f4;
- font-size: 0.875rem;
- user-select: none;
- }
-
- .M3Unator-url-container {
- display: flex;
- align-items: center;
- background: rgba(30, 30, 46, 0.6);
- border: 1px solid #313244;
- border-radius: 6px;
- padding: 0.618rem;
- margin-bottom: 1rem;
- transition: all 0.2s ease;
- }
-
- .M3Unator-url-container:hover {
- border-color: #45475a;
- }
-
- .M3Unator-url-icon {
- color: #6c7086;
- margin-right: 0.618rem;
- flex-shrink: 0;
- }
-
- .M3Unator-url-input {
- flex: 1;
- background: transparent;
- border: none;
- color: #cdd6f4;
- font-size: 0.875rem;
- padding: 0;
- margin: 0;
- width: 100%;
- }
-
- .M3Unator-url-input:focus {
- outline: none;
- }
-
- .M3Unator-url-copy {
- background: transparent;
- border: none;
- color: #6c7086;
- padding: 0.382rem;
- margin-left: 0.618rem;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .M3Unator-url-copy:hover {
- color: #cdd6f4;
- background: rgba(205, 214, 244, 0.1);
- }
-
- .M3Unator-url-copy.copied {
- color: #a6e3a1;
- animation: copyPulse 0.3s ease;
- }
-
- @keyframes copyPulse {
- 0% { transform: scale(1); }
- 50% { transform: scale(1.2); }
- 100% { transform: scale(1); }
- }
-
- .M3Unator-toggle-group {
- display: flex;
- gap: 1.25rem;
- margin: 1.5rem 0;
- justify-content: center;
- background: rgba(30, 30, 46, 0.4);
- padding: 1.25rem;
- border-radius: 16px;
- backdrop-filter: blur(8px);
- }
-
- .M3Unator-toggle-container {
- position: relative;
- }
-
- .M3Unator-toggle-container span {
- width: 64px;
- height: 64px;
- background: #1e1e2e;
- border: 2px solid #45475a;
- border-radius: 16px;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span {
- background: rgba(203, 166, 247, 0.1);
- border-color: #cba6f7;
- box-shadow: 0 0 20px rgba(203, 166, 247, 0.2);
- transform: translateY(-2px);
- }
-
- .M3Unator-toggle-container span:hover {
- background: #313244;
- transform: translateY(-2px);
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
- }
-
- .M3Unator-toggle-container svg {
- width: 32px;
- height: 32px;
- opacity: 0.7;
- transition: all 0.3s ease;
- }
-
- .M3Unator-toggle-container input[type="checkbox"]:checked + span svg {
- opacity: 1;
- color: #cba6f7;
- filter: drop-shadow(0 0 8px rgba(203, 166, 247, 0.4));
- }
-
- .M3Unator-progress {
- background: rgba(30, 30, 46, 0.6);
- border-radius: 12px;
- padding: 1rem;
- margin: 1rem 0;
- backdrop-filter: blur(8px);
- border: 1px solid rgba(203, 166, 247, 0.2);
- }
-
- .M3Unator-progress-text {
- color: #f5c2e7;
- font-weight: 600;
- text-align: center;
- margin-bottom: 0.5rem;
- font-size: 1.1rem;
- }
-
- .M3Unator-progress-spinner {
- width: 24px;
- height: 24px;
- border: 3px solid rgba(245, 194, 231, 0.1);
- border-top-color: #f5c2e7;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto;
- }
-
- .M3Unator-controls {
- display: flex;
- gap: 0.75rem;
- margin: 0.75rem 0;
- justify-content: center;
- }
-
- .M3Unator-control-btn {
- display: none;
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- font-weight: 600;
- font-size: 0.95rem;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- align-items: center;
- gap: 0.75rem;
- min-width: 160px;
- justify-content: center;
- background: rgba(30, 30, 46, 0.6);
- backdrop-filter: blur(8px);
- width: 160px;
- }
-
- .M3Unator-control-btn.pause {
- background: rgba(250, 179, 135, 0.1);
- border: 2px solid #fab387;
- color: #fab387;
- }
-
- .M3Unator-control-btn.resume {
- background: rgba(148, 226, 213, 0.1);
- border: 2px solid #94e2d5;
- color: #94e2d5;
- }
-
- .M3Unator-control-btn.cancel {
- background: rgba(243, 139, 168, 0.1);
- border: 2px solid #f38ba8;
- color: #f38ba8;
- }
-
- .M3Unator-control-btn:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
- }
-
- .M3Unator-control-btn svg {
- width: 20px;
- height: 20px;
- }
-
- .M3Unator-layers-icon {
- width: 20px;
- height: 20px;
- margin-right: 0.5rem;
- }
-
- .M3Unator-input:-webkit-autofill,
- .M3Unator-input:-webkit-autofill:hover,
- .M3Unator-input:-webkit-autofill:focus,
- .M3Unator-input:-webkit-autofill:active {
- -webkit-text-fill-color: #cdd6f4 !important;
- -webkit-box-shadow: 0 0 0 30px #1e1e2e inset !important;
- box-shadow: 0 0 0 30px #1e1e2e inset !important;
- background-color: #1e1e2e !important;
- color: #cdd6f4 !important;
- caret-color: #cdd6f4 !important;
- transition: background-color 5000s ease-in-out 0s !important;
- text-decoration: none !important;
- -webkit-text-decoration: none !important;
- }
-
- .M3Unator-input:-moz-autofill,
- .M3Unator-input:-moz-autofill-preview {
- background-color: #1e1e2e !important;
- color: #cdd6f4 !important;
- text-decoration: none !important;
- }
-
- .M3Unator-input:-ms-input-placeholder {
- background-color: #1e1e2e !important;
- color: #cdd6f4 !important;
- text-decoration: none !important;
- }
-
- .M3Unator-log-container {
- margin: 0;
- }
-
- .M3Unator-log-toggle {
- width: 100%;
- padding: 0.5rem 0.75rem;
- background: rgba(203, 166, 247, 0.05);
- border: none;
- color: #cdd6f4;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
- transition: all 0.2s ease;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-size: 0.875rem;
- font-weight: 500;
- border-radius: 6px;
- }
-
- .M3Unator-log-toggle:hover {
- background: rgba(203, 166, 247, 0.1);
- }
-
- .M3Unator-activity-indicator {
- width: 14px;
- height: 14px;
- border-radius: 50%;
- background: #45475a; /* Darker gray */
- margin-left: auto;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- .M3Unator-activity-indicator.active {
- background: #89dceb; /* Brighter blue */
- box-shadow: 0 0 0 3px rgba(137, 220, 235, 0.2);
- animation: pulseActive 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
-
- .M3Unator-activity-indicator.paused {
- background: #f9e2af; /* More visible yellow */
- box-shadow: 0 0 0 3px rgba(249, 226, 175, 0.2);
- }
-
- .M3Unator-activity-indicator.error {
- background: #f38ba8; /* Current red is good */
- box-shadow: 0 0 0 3px rgba(243, 139, 168, 0.2);
- }
-
- .M3Unator-activity-indicator.completed {
- background: #94e2d5; /* Brighter green-turquoise */
- box-shadow: 0 0 0 3px rgba(148, 226, 213, 0.2);
- }
-
- @keyframes pulseActive {
- 0% {
- box-shadow: 0 0 0 0 rgba(137, 220, 235, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 8px rgba(137, 220, 235, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(137, 220, 235, 0);
- }
- }
-
- @keyframes pulsePaused {
- 0% {
- box-shadow: 0 0 0 0 rgba(249, 226, 175, 0.4);
- }
- 70% {
- box-shadow: 0 0 0 8px rgba(249, 226, 175, 0);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(249, 226, 175, 0);
- }
- }
-
- @keyframes completeScale {
- 0% {
- transform: scale(0.8);
- opacity: 0.5;
- }
- 50% {
- transform: scale(1.2);
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
-
- .M3Unator-log-toggle:hover .M3Unator-activity-indicator {
- background: #6c7086;
- animation: none;
- }
-
- .M3Unator-log-toggle.active .M3Unator-activity-indicator {
- background: #6c7086;
- animation: none;
- }
-
- .M3Unator-log-toggle .toggle-text {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- .M3Unator-log {
- height: 0;
- max-height: 0;
- overflow: hidden;
- transition: all 0.3s ease;
- background: #11111b;
- padding: 0;
- border-top: none;
- margin: 0;
- }
-
- .M3Unator-log.expanded {
- height: auto;
- max-height: 300px;
- padding: 0.75rem;
- border-top: 1px solid #313244;
- overflow-y: auto;
- }
-
- .M3Unator-log-entry {
- padding: 0.25rem 0.5rem;
- border-bottom: 1px solid rgba(49, 50, 68, 0.5);
- font-size: 0.875rem;
- }
-
- .M3Unator-log-entry:last-child {
- border-bottom: none;
- }
-
- .M3Unator-log-time {
- color: #6c7086;
- margin-right: 0.5rem;
- }
-
- .M3Unator-log-entry.success {
- color: #94e2d5;
- }
-
- .M3Unator-log-entry.error {
- color: #f38ba8;
- }
-
- .M3Unator-log-entry.warning {
- color: #fab387;
- }
-
- .M3Unator-log-entry.info {
- color: #89b4fa;
- }
-
- .M3Unator-log-entry.final {
- color: #a6e3a1;
- font-weight: 500;
- }
-
- .M3Unator-log {
- margin-top: 0.75rem;
- max-height: calc(100vh - 70vh);
- font-size: 0.8125rem;
- line-height: 1.4;
- }
-
- .M3Unator-log-entry {
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- }
-
- .M3Unator-log-toggle {
- padding: 10px 12px;
- height: 42px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .M3Unator-log-counter {
- padding: 0.125rem 0.375rem;
- font-size: 0.75rem;
- border-radius: 4px;
- }
-
- .M3Unator-log-time {
- font-size: 0.75rem;
- opacity: 0.7;
- margin-right: 0.5rem;
- }
- `);
-
- GM_addStyle(`
- .M3Unator-popup {
- position: fixed;
- background: #11111b;
- color: #cdd6f4;
- width: 100%;
- max-width: 480px;
- border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
- overflow: hidden;
- animation: slideUp 0.3s ease;
- z-index: 9999;
- }
-
- .M3Unator-header {
- padding: 1rem 1.25rem;
- background: #1e1e2e;
- color: #cdd6f4;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: move;
- user-select: none;
- border-bottom: 1px solid #313244;
- }
-
- .M3Unator-container {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.75);
- backdrop-filter: blur(8px);
- display: none;
- place-items: center;
- z-index: 9999;
- }
- `);
-
- GM_addStyle(`
- /* Info Modal Styles */
- .info-modal {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.75);
- backdrop-filter: blur(8px);
- z-index: 10000;
- }
-
- .info-modal-content {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: #1e1e2e;
- border: 1px solid #45475a;
- border-radius: 12px;
- width: 90%;
- max-width: 600px;
- color: #cdd6f4;
- }
-
- .info-modal-header {
- padding: 1rem 1.5rem;
- border-bottom: 1px solid #45475a;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .info-modal-header h3 {
- margin: 0;
- color: #f5c2e7;
- font-size: 1.25rem;
- }
-
- .info-modal-body {
- padding: 1.5rem;
- line-height: 1.6;
- }
-
- .info-modal-body p {
- margin: 0 0 1rem;
- }
-
- .info-modal-body h4 {
- margin: 1.5rem 0 0.75rem;
- color: #f5c2e7;
- }
-
- .info-modal-body ul {
- margin: 0.75rem 0;
- padding-left: 1.5rem;
- }
-
- .info-modal-body li {
- margin: 0.5rem 0;
- }
-
- .info-modal-body a {
- color: #89b4fa;
- text-decoration: none;
- }
-
- .info-modal-body a:hover {
- text-decoration: underline;
- }
-
- .info-close {
- cursor: pointer;
- color: #6c7086;
- transition: color 0.2s ease;
- }
-
- .info-close:hover {
- color: #f5c2e7;
- }
- `);
-
- GM_addStyle(`
- .m3unator-input-group {
- position: relative;
- width: 100%;
- }
-
- .m3unator-input {
- width: 100%;
- padding-right: 80px !important;
- transition: all 0.2s ease;
- }
-
- .m3unator-dropdown {
- position: absolute;
- right: 8px;
- top: 50%;
- transform: translateY(-50%);
- display: none;
- z-index: 1;
- width: 70px;
- }
-
- .m3unator-dropdown.active {
- display: block;
- }
-
- .m3unator-dropdown-button {
- width: 100%;
- padding: 4px 8px;
- border-radius: 6px;
- background: rgba(30, 30, 46, 0.6);
- border: 1px solid rgba(69, 71, 90, 0.6);
- color: #f5c2e7;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 4px;
- transition: all 0.2s ease;
- }
-
- .m3unator-dropdown-button:hover {
- background: rgba(30, 30, 46, 0.8);
- border-color: rgba(69, 71, 90, 0.8);
- }
-
- .m3unator-dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- width: 100%;
- margin-top: 4px;
- background: rgba(30, 30, 46, 0.95);
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- padding: 4px;
- display: none;
- }
-
- .m3unator-dropdown.active .m3unator-dropdown-menu {
- display: block;
- }
-
- .m3unator-dropdown-item {
- padding: 0.618rem;
- color: #cdd6f4;
- cursor: pointer;
- transition: all 0.2s ease;
- user-select: none;
- }
-
- .m3unator-dropdown-item:hover {
- background: rgba(203, 166, 247, 0.1);
- }
-
- .m3unator-dropdown-item.selected {
- background: rgba(203, 166, 247, 0.1);
- color: #cba6f7;
- }
- `);
-
- GM_addStyle(`
- .M3Unator-container {
- max-width: 400px;
- width: 100%;
- background: none;
- backdrop-filter: none;
- }
-
- .M3Unator-popup {
- background: #1e1e2e;
- border-radius: 12px;
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
- border: 1px solid rgba(69, 71, 90, 0.6);
- }
-
- .M3Unator-content {
- padding: 0.75rem;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- max-width: 100%;
- overflow: hidden;
- background: none;
- }
-
- .M3Unator-header {
- padding: 0.75rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: none;
- border-bottom: 1px solid rgba(69, 71, 90, 0.6);
- }
-
- .M3Unator-input {
- width: 100%;
- min-width: 0;
- padding: 8px 80px 8px 12px;
- box-sizing: border-box;
- transition: all 0.2s ease;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- color: #f5c2e7;
- font-size: 14px;
- }
-
- .M3Unator-dropdown-button {
- width: 100%;
- padding: 4px 8px;
- border-radius: 6px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- color: #f5c2e7;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 4px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- font-size: 14px;
- font-family: monospace;
- }
-
- .M3Unator-dropdown-button span {
- min-width: 40px;
- text-align: left;
- }
-
- .M3Unator-dropdown-button svg {
- width: 16px;
- height: 16px;
- min-width: 16px;
- min-height: 16px;
- margin-left: auto;
- }
-
- .M3Unator-dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- width: 100%;
- margin-top: 4px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- padding: 4px;
- display: none;
- box-sizing: border-box;
- z-index: 9999;
- }
- `);
-
- GM_addStyle(`
- .M3Unator-content {
- padding: 0.75rem;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .M3Unator-toggle-group {
- margin: 0;
- display: flex;
- gap: 0.75rem;
- justify-content: center;
- background: rgba(30, 30, 46, 0.4);
- padding: 0.75rem;
- border-radius: 12px;
- }
-
- .M3Unator-button {
- margin: 0;
- }
-
- .M3Unator-log-container {
- margin: 0;
- }
-
- .M3Unator-stats-bar {
- margin: 0;
- }
- `);
-
- GM_addStyle(`
- /* Dropdown Styles */
- .M3Unator-dropdown {
- position: relative;
- display: none;
- }
-
- .M3Unator-dropdown-button {
- width: 100%;
- padding: 4px 8px;
- border-radius: 6px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- color: #f5c2e7;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 4px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- font-size: 14px;
- font-family: monospace;
- }
-
- .M3Unator-dropdown-button span {
- min-width: 40px;
- text-align: left;
- }
-
- .M3Unator-dropdown-button svg {
- width: 16px;
- height: 16px;
- min-width: 16px;
- min-height: 16px;
- margin-left: auto;
- transition: transform 0.2s ease;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-button svg {
- transform: rotate(180deg);
- }
-
- .M3Unator-dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- margin-top: 4px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- overflow: hidden;
- z-index: 1000;
- display: none;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-menu {
- display: block;
- }
-
- .M3Unator-dropdown-item {
- padding: 6px 12px;
- color: #f5c2e7;
- cursor: pointer;
- transition: all 0.2s ease;
- font-family: monospace;
- }
-
- .M3Unator-dropdown-item:hover {
- background: rgba(69, 71, 90, 0.3);
- }
-
- .M3Unator-dropdown-item:not(:last-child) {
- border-bottom: 1px solid rgba(69, 71, 90, 0.3);
- }
-
- /* Input Styles */
- .M3Unator-input {
- width: 100%;
- height: 42px;
- padding: 0 12px;
- border: 1px solid #45475a;
- border-radius: 8px;
- background: #1e1e2e;
- color: #f5c2e7;
- font-size: 14px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- }
-
- .M3Unator-input:focus {
- outline: none;
- border-color: #f5c2e7;
- box-shadow: 0 0 0 2px rgba(245, 194, 231, 0.1);
- }
-
- /* Button Styles */
- .M3Unator-button {
- height: 42px;
- padding: 0 16px;
- border: none;
- border-radius: 8px;
- background: #f5c2e7;
- color: #1e1e2e;
- font-weight: 600;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- }
-
- /* Toggle Container Styles */
- .M3Unator-toggle-container {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- transition: all 0.2s ease;
- }
-
- /* Control Button Styles */
- .M3Unator-control-btn {
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- font-weight: 600;
- font-size: 0.95rem;
- min-width: 160px;
- background: rgba(30, 30, 46, 0.6);
- backdrop-filter: blur(8px);
- }
-
- .M3Unator-control-btn.pause {
- border-color: #fab387;
- color: #fab387;
- }
-
- .M3Unator-control-btn.resume {
- border-color: #94e2d5;
- color: #94e2d5;
- }
-
- .M3Unator-control-btn.cancel {
- border-color: #f38ba8;
- color: #f38ba8;
- }
-
- /* Stats Styles */
- .M3Unator-stat {
- display: inline-flex;
- align-items: center;
- gap: 0.382rem;
- font-size: 0.875rem;
- cursor: help;
- min-width: 52px;
- padding: 0 0.382rem;
- }
- `);
-
- GM_addStyle(`
- .M3Unator-toast.success {
- color: #a6e3a1;
- border-color: #a6e3a1;
- background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(166, 227, 161, 0.1);
- }
-
- .M3Unator-toast.error {
- color: #f38ba8;
- border-color: #f38ba8;
- background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(243, 139, 168, 0.1);
- }
-
- .M3Unator-toast.warning {
- color: #fab387;
- border-color: #fab387;
- background: linear-gradient(rgba(17, 17, 27, 0.95), rgba(17, 17, 27, 0.95)), rgba(250, 179, 135, 0.1);
- }
- `);
-
- class LogCache {
- constructor(maxSize = 100) {
- this.maxSize = maxSize;
- this.logs = [];
- this.stats = {
- totalLogs: 0,
- skippedLogs: 0
- };
- }
-
- add(message, type = '') {
- const timestamp = new Date().toLocaleTimeString();
- this.logs.push({ message, type, timestamp });
- this.stats.totalLogs++;
-
- if (this.logs.length > this.maxSize) {
- this.logs.shift();
- this.stats.skippedLogs++;
- }
- }
-
- getSummary() {
- return {
- logs: [...this.logs],
- stats: { ...this.stats }
- };
- }
-
- clear() {
- this.logs = [];
- this.stats.totalLogs = 0;
- this.stats.skippedLogs = 0;
- }
- }
-
- class PlaylistGenerator {
- constructor() {
- this.initialStats = {
- directories: {
- total: 0,
- depth: 0
- },
- files: {
- video: {
- total: 0,
- current: 0
- },
- audio: {
- total: 0,
- current: 0
- }
- },
- errors: {
- total: 0,
- skipped: 0
- },
- totalFiles: 0
- };
-
- this.videoFormats = [
- '.mp4', '.mkv', '.avi', '.webm', '.mov', '.flv', '.wmv',
- '.m4v', '.mpg', '.mpeg', '.3gp', '.vob', '.ts', '.mts',
- '.m2ts', '.divx', '.xvid', '.asf', '.ogv', '.rm', '.rmvb',
- '.wtv', '.qt', '.hevc', '.f4v', '.swf', '.vro', '.ogx',
- '.drc', '.gifv', '.mxf', '.roq', '.nsv'
- ];
-
- this.audioFormats = [
- '.mp3', '.m4a', '.wav', '.flac', '.aac', '.ogg', '.wma',
- '.opus', '.aiff', '.ape', '.mka', '.ac3', '.dts', '.m4b',
- '.m4p', '.m4r', '.mid', '.midi', '.mp2', '.mpa', '.mpc',
- '.ra', '.tta', '.voc', '.vox', '.amr', '.awb', '.dsf',
- '.dff', '.alac', '.wv', '.oga', '.sln', '.aif', '.pcm'
- ];
-
- // Create Map for file extensions
- this.extensionMap = new Map();
-
- // Add video extensions to Map
- this.videoFormats.forEach(ext => {
- this.extensionMap.set(ext.slice(1), 'video'); // .mp4 -> mp4
- });
-
- // Add audio extensions to Map
- this.audioFormats.forEach(ext => {
- this.extensionMap.set(ext.slice(1), 'audio'); // .mp3 -> mp3
- });
-
- this.domElements = {};
-
- this.state = {
- isGenerating: false,
- isPaused: false,
- selectedFormat: 'm3u',
- includeVideo: false,
- includeAudio: false,
- maxEntries: 1000000,
- timeoutMs: 5000,
- retryCount: 2,
- maxDepth: 0,
- maxSeenUrls: 5000,
- stats: { ...this.initialStats }
- };
-
- this.sortOptions = { numeric: true, sensitivity: 'base' };
-
- this.entries = [];
- this.seenUrls = new Set();
- this.toastQueue = [];
- this.isProcessingToast = false;
-
- this.icons = {
- 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">
- <polygon points="23 7 16 12 23 17 23 7"/>
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
- </svg>`,
- 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">
- <path d="M9 18V5l12-2v13"/>
- <circle cx="6" cy="18" r="3"/>
- <circle cx="18" cy="16" r="3"/>
- </svg>`,
- 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">
- <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"/>
- </svg>`,
- 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">
- <circle cx="12" cy="12" r="10"/>
- <line x1="12" y1="16" x2="12" y2="12"/>
- <line x1="12" y1="8" x2="12.01" y2="8"/>
- </svg>`,
- 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">
- <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
- <polyline points="13 2 13 9 20 9"/>
- </svg>`,
- 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">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
- <polyline points="7 10 12 15 17 10"/>
- <line x1="12" y1="15" x2="12" y2="3"/>
- </svg>`,
- 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">
- <line x1="18" y1="6" x2="6" y2="18"/>
- <line x1="6" y1="6" x2="18" y2="18"/>
- </svg>`,
- 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">
- <rect x="6" y="4" width="4" height="16"/>
- <rect x="14" y="4" width="4" height="16"/>
- </svg>`,
- 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">
- <polygon points="5 3 19 12 5 21 5 3"/>
- </svg>`,
- 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">
- <circle cx="12" cy="12" r="10"/>
- <line x1="15" y1="9" x2="9" y2="15"/>
- <line x1="9" y1="9" x2="15" y2="15"/>
- </svg>`,
- 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">
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
- <polyline points="22 4 12 14.01 9 11.01"/>
- </svg>`,
- 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">
- <circle cx="12" cy="12" r="10"/>
- <line x1="12" y1="8" x2="12" y2="12"/>
- <line x1="12" y1="16" x2="12.01" y2="16"/>
- </svg>`,
- 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">
- <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"/>
- <line x1="12" y1="9" x2="12" y2="13"/>
- <line x1="12" y1="17" x2="12.01" y2="17"/>
- </svg>`,
- github: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
- <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"/>
- </svg>`,
- twitter: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
- <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"/>
- </svg>`,
- 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">
- <polyline points="6 9 12 15 18 9"/>
- </svg>`,
- 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">
- <polygon points="12 2 2 7 12 12 22 7 12 2"/>
- <polyline points="2 17 12 22 22 17"/>
- <polyline points="2 12 12 17 22 12"/>
- </svg>`,
- 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">
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
- </svg>`
- };
-
- this.templates = {
- toggleButton: (id, title, icon, checked = false) => `
- <div class="M3Unator-toggle-container">
- <label>
- <input type="checkbox" id="${id}" ${checked ? 'checked' : ''}>
- <span title="${title}">${icon}</span>
- </label>
- </div>
- `,
- controlButton: (type, icon, text) => `
- <button class="M3Unator-control-btn ${type}">
- ${icon}
- <span>${text}</span>
- </button>
- `,
- statsItem: (icon, id, title, className = '') => `
- <span class="M3Unator-stat ${className}" title="${title}">
- ${icon}
- <span id="${id}">0</span>
- </span>
- `
- };
-
- this.baseStyles = `
- .M3Unator-btn-base {
- border: none;
- border-radius: 8px;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- }
-
- .M3Unator-toggle-base {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- transition: all 0.2s ease;
- }
-
- .M3Unator-control-base {
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- font-weight: 600;
- font-size: 0.95rem;
- min-width: 160px;
- background: rgba(30, 30, 46, 0.6);
- backdrop-filter: blur(8px);
- }
-
- .M3Unator-stat-base {
- display: inline-flex;
- align-items: center;
- gap: 0.382rem;
- font-size: 0.875rem;
- cursor: help;
- min-width: 52px;
- padding: 0 0.382rem;
- }
-
- .M3Unator-icon-base {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- transition: all 0.2s ease;
- }
- `;
-
- GM_addStyle(this.baseStyles);
-
- this.updateActivityIndicator = (status) => {
- const indicator = this.domElements.activityIndicator;
- if (!indicator) return;
-
- // Remove all classes first
- indicator.classList.remove('active', 'paused', 'cancelled', 'completed');
-
- // Status check
- if (this.state.isGenerating) {
- if (this.state.isPaused) {
- indicator.classList.add('paused');
- } else {
- indicator.classList.add('active');
- }
- } else if (status === 'cancelled') {
- indicator.classList.add('cancelled');
- } else if (status === 'completed') {
- indicator.classList.add('completed');
- }
- };
-
- this.logCache = new LogCache(100);
- }
-
- createComponent(type, props) {
- switch (type) {
- case 'toggle':
- return this.templates.toggleButton(
- props.id,
- props.title,
- props.icon,
- props.checked
- );
- case 'control':
- return this.templates.controlButton(
- props.type,
- props.icon,
- props.text
- );
- case 'stats':
- return `
- <span class="M3Unator-stat ${props.class}" title="${props.title}">
- ${props.icon}
- <span id="${props.id}">0</span>
- </span>
- `;
- default:
- return '';
- }
- }
-
- async init() {
- const container = document.createElement('div');
- container.className = 'M3Unator-container';
-
- const toggleButtons = [
- {
- id: 'includeVideo',
- title: 'Video (.mp4, .mkv)',
- icon: this.icons.video,
- checked: true
- },
- {
- id: 'includeAudio',
- title: 'Audio (.mp3, .m4a)',
- icon: this.icons.audio,
- checked: true
- },
- {
- id: 'recursiveSearch',
- title: 'Scan Subdirectories',
- icon: this.icons.folder,
- checked: true
- }
- ].map(props => this.createComponent('toggle', props)).join('');
-
- const controlButtons = [
- {
- type: 'pause',
- icon: this.icons.pause,
- text: 'Pause'
- },
- {
- type: 'resume',
- icon: this.icons.resume,
- text: 'Resume'
- },
- {
- type: 'cancel',
- icon: this.icons.cancel,
- text: 'Cancel'
- }
- ].map(props => this.createComponent('control', props)).join('');
-
- const statsItems = [
- {
- icon: this.icons.file,
- id: 'totalFiles',
- title: 'Total Files',
- class: ''
- },
- {
- icon: this.icons.video,
- id: 'videoFiles',
- title: 'Video (.mp4, .mkv)',
- class: 'video'
- },
- {
- icon: this.icons.audio,
- id: 'audioFiles',
- title: 'Audio (.mp3, .m4a)',
- class: 'audio'
- },
- {
- icon: this.icons.folder,
- id: 'directories',
- title: 'Subdirectories',
- class: 'dir'
- },
- {
- icon: this.icons.layers,
- id: 'depthLevel',
- title: 'Depth Level',
- class: 'depth'
- },
- {
- icon: this.icons.error,
- id: 'errors',
- title: 'Error',
- class: 'error'
- }
- ].map(props => this.createComponent('stats', props)).join('');
-
- container.innerHTML = `
- <div class="M3Unator-popup">
- <div class="M3Unator-header">
- <h3 class="M3Unator-title">
- ${this.icons.video}
- <span>M3Unator</span>
- </h3>
- <div style="display: flex; align-items: center;">
- <div class="M3Unator-social">
- <a class="info-link">
- ${this.icons.info}
- </a>
- <a href="https://github.com/hasanbeder/M3Unator" target="_blank" rel="noopener noreferrer" class="github-icon">
- ${this.icons.github}
- </a>
- <a href="https://x.com/hasanbeder" target="_blank" rel="noopener noreferrer">
- ${this.icons.twitter}
- </a>
- </div>
- <button class="M3Unator-close">${this.icons.close}</button>
- </div>
- </div>
- <div class="info-modal">
- <div class="info-modal-content">
- <div class="info-modal-header">
- <h3>About M3Unator</h3>
- <span class="info-close">${this.icons.close}</span>
- </div>
- <div class="info-modal-body">
- <p><strong>M3Unator v1.0.2</strong> - The Ultimate Web Directory Playlist Creator</p>
- <p>Create M3U/M3U8 playlists effortlessly from any web directory. Experience ultrafast scanning and intelligent media detection.</p>
- <h4>Key Features:</h4>
- <ul>
- <li>⚡ Ultrafast directory scanning with parallel processing</li>
- <li>🎥 Comprehensive media support (MP4, MKV, MP3, FLAC, etc.)</li>
- <li>🔍 Smart recursive directory scanning</li>
- <li>🛡️ Enhanced error handling and stability</li>
- <li>🌙 Modern dark theme interface</li>
- </ul>
- <p>For updates and more information, visit the <a href="https://github.com/hasanbeder/M3Unator" target="_blank">GitHub repository</a>.</p>
- </div>
- </div>
- </div>
- <div class="M3Unator-content">
- <div class="M3Unator-input-row">
- <div class="M3Unator-input-group">
- <input type="text"
- id="playlistName"
- class="M3Unator-input"
- placeholder="Playlist Name"
- required
- spellcheck="false"
- autocomplete="off"
- autocorrect="off"
- autocapitalize="off">
-
- <div class="M3Unator-dropdown">
- <button type="button" class="M3Unator-dropdown-button">
- <span>.m3u</span>
- ${this.icons.chevronDown}
- </button>
- <div class="M3Unator-dropdown-menu">
- <div class="M3Unator-dropdown-item selected" data-value="m3u">.m3u</div>
- <div class="M3Unator-dropdown-divider"></div>
- <div class="M3Unator-dropdown-item" data-value="m3u8">.m3u8</div>
- </div>
- </div>
- </div>
- </div>
-
- <div class="M3Unator-toggle-group">
- ${toggleButtons}
- </div>
-
- <div class="M3Unator-depth-controls">
- <div class="M3Unator-radio-group">
- <label class="M3Unator-radio">
- <input type="radio" name="depthType" value="current" id="currentDepth">
- <span class="radio-mark"></span>
- <span class="radio-label">Current directory</span>
- </label>
- <label class="M3Unator-radio">
- <input type="radio" name="depthType" value="custom" id="customDepth">
- <span class="radio-mark"></span>
- <span class="radio-label">Custom depth:</span>
- <input type="number"
- id="maxDepth"
- value="1"
- min="1"
- max="99"
- class="M3Unator-depth-input"
- title="Subdirectory scan depth"
- style="width: 64px;"
- inputmode="numeric"
- pattern="[0-9]*">
- </label>
- </div>
- </div>
-
- <button class="M3Unator-button" id="generateBtn">
- ${this.icons.download}
- <span>Create Playlist</span>
- </button>
-
- <div class="M3Unator-controls">
- ${controlButtons}
- </div>
-
- <div class="M3Unator-stats-bar">
- <div class="M3Unator-stats">
- ${statsItems}
- </div>
- </div>
-
- <div class="M3Unator-log-container">
- <button class="M3Unator-log-toggle">
- <div class="toggle-text">
- ${this.icons.logToggle}
- <span>Recent Activity</span>
- </div>
- <div class="M3Unator-activity-indicator"></div>
- </button>
- <div id="scanLog" class="M3Unator-log collapsed"></div>
- </div>
- </div>
-
- <style>
- .M3Unator-container {
- max-width: 400px;
- width: 100%;
- background: transparent;
- backdrop-filter: none;
- }
-
- .M3Unator-popup {
- background: #1e1e2e;
- border-radius: 12px;
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
- border: 1px solid rgba(69, 71, 90, 0.6);
- }
-
- .info-modal {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.75);
- z-index: 99999;
- }
-
- .info-content {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: #1e1e2e;
- padding: 2rem;
- border-radius: 12px;
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- overflow-y: auto;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(69, 71, 90, 0.6);
- }
-
- .M3Unator-content {
- padding: 0.75rem;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- max-width: 100%;
- overflow: hidden;
- }
-
- .M3Unator-input-row {
- display: flex;
- width: 100%;
- position: relative;
- max-width: 100%;
- overflow: visible;
- }
-
- .M3Unator-input-group {
- flex: 1;
- min-width: 0;
- position: relative;
- }
-
- .M3Unator-input {
- width: 100%;
- min-width: 0;
- padding-right: 80px;
- box-sizing: border-box;
- transition: all 0.2s ease;
- background: rgba(30, 30, 46, 0.6);
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- color: #f5c2e7;
- padding: 8px 80px 8px 12px;
- font-size: 14px;
- }
-
- .M3Unator-dropdown {
- position: absolute;
- right: 8px;
- top: 50%;
- transform: translateY(-50%);
- width: 70px;
- z-index: 9999;
- display: none;
- }
-
- .M3Unator-dropdown.active {
- display: block;
- }
-
- .M3Unator-dropdown-button {
- width: 100%;
- padding: 4px 8px;
- border-radius: 6px;
- background: rgba(30, 30, 46, 0.8);
- border: 1px solid rgba(69, 71, 90, 0.6);
- color: #f5c2e7;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 4px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- font-size: 14px;
- }
-
- .M3Unator-dropdown-button:hover {
- background: rgba(30, 30, 46, 0.9);
- border-color: rgba(69, 71, 90, 0.8);
- }
-
- .M3Unator-dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- width: 100%;
- margin-top: 4px;
- background: rgba(30, 30, 46, 0.95);
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- padding: 4px;
- display: none;
- box-sizing: border-box;
- z-index: 9999;
- }
-
- .M3Unator-dropdown.active .M3Unator-dropdown-menu {
- display: block;
- }
-
- .M3Unator-dropdown-item {
- padding: 8px 12px;
- cursor: pointer;
- border-radius: 4px;
- transition: all 0.2s ease;
- text-align: center;
- font-size: 14px;
- font-family: monospace;
- color: #cdd6f4;
- }
-
- .M3Unator-dropdown-divider {
- height: 1px;
- background: rgba(69, 71, 90, 0.6);
- margin: 6px 0;
- }
-
- .M3Unator-dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
- width: 100%;
- margin-top: 4px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- border-radius: 6px;
- padding: 6px;
- display: none;
- box-sizing: border-box;
- z-index: 9999;
- }
-
- .M3Unator-dropdown-button {
- width: 100%;
- padding: 4px 8px;
- border-radius: 6px;
- background: #1e1e2e;
- border: 1px solid rgba(69, 71, 90, 0.6);
- color: #f5c2e7;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 4px;
- transition: all 0.2s ease;
- box-sizing: border-box;
- font-size: 14px;
- font-family: monospace;
- }
-
- .M3Unator-dropdown-item:hover {
- background: rgba(203, 166, 247, 0.1);
- color: #f5c2e7;
- }
-
- .M3Unator-dropdown-item.selected {
- background: rgba(203, 166, 247, 0.1);
- color: #cba6f7;
- }
- </style>
- </div>
- `;
-
- document.body.appendChild(container);
- const launcher = document.createElement('button');
- launcher.className = 'M3Unator-launcher';
- launcher.innerHTML = `
- ${this.icons.video}
- <span>M3Unator</span>
- `;
-
- document.body.appendChild(launcher);
- const popup = container.querySelector('.M3Unator-popup');
- const header = container.querySelector('.M3Unator-header');
- this.makeDraggable(popup, header);
- const statsBar = container.querySelector('.M3Unator-stats-bar');
- if (statsBar) {
- statsBar.style.display = 'block';
- }
-
- this.domElements = {
- container,
- popup: container.querySelector('.M3Unator-popup'),
- header: container.querySelector('.M3Unator-header'),
- closeBtn: container.querySelector('.M3Unator-close'),
- generateBtn: container.querySelector('#generateBtn'),
- playlistInput: container.querySelector('#playlistName'),
- includeVideo: container.querySelector('#includeVideo'),
- includeAudio: container.querySelector('#includeAudio'),
- recursiveSearch: container.querySelector('#recursiveSearch'),
- controls: container.querySelector('.M3Unator-controls'),
- scanLog: container.querySelector('#scanLog'),
- statsBar: container.querySelector('.M3Unator-stats-bar'),
- dropdown: container.querySelector('.M3Unator-dropdown'),
- launcher,
- stats: {
- totalFiles: container.querySelector('#totalFiles'),
- videoFiles: container.querySelector('#videoFiles'),
- audioFiles: container.querySelector('#audioFiles'),
- directories: container.querySelector('#directories'),
- depthLevel: container.querySelector('#depthLevel'),
- errors: container.querySelector('#errors')
- },
- depthControls: container.querySelector('.M3Unator-depth-controls'),
- currentDepth: container.querySelector('#currentDepth'),
- customDepth: container.querySelector('#customDepth'),
- maxDepth: container.querySelector('#maxDepth'),
- logToggle: container.querySelector('.M3Unator-log-toggle'),
- logCounter: container.querySelector('.M3Unator-log-counter'),
- activityIndicator: container.querySelector('.M3Unator-activity-indicator'),
- };
-
- launcher.onclick = () => {
- this.domElements.container.setAttribute('data-visible', 'true');
- const overlay = document.createElement('div');
- overlay.className = 'M3Unator-overlay';
- document.body.appendChild(overlay);
-
- const popup = this.domElements.popup;
- const rect = popup.getBoundingClientRect();
- const centerX = (window.innerWidth - rect.width) / 2;
- const centerY = (window.innerHeight - rect.height) / 2;
- popup.style.left = `${centerX}px`;
- popup.style.top = `${centerY}px`;
- };
-
- document.querySelector('.M3Unator-close').onclick = () => {
- if (this.state.isGenerating) {
- this.state.isGenerating = false;
- this.state.isPaused = false;
- this.reset({ isCancelled: true, enableToggles: true });
- this.showToast('Scan cancelled', 'warning');
- }
- this.domElements.container.removeAttribute('data-visible');
- const overlay = document.querySelector('.M3Unator-overlay');
- if (overlay) overlay.remove();
- };
-
- this.setupPopupHandlers();
-
- this.updateCounter(0);
-
- this.domElements.logToggle.addEventListener('click', () => {
- const log = this.domElements.scanLog;
- const toggle = this.domElements.logToggle;
-
- if (log.classList.contains('expanded')) {
- log.classList.remove('expanded');
- toggle.classList.remove('active');
- } else {
- log.classList.add('expanded');
- toggle.classList.add('active');
- log.scrollTop = log.scrollHeight;
- }
- });
-
- this.domElements.scanLog.classList.remove('expanded');
- this.domElements.logToggle.classList.remove('active');
-
- this.logCount = 0;
- }
-
- updateStyles() {
- GM_addStyle(`
- .M3Unator-toggle-container {
- @extend .M3Unator-toggle-base;
- }
-
- .M3Unator-control-btn {
- @extend .M3Unator-control-base;
- }
-
- .M3Unator-stat {
- @extend .M3Unator-stat-base;
- }
-
- .M3Unator-toggle-container span {
- @extend .M3Unator-icon-base;
- background: #1e1e2e;
- border: 2px solid #45475a;
- border-radius: 16px;
- }
-
- .M3Unator-control-btn.pause {
- border-color: #fab387;
- color: #fab387;
- }
-
- .M3Unator-control-btn.resume {
- border-color: #94e2d5;
- color: #94e2d5;
- }
-
- .M3Unator-control-btn.cancel {
- border-color: #f38ba8;
- color: #f38ba8;
- }
-
- `);
- }
-
- makeDraggable(element, handle) {
- let isDragging = false;
- let currentX;
- let currentY;
- let initialX;
- let initialY;
- let xOffset = 0;
- let yOffset = 0;
-
- const centerWindow = () => {
- const rect = element.getBoundingClientRect();
- const centerX = (window.innerWidth - rect.width) / 2;
- const centerY = (window.innerHeight - rect.height) / 2;
-
- element.style.left = `${centerX}px`;
- element.style.top = `${centerY}px`;
-
- xOffset = centerX;
- yOffset = centerY;
-
- element.style.transform = 'none';
- };
-
- centerWindow();
-
- const getPosition = (e) => {
- return {
- x: e.type.includes('touch') ? e.touches[0].clientX : e.clientX,
- y: e.type.includes('touch') ? e.touches[0].clientY : e.clientY
- };
- };
-
- const dragStart = (e) => {
- if (e.target === handle || handle.contains(e.target)) {
- e.preventDefault();
- const pos = getPosition(e);
-
- isDragging = true;
-
- const rect = element.getBoundingClientRect();
- xOffset = rect.left;
- yOffset = rect.top;
-
- initialX = pos.x - xOffset;
- initialY = pos.y - yOffset;
-
- handle.style.cursor = 'grabbing';
- }
- };
-
- const drag = (e) => {
- if (isDragging) {
- e.preventDefault();
- const pos = getPosition(e);
-
- currentX = pos.x - initialX;
- currentY = pos.y - initialY;
-
- const rect = element.getBoundingClientRect();
- const maxX = window.innerWidth - rect.width;
- const maxY = window.innerHeight - rect.height;
-
- currentX = Math.min(Math.max(0, currentX), maxX);
- currentY = Math.min(Math.max(0, currentY), maxY);
-
- element.style.left = `${currentX}px`;
- element.style.top = `${currentY}px`;
-
- xOffset = currentX;
- yOffset = currentY;
- }
- };
-
- const dragEnd = () => {
- if (isDragging) {
- isDragging = false;
- handle.style.cursor = 'grab';
- }
- };
-
- handle.addEventListener('mousedown', dragStart);
- document.addEventListener('mousemove', drag);
- document.addEventListener('mouseup', dragEnd);
-
- handle.addEventListener('touchstart', dragStart, { passive: false });
- document.addEventListener('touchmove', drag, { passive: false });
- document.addEventListener('touchend', dragEnd);
-
- window.addEventListener('resize', () => {
- if (!isDragging) {
- centerWindow();
- }
- });
-
- handle.style.cursor = 'grab';
- handle.style.userSelect = 'none';
- handle.style.touchAction = 'none';
-
- element.style.position = 'fixed';
- element.style.margin = '0';
- element.style.touchAction = 'none';
- element.style.transition = 'none';
- }
-
- showToast(message, type = 'success', duration = 3000) {
- let toastContainer = document.querySelector('.M3Unator-toast-container');
- if (!toastContainer) {
- toastContainer = document.createElement('div');
- toastContainer.className = 'M3Unator-toast-container';
- document.body.appendChild(toastContainer);
- }
-
- // Remove previous toasts
- const existingToasts = toastContainer.querySelectorAll('.M3Unator-toast');
- existingToasts.forEach(toast => {
- toast.classList.add('removing');
- setTimeout(() => toast.remove(), 300);
- });
-
- const toast = document.createElement('div');
- toast.className = `M3Unator-toast ${type}`;
-
- const icon = this.icons[type] || this.icons.info;
- toast.innerHTML = `${icon}<span>${message}</span>`;
-
- toastContainer.appendChild(toast);
-
- // Force a reflow to ensure the animation plays
- void toast.offsetWidth;
-
- // Add show class to trigger animation
- requestAnimationFrame(() => {
- toast.classList.add('show');
- });
-
- setTimeout(() => {
- toast.classList.add('removing');
- toast.classList.remove('show');
- setTimeout(() => {
- if (toast.parentNode === toastContainer) {
- toast.remove();
- }
- if (toastContainer.children.length === 0) {
- toastContainer.remove();
- }
- }, 300);
- }, duration);
- }
-
- setupPopupHandlers() {
- const generateBtn = this.domElements.generateBtn;
- const playlistInput = this.domElements.playlistInput;
- const includeVideo = this.domElements.includeVideo;
- const includeAudio = this.domElements.includeAudio;
- const recursiveSearch = this.domElements.recursiveSearch;
- const controls = this.domElements.controls;
-
- const dropdown = this.domElements.dropdown;
- const dropdownButton = dropdown.querySelector('.M3Unator-dropdown-button');
- const dropdownItems = dropdown.querySelectorAll('.M3Unator-dropdown-item');
-
- const controlButtons = controls.querySelectorAll('.M3Unator-control-btn');
- const pauseBtn = controlButtons[0];
- const resumeBtn = controlButtons[1];
- const cancelBtn = controlButtons[2];
-
- dropdownButton.addEventListener('click', () => {
- dropdown.classList.toggle('active');
- });
-
- document.addEventListener('click', (e) => {
- if (!dropdown.contains(e.target)) {
- dropdown.classList.remove('active');
- }
- });
-
- dropdownItems.forEach(item => {
- item.addEventListener('click', () => {
- dropdownItems.forEach(i => i.classList.remove('selected'));
- item.classList.add('selected');
- dropdownButton.querySelector('span').textContent = item.textContent;
- this.state.selectedFormat = item.dataset.value;
- dropdown.classList.remove('active');
- });
- });
-
- recursiveSearch.checked = true;
- this.state.recursiveSearch = true;
- includeVideo.checked = true;
- includeAudio.checked = true;
- this.state.includeVideo = true;
- this.state.includeAudio = true;
-
- includeVideo.addEventListener('change', (e) => {
- this.state.includeVideo = e.target.checked;
- this.addLogEntry(
- e.target.checked ?
- 'Video files will be included' :
- 'Video files will not be included',
- 'info'
- );
- });
-
- includeAudio.addEventListener('change', (e) => {
- this.state.includeAudio = e.target.checked;
- this.addLogEntry(
- e.target.checked ?
- 'Audio files will be included' :
- 'Audio files will not be included',
- 'info'
- );
- });
-
- const currentDepth = this.domElements.currentDepth;
- const customDepth = this.domElements.customDepth;
- const maxDepth = this.domElements.maxDepth;
- const depthControls = this.domElements.depthControls;
-
- depthControls.style.display = 'none';
- depthControls.classList.remove('active');
- this.state.maxDepth = -1;
-
- currentDepth.checked = true;
- customDepth.checked = false;
- maxDepth.disabled = true;
- maxDepth.value = '1';
-
- recursiveSearch.addEventListener('change', (e) => {
- if (!e.target.checked) {
- depthControls.style.display = 'block';
- depthControls.classList.add('active');
-
- currentDepth.checked = true;
- customDepth.checked = false;
- maxDepth.disabled = true;
- this.state.maxDepth = 0;
-
- this.addLogEntry('Directory scanning disabled, only current directory will be scanned', 'info');
- } else {
- depthControls.style.display = 'none';
- depthControls.classList.remove('active');
-
- this.state.maxDepth = -1;
- this.state.recursiveSearch = true;
-
- this.addLogEntry('Directory scanning active, all directories will be scanned', 'info');
- }
- });
-
- this.domElements.currentDepth.addEventListener('change', (e) => {
- if (e.target.checked && !recursiveSearch.checked) {
- this.state.maxDepth = 0;
- this.domElements.maxDepth.disabled = true;
- this.addLogEntry('Only current directory will be scanned', 'info');
- }
- });
-
- this.domElements.customDepth.addEventListener('change', (e) => {
- if (e.target.checked && !recursiveSearch.checked) {
- const depthValue = parseInt(this.domElements.maxDepth.value) || 1;
- this.state.maxDepth = depthValue;
- this.domElements.maxDepth.disabled = false;
- this.addLogEntry(
- `Directory scanning depth: ${depthValue} ` +
- `(current directory + ${depthValue} sublevels)`,
- 'info'
- );
- }
- });
-
- this.domElements.maxDepth.addEventListener('input', (e) => {
- if (this.domElements.customDepth.checked && !recursiveSearch.checked) {
- const value = Math.min(99, Math.max(1, parseInt(e.target.value) || 1));
- e.target.value = value;
- this.state.maxDepth = value;
- this.addLogEntry(
- `Directory scanning depth updated: ${value} ` +
- `(current directory + ${value} sublevels)`,
- 'info'
- );
- }
- });
-
- pauseBtn.addEventListener('click', () => {
- this.state.isPaused = true;
- this.updateActivityIndicator('paused');
- pauseBtn.style.display = 'none';
- resumeBtn.style.display = 'flex';
-
- generateBtn.innerHTML = `
- <div class="M3Unator-spinner" style="animation-play-state: paused;"></div>
- <span>Scan paused</span>
- `;
-
- this.showToast('Scan paused', 'warning');
- this.addLogEntry('Scan paused...', 'warning');
- });
-
- resumeBtn.addEventListener('click', () => {
- this.state.isPaused = false;
- this.updateActivityIndicator('active');
- resumeBtn.style.display = 'none';
- pauseBtn.style.display = 'flex';
-
- generateBtn.innerHTML = `
- <div class="M3Unator-spinner"></div>
- <span>Creating...</span>
- `;
-
- this.showToast('Scan resumed', 'success');
- this.addLogEntry('Scan in progress...', 'success');
- });
-
- cancelBtn.addEventListener('click', () => {
- this.state.isGenerating = false;
- this.state.isPaused = false;
- this.updateActivityIndicator('cancelled');
-
- setTimeout(() => {
- this.reset({ isCancelled: true, enableToggles: true });
- this.showToast('Scan cancelled', 'warning');
- }, 100);
- });
-
- generateBtn.addEventListener('click', async () => {
- const playlistName = this.sanitizeInput(playlistInput.value.trim());
-
- if (!playlistName) {
- this.showToast('Please enter a valid playlist name', 'warning');
- playlistInput.focus();
- return;
- }
-
- if (!this.state.includeVideo && !this.state.includeAudio) {
- this.showToast('Please select at least one media type', 'warning');
- return;
- }
-
- try {
- this.entries = [];
- this.seenUrls.clear();
- this.logCount = 0;
- if (this.domElements.scanLog) {
- this.domElements.scanLog.innerHTML = '';
- }
- this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
-
- this.state.isGenerating = true;
- this.state.isPaused = false;
- this.updateActivityIndicator('active');
- this.showToast('Scan started', 'success');
-
- generateBtn.disabled = true;
- generateBtn.innerHTML = `
- <div class="M3Unator-spinner"></div>
- <span>Creating...</span>
- `;
-
- this.domElements.includeVideo.disabled = true;
- this.domElements.includeAudio.disabled = true;
- this.domElements.recursiveSearch.disabled = true;
- this.domElements.currentDepth.disabled = true;
- this.domElements.customDepth.disabled = true;
- this.domElements.maxDepth.disabled = true;
-
- controls.style.display = 'flex';
- controls.classList.add('active');
-
- if (pauseBtn) {
- pauseBtn.style.display = 'flex';
- resumeBtn.style.display = 'none';
- cancelBtn.style.display = 'flex';
- }
-
- this.domElements.statsBar.style.display = 'block';
- this.domElements.statsBar.classList.add('active');
-
- const entries = await this.scanDirectory(window.location.href, '', 0);
-
- if (!this.state.isGenerating) {
- return;
- }
-
- if (entries.length === 0) {
- this.state.isGenerating = false;
- this.updateActivityIndicator('cancelled');
- this.showToast('No media files found', 'error');
- this.reset({ isCancelled: true });
- return;
- }
-
- this.addLogEntry(`Total ${entries.length} files found.`, 'success');
- this.updateCounter(entries.length);
-
- const content = this.createPlaylist(entries);
- const fileName = `${playlistName}.${this.state.selectedFormat}`;
-
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = fileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- this.showToast(`Playlist "${fileName}" created successfully`, 'success');
- this.updateActivityIndicator('completed');
- this.reset({ keepLogs: true, keepUI: true, enableToggles: true });
-
- } catch (error) {
- this.state.isGenerating = false;
- this.state.isPaused = false;
- this.updateActivityIndicator('cancelled');
- console.error('Error creating playlist:', error);
- this.addLogEntry(`Error: ${error.message}`, 'error');
- this.showToast('Error creating playlist', 'error');
- this.reset({ isCancelled: true });
- }
- });
- }
-
- reset(options = {}) {
- const {
- isCancelled = false,
- uiOnly = false,
- keepLogs = false,
- keepUI = false,
- enableToggles = false,
- wasGenerating = this.state.isGenerating
- } = options;
-
- // Save current state before updating
- const wasPaused = this.state.isPaused;
-
- this.state.isGenerating = false;
- this.state.isPaused = false;
-
- // Update activity indicator
- if (isCancelled || wasPaused) {
- this.updateActivityIndicator('cancelled');
- } else if (wasGenerating) {
- this.updateActivityIndicator('completed');
- } else {
- this.updateActivityIndicator(null);
- }
-
- if (!uiOnly) {
- this.entries = [];
- this.seenUrls.clear();
-
- if (!keepLogs) {
- this.logCount = 0;
- if (this.domElements.scanLog) {
- this.domElements.scanLog.innerHTML = '';
- }
- if (this.domElements.logCounter) {
- this.domElements.logCounter.textContent = '0';
- }
- }
-
- if (wasGenerating && !isCancelled) {
- const stats = this.domElements.stats;
- const summary = [
- `Scan completed:`,
- `• Video files: ${stats.videoFiles.textContent}`,
- `• Audio files: ${stats.audioFiles.textContent}`,
- `• Scanned directories: ${stats.directories.textContent}`,
- `• Maximum depth: ${stats.depthLevel.textContent}`,
- stats.errors.textContent > 0 ? `• Errors: ${stats.errors.textContent} (${this.state.stats.errors.skipped} skipped)` : null
- ].filter(Boolean).join('\n');
-
- this.addLogEntry(summary, 'final');
- }
- }
-
- const elements = this.domElements;
-
- if (elements.generateBtn) {
- elements.generateBtn.disabled = false;
- elements.generateBtn.innerHTML = `${this.icons.download}<span>Create Playlist</span>`;
- }
-
- if (elements.controls) {
- elements.controls.style.display = 'none';
- elements.controls.classList.remove('active');
-
- const pauseBtn = elements.controls.querySelector('.M3Unator-control-btn.pause');
- const resumeBtn = elements.controls.querySelector('.M3Unator-control-btn.resume');
- const cancelBtn = elements.controls.querySelector('.M3Unator-control-btn.cancel');
-
- if (pauseBtn) pauseBtn.style.display = 'none';
- if (resumeBtn) resumeBtn.style.display = 'none';
- if (cancelBtn) cancelBtn.style.display = 'none';
- }
-
- if (enableToggles) {
- if (elements.includeVideo) elements.includeVideo.disabled = false;
- if (elements.includeAudio) elements.includeAudio.disabled = false;
- if (elements.recursiveSearch) elements.recursiveSearch.disabled = false;
- if (elements.currentDepth) elements.currentDepth.disabled = false;
- if (elements.customDepth) elements.customDepth.disabled = false;
- if (elements.maxDepth) elements.maxDepth.disabled = elements.customDepth ? !elements.customDepth.checked : true;
- }
-
- if (uiOnly) return;
-
- if (isCancelled) {
- this.state.stats = JSON.parse(JSON.stringify(this.initialStats));
-
- if (!keepLogs) {
- if (elements.scanLog) {
- elements.scanLog.innerHTML = '';
- elements.scanLog.classList.add('collapsed');
- }
-
- if (elements.logToggle) {
- elements.logToggle.classList.remove('active');
- }
-
- if (this.domElements.stats) {
- Object.entries(this.domElements.stats).forEach(([key, element]) => {
- if (element) {
- element.textContent = '0';
- const statContainer = element.closest('.M3Unator-stat');
- if (statContainer) {
- statContainer.style.opacity = '0.5';
- if (key === 'depthLevel') {
- statContainer.dataset.progress = '';
- statContainer.title = 'Depth Level: 0';
- }
- }
- }
- });
- }
- }
- }
-
- if (elements.recursiveSearch) {
- elements.recursiveSearch.checked = true;
- this.state.recursiveSearch = true;
- this.state.maxDepth = -1;
- }
-
- if (elements.currentDepth) {
- elements.currentDepth.checked = false;
- }
-
- if (elements.customDepth) {
- elements.customDepth.checked = false;
- }
-
- if (elements.maxDepth) {
- elements.maxDepth.disabled = true;
- elements.maxDepth.value = '1';
- }
-
- if (elements.depthControls) {
- elements.depthControls.classList.remove('active');
- }
- }
-
- handleError(error, context = '') {
- let userMessage = 'An error occurred';
- let logMessage = error.message;
- let type = 'error';
-
- switch (true) {
- case error.name === 'AbortError':
- userMessage = 'Server not responding, operation timed out';
- logMessage = `Timeout: ${context}`;
- type = 'warning';
- break;
-
- case error.message.includes('HTTP error'):
- const status = error.message.match(/\d+/)?.[0];
- switch (status) {
- case '403':
- userMessage = 'Access denied to this directory';
- break;
- case '404':
- userMessage = 'Directory or file not found';
- break;
- case '429':
- userMessage = 'Too many requests, please wait a while';
- break;
- case '500':
- case '502':
- case '503':
- userMessage = 'Server is currently unable to respond, please try again later';
- break;
- default:
- userMessage = 'Error communicating with server';
- }
- logMessage = `${error.message} (${context})`;
- break;
-
- case error.message.includes('decode'):
- userMessage = 'Filename or path could not be read';
- logMessage = `Decode error: ${context} - ${error.message}`;
- type = 'warning';
- break;
-
- case error.message.includes('NetworkError'):
- userMessage = 'Network connection error, please check your connection';
- logMessage = `Network error: ${context}`;
- break;
-
- case error.message.includes('SecurityError'):
- userMessage = 'Operation not allowed due to security restrictions';
- logMessage = `Security error: ${context}`;
- break;
-
- default:
- userMessage = 'Unexpected error occurred';
- logMessage = `${error.name}: ${error.message} (${context})`;
- }
-
- console.error(`[${context}]`, error);
-
- this.showToast(userMessage, type);
-
- this.addLogEntry(logMessage, type);
-
- this.state.stats.errors.total++;
- }
-
- async fetchWithRetry(url, options = {}, retries = 3) {
- let lastError;
- for (let i = 0; i < retries; i++) {
- try {
- const response = await fetch(url, {
- ...options,
- headers: {
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
- 'Accept-Language': 'en-US,en;q=0.9',
- 'Connection': 'keep-alive',
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
- },
- timeout: options.timeout || 30000,
- signal: options.signal
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
- }
-
- const text = await response.text();
- return {
- decodedText: text,
- status: response.status,
- ok: response.ok
- };
- } catch (error) {
- lastError = error;
- if (error.name === 'AbortError' || error.message.includes('404')) {
- throw error; // Throw these errors immediately
- }
- if (i < retries - 1) {
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
- continue;
- }
- }
- }
- throw lastError;
- }
-
- sanitizeInput(input) {
- if (!input || typeof input !== 'string') {
- return '';
- }
-
- const sanitized = input
- .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '')
- .trim()
- .replace(/[\x00-\x1F\x7F]/g, '')
- .replace(/[\u200B-\u200D\uFEFF]/g, '')
- .replace(/[^\w\s\-_.()[\]{}#@!$%^&+=]/g, '');
-
- if (!sanitized) {
- return 'playlist';
- }
-
- if (sanitized.length > 255) {
- return sanitized.slice(0, 255);
- }
-
- return sanitized;
- }
-
- decodeString(str, type = 'both') {
- if (!str) return str;
-
- try {
- let decoded = str;
-
- if (type === 'html' || type === 'both') {
- decoded = decoded.replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, "'")
- .replace(/'/g, "'")
- .replace(///g, "/");
- }
-
- if (type === 'url' || type === 'both') {
- try {
- decoded = decodeURIComponent(decoded);
- } catch (e) {
- decoded = decoded.replace(/%([0-9A-F]{2})/gi, (match, hex) => {
- try {
- return String.fromCharCode(parseInt(hex, 16));
- } catch {
- return match;
- }
- });
- }
- }
-
- return decoded;
- } catch (error) {
- console.warn('Decode error:', error);
- return str;
- }
- }
-
- extractFileInfo(path) {
- try {
- const decodedPath = this.decodeString(path);
- const parts = decodedPath.split('/');
- const fileName = parts.pop() || '';
- const dirPath = parts.join('/');
-
- return {
- fileName,
- dirPath,
- original: {
- fileName: path.split('/').pop() || '',
- dirPath: path.split('/').slice(0, -1).join('/')
- }
- };
- } catch (error) {
- this.handleError(error, `Path decode error: ${path}`);
- const parts = path.split('/');
- return {
- fileName: parts.pop() || '',
- dirPath: parts.join('/'),
- original: {
- fileName: parts.pop() || '',
- dirPath: parts.join('/')
- }
- };
- }
- }
-
- normalizeUrl(url) {
- let normalized = url.replace(/([^:]\/)\/+/g, "$1");
- return normalized.endsWith('/') ? normalized : normalized + '/';
- }
-
- isMediaFile(fileName, type) {
- const lowerFileName = fileName.toLowerCase();
- return type === 'video'
- ? this.videoFormats.some(ext => lowerFileName.endsWith(ext))
- : this.audioFormats.some(ext => lowerFileName.endsWith(ext));
- }
-
- resetCurrentStats() {
- this.state.stats.files.video.current = 0;
- this.state.stats.files.audio.current = 0;
- }
-
- updateFileStats(type) {
- this.state.stats.files[type].total++;
- this.state.stats.files[type].current++;
- }
-
- getCurrentStatsText() {
- const { video, audio } = this.state.stats.files;
- const details = [];
-
- if (video.current > 0) details.push(`${video.current} video`);
- if (audio.current > 0) details.push(`${audio.current} audio`);
-
- return details.join(' and ');
- }
-
- async scanDirectory(url, currentPath = '', depth = 0) {
- try {
- this.resetCurrentStats();
-
- if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) {
- return this.entries;
- }
-
- while (this.state.isPaused && this.state.isGenerating) {
- await new Promise(resolve => setTimeout(resolve, 100));
- }
-
- // Encode special characters properly
- const normalizedUrl = this.normalizeUrl(url).replace(/#/g, '%23')
- .replace(/\s+/g, '%20')
- .replace(/\[/g, '%5B')
- .replace(/\]/g, '%5D')
- .replace(/'/g, '%27')
- .replace(/"/g, '%22');
-
- if (depth > this.state.stats.directories.depth) {
- this.state.stats.directories.depth = depth;
- }
-
- this.state.stats.directories.total++;
-
- this.addLogEntry(`Scanning directory (level ${depth}): ${decodeURIComponent(normalizedUrl)}`);
-
- if (this.seenUrls.has(normalizedUrl)) {
- this.addLogEntry(`This directory was previously scanned: ${decodeURIComponent(normalizedUrl)}`);
- return this.entries;
- }
-
- this.seenUrls.add(normalizedUrl);
- if (this.seenUrls.size > this.state.maxSeenUrls) {
- const keepCount = Math.floor(this.state.maxSeenUrls * 0.75);
- const urlsArray = Array.from(this.seenUrls);
- const keepUrls = urlsArray.slice(-keepCount);
- this.seenUrls = new Set(keepUrls);
- this.addLogEntry(
- `Cache cleared (${urlsArray.length} -> ${keepUrls.length})`,
- 'info'
- );
- }
-
- let response;
- try {
- response = await this.fetchWithRetry(normalizedUrl, {
- signal: null, // Remove cancel signal
- timeout: 30000 // 30 second timeout
- });
- } catch (error) {
- if (error.name === 'AbortError') {
- this.addLogEntry(`Request cancelled: ${decodeURIComponent(normalizedUrl)}`, 'warning');
- } else if (error.message.includes('404')) {
- this.addLogEntry(`Directory not found: ${decodeURIComponent(normalizedUrl)}`, 'warning');
- } else {
- this.addLogEntry(`Connection error: ${error.message}`, 'error');
- }
- return this.entries;
- }
-
- const html = response.decodedText;
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- const isLiteSpeed = doc.querySelector('div#table-list') !== null;
- let hrefs = [];
-
- if (isLiteSpeed) {
- const rows = doc.querySelectorAll('#table-content tr');
- rows.forEach(row => {
- const linkElement = row.querySelector('a');
- if (linkElement && !linkElement.textContent.includes('Parent Directory')) {
- const href = linkElement.getAttribute('href');
- if (href) hrefs.push(href);
- }
- });
- } else {
- const hrefRegex = /href="([^"]+)"/gi;
- const matches = html.matchAll(hrefRegex);
- hrefs = Array.from(matches, m => m[1]).filter(href =>
- href &&
- !href.startsWith('?') &&
- !href.startsWith('/') &&
- href !== '../' &&
- !href.includes('Parent Directory')
- );
- }
-
- // Separate directories and files
- const directories = [];
- const files = [];
-
- for (const href of hrefs) {
- if (href.endsWith('/')) {
- directories.push(href);
- } else {
- files.push(href);
- }
- }
-
- // Process files in batches
- const batchSize = 100; // Increased from 50 to 100
- for (let i = 0; i < files.length; i += batchSize) {
- if (!this.state.isGenerating || this.entries.length >= this.state.maxEntries) break;
-
- const batch = files.slice(i, i + batchSize);
- const batchProgress = {
- total: batch.length,
- processed: 0,
- success: 0,
- errors: 0
- };
-
- // Use Set for better performance
- const processedUrls = new Set();
-
- await Promise.all(batch.map(async href => {
- try {
- // Skip if URL was previously processed
- const fullUrl = new URL(href, normalizedUrl).toString();
- if (processedUrls.has(fullUrl)) return;
- processedUrls.add(fullUrl);
-
- const decodedHref = this.decodeString(href);
- const { fileName } = this.extractFileInfo(decodedHref);
- const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
-
- this.state.stats.totalFiles = (this.state.stats.totalFiles || 0) + 1;
-
- // Check file type using Map
- const mediaType = this.isMediaFileOptimized(fileName);
-
- if (mediaType && ((mediaType === 'video' && this.state.includeVideo) ||
- (mediaType === 'audio' && this.state.includeAudio))) {
- if (mediaType === 'video') {
- this.updateFileStats('video');
- } else {
- this.updateFileStats('audio');
- }
-
- this.entries.push({
- title: fullPath,
- url: fullUrl
- });
- batchProgress.success++;
- }
- } catch (error) {
- console.error('URL processing error:', error);
- this.state.stats.errors.total++;
- batchProgress.errors++;
- } finally {
- batchProgress.processed++;
-
- // Update progress every 20 operations (instead of 10)
- if (batchProgress.processed % 20 === 0 || batchProgress.processed === batchProgress.total) {
- const progress = Math.floor((batchProgress.processed / batchProgress.total) * 100);
- this.addLogEntry(
- `Batch Processing: ${progress}% (${batchProgress.processed}/${batchProgress.total}, ` +
- `Success: ${batchProgress.success}, Error: ${batchProgress.errors})`,
- 'info'
- );
- }
- }
- }));
-
- // Increase interval for memory cleanup
- if (i > 0 && i % (batchSize * 20) === 0) {
- global.gc && global.gc();
- }
- }
-
- // Scan directories in parallel
- const shouldScanSubdir =
- this.state.maxDepth === -1 ||
- (this.state.maxDepth > 0 && depth < this.state.maxDepth);
-
- if (shouldScanSubdir && directories.length > 0) {
- const parallelLimit = 15; // Parallel limit increased to 15
- const queue = [...directories];
- const activeRequests = new Set();
-
- while (queue.length > 0 || activeRequests.size > 0) {
- // Start new request if there are items in queue and active request limit is not reached
- while (queue.length > 0 && activeRequests.size < parallelLimit) {
- const dir = queue.shift();
- try {
- const decodedDir = this.decodeString(dir);
- const fullUrl = new URL(decodedDir, normalizedUrl).toString();
- const { fileName } = this.extractFileInfo(decodedDir);
- const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
-
- const promise = this.scanDirectory(fullUrl, fullPath, depth + 1)
- .finally(() => {
- activeRequests.delete(promise);
- });
-
- activeRequests.add(promise);
- this.addLogEntry(`Entering subdirectory: ${fullPath}`);
- } catch (error) {
- console.error('Directory scanning error:', error);
- this.state.stats.errors.total++;
- }
- }
-
- // Wait for one of the active requests to complete
- if (activeRequests.size > 0) {
- await Promise.race(activeRequests);
- }
- }
- }
-
- this.updateCounter(this.state.stats.totalFiles);
- return this.entries;
-
- } catch (error) {
- this.state.stats.errors.total++;
- this.addLogEntry(`Scan error (${currentPath || url}): ${error.message}`, 'error');
- return this.entries;
- }
- }
-
- createPlaylist(entries) {
- let content = '#EXTM3U\n';
-
- const decodedEntries = entries.map(entry => {
- try {
- let title = this.decodeString(entry.title);
- const depth = (title.match(/\//g) || []).length;
- const isVideo = this.videoFormats.some(ext => title.toLowerCase().endsWith(ext));
- const isAudio = this.audioFormats.some(ext => title.toLowerCase().endsWith(ext));
-
- return {
- ...entry,
- decodedTitle: title,
- depth: depth,
- isVideo: isVideo,
- isAudio: isAudio
- };
- } catch (error) {
- return {
- ...entry,
- decodedTitle: entry.title,
- depth: 0,
- isVideo: false,
- isAudio: false
- };
- }
- });
-
- const videoEntries = decodedEntries.filter(entry => entry.isVideo);
- const audioEntries = decodedEntries.filter(entry => entry.isAudio);
-
- const apacheSort = (a, b) => {
- if (a.depth !== b.depth) {
- return a.depth - b.depth;
- }
-
- const aStartsWithNumber = /^\d/.test(a.decodedTitle);
- const bStartsWithNumber = /^\d/.test(b.decodedTitle);
-
- if (aStartsWithNumber !== bStartsWithNumber) {
- return aStartsWithNumber ? -1 : 1;
- }
-
- return a.decodedTitle.localeCompare(b.decodedTitle, undefined, {
- numeric: true,
- sensitivity: 'base'
- });
- };
-
- const sortedVideoEntries = videoEntries.sort(apacheSort);
- const sortedAudioEntries = audioEntries.sort(apacheSort);
-
- const sortedEntries = [...sortedVideoEntries, ...sortedAudioEntries];
-
- sortedEntries.forEach(entry => {
- content += `#EXTINF:-1,${entry.decodedTitle}\n${entry.url}\n`;
- });
-
- return content;
- }
-
- addLogEntry(message, type = '') {
- if ((this.state.isPaused || !this.state.isGenerating) && type !== 'final') {
- return;
- }
-
- let decodedMessage = message;
- try {
- if (message.includes('http')) {
- const urlRegex = /(https?:\/\/[^\s]+)/g;
- decodedMessage = message.replace(urlRegex, (url) => {
- try {
- return decodeURIComponent(url);
- } catch (e) {
- return url;
- }
- });
- }
- } catch (error) {
- console.warn('Decode error:', error);
- }
-
- // Add only to cache
- this.logCache.add(decodedMessage, type);
-
- // Update UI every 10 logs
- if (this.logCache.stats.totalLogs % 10 === 0) {
- this.updateLogUI();
- }
- }
-
- updateLogUI() {
- const scanLog = this.domElements.scanLog;
- if (!scanLog) return;
-
- // Add throttle for performance
- if (this._updateLogUITimeout) {
- clearTimeout(this._updateLogUITimeout);
- }
-
- this._updateLogUITimeout = setTimeout(() => {
- requestAnimationFrame(() => {
- const wasAtBottom = Math.abs(scanLog.scrollHeight - scanLog.clientHeight - scanLog.scrollTop) < 50;
-
- // Use fragment to minimize DOM manipulation
- const fragment = document.createDocumentFragment();
-
- // Show last 50 logs (instead of 100)
- const recentLogs = this.logCache.logs.slice(-50);
-
- recentLogs.forEach(log => {
- const div = document.createElement('div');
- div.className = `M3Unator-log-entry ${log.type}`;
- div.innerHTML = `
- <span class="M3Unator-log-time">${log.timestamp}</span>
- <span class="M3Unator-log-content">${log.message}</span>
- `;
- fragment.appendChild(div);
- });
-
- scanLog.innerHTML = '';
- scanLog.appendChild(fragment);
-
- if (wasAtBottom) {
- scanLog.scrollTop = scanLog.scrollHeight;
- }
- });
- }, 100); // 100ms throttle
- }
-
- generateScanReport() {
- const stats = this.state.stats;
- const logCache = this.logCache;
-
- const summary = [
- `📊 Scan Summary`,
- `───────────────`,
- `📁 Total Files: ${stats.totalFiles}`,
- `🎥 Video Files: ${stats.files.video.total}`,
- `🎵 Audio Files: ${stats.files.audio.total}`,
- `📂 Directories: ${stats.directories.total}`,
- `↕️ Maximum Depth: ${stats.directories.depth}`,
- stats.errors.total > 0 ?
- `⚠️ Errors: ${stats.errors.total} (${stats.errors.skipped} skipped)` :
- null,
- ``,
- `📝 Log Statistics`,
- `───────────────`,
- `Total Logs: ${logCache.stats.totalLogs}`,
- logCache.stats.skippedLogs > 0 ?
- `Skipped Logs: ${logCache.stats.skippedLogs}` :
- null,
- ``,
- `🔍 Last ${logCache.maxSize} Log Entries`,
- `───────────────`,
- ...logCache.logs.map(log =>
- `[${log.timestamp}] ${log.type === 'error' ? '❌' :
- log.type === 'warning' ? '⚠️' :
- log.type === 'success' ? '✅' : 'ℹ️'} ${log.message}`)
- ].filter(Boolean).join('\n');
-
- return summary;
- }
-
- updateCounter(count) {
- if (!this.domElements.stats || !this.domElements.statsBar) {
- return;
- }
-
- const stats = this.state.stats;
- const elements = this.domElements.stats;
- const statsBar = this.domElements.statsBar;
-
- statsBar.style.display = 'block';
-
- const updates = {
- 'totalFiles': count,
- 'videoFiles': stats.files.video.total,
- 'audioFiles': stats.files.audio.total,
- 'directories': stats.directories.total,
- 'depthLevel': stats.directories.depth,
- 'errors': stats.errors.total
- };
-
- Object.entries(updates).forEach(([key, value]) => {
- const element = elements[key];
- if (element) {
- element.textContent = value;
-
- const statContainer = element.closest('.M3Unator-stat');
- if (statContainer) {
- statContainer.style.opacity = value > 0 ? '1' : '0.5';
-
- if (key === 'depthLevel') {
- const maxDepth = this.state.maxDepth || 0;
- if (maxDepth > 0) {
- const progress = (value / maxDepth) * 100;
- statContainer.dataset.progress =
- progress >= 100 ? 'high' :
- progress >= 75 ? 'medium' :
- progress >= 50 ? 'low' : '';
-
- statContainer.title = `Depth Level: ${value}/${maxDepth}`;
- } else {
- statContainer.dataset.progress = '';
- statContainer.title = `Depth Level: ${value}`;
- }
- }
- }
- }
- });
- }
-
- // Add new method for file type checking
- isMediaFileOptimized(fileName) {
- const extension = fileName.toLowerCase().split('.').pop();
- return this.extensionMap.get(extension);
- }
- }
-
- const generator = new PlaylistGenerator();
- generator.init();
-
- // Event listeners for info modal
- document.querySelector('.info-link').addEventListener('click', () => {
- document.querySelector('.info-modal').style.display = 'block';
- document.body.classList.add('modal-open');
- });
-
- document.querySelector('.info-close').addEventListener('click', () => {
- document.querySelector('.info-modal').style.display = 'none';
- document.body.classList.remove('modal-open');
- });
-
- window.addEventListener('click', (event) => {
- const modal = document.querySelector('.info-modal');
- if (event.target === modal) {
- modal.style.display = 'none';
- document.body.classList.remove('modal-open');
- }
- });
-
- // Event listener for playlist name input
- generator.domElements.playlistInput.addEventListener('input', (e) => {
- const dropdown = e.target.parentElement.querySelector('.M3Unator-dropdown');
- if (e.target.value.trim()) {
- dropdown.style.display = 'block';
- } else {
- dropdown.style.display = 'none';
- }
- });
- })();