mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
Remove offline functionality and PWA features
Removed service worker, PWA manifest, offline page, and all related UI components to simplify the application and reduce bloat. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -219,23 +219,6 @@ current_dir = PathLib(__file__).parent
|
||||
static_dir = current_dir / "static"
|
||||
templates_dir = current_dir / "templates"
|
||||
|
||||
# Serve service worker with proper header to allow root scope
|
||||
@app.get("/sw.js")
|
||||
async def service_worker():
|
||||
"""Serve service worker from root with Service-Worker-Allowed header."""
|
||||
from fastapi.responses import FileResponse
|
||||
sw_path = static_dir / "sw.js"
|
||||
return FileResponse(
|
||||
sw_path,
|
||||
media_type="application/javascript",
|
||||
headers={
|
||||
"Service-Worker-Allowed": "/",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0"
|
||||
}
|
||||
)
|
||||
|
||||
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
@@ -1310,15 +1293,6 @@ def get_daily_verse(date_str=None):
|
||||
|
||||
|
||||
|
||||
@app.get("/offline", response_class=HTMLResponse)
|
||||
async def offline_reader(request: Request):
|
||||
"""Offline Bible reader - renders chapters from cached JSON."""
|
||||
return templates.TemplateResponse(
|
||||
"offline.html",
|
||||
{"request": request}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_root(request: Request):
|
||||
books = bible.get_books()
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "KJV Study - Bible Commentary Platform",
|
||||
"short_name": "KJV Study",
|
||||
"description": "Study the King James Bible with AI-powered commentary and insights",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fdfcf9",
|
||||
"theme_color": "#4b2e83",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"lang": "en",
|
||||
"categories": ["education", "books", "reference"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'%3E%3Crect width='192' height='192' fill='%234b2e83' rx='24'/%3E%3Ctext x='96' y='120' text-anchor='middle' font-size='80' fill='white'%3E📖%3C/text%3E%3C/svg%3E",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Crect width='512' height='512' fill='%234b2e83' rx='64'/%3E%3Ctext x='256' y='320' text-anchor='middle' font-size='200' fill='white'%3E📖%3C/text%3E%3C/svg%3E",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Browse Books",
|
||||
"short_name": "Books",
|
||||
"description": "Browse all Bible books",
|
||||
"url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234b2e83'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 540 720'%3E%3Crect width='540' height='720' fill='%23fdfcf9'/%3E%3Crect x='20' y='20' width='500' height='680' fill='white' stroke='%23e5e7eb' stroke-width='1' rx='8'/%3E%3Ctext x='270' y='80' text-anchor='middle' font-size='32' fill='%234b2e83' font-family='serif'%3EGenesis 1%3C/text%3E%3Ctext x='40' y='140' font-size='18' fill='%23333' font-family='serif'%3E1 In the beginning God created the heaven and the earth.%3C/text%3E%3Ctext x='40' y='180' font-size='18' fill='%23333' font-family='serif'%3E2 And the earth was without form, and void...%3C/text%3E%3C/svg%3E",
|
||||
"sizes": "540x720",
|
||||
"type": "image/svg+xml",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false,
|
||||
"related_applications": [],
|
||||
"features": [
|
||||
"Cross Platform",
|
||||
"offline-reading",
|
||||
"note-taking",
|
||||
"highlighting",
|
||||
"search"
|
||||
]
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
// KJV Study Service Worker - Offline Bible Access
|
||||
const CACHE_VERSION = 'v6';
|
||||
const STATIC_CACHE = 'kjvstudy-static-' + CACHE_VERSION;
|
||||
const PAGE_CACHE = 'kjvstudy-pages-' + CACHE_VERSION;
|
||||
|
||||
// Static assets to cache immediately on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/offline',
|
||||
'/static/tufte.css',
|
||||
'/static/style.css',
|
||||
'/static/manifest.json',
|
||||
'/static/verses-1769.json',
|
||||
'/books'
|
||||
];
|
||||
|
||||
// Install event - cache static assets only
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing service worker...');
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => {
|
||||
console.log('[SW] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[SW] Static assets cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[SW] Failed to cache static assets:', err);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating service worker...');
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName.startsWith('kjvstudy-') &&
|
||||
cacheName !== STATIC_CACHE &&
|
||||
cacheName !== PAGE_CACHE) {
|
||||
console.log('[SW] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[SW] Service worker activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Parse sitemap XML and extract URLs
|
||||
async function parseSitemap(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
const urls = [];
|
||||
|
||||
// Extract URLs from <loc> tags
|
||||
const locRegex = /<loc>([^<]+)<\/loc>/g;
|
||||
let match;
|
||||
while ((match = locRegex.exec(text)) !== null) {
|
||||
// Convert absolute URL to relative path
|
||||
let path = match[1].replace(/^https?:\/\/[^\/]+/, '');
|
||||
if (path) urls.push(path);
|
||||
}
|
||||
|
||||
return urls;
|
||||
} catch (err) {
|
||||
console.error('[SW] Failed to parse sitemap:', url, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get all URLs from sitemaps
|
||||
async function getAllUrlsFromSitemaps() {
|
||||
const allUrls = new Set();
|
||||
|
||||
// First, fetch the sitemap index
|
||||
try {
|
||||
const indexResponse = await fetch('/sitemap.xml');
|
||||
const indexText = await indexResponse.text();
|
||||
|
||||
// Check if it's a sitemap index (has <sitemapindex>)
|
||||
if (indexText.includes('<sitemapindex')) {
|
||||
// Extract child sitemap URLs
|
||||
const sitemapUrls = [];
|
||||
const locRegex = /<loc>([^<]+)<\/loc>/g;
|
||||
let match;
|
||||
while ((match = locRegex.exec(indexText)) !== null) {
|
||||
let sitemapUrl = match[1].replace(/^https?:\/\/[^\/]+/, '');
|
||||
sitemapUrls.push(sitemapUrl);
|
||||
}
|
||||
|
||||
// Fetch each child sitemap
|
||||
for (const sitemapUrl of sitemapUrls) {
|
||||
const urls = await parseSitemap(sitemapUrl);
|
||||
urls.forEach(url => allUrls.add(url));
|
||||
}
|
||||
} else {
|
||||
// It's a regular sitemap
|
||||
const urls = await parseSitemap('/sitemap.xml');
|
||||
urls.forEach(url => allUrls.add(url));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SW] Failed to fetch sitemap index:', err);
|
||||
}
|
||||
|
||||
return Array.from(allUrls);
|
||||
}
|
||||
|
||||
// Background pre-caching - only triggered when user requests it
|
||||
let cachingInProgress = false;
|
||||
let cachedCount = 0;
|
||||
let totalToCache = 0;
|
||||
|
||||
async function startBackgroundCaching() {
|
||||
if (cachingInProgress) return;
|
||||
cachingInProgress = true;
|
||||
cachedCount = 0;
|
||||
|
||||
console.log('[SW] Fetching URLs from sitemaps...');
|
||||
notifyClients({ type: 'CACHE_STATUS', status: 'Fetching page list from sitemaps...' });
|
||||
|
||||
// Get all URLs from sitemaps
|
||||
const allUrls = await getAllUrlsFromSitemaps();
|
||||
|
||||
console.log('[SW] Found', allUrls.length, 'URLs in sitemaps');
|
||||
|
||||
if (allUrls.length === 0) {
|
||||
notifyClients({ type: 'CACHE_ERROR', error: 'No URLs found in sitemaps' });
|
||||
cachingInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = await caches.open(PAGE_CACHE);
|
||||
|
||||
// Check which pages are already cached
|
||||
const uncachedPages = [];
|
||||
for (const url of allUrls) {
|
||||
const cached = await cache.match(url);
|
||||
if (!cached) {
|
||||
uncachedPages.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
totalToCache = uncachedPages.length;
|
||||
console.log('[SW] Need to cache', totalToCache, 'pages');
|
||||
|
||||
if (totalToCache === 0) {
|
||||
notifyClients({ type: 'CACHE_COMPLETE', total: allUrls.length });
|
||||
cachingInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
notifyClients({
|
||||
type: 'CACHE_PROGRESS',
|
||||
cached: 0,
|
||||
total: totalToCache,
|
||||
status: `Downloading ${totalToCache.toLocaleString()} pages...`
|
||||
});
|
||||
|
||||
// Concurrent pool - keep N requests in flight at all times
|
||||
const CONCURRENCY = 80; // Number of concurrent requests
|
||||
const PROGRESS_INTERVAL = 200; // Notify every N completions
|
||||
|
||||
let nextIndex = 0;
|
||||
let lastNotified = 0;
|
||||
|
||||
async function cacheUrl(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
await cache.put(url, response);
|
||||
cachedCount++;
|
||||
|
||||
// Notify progress periodically
|
||||
if (cachedCount - lastNotified >= PROGRESS_INTERVAL) {
|
||||
lastNotified = cachedCount;
|
||||
notifyClients({
|
||||
type: 'CACHE_PROGRESS',
|
||||
cached: cachedCount,
|
||||
total: totalToCache
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Silent fail for individual pages
|
||||
}
|
||||
}
|
||||
|
||||
async function worker() {
|
||||
while (nextIndex < uncachedPages.length) {
|
||||
const url = uncachedPages[nextIndex++];
|
||||
await cacheUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
// Start concurrent workers
|
||||
const workers = [];
|
||||
for (let i = 0; i < Math.min(CONCURRENCY, uncachedPages.length); i++) {
|
||||
workers.push(worker());
|
||||
}
|
||||
|
||||
// Wait for all workers to complete
|
||||
await Promise.all(workers);
|
||||
|
||||
console.log('[SW] Background caching complete!', cachedCount, 'pages cached');
|
||||
cachingInProgress = false;
|
||||
|
||||
notifyClients({ type: 'CACHE_COMPLETE', total: allUrls.length });
|
||||
}
|
||||
|
||||
// Notify all clients of caching progress
|
||||
async function notifyClients(message) {
|
||||
const clients = await self.clients.matchAll();
|
||||
clients.forEach(client => client.postMessage(message));
|
||||
}
|
||||
|
||||
// Fetch event - serve from cache, fallback to network
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// Only handle same-origin requests
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip non-GET requests
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip API requests - network first with cache fallback
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(PAGE_CACHE).then((cache) => {
|
||||
cache.put(event.request, responseClone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(event.request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - cache first
|
||||
if (url.pathname.startsWith('/static/')) {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(event.request).then((response) => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(STATIC_CACHE).then((cache) => {
|
||||
cache.put(event.request, responseClone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All other requests - network first, cache fallback, offline reader as last resort
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(PAGE_CACHE).then((cache) => {
|
||||
cache.put(event.request, responseClone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// For HTML pages, try to redirect to offline reader with context
|
||||
if (event.request.headers.get('Accept')?.includes('text/html')) {
|
||||
// Try to extract book/chapter from various URL patterns
|
||||
const bookChapterMatch = url.pathname.match(/\/(?:book|interlinear)\/([^\/]+)(?:\/(?:chapter\/)?(\d+))?/);
|
||||
if (bookChapterMatch) {
|
||||
const book = decodeURIComponent(bookChapterMatch[1]);
|
||||
const chapter = bookChapterMatch[2] || '1';
|
||||
return Response.redirect('/offline?book=' + encodeURIComponent(book) + '&chapter=' + chapter, 302);
|
||||
}
|
||||
|
||||
// Default to offline page
|
||||
return caches.match('/offline');
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle messages from the main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'START_CACHING') {
|
||||
console.log('[SW] Received START_CACHING request');
|
||||
startBackgroundCaching();
|
||||
}
|
||||
|
||||
// Report current caching status
|
||||
if (event.data && event.data.type === 'GET_CACHE_STATUS') {
|
||||
if (cachingInProgress) {
|
||||
event.source.postMessage({
|
||||
type: 'CACHE_PROGRESS',
|
||||
cached: cachedCount,
|
||||
total: totalToCache,
|
||||
inProgress: true
|
||||
});
|
||||
} else {
|
||||
event.source.postMessage({
|
||||
type: 'CACHE_IDLE',
|
||||
inProgress: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1306,15 +1306,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Enable Offline -->
|
||||
<div style="margin-top: 1rem;">
|
||||
<p style="font-size: 0.85rem; color: var(--text-tertiary); text-align: center;">
|
||||
<a href="/offline" style="color: var(--text-tertiary); text-decoration: none; border-bottom: 1px dotted var(--border-color-dark);">
|
||||
Enable Offline
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Flourish -->
|
||||
<div class="sidebar-flourish">⁂</div>
|
||||
</nav>
|
||||
@@ -2255,122 +2246,6 @@
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
{% endif %}
|
||||
|
||||
// Service Worker for offline support
|
||||
(function() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
||||
.then(function(registration) {
|
||||
console.log('[App] Service Worker registered:', registration.scope);
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(function() {
|
||||
registration.update();
|
||||
}, 60 * 60 * 1000); // Check every hour
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('[App] Service Worker registration failed:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for cache progress messages from service worker
|
||||
navigator.serviceWorker.addEventListener('message', function(event) {
|
||||
var data = event.data;
|
||||
if (data.type === 'CACHE_PROGRESS') {
|
||||
showCacheProgress(data.cached, data.total);
|
||||
} else if (data.type === 'CACHE_COMPLETE') {
|
||||
showCacheComplete(data.total);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cache progress indicator
|
||||
var cacheIndicator = null;
|
||||
function showCacheProgress(cached, total) {
|
||||
if (!cacheIndicator) {
|
||||
cacheIndicator = document.createElement('a');
|
||||
cacheIndicator.id = 'cache-indicator';
|
||||
cacheIndicator.href = '/offline';
|
||||
cacheIndicator.style.cssText = 'position:fixed;bottom:1rem;left:1rem;padding:0.5rem 1rem;background:#4a7c59;color:white;border-radius:4px;font-size:0.85rem;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:opacity 0.3s;text-decoration:none;cursor:pointer;';
|
||||
document.body.appendChild(cacheIndicator);
|
||||
}
|
||||
var pct = Math.round((cached / total) * 100);
|
||||
cacheIndicator.innerHTML = 'Caching for offline: ' + pct + '%';
|
||||
cacheIndicator.style.opacity = '1';
|
||||
}
|
||||
|
||||
function showCacheComplete(total) {
|
||||
if (cacheIndicator) {
|
||||
cacheIndicator.innerHTML = 'Ready for offline! (' + total.toLocaleString() + ' pages)';
|
||||
setTimeout(function() {
|
||||
cacheIndicator.style.opacity = '0';
|
||||
setTimeout(function() {
|
||||
if (cacheIndicator && cacheIndicator.parentNode) {
|
||||
cacheIndicator.parentNode.removeChild(cacheIndicator);
|
||||
cacheIndicator = null;
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Offline/Online indicator and PDF button handling
|
||||
function updateOnlineStatus() {
|
||||
var indicator = document.getElementById('offline-indicator');
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('div');
|
||||
indicator.id = 'offline-indicator';
|
||||
indicator.style.cssText = 'position:fixed;bottom:1rem;right:1rem;padding:0.5rem 1rem;background:#c41e3a;color:white;border-radius:4px;font-size:0.85rem;z-index:9999;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.2);';
|
||||
indicator.innerHTML = 'Offline Mode';
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
// Find all PDF links/buttons
|
||||
var pdfLinks = document.querySelectorAll('a[href*="/pdf"], .pdf-btn, a[href$=".pdf"]');
|
||||
|
||||
if (!navigator.onLine) {
|
||||
indicator.style.display = 'block';
|
||||
// Disable PDF buttons when offline
|
||||
pdfLinks.forEach(function(link) {
|
||||
link.dataset.originalHref = link.href;
|
||||
link.removeAttribute('href');
|
||||
link.style.opacity = '0.4';
|
||||
link.style.cursor = 'not-allowed';
|
||||
link.style.textDecoration = 'line-through';
|
||||
link.title = 'PDF unavailable offline';
|
||||
link.addEventListener('click', preventClick);
|
||||
});
|
||||
} else {
|
||||
indicator.style.display = 'none';
|
||||
// Re-enable PDF buttons when online
|
||||
pdfLinks.forEach(function(link) {
|
||||
if (link.dataset.originalHref) {
|
||||
link.href = link.dataset.originalHref;
|
||||
}
|
||||
link.style.opacity = '';
|
||||
link.style.cursor = '';
|
||||
link.style.textDecoration = '';
|
||||
link.title = '';
|
||||
link.removeEventListener('click', preventClick);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function preventClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
window.addEventListener('online', updateOnlineStatus);
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
// Run after DOM is ready to catch all PDF links
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', updateOnlineStatus);
|
||||
} else {
|
||||
updateOnlineStatus();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user