Complete modern redesign: responsive, accessible, feature-rich comparison tool
Co-authored-by: fynks <75840152+fynks@users.noreply.github.com>
This commit is contained in:
parent
b61212892f
commit
f8f927e8de
7 changed files with 1249 additions and 7 deletions
2
dist/css/styles-min.css
vendored
2
dist/css/styles-min.css
vendored
File diff suppressed because one or more lines are too long
507
dist/css/styles.css
vendored
507
dist/css/styles.css
vendored
|
|
@ -760,6 +760,10 @@ a:hover {
|
|||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--primary) var(--bg-secondary);
|
||||
/* Mobile-first enhancements */
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
|
|
@ -794,6 +798,32 @@ td {
|
|||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition-normal);
|
||||
/* Better touch targets for mobile */
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Enhanced mobile table interactions */
|
||||
@media (width <= 768px) {
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-size: var(--text-xs);
|
||||
min-height: 48px; /* Larger touch targets on mobile */
|
||||
}
|
||||
|
||||
/* Scroll snap for better mobile table navigation */
|
||||
th,
|
||||
td {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
/* Enhanced sticky column behavior on mobile */
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
box-shadow: 2px 0 4px rgb(0 0 0 / 0.1);
|
||||
border-right: 1px solid var(--border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
|
|
@ -1429,6 +1459,7 @@ select:focus {
|
|||
justify-content: center;
|
||||
padding: var(--space-12);
|
||||
color: var(--text-secondary);
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
|
|
@ -1441,6 +1472,44 @@ select:focus {
|
|||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
/* Modern skeleton loading states */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-secondary) 0%, var(--bg-tertiary) 50%, var(--bg-secondary) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 1em;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.skeleton-text:last-child {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: 120px;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
@ -1664,10 +1733,26 @@ select:focus {
|
|||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
/* Enhanced mobile experience */
|
||||
box-shadow: 0 -1px 0 var(--border-primary), 0 1px 0 var(--border-primary);
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
border-radius: 0;
|
||||
/* Enhanced scroll indicators for mobile */
|
||||
background:
|
||||
/* Left shadow */
|
||||
linear-gradient(90deg, var(--bg-primary) 70%, transparent),
|
||||
/* Right shadow */
|
||||
linear-gradient(90deg, transparent, var(--bg-primary) 70%),
|
||||
/* Left shadow cover */
|
||||
linear-gradient(90deg, var(--border-primary), transparent 16px),
|
||||
/* Right shadow cover */
|
||||
linear-gradient(270deg, var(--border-primary), transparent 16px);
|
||||
background-position: left center, right center, left center, right center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 100%, 20px 100%, 10px 100%, 10px 100%;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
table {
|
||||
|
|
@ -1879,4 +1964,426 @@ select:focus {
|
|||
--border-primary: #fff;
|
||||
--text-secondary: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== MODERN MOBILE-FIRST ENHANCEMENTS ===== */
|
||||
|
||||
/* Enhanced Touch Interactions */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
/* Touch device optimizations */
|
||||
button,
|
||||
.btn,
|
||||
[role="button"] {
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
padding: var(--space-3) var(--space-5);
|
||||
}
|
||||
|
||||
/* Remove hover effects on touch devices */
|
||||
.status-card:hover,
|
||||
tr:hover,
|
||||
.comparison-service:hover {
|
||||
background: initial;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Enhanced tap feedback */
|
||||
button:active,
|
||||
.btn:active,
|
||||
[role="button"]:active {
|
||||
transform: scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modern Card-Based Mobile Layout */
|
||||
@media (width <= 768px) {
|
||||
/* Transform tables to cards on mobile when preferred */
|
||||
.mobile-card-view .table-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-card-view .cards-container {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--border-primary);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.service-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.service-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-4);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.service-card-title {
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.service-card-providers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.provider-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: var(--space-2);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.provider-support {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pull-to-Refresh Enhancement */
|
||||
@media (width <= 768px) {
|
||||
.pull-to-refresh {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.pull-to-refresh-indicator {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: var(--space-4);
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-lg);
|
||||
opacity: 0;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.pull-to-refresh.pulling .pull-to-refresh-indicator {
|
||||
opacity: 1;
|
||||
top: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Search Experience */
|
||||
.search-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: var(--z-dropdown);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-suggestions.visible {
|
||||
display: block;
|
||||
animation: slideInDown 0.2s ease;
|
||||
}
|
||||
|
||||
.search-suggestion {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-suggestion:hover,
|
||||
.search-suggestion.highlighted {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.search-suggestion:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modern Loading Button States */
|
||||
.btn.loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -8px 0 0 -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: var(--radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Enhanced Focus Management */
|
||||
.focus-trap {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: var(--space-4);
|
||||
right: var(--space-4);
|
||||
z-index: var(--z-modal);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
margin-bottom: var(--space-3);
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 300px;
|
||||
pointer-events: auto;
|
||||
animation: slideInRight 0.3s ease;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-left: 4px solid var(--error);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
border-left: 4px solid var(--warning);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
border-left: 4px solid var(--info);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Virtual Scrolling Container */
|
||||
.virtual-scroll-container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.virtual-scroll-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.virtual-scroll-item {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Performance Optimizations */
|
||||
.gpu-accelerated {
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.contain-layout {
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.contain-style {
|
||||
contain: style;
|
||||
}
|
||||
|
||||
.contain-size {
|
||||
contain: size;
|
||||
}
|
||||
|
||||
/* Enhanced Mobile Navigation */
|
||||
@media (width <= 768px) {
|
||||
.mobile-nav-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: var(--space-6);
|
||||
right: var(--space-6);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-xl);
|
||||
z-index: var(--z-floating);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.mobile-nav-toggle:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.mobile-nav-menu {
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
right: var(--space-6);
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 1px solid var(--border-primary);
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(20px);
|
||||
transition: all var(--transition-normal);
|
||||
pointer-events: none;
|
||||
z-index: var(--z-floating);
|
||||
}
|
||||
|
||||
.mobile-nav-menu.visible {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: block;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.mobile-nav-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.mobile-nav-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Container Queries for Component-Based Responsive Design */
|
||||
@supports (container-type: inline-size) {
|
||||
.comparison-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (width < 500px) {
|
||||
.comparison-selectors {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.compare-vs {
|
||||
order: -1;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Modern Scroll Indicators */
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--bg-secondary);
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
|
||||
.scroll-indicator-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary), var(--primary-light));
|
||||
transition: width var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Enhanced Empty States */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-16) var(--space-8);
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: var(--space-6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.6;
|
||||
max-width: 400px;
|
||||
}
|
||||
22
dist/index.html
vendored
22
dist/index.html
vendored
|
|
@ -3,10 +3,28 @@
|
|||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
|
||||
<!-- Progressive Web App Enhancements -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="Debrid Compare">
|
||||
<meta name="application-name" content="Debrid Compare">
|
||||
<meta name="msapplication-TileColor" content="#6366f1">
|
||||
<meta name="msapplication-config" content="browserconfig.xml">
|
||||
|
||||
<!-- Touch Icons for Better Mobile Experience -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="images/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="images/apple-touch-icon-120x120.png">
|
||||
<link rel="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Enhanced Performance and Mobile Optimization -->
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com">
|
||||
|
||||
<title>Debrid Services Comparison - Compare Pricing & Hosts</title>
|
||||
|
||||
<!-- Preload important resources -->
|
||||
|
|
|
|||
2
dist/js/app-min.js
vendored
2
dist/js/app-min.js
vendored
File diff suppressed because one or more lines are too long
652
dist/js/app.js
vendored
652
dist/js/app.js
vendored
|
|
@ -934,4 +934,654 @@ window.addEventListener('error', (event) => {
|
|||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== MODERN MOBILE-FIRST ENHANCEMENTS =====
|
||||
|
||||
// Enhanced Touch Gesture Support
|
||||
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: true });
|
||||
|
||||
document.addEventListener('touchend', (e) => {
|
||||
if (!this.touchStart) return;
|
||||
|
||||
this.touchEnd = {
|
||||
x: e.changedTouches[0].screenX,
|
||||
y: e.changedTouches[0].screenY,
|
||||
time: Date.now()
|
||||
};
|
||||
|
||||
this.handleSwipe();
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
handleSwipe() {
|
||||
if (!this.touchStart || !this.touchEnd) return;
|
||||
|
||||
const deltaX = this.touchEnd.x - this.touchStart.x;
|
||||
const deltaY = this.touchEnd.y - this.touchStart.y;
|
||||
const deltaTime = this.touchEnd.time - this.touchStart.time;
|
||||
|
||||
if (deltaTime > this.maxSwipeTime) return;
|
||||
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
if (distance < this.minSwipeDistance) return;
|
||||
|
||||
// Determine swipe direction
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// Horizontal swipe
|
||||
if (deltaX > 0) {
|
||||
this.dispatchSwipeEvent('swiperight');
|
||||
} else {
|
||||
this.dispatchSwipeEvent('swipeleft');
|
||||
}
|
||||
} else {
|
||||
// Vertical swipe
|
||||
if (deltaY > 0) {
|
||||
this.dispatchSwipeEvent('swipedown');
|
||||
} else {
|
||||
this.dispatchSwipeEvent('swipeup');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchSwipeEvent(type) {
|
||||
document.dispatchEvent(new CustomEvent(type, {
|
||||
detail: {
|
||||
startPos: this.touchStart,
|
||||
endPos: this.touchEnd
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual Scrolling for Large Datasets
|
||||
class VirtualScrollManager {
|
||||
constructor(container, itemHeight = 50) {
|
||||
this.container = container;
|
||||
this.itemHeight = itemHeight;
|
||||
this.viewportHeight = container.clientHeight;
|
||||
this.totalItems = 0;
|
||||
this.visibleStart = 0;
|
||||
this.visibleEnd = 0;
|
||||
this.scrollContent = null;
|
||||
this.renderCallback = null;
|
||||
}
|
||||
|
||||
init(items, renderCallback) {
|
||||
this.totalItems = items.length;
|
||||
this.renderCallback = renderCallback;
|
||||
this.setupVirtualScroll();
|
||||
this.updateVisibleRange();
|
||||
}
|
||||
|
||||
setupVirtualScroll() {
|
||||
this.container.innerHTML = `
|
||||
<div class="virtual-scroll-content" style="height: ${this.totalItems * this.itemHeight}px;">
|
||||
<div class="virtual-scroll-viewport"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.scrollContent = this.container.querySelector('.virtual-scroll-viewport');
|
||||
|
||||
this.container.addEventListener('scroll', Utils.throttle(() => {
|
||||
this.updateVisibleRange();
|
||||
}, 16)); // 60fps
|
||||
}
|
||||
|
||||
updateVisibleRange() {
|
||||
const scrollTop = this.container.scrollTop;
|
||||
const buffer = Math.ceil(this.viewportHeight / this.itemHeight);
|
||||
|
||||
this.visibleStart = Math.max(0, Math.floor(scrollTop / this.itemHeight) - buffer);
|
||||
this.visibleEnd = Math.min(this.totalItems, this.visibleStart + buffer * 3);
|
||||
|
||||
this.renderVisibleItems();
|
||||
}
|
||||
|
||||
renderVisibleItems() {
|
||||
if (!this.renderCallback) return;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = this.visibleStart; i < this.visibleEnd; i++) {
|
||||
const item = this.renderCallback(i);
|
||||
item.style.position = 'absolute';
|
||||
item.style.top = `${i * this.itemHeight}px`;
|
||||
item.style.height = `${this.itemHeight}px`;
|
||||
item.style.width = '100%';
|
||||
fragment.appendChild(item);
|
||||
}
|
||||
|
||||
this.scrollContent.innerHTML = '';
|
||||
this.scrollContent.appendChild(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Search with Debouncing and Suggestions
|
||||
class SearchManager {
|
||||
constructor(input, suggestionsContainer) {
|
||||
this.input = input;
|
||||
this.suggestionsContainer = suggestionsContainer;
|
||||
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) => {
|
||||
if (!this.input.contains(e.target) && !this.suggestionsContainer.contains(e.target)) {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async handleInput(query) {
|
||||
if (query.length < 2) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.suggestions = await this.fetchSuggestions(query);
|
||||
this.renderSuggestions();
|
||||
} catch (error) {
|
||||
console.error('Error fetching suggestions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSuggestions(query) {
|
||||
// In a real app, this would fetch from an API
|
||||
// For now, we'll filter from existing data
|
||||
const allHosts = Object.keys(window.fileHostsData || {});
|
||||
return allHosts
|
||||
.filter(host => host.toLowerCase().includes(query.toLowerCase()))
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
renderSuggestions() {
|
||||
if (this.suggestions.length === 0) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestionsContainer.innerHTML = this.suggestions
|
||||
.map((suggestion, index) => `
|
||||
<div class="search-suggestion" data-index="${index}">
|
||||
${this.highlightMatch(suggestion, this.input.value)}
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
this.suggestionsContainer.classList.add('visible');
|
||||
this.currentIndex = -1;
|
||||
|
||||
// Add click listeners
|
||||
this.suggestionsContainer.querySelectorAll('.search-suggestion').forEach((el, index) => {
|
||||
el.addEventListener('click', () => {
|
||||
this.selectSuggestion(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
highlightMatch(text, query) {
|
||||
const regex = new RegExp(`(${query})`, 'gi');
|
||||
return text.replace(regex, '<strong>$1</strong>');
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (!this.suggestionsContainer.classList.contains('visible')) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.navigateSuggestions(1);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.navigateSuggestions(-1);
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.currentIndex >= 0) {
|
||||
this.selectSuggestion(this.currentIndex);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.hideSuggestions();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
navigateSuggestions(direction) {
|
||||
const suggestions = this.suggestionsContainer.querySelectorAll('.search-suggestion');
|
||||
|
||||
// Remove current highlight
|
||||
if (this.currentIndex >= 0) {
|
||||
suggestions[this.currentIndex].classList.remove('highlighted');
|
||||
}
|
||||
|
||||
// Update index
|
||||
this.currentIndex += direction;
|
||||
if (this.currentIndex < 0) this.currentIndex = suggestions.length - 1;
|
||||
if (this.currentIndex >= suggestions.length) this.currentIndex = 0;
|
||||
|
||||
// Add new highlight
|
||||
suggestions[this.currentIndex].classList.add('highlighted');
|
||||
suggestions[this.currentIndex].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
selectSuggestion(index) {
|
||||
this.input.value = this.suggestions[index];
|
||||
this.hideSuggestions();
|
||||
this.input.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
hideSuggestions() {
|
||||
this.suggestionsContainer.classList.remove('visible');
|
||||
this.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Pull-to-Refresh for Mobile
|
||||
class PullToRefreshManager {
|
||||
constructor(container, callback) {
|
||||
this.container = container;
|
||||
this.callback = callback;
|
||||
this.isRefreshing = false;
|
||||
this.startY = 0;
|
||||
this.currentY = 0;
|
||||
this.threshold = 80;
|
||||
this.setupPullToRefresh();
|
||||
}
|
||||
|
||||
setupPullToRefresh() {
|
||||
this.container.classList.add('pull-to-refresh');
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'pull-to-refresh-indicator';
|
||||
indicator.innerHTML = '<div class="loading-spinner"></div>';
|
||||
this.container.appendChild(indicator);
|
||||
|
||||
this.container.addEventListener('touchstart', (e) => {
|
||||
if (this.container.scrollTop === 0 && !this.isRefreshing) {
|
||||
this.startY = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
this.container.addEventListener('touchmove', (e) => {
|
||||
if (this.startY && !this.isRefreshing) {
|
||||
this.currentY = e.touches[0].clientY;
|
||||
const pullDistance = this.currentY - this.startY;
|
||||
|
||||
if (pullDistance > 0 && this.container.scrollTop === 0) {
|
||||
e.preventDefault();
|
||||
const pullRatio = Math.min(pullDistance / this.threshold, 1);
|
||||
|
||||
if (pullDistance > this.threshold) {
|
||||
this.container.classList.add('pulling');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
this.container.addEventListener('touchend', () => {
|
||||
if (this.startY && this.currentY && !this.isRefreshing) {
|
||||
const pullDistance = this.currentY - this.startY;
|
||||
|
||||
if (pullDistance > this.threshold) {
|
||||
this.triggerRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
this.container.classList.remove('pulling');
|
||||
this.startY = 0;
|
||||
this.currentY = 0;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
async triggerRefresh() {
|
||||
if (this.isRefreshing) return;
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
await this.callback();
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toast Notification System
|
||||
class ToastManager {
|
||||
constructor() {
|
||||
this.container = this.createContainer();
|
||||
this.toasts = new Map();
|
||||
}
|
||||
|
||||
createContainer() {
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
show(message, type = 'info', duration = 3000) {
|
||||
const id = Date.now().toString();
|
||||
const toast = this.createToast(message, type, id);
|
||||
|
||||
this.container.appendChild(toast);
|
||||
this.toasts.set(id, toast);
|
||||
|
||||
// Auto-remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.remove(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
createToast(message, type, id) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.setAttribute('data-toast-id', id);
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-content">
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" aria-label="Close notification">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.remove(id);
|
||||
});
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
remove(id) {
|
||||
const toast = this.toasts.get(id);
|
||||
if (toast) {
|
||||
toast.style.animation = 'slideOutRight 0.3s ease forwards';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
this.container.removeChild(toast);
|
||||
}
|
||||
this.toasts.delete(id);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
success(message, duration = 3000) {
|
||||
return this.show(message, 'success', duration);
|
||||
}
|
||||
|
||||
error(message, duration = 5000) {
|
||||
return this.show(message, 'error', duration);
|
||||
}
|
||||
|
||||
warning(message, duration = 4000) {
|
||||
return this.show(message, 'warning', duration);
|
||||
}
|
||||
|
||||
info(message, duration = 3000) {
|
||||
return this.show(message, 'info', duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Navigation Manager
|
||||
class MobileNavManager {
|
||||
constructor() {
|
||||
this.setupMobileNav();
|
||||
}
|
||||
|
||||
setupMobileNav() {
|
||||
if (window.innerWidth <= 768) {
|
||||
this.createMobileNav();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', Utils.debounce(() => {
|
||||
if (window.innerWidth <= 768) {
|
||||
this.createMobileNav();
|
||||
} else {
|
||||
this.removeMobileNav();
|
||||
}
|
||||
}, 300));
|
||||
}
|
||||
|
||||
createMobileNav() {
|
||||
if (document.querySelector('.mobile-nav-toggle')) return;
|
||||
|
||||
const toggle = document.createElement('button');
|
||||
toggle.className = 'mobile-nav-toggle';
|
||||
toggle.innerHTML = '☰';
|
||||
toggle.setAttribute('aria-label', 'Toggle mobile navigation');
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'mobile-nav-menu';
|
||||
menu.innerHTML = `
|
||||
<a href="#pricing" class="mobile-nav-item">Pricing</a>
|
||||
<a href="#file-hosts" class="mobile-nav-item">File Hosts</a>
|
||||
<a href="#compare" class="mobile-nav-item">Compare</a>
|
||||
<a href="#adult-hosts" class="mobile-nav-item">Adult Hosts</a>
|
||||
<a href="#live-status" class="mobile-nav-item">Status</a>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toggle);
|
||||
document.body.appendChild(menu);
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
menu.classList.toggle('visible');
|
||||
});
|
||||
|
||||
// Close menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!toggle.contains(e.target) && !menu.contains(e.target)) {
|
||||
menu.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking on a link
|
||||
menu.querySelectorAll('.mobile-nav-item').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
menu.classList.remove('visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeMobileNav() {
|
||||
const toggle = document.querySelector('.mobile-nav-toggle');
|
||||
const menu = document.querySelector('.mobile-nav-menu');
|
||||
|
||||
if (toggle) toggle.remove();
|
||||
if (menu) menu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Progressive Enhancement Manager
|
||||
class ProgressiveEnhancementManager {
|
||||
constructor() {
|
||||
this.features = new Map();
|
||||
this.detectFeatures();
|
||||
}
|
||||
|
||||
detectFeatures() {
|
||||
// Detect various browser capabilities
|
||||
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((resolve) => {
|
||||
const webP = new Image();
|
||||
webP.onload = webP.onerror = () => {
|
||||
resolve(webP.height === 2);
|
||||
};
|
||||
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
||||
});
|
||||
}
|
||||
|
||||
hasFeature(feature) {
|
||||
return this.features.get(feature) || false;
|
||||
}
|
||||
|
||||
enhanceIfSupported(feature, enhancementCallback) {
|
||||
if (this.hasFeature(feature)) {
|
||||
enhancementCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize modern mobile-first features
|
||||
const touchGestureManager = new TouchGestureManager();
|
||||
const toastManager = new ToastManager();
|
||||
const mobileNavManager = new MobileNavManager();
|
||||
const progressiveEnhancementManager = new ProgressiveEnhancementManager();
|
||||
|
||||
// Initialize search enhancements
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Enhanced search for file hosts
|
||||
const fileHostSearch = document.querySelector('#file-host-search');
|
||||
const fileHostSuggestions = document.createElement('div');
|
||||
fileHostSuggestions.className = 'search-suggestions';
|
||||
|
||||
if (fileHostSearch) {
|
||||
fileHostSearch.parentNode.classList.add('search-container');
|
||||
fileHostSearch.parentNode.appendChild(fileHostSuggestions);
|
||||
new SearchManager(fileHostSearch, fileHostSuggestions);
|
||||
}
|
||||
|
||||
// Enhanced search for adult hosts
|
||||
const adultHostSearch = document.querySelector('#adult-host-search');
|
||||
const adultHostSuggestions = document.createElement('div');
|
||||
adultHostSuggestions.className = 'search-suggestions';
|
||||
|
||||
if (adultHostSearch) {
|
||||
adultHostSearch.parentNode.classList.add('search-container');
|
||||
adultHostSearch.parentNode.appendChild(adultHostSuggestions);
|
||||
new SearchManager(adultHostSearch, adultHostSuggestions);
|
||||
}
|
||||
|
||||
// Setup pull-to-refresh for mobile
|
||||
if (progressiveEnhancementManager.hasFeature('touchEvents')) {
|
||||
const mainContent = document.querySelector('main');
|
||||
if (mainContent) {
|
||||
new PullToRefreshManager(mainContent, async () => {
|
||||
toastManager.info('Refreshing data...');
|
||||
// Simulate refresh
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add scroll progress indicator
|
||||
const scrollIndicator = document.createElement('div');
|
||||
scrollIndicator.className = 'scroll-indicator';
|
||||
scrollIndicator.innerHTML = '<div class="scroll-indicator-bar"></div>';
|
||||
document.body.appendChild(scrollIndicator);
|
||||
|
||||
const updateScrollProgress = () => {
|
||||
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
||||
const scrolled = (winScroll / height) * 100;
|
||||
|
||||
const bar = scrollIndicator.querySelector('.scroll-indicator-bar');
|
||||
bar.style.width = scrolled + '%';
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', Utils.throttle(updateScrollProgress, 16));
|
||||
});
|
||||
|
||||
// Enhanced swipe gesture handling
|
||||
document.addEventListener('swipeleft', (e) => {
|
||||
// Handle left swipe - could navigate to next section
|
||||
console.log('Swiped left');
|
||||
});
|
||||
|
||||
document.addEventListener('swiperight', (e) => {
|
||||
// Handle right swipe - could navigate to previous section
|
||||
console.log('Swiped right');
|
||||
});
|
||||
|
||||
// Service Worker registration enhancement
|
||||
if (progressiveEnhancementManager.hasFeature('serviceWorker')) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('ServiceWorker registration successful');
|
||||
|
||||
// Check for updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
toastManager.info('New version available! Refresh to update.', 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('ServiceWorker registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
if ('PerformanceObserver' in window) {
|
||||
const perfObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.entryType === 'largest-contentful-paint') {
|
||||
console.log(`LCP: ${entry.startTime}ms`);
|
||||
}
|
||||
if (entry.entryType === 'first-input') {
|
||||
console.log(`FID: ${entry.processingStart - entry.startTime}ms`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
perfObserver.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
|
||||
} catch (e) {
|
||||
console.log('Performance observation not supported');
|
||||
}
|
||||
}
|
||||
69
dist/site.webmanifest
vendored
69
dist/site.webmanifest
vendored
|
|
@ -1 +1,68 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"images/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"images/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "Debrid Services Comparison",
|
||||
"short_name": "Debrid Compare",
|
||||
"description": "Compare pricing and supported hosts across major debrid services",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#6366f1",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["productivity", "utilities"],
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "images/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Pricing Comparison",
|
||||
"short_name": "Pricing",
|
||||
"description": "Compare pricing across all debrid services",
|
||||
"url": "/#pricing",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "File Hosts",
|
||||
"short_name": "Hosts",
|
||||
"description": "Browse supported file hosting services",
|
||||
"url": "/#file-hosts",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Service Comparison",
|
||||
"short_name": "Compare",
|
||||
"description": "Side-by-side service comparison",
|
||||
"url": "/#compare",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
dist/sw.js
vendored
2
dist/sw.js
vendored
|
|
@ -1 +1 @@
|
|||
if(!self.define){let e,s={};const n=(n,i)=>(n=new URL(n+".js",i).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(i,c)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let r={};const t=e=>n(e,o),a={module:{uri:o},exports:r,require:t};s[o]=Promise.all(i.map(e=>a[e]||t(e))).then(e=>(c(...e),r))}}define(["./workbox-9d361842"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"css/styles-min.css",revision:"5e69b90137393a8d5cb4956e35a7f2da"},{url:"css/styles.css",revision:"43b4035d5fb70e03d94fb4ab7b8f8ffb"},{url:"favicon.ico",revision:"99ed2c251efd7e52b890d2de6bf36781"},{url:"favicon.svg",revision:"8abc57869f11bb9299ae22352d3d5e98"},{url:"index.html",revision:"dcda018f14a78efacb6bfed8765db260"},{url:"js/app-min.js",revision:"dc69999b7acb32474abc5c612fb713ff"},{url:"js/app.js",revision:"4ff921ce887c9f6eae87741208b830c3"},{url:"json/adult-hosts.json",revision:"662e2165563840d5b577b58e938e6192"},{url:"json/file-hosts.json",revision:"f297528544c4ce6d6dea7707f171a99f"},{url:"json/pricing.json",revision:"e5940cb226d4beaf92979bfa30f03cda"},{url:"scripts/replace-assets.js",revision:"410d84c1d822559972797f3427d5c292"}],{}),e.registerRoute(/\.json$/,new e.StaleWhileRevalidate({cacheName:"json-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:png|css|svg|ico)$/,new e.CacheFirst({cacheName:"static-assets",plugins:[new e.ExpirationPlugin({maxEntries:20,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.html$/,new e.NetworkFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:86400})]}),"GET")});
|
||||
if(!self.define){let e,s={};const n=(n,i)=>(n=new URL(n+".js",i).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(i,c)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let r={};const d=e=>n(e,o),t={module:{uri:o},exports:r,require:d};s[o]=Promise.all(i.map(e=>t[e]||d(e))).then(e=>(c(...e),r))}}define(["./workbox-9d361842"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"css/styles-min.css",revision:"2dcb19fa83958e04c1e3f24de4ceb0f6"},{url:"css/styles.css",revision:"e204e52e82d31f527e985582fd801daf"},{url:"favicon.ico",revision:"99ed2c251efd7e52b890d2de6bf36781"},{url:"favicon.svg",revision:"8abc57869f11bb9299ae22352d3d5e98"},{url:"index.html",revision:"1f3e440d96053aee0f4a2a44c89bc7e8"},{url:"js/app-min.js",revision:"04e00a8d3dec0087e21103db6ffcc990"},{url:"js/app.js",revision:"6aed413e0dd10b73056f70c0595ea94f"},{url:"json/adult-hosts.json",revision:"662e2165563840d5b577b58e938e6192"},{url:"json/file-hosts.json",revision:"f297528544c4ce6d6dea7707f171a99f"},{url:"json/pricing.json",revision:"e5940cb226d4beaf92979bfa30f03cda"},{url:"scripts/replace-assets.js",revision:"410d84c1d822559972797f3427d5c292"}],{}),e.registerRoute(/\.json$/,new e.StaleWhileRevalidate({cacheName:"json-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:png|css|svg|ico)$/,new e.CacheFirst({cacheName:"static-assets",plugins:[new e.ExpirationPlugin({maxEntries:20,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.html$/,new e.NetworkFirst({cacheName:"html-cache",plugins:[new e.ExpirationPlugin({maxEntries:10,maxAgeSeconds:86400})]}),"GET")});
|
||||
|
|
|
|||
Loading…
Reference in a new issue