1 line
No EOL
32 KiB
JavaScript
1 line
No EOL
32 KiB
JavaScript
const Utils={debounce:(e,t=300,s=!1)=>{let n;return function(...i){const a=s&&!n;clearTimeout(n),n=setTimeout(()=>{n=null,s||e.apply(this,i)},t),a&&e.apply(this,i)}},throttle:(e,t)=>{let s;return function(){const n=arguments,i=this;s||(e.apply(i,n),s=!0,setTimeout(()=>s=!1,t))}},animateOnScroll:(e,t={})=>{const s=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(e.target.classList.add("animate-in"),s.unobserve(e.target))})},{threshold:.1,rootMargin:"0px 0px -50px 0px",...t});e.forEach(e=>s.observe(e))}};class LoadingManager{constructor(){this.activeLoaders=new Set}show(e,t="Loading..."){const s=Date.now().toString(),n=document.createElement("div");return n.className="loading-overlay",n.setAttribute("data-loader-id",s),n.innerHTML=`\n <div class="loading-content">\n <div class="loading-spinner">\n <div class="spinner-ring"></div>\n <div class="spinner-ring"></div>\n <div class="spinner-ring"></div>\n </div>\n <p class="loading-text">${t}</p>\n </div>\n `,"string"==typeof e&&(e=document.querySelector(e)),e.style.position="relative",e.appendChild(n),this.activeLoaders.add(s),requestAnimationFrame(()=>{n.classList.add("loading-overlay--visible")}),s}hide(e,t){"string"==typeof e&&(e=document.querySelector(e));const s=e.querySelector(`[data-loader-id="${t}"]`);s&&(s.classList.add("loading-overlay--hiding"),this.activeLoaders.delete(t),setTimeout(()=>{s.parentNode&&e.removeChild(s)},300))}hideAll(){this.activeLoaders.forEach(e=>{const t=document.querySelector(`[data-loader-id="${e}"]`);t&&this.hide(t.parentNode,e)})}}class TableManager{constructor(e,t,s,n={}){this.container=document.querySelector(`#${e}`),this.searchInput=document.querySelector(`#${t}`),this.clearIcon=document.querySelector(`#${s}`),this.options={sortable:!0,filterable:!0,pagination:!1,itemsPerPage:50,...n},this.currentData={},this.filteredData={},this.currentSort={column:null,direction:"asc"},this.currentPage=1,this._setupSearchFunctionality(),this._setupKeyboardNavigation()}generateTable(e={}){if(!Object.keys(e).length)return void(this.container.innerHTML='<div class="empty-state"><p>No data available.</p></div>');this.currentData=e,this.filteredData={...e};const t=Object.keys(e[Object.keys(e)[0]]),s=this.container.id.replace("-container","");this.container.innerHTML=`\n <div class="table-wrapper">\n <table id="${s}" class="enhanced-table" aria-label="Service Comparison Table">\n <thead>\n <tr>\n <th class="sortable" data-column="service" tabindex="0" role="columnheader" aria-sort="none">\n <span>Service Name</span>\n <svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M12 5v14M5 12l7-7 7 7"/>\n </svg>\n </th>\n ${t.map(e=>`\n <th class="sortable" data-column="${e}" tabindex="0" role="columnheader" aria-sort="none">\n <span>${e}</span>\n <svg class="sort-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M12 5v14M5 12l7-7 7 7"/>\n </svg>\n </th>\n `).join("")}\n </tr>\n </thead>\n <tbody>\n ${this._generateTableRows(this.filteredData,t)}\n </tbody>\n </table>\n </div>\n ${this.options.pagination?this._createPagination():""}\n `,this._setupTableFeatures(),this._setupAccessibility()}_generateTableRows(e,t){return Object.entries(e).map(([e,s])=>`\n <tr data-host="${e.toLowerCase()}" role="row">\n <td class="service-cell" role="gridcell">\n <div class="service-info">\n <span class="service-name">${e}</span>\n </div>\n </td>\n ${t.map(e=>`\n <td class="status-cell" role="gridcell" data-status="${s[e]}">\n <span class="status-indicator ${"yes"===s[e]?"supported":"not-supported"}" \n aria-label="${"yes"===s[e]?"Supported":"Not supported"}">\n ${"yes"===s[e]?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline></svg>':'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>'}\n </span>\n </td>\n `).join("")}\n </tr>\n `).join("")}_setupSearchFunctionality(){const e=Utils.debounce(()=>{const e=this.searchInput.value.toLowerCase().trim();e?(this.filteredData={},Object.entries(this.currentData).forEach(([t,s])=>{t.toLowerCase().includes(e)&&(this.filteredData[t]=s)}),this.clearIcon.style.display="block"):(this.filteredData={...this.currentData},this.clearIcon.style.display="none"),this._updateTableContent(),this._updateSearchResults(e)},300);this.searchInput?.addEventListener("input",e),this.searchInput?.addEventListener("keydown",e=>{"Escape"===e.key&&this._clearSearch()}),this.clearIcon?.addEventListener("click",()=>{this._clearSearch()})}_clearSearch(){this.searchInput.value="",this.filteredData={...this.currentData},this.clearIcon.style.display="none",this._updateTableContent(),this._updateSearchResults("")}_updateSearchResults(e){const t=Object.keys(this.filteredData).length,s=Object.keys(this.currentData).length;let n=this.container.querySelector(".search-results");n||(n=document.createElement("div"),n.className="search-results",this.container.insertBefore(n,this.container.querySelector(".table-wrapper"))),e?(n.textContent=`Showing ${t} of ${s} services`,n.style.display="block"):n.style.display="none"}_setupTableFeatures(){this.options.sortable&&this.container.querySelectorAll("th.sortable").forEach(e=>{e.addEventListener("click",()=>this._handleSort(e)),e.addEventListener("keydown",t=>{"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),this._handleSort(e))})})}_handleSort(e){const t=e.getAttribute("data-column"),s="asc"===(this.currentSort.column===t?this.currentSort.direction:"asc")?"desc":"asc";this.currentSort={column:t,direction:s},this.container.querySelectorAll("th.sortable").forEach(e=>{e.setAttribute("aria-sort","none"),e.classList.remove("sorted-asc","sorted-desc")}),e.setAttribute("aria-sort","asc"===s?"ascending":"descending"),e.classList.add(`sorted-${s}`),this._sortData(),this._updateTableContent()}_sortData(){const{column:e,direction:t}=this.currentSort,s=Object.entries(this.filteredData);s.sort(([s,n],[i,a])=>{let r,o;return"service"===e?(r=s.toLowerCase(),o=i.toLowerCase()):(r="yes"===n[e]?1:0,o="yes"===a[e]?1:0),"asc"===t?r>o?1:r<o?-1:0:r<o?1:r>o?-1:0}),this.filteredData=Object.fromEntries(s)}_updateTableContent(){const e=this.container.querySelector("tbody"),t=Object.keys(this.currentData[Object.keys(this.currentData)[0]]);e&&(e.innerHTML=this._generateTableRows(this.filteredData,t),this._setupTableFeatures())}_setupKeyboardNavigation(){this.container.addEventListener("keydown",e=>{!e.target.matches("th.sortable")||"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),this._handleSort(e.target))})}_setupAccessibility(){const e=this.container.querySelector("table");e&&(e.setAttribute("role","grid"),e.setAttribute("aria-label","Debrid services comparison table"))}_createPagination(){return`\n <div class="pagination">\n <button class="pagination-btn" data-action="prev" aria-label="Previous page">Previous</button>\n <span class="pagination-info">Page ${this.currentPage}</span>\n <button class="pagination-btn" data-action="next" aria-label="Next page">Next</button>\n </div>\n `}}class ComparisonManager{constructor(e,t,s,n){this.container=document.querySelector(`#${e}`),this.select1=document.querySelector(`#${t}`),this.select2=document.querySelector(`#${s}`),this.data=n,this.isComparing=!1,this._populateDropdowns(),this._setupEventListeners(),this._setupKeyboardShortcuts()}_populateDropdowns(){const e=Object.keys(this.data[Object.keys(this.data)[0]]);[this.select1,this.select2].forEach((t,s)=>{t.innerHTML='<option value="">Loading services...</option>',setTimeout(()=>{t.innerHTML='<option value="">Choose a service...</option>',e.forEach(e=>{const s=document.createElement("option");s.value=e,s.textContent=e,t.appendChild(s)})},100*(s+1))})}_generateCompareTable(){const e=this.select1.value,t=this.select2.value;if(!e||!t)return void this._showEmptyState();if(e===t)return void this._showSameProviderWarning();this.isComparing=!0;const s=loadingManager.show(this.container,"Generating comparison...");setTimeout(()=>{this._renderComparisonTable(e,t),loadingManager.hide(this.container,s),this._trackComparison(e,t)},500)}_renderComparisonTable(e,t){const s=this._calculateComparisonStats(e,t);this.container.innerHTML=`\n <div class="comparison-header">\n <h3>Comparing ${e} vs ${t}</h3>\n <div class="comparison-stats">\n <div class="stat">\n <span class="stat-label">Shared Support</span>\n <span class="stat-value">${s.shared}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${e} Only</span>\n <span class="stat-value">${s.provider1Only}</span>\n </div>\n <div class="stat">\n <span class="stat-label">${t} Only</span>\n <span class="stat-value">${s.provider2Only}</span>\n </div>\n </div>\n <div class="comparison-actions">\n <button id="close-compare" class="btn btn-secondary">\n <span>Close</span>\n <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\n <path d="M18 6L6 18M6 6l12 12"/>\n </svg>\n </button>\n </div>\n </div>\n \n <div class="comparison-filters">\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="all" checked>\n <span>All Services</span>\n </label>\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="both">\n <span>Supported by Both</span>\n </label>\n <label class="filter-option">\n <input type="radio" name="comparison-filter" value="different">\n <span>Different Support</span>\n </label>\n </div>\n\n <div class="table-wrapper">\n <table id="compare-table" class="comparison-table" aria-label="Provider Comparison Table">\n <thead>\n <tr>\n <th>Service Name</th>\n <th class="provider-header ${e.toLowerCase()}">${e}</th>\n <th class="provider-header ${t.toLowerCase()}">${t}</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n ${this._generateComparisonRows(e,t)}\n </tbody>\n </table>\n </div>\n `,this.container.style.display="block",this._setupComparisonFeatures(),requestAnimationFrame(()=>{this.container.classList.add("comparison-visible")})}_generateComparisonRows(e,t){return Object.entries(this.data).map(([s,n])=>{const i="yes"===n[e],a="yes"===n[t];let r="",o="";return i&&a?(r="both-supported",o="Both"):i?(r="provider1-only",o=`${e} only`):a?(r="provider2-only",o=`${t} only`):(r="neither-supported",o="Neither"),`\n <tr class="comparison-row ${r}" data-status="${r}">\n <td class="service-name">${s}</td>\n <td class="support-status ${i?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${i?"Supported":"Not supported"}">\n ${i?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline></svg>':'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>'}\n </span>\n </td>\n <td class="support-status ${a?"supported":"not-supported"}">\n <span class="status-indicator" aria-label="${a?"Supported":"Not supported"}">\n ${a?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline></svg>':'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>'}\n </span>\n </td>\n <td class="status-text">\n <span class="status-badge ${r}">${o}</span>\n </td>\n </tr>\n `}).join("")}_calculateComparisonStats(e,t){let s=0,n=0,i=0;return Object.values(this.data).forEach(a=>{const r="yes"===a[e],o="yes"===a[t];r&&o?s++:r?n++:o&&i++}),{shared:s,provider1Only:n,provider2Only:i}}_setupComparisonFeatures(){this.container.querySelector("#close-compare")?.addEventListener("click",()=>{this._closeComparison()}),this.container.querySelectorAll('input[name="comparison-filter"]').forEach(e=>{e.addEventListener("change",e=>{this._filterComparison(e.target.value)})})}_filterComparison(e){this.container.querySelectorAll(".comparison-row").forEach(t=>{const s=t.getAttribute("data-status");let n=!0;switch(e){case"both":n="both-supported"===s;break;case"different":n="provider1-only"===s||"provider2-only"===s;break;default:n=!0}t.style.display=n?"":"none"});const t=this.container.querySelectorAll('.comparison-row:not([style*="display: none"])').length;let s=this.container.querySelector(".filter-results");s||(s=document.createElement("div"),s.className="filter-results",this.container.querySelector(".table-wrapper").insertAdjacentElement("beforebegin",s)),s.textContent=`Showing ${t} services`}_closeComparison(){this.container.classList.add("comparison-hiding"),setTimeout(()=>{this.container.style.display="none",this.container.classList.remove("comparison-visible","comparison-hiding"),this.isComparing=!1,this._showEmptyState()},300)}_showEmptyState(){this.container.innerHTML='\n <div class="empty-state">\n <div class="empty-state-icon">⚖️</div>\n <h3>Ready to Compare</h3>\n <p>Select two services above to see a detailed comparison</p>\n </div>\n ',this.container.style.display="block"}_showSameProviderWarning(){this.container.innerHTML='\n <div class="warning-state">\n <div class="warning-icon">⚠️</div>\n <h3>Same Service Selected</h3>\n <p>Please select two different services to compare</p>\n </div>\n ',this.container.style.display="block"}_setupEventListeners(){[this.select1,this.select2].forEach(e=>{e?.addEventListener("change",()=>{this._generateCompareTable()})})}_setupKeyboardShortcuts(){document.addEventListener("keydown",e=>{this.isComparing&&"Escape"===e.key&&this._closeComparison()})}_trackComparison(e,t){"undefined"!=typeof gtag&>ag("event","comparison_generated",{provider_1:e,provider_2:t})}}class PricingManager{constructor(e){this.container=document.querySelector(`#${e}-table-container`),this.currentData=null}generatePricingTable(e={}){if(!e.plans?.length)return void(this.container.innerHTML='<div class="error-state"><p>Error: Invalid pricing data structure.</p></div>');this.currentData=e;const t=Object.keys(e.plans[0]).filter(e=>"name"!==e);this.container.innerHTML=`\n <div class="table-wrapper">\n <table id="pricing-table" class="pricing-table" aria-label="Service Pricing Table">\n <thead>\n <tr>\n <th>Plans</th>\n ${t.map(e=>`\n <th class="service-column">\n <div class="service-header">\n <span class="service-name">${e}</span>\n </div>\n </th>\n `).join("")}\n </tr>\n </thead>\n <tbody>\n ${e.plans.map((e,s)=>`\n <tr class="pricing-row ${s%2==0?"even":"odd"}">\n <td class="plan-name">\n <strong>${e.name}</strong>\n </td>\n ${t.map(t=>{const s=e[t],n=!isNaN(parseFloat(s));return`\n <td class="price-cell ${n?"numeric-price":"text-price"}">\n <div class="price-content">\n <span class="price-value">${s}</span>\n ${n?'<span class="price-period">/month</span>':""}\n </div>\n </td>\n `}).join("")}\n </tr>\n `).join("")}\n </tbody>\n </table>\n </div>\n `,this._setupPricingFeatures()}_setupPricingFeatures(){this.container.querySelectorAll(".price-cell").forEach(e=>{e.addEventListener("mouseenter",()=>{const t=Array.from(e.parentNode.children).indexOf(e);this._highlightColumn(t)}),e.addEventListener("mouseleave",()=>{this._removeColumnHighlight()})})}_highlightColumn(e){this.container.querySelector("table").querySelectorAll(`td:nth-child(${e+1}), th:nth-child(${e+1})`).forEach(e=>e.classList.add("highlighted"))}_removeColumnHighlight(){this.container.querySelectorAll(".highlighted").forEach(e=>e.classList.remove("highlighted"))}}class PerformanceMonitor{constructor(){this.metrics={loadStart:performance.now(),domReady:null,fullyLoaded:null}}markDOMReady(){this.metrics.domReady=performance.now(),console.log(`DOM Ready: ${(this.metrics.domReady-this.metrics.loadStart).toFixed(2)}ms`)}markFullyLoaded(){this.metrics.fullyLoaded=performance.now(),console.log(`Fully Loaded: ${(this.metrics.fullyLoaded-this.metrics.loadStart).toFixed(2)}ms`)}getMetrics(){return{...this.metrics,totalLoadTime:this.metrics.fullyLoaded-this.metrics.loadStart,domLoadTime:this.metrics.domReady-this.metrics.loadStart}}}const loadingManager=new LoadingManager,performanceMonitor=new PerformanceMonitor;function setupURLComparison(){const e=new URLSearchParams(window.location.search),t=e.get("compare"),s=e.get("with");t&&s&&setTimeout(()=>{const e=document.querySelector("#provider1"),n=document.querySelector("#provider2");e&&n&&(e.value=t,n.value=s,e.dispatchEvent(new Event("change")))},1e3)}function setupScrollAnimations(){const e=document.querySelectorAll(".section, .card, .quick-nav-card");Utils.animateOnScroll(e)}function setupOfflineDetection(){window.addEventListener("online",()=>{console.log("Connection restored")}),window.addEventListener("offline",()=>{console.log("You are offline. Some features may not work.")})}document.addEventListener("DOMContentLoaded",async()=>{performanceMonitor.markDOMReady();try{const e=new TableManager("file-hosts-table-container","host-search-input","clear-host-search",{sortable:!0}),t=new TableManager("adult-hosts-table-container","adult-host-search-input","clear-adult-host-search",{sortable:!0}),s=new PricingManager("pricing"),n=loadingManager.show("#file-hosts-table-container","Loading file hosts..."),i=loadingManager.show("#adult-hosts-table-container","Loading adult hosts..."),a=loadingManager.show("#pricing-table-container","Loading pricing data..."),r=async(e,t=3)=>{for(let s=0;s<t;s++)try{const t=await fetch(e);if(!t.ok)throw new Error(`HTTP ${t.status}`);return await t.json()}catch(e){if(s===t-1)throw e;await new Promise(e=>setTimeout(e,1e3*(s+1)))}},[o,l,c]=await Promise.allSettled([r("./json/file-hosts.json"),r("./json/adult-hosts.json"),r("./json/pricing.json")]);"fulfilled"===o.status?(loadingManager.hide("#file-hosts-table-container",n),e.generateTable(o.value),new ComparisonManager("compare-table-container","provider1","provider2",o.value)):(loadingManager.hide("#file-hosts-table-container",n),console.error("File hosts error:",o.reason)),"fulfilled"===l.status?(loadingManager.hide("#adult-hosts-table-container",i),t.generateTable(l.value)):(loadingManager.hide("#adult-hosts-table-container",i),console.error("Adult hosts error:",l.reason)),"fulfilled"===c.status?(loadingManager.hide("#pricing-table-container",a),s.generatePricingTable(c.value)):(loadingManager.hide("#pricing-table-container",a),console.error("Pricing error:",c.reason)),setupURLComparison(),setupScrollAnimations(),setupOfflineDetection(),performanceMonitor.markFullyLoaded()}catch(e){console.error("Critical error initializing application:",e),loadingManager.hideAll()}}),window.addEventListener("error",e=>{console.error("Global error:",e.error)}),window.addEventListener("unhandledrejection",e=>{console.error("Unhandled promise rejection:",e.reason),e.preventDefault()});class TouchGestureManager{constructor(){this.touchStart=null,this.touchEnd=null,this.minSwipeDistance=100,this.maxSwipeTime=300,this.setupTouchListeners()}setupTouchListeners(){document.addEventListener("touchstart",e=>{this.touchStart={x:e.changedTouches[0].screenX,y:e.changedTouches[0].screenY,time:Date.now()}},{passive:!0}),document.addEventListener("touchend",e=>{this.touchStart&&(this.touchEnd={x:e.changedTouches[0].screenX,y:e.changedTouches[0].screenY,time:Date.now()},this.handleSwipe())},{passive:!0})}handleSwipe(){if(!this.touchStart||!this.touchEnd)return;const e=this.touchEnd.x-this.touchStart.x,t=this.touchEnd.y-this.touchStart.y;if(this.touchEnd.time-this.touchStart.time>this.maxSwipeTime)return;Math.sqrt(e*e+t*t)<this.minSwipeDistance||(Math.abs(e)>Math.abs(t)?e>0?this.dispatchSwipeEvent("swiperight"):this.dispatchSwipeEvent("swipeleft"):t>0?this.dispatchSwipeEvent("swipedown"):this.dispatchSwipeEvent("swipeup"))}dispatchSwipeEvent(e){document.dispatchEvent(new CustomEvent(e,{detail:{startPos:this.touchStart,endPos:this.touchEnd}}))}}class VirtualScrollManager{constructor(e,t=50){this.container=e,this.itemHeight=t,this.viewportHeight=e.clientHeight,this.totalItems=0,this.visibleStart=0,this.visibleEnd=0,this.scrollContent=null,this.renderCallback=null}init(e,t){this.totalItems=e.length,this.renderCallback=t,this.setupVirtualScroll(),this.updateVisibleRange()}setupVirtualScroll(){this.container.innerHTML=`\n <div class="virtual-scroll-content" style="height: ${this.totalItems*this.itemHeight}px;">\n <div class="virtual-scroll-viewport"></div>\n </div>\n `,this.scrollContent=this.container.querySelector(".virtual-scroll-viewport"),this.container.addEventListener("scroll",Utils.throttle(()=>{this.updateVisibleRange()},16))}updateVisibleRange(){const e=this.container.scrollTop,t=Math.ceil(this.viewportHeight/this.itemHeight);this.visibleStart=Math.max(0,Math.floor(e/this.itemHeight)-t),this.visibleEnd=Math.min(this.totalItems,this.visibleStart+3*t),this.renderVisibleItems()}renderVisibleItems(){if(!this.renderCallback)return;const e=document.createDocumentFragment();for(let t=this.visibleStart;t<this.visibleEnd;t++){const s=this.renderCallback(t);s.style.position="absolute",s.style.top=t*this.itemHeight+"px",s.style.height=`${this.itemHeight}px`,s.style.width="100%",e.appendChild(s)}this.scrollContent.innerHTML="",this.scrollContent.appendChild(e)}}class SearchManager{constructor(e,t){this.input=e,this.suggestionsContainer=t,this.currentIndex=-1,this.suggestions=[],this.setupEventListeners()}setupEventListeners(){this.input.addEventListener("input",Utils.debounce(e=>{this.handleInput(e.target.value)},300)),this.input.addEventListener("keydown",e=>{this.handleKeyDown(e)}),this.input.addEventListener("blur",()=>{setTimeout(()=>this.hideSuggestions(),200)}),document.addEventListener("click",e=>{this.input.contains(e.target)||this.suggestionsContainer.contains(e.target)||this.hideSuggestions()})}async handleInput(e){if(e.length<2)this.hideSuggestions();else try{this.suggestions=await this.fetchSuggestions(e),this.renderSuggestions()}catch(e){console.error("Error fetching suggestions:",e)}}async fetchSuggestions(e){return Object.keys(window.fileHostsData||{}).filter(t=>t.toLowerCase().includes(e.toLowerCase())).slice(0,5)}renderSuggestions(){0!==this.suggestions.length?(this.suggestionsContainer.innerHTML=this.suggestions.map((e,t)=>`\n <div class="search-suggestion" data-index="${t}">\n ${this.highlightMatch(e,this.input.value)}\n </div>\n `).join(""),this.suggestionsContainer.classList.add("visible"),this.currentIndex=-1,this.suggestionsContainer.querySelectorAll(".search-suggestion").forEach((e,t)=>{e.addEventListener("click",()=>{this.selectSuggestion(t)})})):this.hideSuggestions()}highlightMatch(e,t){const s=new RegExp(`(${t})`,"gi");return e.replace(s,"<strong>$1</strong>")}handleKeyDown(e){if(this.suggestionsContainer.classList.contains("visible"))switch(e.key){case"ArrowDown":e.preventDefault(),this.navigateSuggestions(1);break;case"ArrowUp":e.preventDefault(),this.navigateSuggestions(-1);break;case"Enter":e.preventDefault(),this.currentIndex>=0&&this.selectSuggestion(this.currentIndex);break;case"Escape":this.hideSuggestions()}}navigateSuggestions(e){const t=this.suggestionsContainer.querySelectorAll(".search-suggestion");this.currentIndex>=0&&t[this.currentIndex].classList.remove("highlighted"),this.currentIndex+=e,this.currentIndex<0&&(this.currentIndex=t.length-1),this.currentIndex>=t.length&&(this.currentIndex=0),t[this.currentIndex].classList.add("highlighted"),t[this.currentIndex].scrollIntoView({block:"nearest"})}selectSuggestion(e){this.input.value=this.suggestions[e],this.hideSuggestions(),this.input.dispatchEvent(new Event("input"))}hideSuggestions(){this.suggestionsContainer.classList.remove("visible"),this.currentIndex=-1}}class PullToRefreshManager{constructor(e,t){this.container=e,this.callback=t,this.isRefreshing=!1,this.startY=0,this.currentY=0,this.threshold=80,this.setupPullToRefresh()}setupPullToRefresh(){this.container.classList.add("pull-to-refresh");const e=document.createElement("div");e.className="pull-to-refresh-indicator",e.innerHTML='<div class="loading-spinner"></div>',this.container.appendChild(e),this.container.addEventListener("touchstart",e=>{0!==this.container.scrollTop||this.isRefreshing||(this.startY=e.touches[0].clientY)},{passive:!0}),this.container.addEventListener("touchmove",e=>{if(this.startY&&!this.isRefreshing){this.currentY=e.touches[0].clientY;const t=this.currentY-this.startY;if(t>0&&0===this.container.scrollTop){e.preventDefault();Math.min(t/this.threshold,1);t>this.threshold&&this.container.classList.add("pulling")}}},{passive:!1}),this.container.addEventListener("touchend",()=>{if(this.startY&&this.currentY&&!this.isRefreshing){this.currentY-this.startY>this.threshold&&this.triggerRefresh()}this.container.classList.remove("pulling"),this.startY=0,this.currentY=0},{passive:!0})}async triggerRefresh(){if(!this.isRefreshing){this.isRefreshing=!0;try{await this.callback()}catch(e){console.error("Refresh failed:",e)}finally{this.isRefreshing=!1}}}}class ToastManager{constructor(){this.container=this.createContainer(),this.toasts=new Map}createContainer(){let e=document.querySelector(".toast-container");return e||(e=document.createElement("div"),e.className="toast-container",document.body.appendChild(e)),e}show(e,t="info",s=3e3){const n=Date.now().toString(),i=this.createToast(e,t,n);return this.container.appendChild(i),this.toasts.set(n,i),s>0&&setTimeout(()=>{this.remove(n)},s),n}createToast(e,t,s){const n=document.createElement("div");n.className=`toast ${t}`,n.setAttribute("data-toast-id",s),n.innerHTML=`\n <div class="toast-content">\n <span class="toast-message">${e}</span>\n <button class="toast-close" aria-label="Close notification">×</button>\n </div>\n `;return n.querySelector(".toast-close").addEventListener("click",()=>{this.remove(s)}),n}remove(e){const t=this.toasts.get(e);t&&(t.style.animation="slideOutRight 0.3s ease forwards",setTimeout(()=>{t.parentNode&&this.container.removeChild(t),this.toasts.delete(e)},300))}success(e,t=3e3){return this.show(e,"success",t)}error(e,t=5e3){return this.show(e,"error",t)}warning(e,t=4e3){return this.show(e,"warning",t)}info(e,t=3e3){return this.show(e,"info",t)}}class MobileNavManager{constructor(){this.setupMobileNav()}setupMobileNav(){window.innerWidth<=768&&this.createMobileNav(),window.addEventListener("resize",Utils.debounce(()=>{window.innerWidth<=768?this.createMobileNav():this.removeMobileNav()},300))}createMobileNav(){if(document.querySelector(".mobile-nav-toggle"))return;const e=document.createElement("button");e.className="mobile-nav-toggle",e.innerHTML="☰",e.setAttribute("aria-label","Toggle mobile navigation");const t=document.createElement("div");t.className="mobile-nav-menu",t.innerHTML='\n <a href="#pricing" class="mobile-nav-item">Pricing</a>\n <a href="#file-hosts" class="mobile-nav-item">File Hosts</a>\n <a href="#compare" class="mobile-nav-item">Compare</a>\n <a href="#adult-hosts" class="mobile-nav-item">Adult Hosts</a>\n <a href="#live-status" class="mobile-nav-item">Status</a>\n ',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",()=>{t.classList.toggle("visible")}),document.addEventListener("click",s=>{e.contains(s.target)||t.contains(s.target)||t.classList.remove("visible")}),t.querySelectorAll(".mobile-nav-item").forEach(e=>{e.addEventListener("click",()=>{t.classList.remove("visible")})})}removeMobileNav(){const e=document.querySelector(".mobile-nav-toggle"),t=document.querySelector(".mobile-nav-menu");e&&e.remove(),t&&t.remove()}}class ProgressiveEnhancementManager{constructor(){this.features=new Map,this.detectFeatures()}detectFeatures(){this.features.set("intersectionObserver","IntersectionObserver"in window),this.features.set("webp",this.supportsWebP()),this.features.set("serviceWorker","serviceWorker"in navigator),this.features.set("touchEvents","ontouchstart"in window),this.features.set("pointerEvents","PointerEvent"in window),this.features.set("containerQueries",CSS.supports("container-type: inline-size"))}async supportsWebP(){return new Promise(e=>{const t=new Image;t.onload=t.onerror=()=>{e(2===t.height)},t.src="data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"})}hasFeature(e){return this.features.get(e)||!1}enhanceIfSupported(e,t){this.hasFeature(e)&&t()}}const touchGestureManager=new TouchGestureManager,toastManager=new ToastManager,mobileNavManager=new MobileNavManager,progressiveEnhancementManager=new ProgressiveEnhancementManager;if(document.addEventListener("DOMContentLoaded",()=>{const e=document.querySelector("#file-host-search"),t=document.createElement("div");t.className="search-suggestions",e&&(e.parentNode.classList.add("search-container"),e.parentNode.appendChild(t),new SearchManager(e,t));const s=document.querySelector("#adult-host-search"),n=document.createElement("div");if(n.className="search-suggestions",s&&(s.parentNode.classList.add("search-container"),s.parentNode.appendChild(n),new SearchManager(s,n)),progressiveEnhancementManager.hasFeature("touchEvents")){const e=document.querySelector("main");e&&new PullToRefreshManager(e,async()=>{toastManager.info("Refreshing data..."),await new Promise(e=>setTimeout(e,1e3)),window.location.reload()})}const i=document.createElement("div");i.className="scroll-indicator",i.innerHTML='<div class="scroll-indicator-bar"></div>',document.body.appendChild(i);window.addEventListener("scroll",Utils.throttle(()=>{const e=(document.body.scrollTop||document.documentElement.scrollTop)/(document.documentElement.scrollHeight-document.documentElement.clientHeight)*100;i.querySelector(".scroll-indicator-bar").style.width=e+"%"},16))}),document.addEventListener("swipeleft",e=>{console.log("Swiped left")}),document.addEventListener("swiperight",e=>{console.log("Swiped right")}),progressiveEnhancementManager.hasFeature("serviceWorker")&&navigator.serviceWorker.register("/sw.js").then(e=>{console.log("ServiceWorker registration successful"),e.addEventListener("updatefound",()=>{const t=e.installing;t.addEventListener("statechange",()=>{"installed"===t.state&&navigator.serviceWorker.controller&&toastManager.info("New version available! Refresh to update.",0)})})}).catch(e=>{console.log("ServiceWorker registration failed:",e)}),"PerformanceObserver"in window){const e=new PerformanceObserver(e=>{for(const t of e.getEntries())"largest-contentful-paint"===t.entryType&&console.log(`LCP: ${t.startTime}ms`),"first-input"===t.entryType&&console.log(`FID: ${t.processingStart-t.startTime}ms`)});try{e.observe({entryTypes:["largest-contentful-paint","first-input"]})}catch(e){console.log("Performance observation not supported")}} |