mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-01-10 18:21:15 +00:00
Add offline play feature
This commit is contained in:
parent
acbb75408f
commit
3b76e96113
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
isle.wasm
|
isle.wasm
|
||||||
isle.js
|
isle.js
|
||||||
|
LEGO
|
||||||
|
|||||||
436
app.js
Normal file
436
app.js
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
var Module = {
|
||||||
|
arguments: ['--ini', '/config/isle.ini'],
|
||||||
|
running: false,
|
||||||
|
preRun: function () {
|
||||||
|
Module["addRunDependency"]("isle");
|
||||||
|
Module.running = true;
|
||||||
|
},
|
||||||
|
canvas: (function () {
|
||||||
|
return document.getElementById('canvas');
|
||||||
|
})(),
|
||||||
|
onExit: function () {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// --- Elements ---
|
||||||
|
const video = document.getElementById('install-video');
|
||||||
|
const soundToggleEmoji = document.getElementById('sound-toggle-emoji');
|
||||||
|
const mainContainer = document.getElementById('main-container');
|
||||||
|
const topContent = document.getElementById('top-content');
|
||||||
|
const controlsWrapper = document.getElementById('controls-wrapper');
|
||||||
|
const footer = document.querySelector('.footer-disclaimer');
|
||||||
|
const allPages = document.querySelectorAll('.page-content');
|
||||||
|
const pageButtons = document.querySelectorAll('[data-target]');
|
||||||
|
const backButtons = document.querySelectorAll('.page-back-button');
|
||||||
|
const languageSelect = document.getElementById('language-select');
|
||||||
|
const installBtn = document.getElementById('install-btn');
|
||||||
|
const uninstallBtn = document.getElementById('uninstall-btn');
|
||||||
|
const controlsContainer = document.querySelector('.offline-play-controls');
|
||||||
|
|
||||||
|
// --- Sound Toggle ---
|
||||||
|
function updateSoundEmojiState() {
|
||||||
|
soundToggleEmoji.textContent = video.muted ? '🔇' : '🔊';
|
||||||
|
soundToggleEmoji.title = video.muted ? 'Unmute Audio' : 'Mute Audio';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video && soundToggleEmoji) {
|
||||||
|
updateSoundEmojiState();
|
||||||
|
soundToggleEmoji.addEventListener('click', function () {
|
||||||
|
video.muted = !video.muted;
|
||||||
|
updateSoundEmojiState();
|
||||||
|
});
|
||||||
|
video.addEventListener('volumechange', updateSoundEmojiState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Control Image Hover ---
|
||||||
|
const imageControls = document.querySelectorAll('.control-img');
|
||||||
|
imageControls.forEach(control => {
|
||||||
|
const hoverImage = new Image();
|
||||||
|
if (control.dataset.on) {
|
||||||
|
hoverImage.src = control.dataset.on;
|
||||||
|
}
|
||||||
|
control.addEventListener('mouseover', function () { if (this.dataset.on) { this.src = this.dataset.on; } });
|
||||||
|
control.addEventListener('mouseout', function () { if (this.dataset.off) { this.src = this.dataset.off; } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Emscripten Launch Logic ---
|
||||||
|
const runGameButton = document.getElementById('run-game-btn');
|
||||||
|
const emscriptenCanvas = document.getElementById('canvas');
|
||||||
|
const canvasWrapper = document.getElementById('canvas-wrapper');
|
||||||
|
const loadingGifOverlay = document.getElementById('loading-gif-overlay');
|
||||||
|
const statusMessageBar = document.getElementById('emscripten-status-message');
|
||||||
|
|
||||||
|
runGameButton.addEventListener('click', function () {
|
||||||
|
if (!Module.running) return;
|
||||||
|
video.muted = true;
|
||||||
|
updateSoundEmojiState();
|
||||||
|
this.src = this.dataset.on;
|
||||||
|
|
||||||
|
mainContainer.style.display = 'none';
|
||||||
|
canvasWrapper.style.display = 'grid';
|
||||||
|
|
||||||
|
document.documentElement.style.overflow = 'hidden';
|
||||||
|
document.documentElement.style.overscrollBehavior = 'none';
|
||||||
|
|
||||||
|
Module["disableOffscreenCanvases"] ||= document.getElementById('renderer-select').value == "0 0x682656f3 0x0 0x0 0x2000000";
|
||||||
|
console.log("disableOffscreenCanvases: " + Module["disableOffscreenCanvases"]);
|
||||||
|
|
||||||
|
Module["removeRunDependency"]("isle");
|
||||||
|
emscriptenCanvas.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
let progressUpdates = 0;
|
||||||
|
emscriptenCanvas.addEventListener('presenterProgress', function (event) {
|
||||||
|
// Intro animation is ready
|
||||||
|
if (event.detail.objectName == 'Lego_Smk' && event.detail.tickleState == 1) {
|
||||||
|
loadingGifOverlay.style.display = 'none';
|
||||||
|
emscriptenCanvas.style.setProperty('display', 'block', 'important');
|
||||||
|
}
|
||||||
|
else if (progressUpdates < 1003) {
|
||||||
|
progressUpdates++;
|
||||||
|
const percent = (progressUpdates / 1003 * 100).toFixed();
|
||||||
|
statusMessageBar.innerHTML = 'Loading LEGO® Island... please wait! <code>' + percent + '%</code>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Page Navigation Logic ---
|
||||||
|
function showPage(pageId, pushState = true) {
|
||||||
|
const page = document.querySelector(pageId);
|
||||||
|
if (!page) return;
|
||||||
|
|
||||||
|
// Hide main content
|
||||||
|
topContent.style.display = 'none';
|
||||||
|
controlsWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
// Show selected page
|
||||||
|
page.style.display = 'flex';
|
||||||
|
window.scroll(0, 0);
|
||||||
|
|
||||||
|
if (pushState) {
|
||||||
|
const newPath = pageId.replace('-page', '');
|
||||||
|
history.pushState({ page: pageId }, '', newPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainMenu() {
|
||||||
|
// Hide all pages
|
||||||
|
allPages.forEach(p => p.style.display = 'none');
|
||||||
|
|
||||||
|
// Show main content
|
||||||
|
topContent.style.display = 'flex';
|
||||||
|
controlsWrapper.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
pageButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const targetId = e.currentTarget.dataset.target;
|
||||||
|
showPage(targetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
backButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
history.back();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', (e) => {
|
||||||
|
if (e.state && e.state.page && e.state.page !== 'main') {
|
||||||
|
showPage(e.state.page, false);
|
||||||
|
} else {
|
||||||
|
showMainMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- OPFS Config Manager ---
|
||||||
|
const configManager = {
|
||||||
|
form: document.querySelector('.config-form'),
|
||||||
|
filePath: 'isle.ini',
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.form) return;
|
||||||
|
await this.loadConfig();
|
||||||
|
this.form.addEventListener('change', () => this.saveConfig());
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFileHandle() {
|
||||||
|
try {
|
||||||
|
const root = await navigator.storage.getDirectory();
|
||||||
|
return await root.getFileHandle(this.filePath, { create: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("OPFS not available or permission denied.", e);
|
||||||
|
document.getElementById('opfs-disabled').style.display = '';
|
||||||
|
document.getElementById('config-form').querySelectorAll('input, select').forEach(element => {
|
||||||
|
element.disabled = true;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveConfig() {
|
||||||
|
// This function now uses an inline Web Worker for maximum compatibility,
|
||||||
|
// especially with Safari, which does not support createWritable().
|
||||||
|
|
||||||
|
let iniContent = '[isle]\n';
|
||||||
|
const elements = this.form.elements;
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
if (!element.name || element.dataset.notIni == "true") continue;
|
||||||
|
|
||||||
|
let value;
|
||||||
|
switch (element.type) {
|
||||||
|
case 'checkbox':
|
||||||
|
value = element.checked ? 'YES' : 'NO';
|
||||||
|
iniContent += `${element.name}=${value}\n`;
|
||||||
|
break;
|
||||||
|
case 'radio':
|
||||||
|
if (element.checked) {
|
||||||
|
value = element.value;
|
||||||
|
iniContent += `${element.name}=${value}\n`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = element.value;
|
||||||
|
iniContent += `${element.name}=${value}\n`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerCode = `
|
||||||
|
self.onmessage = async (e) => {
|
||||||
|
if (e.data.action === 'save') {
|
||||||
|
try {
|
||||||
|
const root = await navigator.storage.getDirectory();
|
||||||
|
const handle = await root.getFileHandle(e.data.filePath, { create: true });
|
||||||
|
const accessHandle = await handle.createSyncAccessHandle();
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encodedData = encoder.encode(e.data.content);
|
||||||
|
|
||||||
|
accessHandle.truncate(0);
|
||||||
|
accessHandle.write(encodedData, { at: 0 });
|
||||||
|
accessHandle.flush();
|
||||||
|
accessHandle.close();
|
||||||
|
|
||||||
|
self.postMessage({ status: 'success', message: 'Config saved to ' + e.data.filePath });
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ status: 'error', message: 'Failed to save config: ' + err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||||
|
const workerUrl = URL.createObjectURL(blob);
|
||||||
|
const worker = new Worker(workerUrl);
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
action: 'save',
|
||||||
|
content: iniContent,
|
||||||
|
filePath: this.filePath
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.onmessage = (e) => {
|
||||||
|
console.log(e.data.message);
|
||||||
|
URL.revokeObjectURL(workerUrl); // Clean up the temporary URL
|
||||||
|
worker.terminate();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (e) => {
|
||||||
|
console.error('An error occurred in the config-saving worker:', e.message);
|
||||||
|
URL.revokeObjectURL(workerUrl);
|
||||||
|
worker.terminate();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfig() {
|
||||||
|
const handle = await this.getFileHandle();
|
||||||
|
if (!handle) return;
|
||||||
|
|
||||||
|
const file = await handle.getFile();
|
||||||
|
const text = await file.text();
|
||||||
|
if (!text) {
|
||||||
|
console.log('No existing config file found, using defaults.');
|
||||||
|
await this.saveConfig();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {};
|
||||||
|
const lines = text.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('[') || !line.includes('=')) continue;
|
||||||
|
const [key, ...valueParts] = line.split('=');
|
||||||
|
const value = valueParts.join('=').trim();
|
||||||
|
config[key.trim()] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyConfigToForm(config);
|
||||||
|
console.log('Config loaded from', this.filePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
applyConfigToForm(config) {
|
||||||
|
const elements = this.form.elements;
|
||||||
|
for (const key in config) {
|
||||||
|
const element = elements[key];
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
|
const value = config[key];
|
||||||
|
|
||||||
|
if (element.type === 'checkbox') {
|
||||||
|
element.checked = (value === 'YES');
|
||||||
|
} else if (element.nodeName === 'RADIO') { // radio nodelist
|
||||||
|
for (const radio of element) {
|
||||||
|
if (radio.value === value) {
|
||||||
|
radio.checked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
element.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle initial page load with a hash
|
||||||
|
const initialHash = window.location.hash;
|
||||||
|
if (initialHash) {
|
||||||
|
const initialPageId = initialHash + '-page';
|
||||||
|
if (document.querySelector(initialPageId)) {
|
||||||
|
const urlPath = window.location.pathname;
|
||||||
|
history.replaceState({ page: 'main' }, '', urlPath);
|
||||||
|
showPage(initialPageId, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
history.replaceState({ page: 'main' }, '', window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.documentElement.requestFullscreen) {
|
||||||
|
const fullscreenElement = document.getElementById('window-fullscreen');
|
||||||
|
const windowedElement = document.getElementById('window-windowed');
|
||||||
|
|
||||||
|
fullscreenElement.addEventListener('change', () => {
|
||||||
|
if (fullscreenElement.checked) {
|
||||||
|
document.documentElement.requestFullscreen().catch(err => {
|
||||||
|
console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
windowedElement.addEventListener('change', () => {
|
||||||
|
if (windowedElement.checked && document.fullscreenElement) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener for changes in fullscreen state (e.g., F11 or Esc key)
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
fullscreenElement.checked = true;
|
||||||
|
} else {
|
||||||
|
windowedElement.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.getElementById('window-form').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.matchMedia('(any-pointer: coarse)').matches) {
|
||||||
|
document.getElementById('touch-section').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
Promise.all([
|
||||||
|
configManager.init(),
|
||||||
|
navigator.serviceWorker.register('/sw.js').then(() => navigator.serviceWorker.ready)
|
||||||
|
]).then(([configResult, swRegistration]) => {
|
||||||
|
checkInitialCacheStatus();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Initialization failed:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
configManager.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressCircular = document.createElement('div');
|
||||||
|
progressCircular.className = 'progress-circular';
|
||||||
|
controlsContainer.appendChild(progressCircular);
|
||||||
|
|
||||||
|
installBtn.addEventListener('click', () => {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
const selectedLanguage = languageSelect.value;
|
||||||
|
installBtn.style.display = 'none';
|
||||||
|
uninstallBtn.style.display = 'none';
|
||||||
|
progressCircular.style.display = 'flex';
|
||||||
|
progressCircular.textContent = '0%';
|
||||||
|
progressCircular.style.background = 'conic-gradient(#FFD700 0deg, #333 0deg)';
|
||||||
|
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
action: 'install_language_pack',
|
||||||
|
language: selectedLanguage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uninstallBtn.addEventListener('click', () => {
|
||||||
|
const selectedLanguage = languageSelect.value;
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
action: 'uninstall_language_pack',
|
||||||
|
language: selectedLanguage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
languageSelect.addEventListener('change', () => {
|
||||||
|
checkInitialCacheStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkInitialCacheStatus() {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
const selectedLanguage = languageSelect.value;
|
||||||
|
navigator.serviceWorker.controller.postMessage({
|
||||||
|
action: 'check_cache_status',
|
||||||
|
language: selectedLanguage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInstallUI(isInstalled, inProgress = false) {
|
||||||
|
progressCircular.style.display = inProgress ? 'flex' : 'none';
|
||||||
|
installBtn.style.display = !isInstalled && !inProgress ? 'block' : 'none';
|
||||||
|
uninstallBtn.style.display = isInstalled && !inProgress ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleServiceWorkerMessage(event) {
|
||||||
|
const { action, language, progress, exists, success } = event.data;
|
||||||
|
if (language !== languageSelect.value) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'cache_status':
|
||||||
|
updateInstallUI(exists);
|
||||||
|
break;
|
||||||
|
case 'install_progress':
|
||||||
|
updateInstallUI(false, true);
|
||||||
|
const angle = (progress / 100) * 360;
|
||||||
|
progressCircular.textContent = `${Math.round(progress)}%`;
|
||||||
|
progressCircular.style.background =
|
||||||
|
`radial-gradient(#181818 60%, transparent 61%), conic-gradient(#FFD700 ${angle}deg, #333 ${angle}deg)`;
|
||||||
|
break;
|
||||||
|
case 'install_complete':
|
||||||
|
updateInstallUI(success);
|
||||||
|
break;
|
||||||
|
case 'uninstall_complete':
|
||||||
|
updateInstallUI(!success);
|
||||||
|
break;
|
||||||
|
case 'install_failed':
|
||||||
|
alert('Download failed. Please check your internet connection and try again.');
|
||||||
|
updateInstallUI(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
1237
index.html
1237
index.html
File diff suppressed because it is too large
Load Diff
BIN
install_off.webp
Normal file
BIN
install_off.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
install_on.webp
Normal file
BIN
install_on.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
762
style.css
Normal file
762
style.css
Normal file
@ -0,0 +1,762 @@
|
|||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #000000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas-wrapper {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
background-color: #000000;
|
||||||
|
outline: none;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-gif-overlay {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: calc(100dvh * (640 / 480));
|
||||||
|
max-height: calc(100dvw * (480 / 640));
|
||||||
|
aspect-ratio: 640 / 480;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
display: none !important;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1 / -1;
|
||||||
|
background-color: #000000;
|
||||||
|
border: none;
|
||||||
|
z-index: 1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-gif-overlay {
|
||||||
|
background-color: #000000;
|
||||||
|
border: none;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-block {
|
||||||
|
max-width: 80%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-block .quote-text {
|
||||||
|
font-size: 0.5em;
|
||||||
|
color: #f0f0f0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-block .quote-attribution {
|
||||||
|
font-size: 0.1em;
|
||||||
|
color: #c0c0c0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info-text {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
max-width: 280px;
|
||||||
|
width: 80%;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #b0b0b0;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px dashed #444;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info-text p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info-text p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message-bar {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 85%;
|
||||||
|
max-width: 340px;
|
||||||
|
background-color: #181818;
|
||||||
|
color: #c0c0c0;
|
||||||
|
font-family: 'Consolas', 'Menlo', 'Courier New', Courier, monospace;
|
||||||
|
font-size: 0.75em;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid #303030;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message-bar code {
|
||||||
|
color: #FFD700;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 25px;
|
||||||
|
background-color: #000000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 95vw;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 95vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#install-video {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sound-toggle-emoji {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
font-size: 26px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 3px black;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 2px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sound-toggle-emoji:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#island-logo-img {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 400px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 567 / 198;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-img {
|
||||||
|
cursor: pointer;
|
||||||
|
height: auto;
|
||||||
|
max-width: 18%;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#run-game-btn {
|
||||||
|
aspect-ratio: 135 / 164;
|
||||||
|
}
|
||||||
|
|
||||||
|
#configure-btn {
|
||||||
|
aspect-ratio: 130 / 147;
|
||||||
|
}
|
||||||
|
|
||||||
|
#free-stuff-btn {
|
||||||
|
aspect-ratio: 134 / 149;
|
||||||
|
}
|
||||||
|
|
||||||
|
#read-me-btn {
|
||||||
|
aspect-ratio: 134 / 149;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancel-btn {
|
||||||
|
aspect-ratio: 93 / 145;
|
||||||
|
}
|
||||||
|
|
||||||
|
#install-btn {
|
||||||
|
aspect-ratio: 94 / 166;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uninstall-btn {
|
||||||
|
aspect-ratio: 122 / 144;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-img:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-disclaimer {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: #888888;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-disclaimer p {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: #f0f0f0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-back-button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-back-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content {
|
||||||
|
max-width: 700px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content h1 {
|
||||||
|
color: #FFD700;
|
||||||
|
/* LEGO yellow */
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content p {
|
||||||
|
color: #c0c0c0;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content a {
|
||||||
|
color: #FFD700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#configure-page .page-inner-content {
|
||||||
|
display: flex;
|
||||||
|
background-color: #181818;
|
||||||
|
border: 1px solid #303030;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-art-panel {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-art-panel img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-legend {
|
||||||
|
color: #FFD700;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-label {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trigger {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #444;
|
||||||
|
color: #eee;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 140%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 220px;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
color: #f0f0f0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 20;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.2s, visibility 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trigger:hover>.tooltip-content,
|
||||||
|
.tooltip-trigger.active>.tooltip-content {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item label::before {
|
||||||
|
content: '';
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-color: #333;
|
||||||
|
border: 1px solid #555;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item input:checked+label::before {
|
||||||
|
background-color: #FFD700;
|
||||||
|
border-color: #fff;
|
||||||
|
box-shadow: 0 0 5px #FFD700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group .option-item label::before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group .option-item label::before {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: #444;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #FFD700;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #FFD700;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper::after {
|
||||||
|
content: '▼';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 12px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #FFD700;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #c0c0c0;
|
||||||
|
background-color: #333;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
display: block;
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
background-color: #252525;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #FFD700;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item p {
|
||||||
|
margin: 0;
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-quote {
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-left: 3px solid #FFD700;
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-quote p {
|
||||||
|
font-style: italic;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-quote footer {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-left: 3px solid #ff0011;
|
||||||
|
background-color: #1c1c1c;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box p {
|
||||||
|
font-style: italic;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-text h4 {
|
||||||
|
color: #FFD700;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-text p {
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#install-btn {
|
||||||
|
max-width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uninstall-btn {
|
||||||
|
max-width: 70%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-circular {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background:
|
||||||
|
radial-gradient(#181818 60%, transparent 61%),
|
||||||
|
conic-gradient(#FFD700 0deg, #333 0deg);
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Consolas', 'Menlo', monospace;
|
||||||
|
transition: background 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#install-video {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#island-logo-img {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-img {
|
||||||
|
max-width: 19%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sound-toggle-emoji {
|
||||||
|
font-size: 24px;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info-text {
|
||||||
|
max-width: 90%;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-inner-content p {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-art-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#configure-page .page-inner-content {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
background-color: #181818;
|
||||||
|
border: 1px solid #303030;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-play-text p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#install-video {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#island-logo-img {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-img {
|
||||||
|
max-width: 45%;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sound-toggle-emoji {
|
||||||
|
font-size: 22px;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-info-text {
|
||||||
|
max-width: 95%;
|
||||||
|
font-size: 0.7em;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content .page-back-button {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
213
sw.js
Normal file
213
sw.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
importScripts('/workbox/workbox-sw.js');
|
||||||
|
|
||||||
|
workbox.setConfig({
|
||||||
|
modulePathPrefix: '/workbox/'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registerRoute } = workbox.routing;
|
||||||
|
const { StaleWhileRevalidate, CacheFirst, Strategy } = workbox.strategies;
|
||||||
|
const { CacheableResponsePlugin } = workbox.cacheableResponse;
|
||||||
|
const { RangeRequestsPlugin } = workbox.rangeRequests;
|
||||||
|
|
||||||
|
const coreAppFiles = [
|
||||||
|
'/', '/index.html', '/cancel_off.webp', '/cancel_on.webp', '/cdspin.gif',
|
||||||
|
'/configure_off.webp', '/configure_on.webp', '/favicon.png', '/favicon.svg',
|
||||||
|
'/free_stuff_off.webp', '/free_stuff_on.webp', '/install.mp4', '/install_off.webp',
|
||||||
|
'/install_on.webp', '/install.webm', '/island.webp', '/isle.js', '/isle.wasm',
|
||||||
|
'/poster.pdf', '/read_me_off.webp', '/read_me_on.webp', '/run_game_off.webp',
|
||||||
|
'/run_game_on.webp', '/shark.webp', '/uninstall_off.webp', '/uninstall_on.webp',
|
||||||
|
'app.js', 'style.css', 'manifest.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
const gameFiles = [
|
||||||
|
"/LEGO/Scripts/CREDITS.SI", "/LEGO/Scripts/INTRO.SI", "/LEGO/Scripts/NOCD.SI", "/LEGO/Scripts/SNDANIM.SI",
|
||||||
|
"/LEGO/Scripts/Act2/ACT2MAIN.SI", "/LEGO/Scripts/Act3/ACT3.SI", "/LEGO/Scripts/Build/COPTER.SI",
|
||||||
|
"/LEGO/Scripts/Build/DUNECAR.SI", "/LEGO/Scripts/Build/JETSKI.SI", "/LEGO/Scripts/Build/RACECAR.SI",
|
||||||
|
"/LEGO/Scripts/Garage/GARAGE.SI", "/LEGO/Scripts/Hospital/HOSPITAL.SI", "/LEGO/Scripts/Infocntr/ELEVBOTT.SI",
|
||||||
|
"/LEGO/Scripts/Infocntr/HISTBOOK.SI", "/LEGO/Scripts/Infocntr/INFODOOR.SI", "/LEGO/Scripts/Infocntr/INFOMAIN.SI",
|
||||||
|
"/LEGO/Scripts/Infocntr/INFOSCOR.SI", "/LEGO/Scripts/Infocntr/REGBOOK.SI", "/LEGO/Scripts/Isle/ISLE.SI",
|
||||||
|
"/LEGO/Scripts/Isle/JUKEBOX.SI", "/LEGO/Scripts/Isle/JUKEBOXW.SI", "/LEGO/Scripts/Police/POLICE.SI",
|
||||||
|
"/LEGO/Scripts/Race/CARRACE.SI", "/LEGO/Scripts/Race/CARRACER.SI", "/LEGO/Scripts/Race/JETRACE.SI",
|
||||||
|
"/LEGO/Scripts/Race/JETRACER.SI", "/LEGO/data/WORLD.WDB"
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATIC_CACHE_NAME = 'static-assets-v1';
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(coreAppFiles);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => coreAppFiles.includes(url.pathname),
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: STATIC_CACHE_NAME,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const rangeRequestsPlugin = new RangeRequestsPlugin();
|
||||||
|
const normalizePathPlugin = {
|
||||||
|
cacheKeyWillBeUsed: async ({ request }) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const normalizedPath = url.pathname.replace(/\/{2,}/g, '/');
|
||||||
|
const normalizedUrl = url.origin + normalizedPath;
|
||||||
|
if (request.url === normalizedUrl) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
return new Request(normalizedUrl, {
|
||||||
|
headers: request.headers, method: request.method,
|
||||||
|
credentials: request.credentials, redirect: request.redirect,
|
||||||
|
referrer: request.referrer, body: request.body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class LegoCacheStrategy extends Strategy {
|
||||||
|
async _handle(request, handler) {
|
||||||
|
const cacheKeyRequest = await normalizePathPlugin.cacheKeyWillBeUsed({ request });
|
||||||
|
const cachedResponse = await caches.match(cacheKeyRequest);
|
||||||
|
|
||||||
|
if (cachedResponse) {
|
||||||
|
return await rangeRequestsPlugin.cachedResponseWillBeUsed({
|
||||||
|
request: cacheKeyRequest,
|
||||||
|
cachedResponse: cachedResponse,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.fetch(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ url }) => url.pathname.startsWith('/LEGO/'),
|
||||||
|
new LegoCacheStrategy()
|
||||||
|
);
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.action) {
|
||||||
|
switch (event.data.action) {
|
||||||
|
case 'install_language_pack':
|
||||||
|
installLanguagePack(event.data.language, event.source);
|
||||||
|
break;
|
||||||
|
case 'uninstall_language_pack':
|
||||||
|
uninstallLanguagePack(event.data.language, event.source);
|
||||||
|
break;
|
||||||
|
case 'check_cache_status':
|
||||||
|
checkCacheStatus(event.data.language, event.source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLanguageCacheName = (language) => `game-assets-${language}`;
|
||||||
|
|
||||||
|
async function installLanguagePack(language, client) {
|
||||||
|
const THROTTLE_MS = 100;
|
||||||
|
const cacheName = getLanguageCacheName(language);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileMetadataPromises = gameFiles.map(fileUrl =>
|
||||||
|
fetch(fileUrl, { method: 'HEAD', headers: { 'Accept-Language': language } })
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error(`Failed to HEAD ${fileUrl}`);
|
||||||
|
return { url: fileUrl, size: Number(response.headers.get('content-length')) || 0 };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const fileMetadata = await Promise.all(fileMetadataPromises);
|
||||||
|
const totalBytesToDownload = fileMetadata.reduce((sum, file) => sum + file.size, 0);
|
||||||
|
let bytesDownloaded = 0;
|
||||||
|
let lastProgressUpdate = 0;
|
||||||
|
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
|
||||||
|
for (const file of fileMetadata) {
|
||||||
|
const request = new Request(file.url, { headers: { 'Accept-Language': language } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`Failed to fetch ${file.url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [streamForCaching, streamForProgress] = response.body.tee();
|
||||||
|
|
||||||
|
const responseToCache = new Response(streamForCaching, response);
|
||||||
|
const cachePromise = cache.put(request, responseToCache);
|
||||||
|
|
||||||
|
const reader = streamForProgress.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
bytesDownloaded += value.length;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - lastProgressUpdate > THROTTLE_MS) {
|
||||||
|
lastProgressUpdate = now;
|
||||||
|
client.postMessage({
|
||||||
|
action: 'install_progress',
|
||||||
|
progress: (bytesDownloaded / totalBytesToDownload) * 100,
|
||||||
|
language: language
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await cachePromise;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Aborting installation due to a persistent error:", error);
|
||||||
|
await caches.delete(cacheName);
|
||||||
|
client.postMessage({ action: 'install_failed', language: language });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.postMessage({
|
||||||
|
action: 'install_progress',
|
||||||
|
progress: 100,
|
||||||
|
language: language
|
||||||
|
});
|
||||||
|
client.postMessage({ action: 'install_complete', success: true, language: language });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during language pack installation:', error);
|
||||||
|
client.postMessage({ action: 'install_failed', success: false, language: language, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstallLanguagePack(language, client) {
|
||||||
|
const cacheName = getLanguageCacheName(language);
|
||||||
|
try {
|
||||||
|
const deleted = await caches.delete(cacheName);
|
||||||
|
if (deleted) {
|
||||||
|
console.log(`Cache ${cacheName} deleted successfully.`);
|
||||||
|
}
|
||||||
|
client.postMessage({ action: 'uninstall_complete', success: deleted, language: language });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during language pack uninstallation:', error);
|
||||||
|
client.postMessage({ action: 'uninstall_complete', success: false, language: language, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkCacheStatus(language, client) {
|
||||||
|
const cacheName = getLanguageCacheName(language);
|
||||||
|
const hasCache = await caches.has(cacheName);
|
||||||
|
client.postMessage({ action: 'cache_status', exists: hasCache, language: language });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName.startsWith('static-assets-') && cacheName !== STATIC_CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
BIN
uninstall_off.webp
Normal file
BIN
uninstall_off.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
uninstall_on.webp
Normal file
BIN
uninstall_on.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
176
workbox/workbox-cacheable-response.dev.js
Normal file
176
workbox/workbox-cacheable-response.dev.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
this.workbox = this.workbox || {};
|
||||||
|
this.workbox.cacheableResponse = (function (exports, assert_js, WorkboxError_js, getFriendlyURL_js, logger_js) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
try {
|
||||||
|
self['workbox:cacheable-response:7.3.0'] && _();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* This class allows you to set up rules determining what
|
||||||
|
* status codes and/or headers need to be present in order for a
|
||||||
|
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||||
|
* to be considered cacheable.
|
||||||
|
*
|
||||||
|
* @memberof workbox-cacheable-response
|
||||||
|
*/
|
||||||
|
class CacheableResponse {
|
||||||
|
/**
|
||||||
|
* To construct a new CacheableResponse instance you must provide at least
|
||||||
|
* one of the `config` properties.
|
||||||
|
*
|
||||||
|
* If both `statuses` and `headers` are specified, then both conditions must
|
||||||
|
* be met for the `Response` to be considered cacheable.
|
||||||
|
*
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||||
|
* `Response` can have and be considered cacheable.
|
||||||
|
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||||
|
* and expected values that a `Response` can have and be considered cacheable.
|
||||||
|
* If multiple headers are provided, only one needs to be present.
|
||||||
|
*/
|
||||||
|
constructor(config = {}) {
|
||||||
|
{
|
||||||
|
if (!(config.statuses || config.headers)) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('statuses-or-headers-required', {
|
||||||
|
moduleName: 'workbox-cacheable-response',
|
||||||
|
className: 'CacheableResponse',
|
||||||
|
funcName: 'constructor'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.statuses) {
|
||||||
|
assert_js.assert.isArray(config.statuses, {
|
||||||
|
moduleName: 'workbox-cacheable-response',
|
||||||
|
className: 'CacheableResponse',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'config.statuses'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.headers) {
|
||||||
|
assert_js.assert.isType(config.headers, 'object', {
|
||||||
|
moduleName: 'workbox-cacheable-response',
|
||||||
|
className: 'CacheableResponse',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'config.headers'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._statuses = config.statuses;
|
||||||
|
this._headers = config.headers;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks a response to see whether it's cacheable or not, based on this
|
||||||
|
* object's configuration.
|
||||||
|
*
|
||||||
|
* @param {Response} response The response whose cacheability is being
|
||||||
|
* checked.
|
||||||
|
* @return {boolean} `true` if the `Response` is cacheable, and `false`
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
isResponseCacheable(response) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isInstance(response, Response, {
|
||||||
|
moduleName: 'workbox-cacheable-response',
|
||||||
|
className: 'CacheableResponse',
|
||||||
|
funcName: 'isResponseCacheable',
|
||||||
|
paramName: 'response'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let cacheable = true;
|
||||||
|
if (this._statuses) {
|
||||||
|
cacheable = this._statuses.includes(response.status);
|
||||||
|
}
|
||||||
|
if (this._headers && cacheable) {
|
||||||
|
cacheable = Object.keys(this._headers).some(headerName => {
|
||||||
|
return response.headers.get(headerName) === this._headers[headerName];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (!cacheable) {
|
||||||
|
logger_js.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_js.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`);
|
||||||
|
logger_js.logger.groupCollapsed(`View cacheability criteria here.`);
|
||||||
|
logger_js.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses));
|
||||||
|
logger_js.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2));
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
const logFriendlyHeaders = {};
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
logFriendlyHeaders[key] = value;
|
||||||
|
});
|
||||||
|
logger_js.logger.groupCollapsed(`View response status and headers here.`);
|
||||||
|
logger_js.logger.log(`Response status: ${response.status}`);
|
||||||
|
logger_js.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2));
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
logger_js.logger.groupCollapsed(`View full response details here.`);
|
||||||
|
logger_js.logger.log(response.headers);
|
||||||
|
logger_js.logger.log(response);
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cacheable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* A class implementing the `cacheWillUpdate` lifecycle callback. This makes it
|
||||||
|
* easier to add in cacheability checks to requests made via Workbox's built-in
|
||||||
|
* strategies.
|
||||||
|
*
|
||||||
|
* @memberof workbox-cacheable-response
|
||||||
|
*/
|
||||||
|
class CacheableResponsePlugin {
|
||||||
|
/**
|
||||||
|
* To construct a new CacheableResponsePlugin instance you must provide at
|
||||||
|
* least one of the `config` properties.
|
||||||
|
*
|
||||||
|
* If both `statuses` and `headers` are specified, then both conditions must
|
||||||
|
* be met for the `Response` to be considered cacheable.
|
||||||
|
*
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||||
|
* `Response` can have and be considered cacheable.
|
||||||
|
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||||
|
* and expected values that a `Response` can have and be considered cacheable.
|
||||||
|
* If multiple headers are provided, only one needs to be present.
|
||||||
|
*/
|
||||||
|
constructor(config) {
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Response} options.response
|
||||||
|
* @return {Response|null}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.cacheWillUpdate = async ({
|
||||||
|
response
|
||||||
|
}) => {
|
||||||
|
if (this._cacheableResponse.isResponseCacheable(response)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
this._cacheableResponse = new CacheableResponse(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.CacheableResponse = CacheableResponse;
|
||||||
|
exports.CacheableResponsePlugin = CacheableResponsePlugin;
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
|
||||||
|
})({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-cacheable-response.dev.js.map
|
||||||
2
workbox/workbox-cacheable-response.prod.js
Normal file
2
workbox/workbox-cacheable-response.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(s){"use strict";try{self["workbox:cacheable-response:7.3.0"]&&_()}catch(s){}class t{constructor(s={}){this._=s.statuses,this.G=s.headers}isResponseCacheable(s){let t=!0;return this._&&(t=this._.includes(s.status)),this.G&&t&&(t=Object.keys(this.G).some((t=>s.headers.get(t)===this.G[t]))),t}}return s.CacheableResponse=t,s.CacheableResponsePlugin=class{constructor(s){this.cacheWillUpdate=async({response:s})=>this.H.isResponseCacheable(s)?s:null,this.H=new t(s)}},s}({});
|
||||||
|
//# sourceMappingURL=workbox-cacheable-response.prod.js.map
|
||||||
1059
workbox/workbox-core.dev.js
Normal file
1059
workbox/workbox-core.dev.js
Normal file
File diff suppressed because it is too large
Load Diff
2
workbox/workbox-core.prod.js
Normal file
2
workbox/workbox-core.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.core=function(t){"use strict";try{self["workbox:core:7.3.0"]&&_()}catch(t){}const e=(t,...e)=>{let n=t;return e.length>0&&(n+=` :: ${JSON.stringify(e)}`),n};class n extends Error{constructor(t,n){super(e(t,n)),this.name=t,this.details=n}}const r=new Set;const o={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},s=t=>[o.prefix,t,o.suffix].filter((t=>t&&t.length>0)).join("-"),i={updateDetails:t=>{(t=>{for(const e of Object.keys(o))t(e)})((e=>{"string"==typeof t[e]&&(o[e]=t[e])}))},getGoogleAnalyticsName:t=>t||s(o.googleAnalytics),getPrecacheName:t=>t||s(o.precache),getPrefix:()=>o.prefix,getRuntimeName:t=>t||s(o.runtime),getSuffix:()=>o.suffix};function c(t,e){const n=new URL(t);for(const t of e)n.searchParams.delete(t);return n.href}let a,u;function f(){if(void 0===u){const t=new Response("");if("body"in t)try{new Response(t.body),u=!0}catch(t){u=!1}u=!1}return u}function l(t){return new Promise((e=>setTimeout(e,t)))}var g=Object.freeze({__proto__:null,assert:null,cacheMatchIgnoreParams:async function(t,e,n,r){const o=c(e.url,n);if(e.url===o)return t.match(e,r);const s=Object.assign(Object.assign({},r),{ignoreSearch:!0}),i=await t.keys(e,s);for(const e of i){if(o===c(e.url,n))return t.match(e,r)}},cacheNames:i,canConstructReadableStream:function(){if(void 0===a)try{new ReadableStream({start(){}}),a=!0}catch(t){a=!1}return a},canConstructResponseFromBodyStream:f,dontWaitFor:function(t){t.then((()=>{}))},Deferred:class{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}},executeQuotaErrorCallbacks:async function(){for(const t of r)await t()},getFriendlyURL:t=>new URL(String(t),location.href).href.replace(new RegExp(`^${location.origin}`),""),logger:null,resultingClientExists:async function(t){if(!t)return;let e=await self.clients.matchAll({type:"window"});const n=new Set(e.map((t=>t.id)));let r;const o=performance.now();for(;performance.now()-o<2e3&&(e=await self.clients.matchAll({type:"window"}),r=e.find((e=>t?e.id===t:!n.has(e.id))),!r);)await l(100);return r},timeout:l,waitUntil:function(t,e){const n=e();return t.waitUntil(n),n},WorkboxError:n});const w={get googleAnalytics(){return i.getGoogleAnalyticsName()},get precache(){return i.getPrecacheName()},get prefix(){return i.getPrefix()},get runtime(){return i.getRuntimeName()},get suffix(){return i.getSuffix()}};return t._private=g,t.cacheNames=w,t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.copyResponse=async function(t,e){let r=null;if(t.url){r=new URL(t.url).origin}if(r!==self.location.origin)throw new n("cross-origin-copy-response",{origin:r});const o=t.clone(),s={headers:new Headers(o.headers),status:o.status,statusText:o.statusText},i=e?e(s):s,c=f()?o.body:await o.blob();return new Response(c,i)},t.registerQuotaErrorCallback=function(t){r.add(t)},t.setCacheNameDetails=function(t){i.updateDetails(t)},t.skipWaiting=function(){self.skipWaiting()},t}({});
|
||||||
|
//# sourceMappingURL=workbox-core.prod.js.map
|
||||||
1201
workbox/workbox-precaching.dev.js
Normal file
1201
workbox/workbox-precaching.dev.js
Normal file
File diff suppressed because it is too large
Load Diff
2
workbox/workbox-precaching.prod.js
Normal file
2
workbox/workbox-precaching.prod.js
Normal file
File diff suppressed because one or more lines are too long
242
workbox/workbox-range-requests.dev.js
Normal file
242
workbox/workbox-range-requests.dev.js
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
this.workbox = this.workbox || {};
|
||||||
|
this.workbox.rangeRequests = (function (exports, WorkboxError_js, assert_js, logger_js) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
try {
|
||||||
|
self['workbox:range-requests:7.3.0'] && _();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {Blob} blob A source blob.
|
||||||
|
* @param {number} [start] The offset to use as the start of the
|
||||||
|
* slice.
|
||||||
|
* @param {number} [end] The offset to use as the end of the slice.
|
||||||
|
* @return {Object} An object with `start` and `end` properties, reflecting
|
||||||
|
* the effective boundaries to use given the size of the blob.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function calculateEffectiveBoundaries(blob, start, end) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isInstance(blob, Blob, {
|
||||||
|
moduleName: 'workbox-range-requests',
|
||||||
|
funcName: 'calculateEffectiveBoundaries',
|
||||||
|
paramName: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const blobSize = blob.size;
|
||||||
|
if (start && start >= blobSize) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('range-not-satisfiable', {
|
||||||
|
size: blobSize,
|
||||||
|
end,
|
||||||
|
start
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let effectiveStart;
|
||||||
|
let effectiveEnd;
|
||||||
|
if (start !== undefined && end !== undefined) {
|
||||||
|
effectiveStart = start;
|
||||||
|
// Range values are inclusive, so add 1 to the value.
|
||||||
|
effectiveEnd = end + 1;
|
||||||
|
} else if (start !== undefined && end === undefined) {
|
||||||
|
effectiveStart = start;
|
||||||
|
effectiveEnd = blobSize;
|
||||||
|
} else if (end !== undefined && start === undefined) {
|
||||||
|
effectiveStart = blobSize - end;
|
||||||
|
effectiveEnd = blobSize;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start: effectiveStart,
|
||||||
|
end: effectiveEnd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {string} rangeHeader A Range: header value.
|
||||||
|
* @return {Object} An object with `start` and `end` properties, reflecting
|
||||||
|
* the parsed value of the Range: header. If either the `start` or `end` are
|
||||||
|
* omitted, then `null` will be returned.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function parseRangeHeader(rangeHeader) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isType(rangeHeader, 'string', {
|
||||||
|
moduleName: 'workbox-range-requests',
|
||||||
|
funcName: 'parseRangeHeader',
|
||||||
|
paramName: 'rangeHeader'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const normalizedRangeHeader = rangeHeader.trim().toLowerCase();
|
||||||
|
if (!normalizedRangeHeader.startsWith('bytes=')) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('unit-must-be-bytes', {
|
||||||
|
normalizedRangeHeader
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Specifying multiple ranges separate by commas is valid syntax, but this
|
||||||
|
// library only attempts to handle a single, contiguous sequence of bytes.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax
|
||||||
|
if (normalizedRangeHeader.includes(',')) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('single-range-only', {
|
||||||
|
normalizedRangeHeader
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader);
|
||||||
|
// We need either at least one of the start or end values.
|
||||||
|
if (!rangeParts || !(rangeParts[1] || rangeParts[2])) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('invalid-range-values', {
|
||||||
|
normalizedRangeHeader
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start: rangeParts[1] === '' ? undefined : Number(rangeParts[1]),
|
||||||
|
end: rangeParts[2] === '' ? undefined : Number(rangeParts[2])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Given a `Request` and `Response` objects as input, this will return a
|
||||||
|
* promise for a new `Response`.
|
||||||
|
*
|
||||||
|
* If the original `Response` already contains partial content (i.e. it has
|
||||||
|
* a status of 206), then this assumes it already fulfills the `Range:`
|
||||||
|
* requirements, and will return it as-is.
|
||||||
|
*
|
||||||
|
* @param {Request} request A request, which should contain a Range:
|
||||||
|
* header.
|
||||||
|
* @param {Response} originalResponse A response.
|
||||||
|
* @return {Promise<Response>} Either a `206 Partial Content` response, with
|
||||||
|
* the response body set to the slice of content specified by the request's
|
||||||
|
* `Range:` header, or a `416 Range Not Satisfiable` response if the
|
||||||
|
* conditions of the `Range:` header can't be met.
|
||||||
|
*
|
||||||
|
* @memberof workbox-range-requests
|
||||||
|
*/
|
||||||
|
async function createPartialResponse(request, originalResponse) {
|
||||||
|
try {
|
||||||
|
if ("dev" !== 'production') {
|
||||||
|
assert_js.assert.isInstance(request, Request, {
|
||||||
|
moduleName: 'workbox-range-requests',
|
||||||
|
funcName: 'createPartialResponse',
|
||||||
|
paramName: 'request'
|
||||||
|
});
|
||||||
|
assert_js.assert.isInstance(originalResponse, Response, {
|
||||||
|
moduleName: 'workbox-range-requests',
|
||||||
|
funcName: 'createPartialResponse',
|
||||||
|
paramName: 'originalResponse'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (originalResponse.status === 206) {
|
||||||
|
// If we already have a 206, then just pass it through as-is;
|
||||||
|
// see https://github.com/GoogleChrome/workbox/issues/1720
|
||||||
|
return originalResponse;
|
||||||
|
}
|
||||||
|
const rangeHeader = request.headers.get('range');
|
||||||
|
if (!rangeHeader) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('no-range-header');
|
||||||
|
}
|
||||||
|
const boundaries = parseRangeHeader(rangeHeader);
|
||||||
|
const originalBlob = await originalResponse.blob();
|
||||||
|
const effectiveBoundaries = calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end);
|
||||||
|
const slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end);
|
||||||
|
const slicedBlobSize = slicedBlob.size;
|
||||||
|
const slicedResponse = new Response(slicedBlob, {
|
||||||
|
// Status code 206 is for a Partial Content response.
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206
|
||||||
|
status: 206,
|
||||||
|
statusText: 'Partial Content',
|
||||||
|
headers: originalResponse.headers
|
||||||
|
});
|
||||||
|
slicedResponse.headers.set('Content-Length', String(slicedBlobSize));
|
||||||
|
slicedResponse.headers.set('Content-Range', `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + `${originalBlob.size}`);
|
||||||
|
return slicedResponse;
|
||||||
|
} catch (error) {
|
||||||
|
{
|
||||||
|
logger_js.logger.warn(`Unable to construct a partial response; returning a ` + `416 Range Not Satisfiable response instead.`);
|
||||||
|
logger_js.logger.groupCollapsed(`View details here.`);
|
||||||
|
logger_js.logger.log(error);
|
||||||
|
logger_js.logger.log(request);
|
||||||
|
logger_js.logger.log(originalResponse);
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
}
|
||||||
|
return new Response('', {
|
||||||
|
status: 416,
|
||||||
|
statusText: 'Range Not Satisfiable'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The range request plugin makes it easy for a request with a 'Range' header to
|
||||||
|
* be fulfilled by a cached response.
|
||||||
|
*
|
||||||
|
* It does this by intercepting the `cachedResponseWillBeUsed` plugin callback
|
||||||
|
* and returning the appropriate subset of the cached response body.
|
||||||
|
*
|
||||||
|
* @memberof workbox-range-requests
|
||||||
|
*/
|
||||||
|
class RangeRequestsPlugin {
|
||||||
|
constructor() {
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Request} options.request The original request, which may or may not
|
||||||
|
* contain a Range: header.
|
||||||
|
* @param {Response} options.cachedResponse The complete cached response.
|
||||||
|
* @return {Promise<Response>} If request contains a 'Range' header, then a
|
||||||
|
* new response with status 206 whose body is a subset of `cachedResponse` is
|
||||||
|
* returned. Otherwise, `cachedResponse` is returned as-is.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.cachedResponseWillBeUsed = async ({
|
||||||
|
request,
|
||||||
|
cachedResponse
|
||||||
|
}) => {
|
||||||
|
// Only return a sliced response if there's something valid in the cache,
|
||||||
|
// and there's a Range: header in the request.
|
||||||
|
if (cachedResponse && request.headers.has('range')) {
|
||||||
|
return await createPartialResponse(request, cachedResponse);
|
||||||
|
}
|
||||||
|
// If there was no Range: header, or if cachedResponse wasn't valid, just
|
||||||
|
// pass it through as-is.
|
||||||
|
return cachedResponse;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.RangeRequestsPlugin = RangeRequestsPlugin;
|
||||||
|
exports.createPartialResponse = createPartialResponse;
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
|
||||||
|
})({}, workbox.core._private, workbox.core._private, workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-range-requests.dev.js.map
|
||||||
2
workbox/workbox-range-requests.prod.js
Normal file
2
workbox/workbox-range-requests.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.rangeRequests=function(t,e,n){"use strict";try{self["workbox:range-requests:7.3.0"]&&_()}catch(t){}async function r(t,n){try{if(206===n.status)return n;const r=t.headers.get("range");if(!r)throw new e.WorkboxError("no-range-header");const s=function(t){const n=t.trim().toLowerCase();if(!n.startsWith("bytes="))throw new e.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:n});if(n.includes(","))throw new e.WorkboxError("single-range-only",{normalizedRangeHeader:n});const r=/(\d*)-(\d*)/.exec(n);if(!r||!r[1]&&!r[2])throw new e.WorkboxError("invalid-range-values",{normalizedRangeHeader:n});return{start:""===r[1]?void 0:Number(r[1]),end:""===r[2]?void 0:Number(r[2])}}(r),a=await n.blob(),o=function(t,n,r){const s=t.size;if(n&&n>=s)throw new e.WorkboxError("range-not-satisfiable",{size:s,end:r,start:n});let a,o;return void 0!==n&&void 0!==r?(a=n,o=r+1):void 0!==n&&void 0===r?(a=n,o=s):void 0!==r&&void 0===n&&(a=s-r,o=s),{start:a,end:o}}(a,s.start,s.end),i=a.slice(o.start,o.end),d=i.size,u=new Response(i,{status:206,statusText:"Partial Content",headers:n.headers});return u.headers.set("Content-Length",String(d)),u.headers.set("Content-Range",`bytes ${o.start}-${o.end-1}/${a.size}`),u}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await r(t,e):e}},t.createPartialResponse=r,t}({},workbox.core._private,workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-range-requests.prod.js.map
|
||||||
884
workbox/workbox-routing.dev.js
Normal file
884
workbox/workbox-routing.dev.js
Normal file
@ -0,0 +1,884 @@
|
|||||||
|
this.workbox = this.workbox || {};
|
||||||
|
this.workbox.routing = (function (exports, assert_js, logger_js, WorkboxError_js, getFriendlyURL_js) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
try {
|
||||||
|
self['workbox:routing:7.3.0'] && _();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The default HTTP method, 'GET', used when there's no specific method
|
||||||
|
* configured for a route.
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const defaultMethod = 'GET';
|
||||||
|
/**
|
||||||
|
* The list of valid HTTP methods associated with requests that could be routed.
|
||||||
|
*
|
||||||
|
* @type {Array<string>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {function()|Object} handler Either a function, or an object with a
|
||||||
|
* 'handle' method.
|
||||||
|
* @return {Object} An object with a handle method.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const normalizeHandler = handler => {
|
||||||
|
if (handler && typeof handler === 'object') {
|
||||||
|
{
|
||||||
|
assert_js.assert.hasMethod(handler, 'handle', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Route',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'handler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return handler;
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
assert_js.assert.isType(handler, 'function', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Route',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'handler'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handle: handler
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* A `Route` consists of a pair of callback functions, "match" and "handler".
|
||||||
|
* The "match" callback determine if a route should be used to "handle" a
|
||||||
|
* request by returning a non-falsy value if it can. The "handler" callback
|
||||||
|
* is called when there is a match and should return a Promise that resolves
|
||||||
|
* to a `Response`.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
*/
|
||||||
|
class Route {
|
||||||
|
/**
|
||||||
|
* Constructor for Route class.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~matchCallback} match
|
||||||
|
* A callback function that determines whether the route matches a given
|
||||||
|
* `fetch` event by returning a non-falsy value.
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resolving to a Response.
|
||||||
|
* @param {string} [method='GET'] The HTTP method to match the Route
|
||||||
|
* against.
|
||||||
|
*/
|
||||||
|
constructor(match, handler, method = defaultMethod) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isType(match, 'function', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Route',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'match'
|
||||||
|
});
|
||||||
|
if (method) {
|
||||||
|
assert_js.assert.isOneOf(method, validMethods, {
|
||||||
|
paramName: 'method'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// These values are referenced directly by Router so cannot be
|
||||||
|
// altered by minificaton.
|
||||||
|
this.handler = normalizeHandler(handler);
|
||||||
|
this.match = match;
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {workbox-routing-handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resolving to a Response
|
||||||
|
*/
|
||||||
|
setCatchHandler(handler) {
|
||||||
|
this.catchHandler = normalizeHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* NavigationRoute makes it easy to create a
|
||||||
|
* {@link workbox-routing.Route} that matches for browser
|
||||||
|
* [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}.
|
||||||
|
*
|
||||||
|
* It will only match incoming Requests whose
|
||||||
|
* {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode}
|
||||||
|
* is set to `navigate`.
|
||||||
|
*
|
||||||
|
* You can optionally only apply this route to a subset of navigation requests
|
||||||
|
* by using one or both of the `denylist` and `allowlist` parameters.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
* @extends workbox-routing.Route
|
||||||
|
*/
|
||||||
|
class NavigationRoute extends Route {
|
||||||
|
/**
|
||||||
|
* If both `denylist` and `allowlist` are provided, the `denylist` will
|
||||||
|
* take precedence and the request will not match this route.
|
||||||
|
*
|
||||||
|
* The regular expressions in `allowlist` and `denylist`
|
||||||
|
* are matched against the concatenated
|
||||||
|
* [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname}
|
||||||
|
* and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search}
|
||||||
|
* portions of the requested URL.
|
||||||
|
*
|
||||||
|
* *Note*: These RegExps may be evaluated against every destination URL during
|
||||||
|
* a navigation. Avoid using
|
||||||
|
* [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),
|
||||||
|
* or else your users may see delays when navigating your site.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Array<RegExp>} [options.denylist] If any of these patterns match,
|
||||||
|
* the route will not handle the request (even if a allowlist RegExp matches).
|
||||||
|
* @param {Array<RegExp>} [options.allowlist=[/./]] If any of these patterns
|
||||||
|
* match the URL's pathname and search parameter, the route will handle the
|
||||||
|
* request (assuming the denylist doesn't match).
|
||||||
|
*/
|
||||||
|
constructor(handler, {
|
||||||
|
allowlist = [/./],
|
||||||
|
denylist = []
|
||||||
|
} = {}) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isArrayOfClass(allowlist, RegExp, {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'NavigationRoute',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'options.allowlist'
|
||||||
|
});
|
||||||
|
assert_js.assert.isArrayOfClass(denylist, RegExp, {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'NavigationRoute',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'options.denylist'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super(options => this._match(options), handler);
|
||||||
|
this._allowlist = allowlist;
|
||||||
|
this._denylist = denylist;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Routes match handler.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {URL} options.url
|
||||||
|
* @param {Request} options.request
|
||||||
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_match({
|
||||||
|
url,
|
||||||
|
request
|
||||||
|
}) {
|
||||||
|
if (request && request.mode !== 'navigate') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pathnameAndSearch = url.pathname + url.search;
|
||||||
|
for (const regExp of this._denylist) {
|
||||||
|
if (regExp.test(pathnameAndSearch)) {
|
||||||
|
{
|
||||||
|
logger_js.logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp.toString()}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) {
|
||||||
|
{
|
||||||
|
logger_js.logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
logger_js.logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* RegExpRoute makes it easy to create a regular expression based
|
||||||
|
* {@link workbox-routing.Route}.
|
||||||
|
*
|
||||||
|
* For same-origin requests the RegExp only needs to match part of the URL. For
|
||||||
|
* requests against third-party servers, you must define a RegExp that matches
|
||||||
|
* the start of the URL.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
* @extends workbox-routing.Route
|
||||||
|
*/
|
||||||
|
class RegExpRoute extends Route {
|
||||||
|
/**
|
||||||
|
* If the regular expression contains
|
||||||
|
* [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references},
|
||||||
|
* the captured values will be passed to the
|
||||||
|
* {@link workbox-routing~handlerCallback} `params`
|
||||||
|
* argument.
|
||||||
|
*
|
||||||
|
* @param {RegExp} regExp The regular expression to match against URLs.
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
* @param {string} [method='GET'] The HTTP method to match the Route
|
||||||
|
* against.
|
||||||
|
*/
|
||||||
|
constructor(regExp, handler, method) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isInstance(regExp, RegExp, {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'RegExpRoute',
|
||||||
|
funcName: 'constructor',
|
||||||
|
paramName: 'pattern'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const match = ({
|
||||||
|
url
|
||||||
|
}) => {
|
||||||
|
const result = regExp.exec(url.href);
|
||||||
|
// Return immediately if there's no match.
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Require that the match start at the first character in the URL string
|
||||||
|
// if it's a cross-origin request.
|
||||||
|
// See https://github.com/GoogleChrome/workbox/issues/281 for the context
|
||||||
|
// behind this behavior.
|
||||||
|
if (url.origin !== location.origin && result.index !== 0) {
|
||||||
|
{
|
||||||
|
logger_js.logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the route matches, but there aren't any capture groups defined, then
|
||||||
|
// this will return [], which is truthy and therefore sufficient to
|
||||||
|
// indicate a match.
|
||||||
|
// If there are capture groups, then it will return their values.
|
||||||
|
return result.slice(1);
|
||||||
|
};
|
||||||
|
super(match, handler, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2018 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The Router can be used to process a `FetchEvent` using one or more
|
||||||
|
* {@link workbox-routing.Route}, responding with a `Response` if
|
||||||
|
* a matching route exists.
|
||||||
|
*
|
||||||
|
* If no route matches a given a request, the Router will use a "default"
|
||||||
|
* handler if one is defined.
|
||||||
|
*
|
||||||
|
* Should the matching Route throw an error, the Router will use a "catch"
|
||||||
|
* handler if one is defined to gracefully deal with issues and respond with a
|
||||||
|
* Request.
|
||||||
|
*
|
||||||
|
* If a request matches multiple routes, the **earliest** registered route will
|
||||||
|
* be used to respond to the request.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
*/
|
||||||
|
class Router {
|
||||||
|
/**
|
||||||
|
* Initializes a new Router.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this._routes = new Map();
|
||||||
|
this._defaultHandlerMap = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return {Map<string, Array<workbox-routing.Route>>} routes A `Map` of HTTP
|
||||||
|
* method name ('GET', etc.) to an array of all the corresponding `Route`
|
||||||
|
* instances that are registered.
|
||||||
|
*/
|
||||||
|
get routes() {
|
||||||
|
return this._routes;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds a fetch event listener to respond to events when a route matches
|
||||||
|
* the event's request.
|
||||||
|
*/
|
||||||
|
addFetchListener() {
|
||||||
|
// See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const {
|
||||||
|
request
|
||||||
|
} = event;
|
||||||
|
const responsePromise = this.handleRequest({
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
});
|
||||||
|
if (responsePromise) {
|
||||||
|
event.respondWith(responsePromise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds a message event listener for URLs to cache from the window.
|
||||||
|
* This is useful to cache resources loaded on the page prior to when the
|
||||||
|
* service worker started controlling it.
|
||||||
|
*
|
||||||
|
* The format of the message data sent from the window should be as follows.
|
||||||
|
* Where the `urlsToCache` array may consist of URL strings or an array of
|
||||||
|
* URL string + `requestInit` object (the same as you'd pass to `fetch()`).
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* type: 'CACHE_URLS',
|
||||||
|
* payload: {
|
||||||
|
* urlsToCache: [
|
||||||
|
* './script1.js',
|
||||||
|
* './script2.js',
|
||||||
|
* ['./script3.js', {mode: 'no-cors'}],
|
||||||
|
* ],
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
addCacheListener() {
|
||||||
|
// See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
|
||||||
|
self.addEventListener('message', event => {
|
||||||
|
// event.data is type 'any'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (event.data && event.data.type === 'CACHE_URLS') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const {
|
||||||
|
payload
|
||||||
|
} = event.data;
|
||||||
|
{
|
||||||
|
logger_js.logger.debug(`Caching URLs from the window`, payload.urlsToCache);
|
||||||
|
}
|
||||||
|
const requestPromises = Promise.all(payload.urlsToCache.map(entry => {
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
entry = [entry];
|
||||||
|
}
|
||||||
|
const request = new Request(...entry);
|
||||||
|
return this.handleRequest({
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
});
|
||||||
|
// TODO(philipwalton): TypeScript errors without this typecast for
|
||||||
|
// some reason (probably a bug). The real type here should work but
|
||||||
|
// doesn't: `Array<Promise<Response> | undefined>`.
|
||||||
|
})); // TypeScript
|
||||||
|
event.waitUntil(requestPromises);
|
||||||
|
// If a MessageChannel was used, reply to the message on success.
|
||||||
|
if (event.ports && event.ports[0]) {
|
||||||
|
void requestPromises.then(() => event.ports[0].postMessage(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Apply the routing rules to a FetchEvent object to get a Response from an
|
||||||
|
* appropriate Route's handler.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Request} options.request The request to handle.
|
||||||
|
* @param {ExtendableEvent} options.event The event that triggered the
|
||||||
|
* request.
|
||||||
|
* @return {Promise<Response>|undefined} A promise is returned if a
|
||||||
|
* registered route can handle the request. If there is no matching
|
||||||
|
* route and there's no `defaultHandler`, `undefined` is returned.
|
||||||
|
*/
|
||||||
|
handleRequest({
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
}) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isInstance(request, Request, {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'handleRequest',
|
||||||
|
paramName: 'options.request'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const url = new URL(request.url, location.href);
|
||||||
|
if (!url.protocol.startsWith('http')) {
|
||||||
|
{
|
||||||
|
logger_js.logger.debug(`Workbox Router only supports URLs that start with 'http'.`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sameOrigin = url.origin === location.origin;
|
||||||
|
const {
|
||||||
|
params,
|
||||||
|
route
|
||||||
|
} = this.findMatchingRoute({
|
||||||
|
event,
|
||||||
|
request,
|
||||||
|
sameOrigin,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
let handler = route && route.handler;
|
||||||
|
const debugMessages = [];
|
||||||
|
{
|
||||||
|
if (handler) {
|
||||||
|
debugMessages.push([`Found a route to handle this request:`, route]);
|
||||||
|
if (params) {
|
||||||
|
debugMessages.push([`Passing the following params to the route's handler:`, params]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we don't have a handler because there was no matching route, then
|
||||||
|
// fall back to defaultHandler if that's defined.
|
||||||
|
const method = request.method;
|
||||||
|
if (!handler && this._defaultHandlerMap.has(method)) {
|
||||||
|
{
|
||||||
|
debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`);
|
||||||
|
}
|
||||||
|
handler = this._defaultHandlerMap.get(method);
|
||||||
|
}
|
||||||
|
if (!handler) {
|
||||||
|
{
|
||||||
|
// No handler so Workbox will do nothing. If logs is set of debug
|
||||||
|
// i.e. verbose, we should print out this information.
|
||||||
|
logger_js.logger.debug(`No route found for: ${getFriendlyURL_js.getFriendlyURL(url)}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// We have a handler, meaning Workbox is going to handle the route.
|
||||||
|
// print the routing details to the console.
|
||||||
|
logger_js.logger.groupCollapsed(`Router is responding to: ${getFriendlyURL_js.getFriendlyURL(url)}`);
|
||||||
|
debugMessages.forEach(msg => {
|
||||||
|
if (Array.isArray(msg)) {
|
||||||
|
logger_js.logger.log(...msg);
|
||||||
|
} else {
|
||||||
|
logger_js.logger.log(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
}
|
||||||
|
// Wrap in try and catch in case the handle method throws a synchronous
|
||||||
|
// error. It should still callback to the catch handler.
|
||||||
|
let responsePromise;
|
||||||
|
try {
|
||||||
|
responsePromise = handler.handle({
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
event,
|
||||||
|
params
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
responsePromise = Promise.reject(err);
|
||||||
|
}
|
||||||
|
// Get route's catch handler, if it exists
|
||||||
|
const catchHandler = route && route.catchHandler;
|
||||||
|
if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
|
||||||
|
responsePromise = responsePromise.catch(async err => {
|
||||||
|
// If there's a route catch handler, process that first
|
||||||
|
if (catchHandler) {
|
||||||
|
{
|
||||||
|
// Still include URL here as it will be async from the console group
|
||||||
|
// and may not make sense without the URL
|
||||||
|
logger_js.logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL_js.getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
|
||||||
|
logger_js.logger.error(`Error thrown by:`, route);
|
||||||
|
logger_js.logger.error(err);
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await catchHandler.handle({
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
event,
|
||||||
|
params
|
||||||
|
});
|
||||||
|
} catch (catchErr) {
|
||||||
|
if (catchErr instanceof Error) {
|
||||||
|
err = catchErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._catchHandler) {
|
||||||
|
{
|
||||||
|
// Still include URL here as it will be async from the console group
|
||||||
|
// and may not make sense without the URL
|
||||||
|
logger_js.logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL_js.getFriendlyURL(url)}. Falling back to global Catch Handler.`);
|
||||||
|
logger_js.logger.error(`Error thrown by:`, route);
|
||||||
|
logger_js.logger.error(err);
|
||||||
|
logger_js.logger.groupEnd();
|
||||||
|
}
|
||||||
|
return this._catchHandler.handle({
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return responsePromise;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks a request and URL (and optionally an event) against the list of
|
||||||
|
* registered routes, and if there's a match, returns the corresponding
|
||||||
|
* route along with any params generated by the match.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {URL} options.url
|
||||||
|
* @param {boolean} options.sameOrigin The result of comparing `url.origin`
|
||||||
|
* against the current origin.
|
||||||
|
* @param {Request} options.request The request to match.
|
||||||
|
* @param {Event} options.event The corresponding event.
|
||||||
|
* @return {Object} An object with `route` and `params` properties.
|
||||||
|
* They are populated if a matching route was found or `undefined`
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
findMatchingRoute({
|
||||||
|
url,
|
||||||
|
sameOrigin,
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
}) {
|
||||||
|
const routes = this._routes.get(request.method) || [];
|
||||||
|
for (const route of routes) {
|
||||||
|
let params;
|
||||||
|
// route.match returns type any, not possible to change right now.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const matchResult = route.match({
|
||||||
|
url,
|
||||||
|
sameOrigin,
|
||||||
|
request,
|
||||||
|
event
|
||||||
|
});
|
||||||
|
if (matchResult) {
|
||||||
|
{
|
||||||
|
// Warn developers that using an async matchCallback is almost always
|
||||||
|
// not the right thing to do.
|
||||||
|
if (matchResult instanceof Promise) {
|
||||||
|
logger_js.logger.warn(`While routing ${getFriendlyURL_js.getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// See https://github.com/GoogleChrome/workbox/issues/2079
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
params = matchResult;
|
||||||
|
if (Array.isArray(params) && params.length === 0) {
|
||||||
|
// Instead of passing an empty array in as params, use undefined.
|
||||||
|
params = undefined;
|
||||||
|
} else if (matchResult.constructor === Object &&
|
||||||
|
// eslint-disable-line
|
||||||
|
Object.keys(matchResult).length === 0) {
|
||||||
|
// Instead of passing an empty object in as params, use undefined.
|
||||||
|
params = undefined;
|
||||||
|
} else if (typeof matchResult === 'boolean') {
|
||||||
|
// For the boolean value true (rather than just something truth-y),
|
||||||
|
// don't set params.
|
||||||
|
// See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353
|
||||||
|
params = undefined;
|
||||||
|
}
|
||||||
|
// Return early if have a match.
|
||||||
|
return {
|
||||||
|
route,
|
||||||
|
params
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no match was found above, return and empty object.
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Define a default `handler` that's called when no routes explicitly
|
||||||
|
* match the incoming request.
|
||||||
|
*
|
||||||
|
* Each HTTP method ('GET', 'POST', etc.) gets its own default handler.
|
||||||
|
*
|
||||||
|
* Without a default handler, unmatched requests will go against the
|
||||||
|
* network as if there were no service worker present.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
* @param {string} [method='GET'] The HTTP method to associate with this
|
||||||
|
* default handler. Each method has its own default.
|
||||||
|
*/
|
||||||
|
setDefaultHandler(handler, method = defaultMethod) {
|
||||||
|
this._defaultHandlerMap.set(method, normalizeHandler(handler));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* If a Route throws an error while handling a request, this `handler`
|
||||||
|
* will be called and given a chance to provide a response.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
*/
|
||||||
|
setCatchHandler(handler) {
|
||||||
|
this._catchHandler = normalizeHandler(handler);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Registers a route with the router.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing.Route} route The route to register.
|
||||||
|
*/
|
||||||
|
registerRoute(route) {
|
||||||
|
{
|
||||||
|
assert_js.assert.isType(route, 'object', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'route'
|
||||||
|
});
|
||||||
|
assert_js.assert.hasMethod(route, 'match', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'route'
|
||||||
|
});
|
||||||
|
assert_js.assert.isType(route.handler, 'object', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'route'
|
||||||
|
});
|
||||||
|
assert_js.assert.hasMethod(route.handler, 'handle', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'route.handler'
|
||||||
|
});
|
||||||
|
assert_js.assert.isType(route.method, 'string', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
className: 'Router',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'route.method'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this._routes.has(route.method)) {
|
||||||
|
this._routes.set(route.method, []);
|
||||||
|
}
|
||||||
|
// Give precedence to all of the earlier routes by adding this additional
|
||||||
|
// route to the end of the array.
|
||||||
|
this._routes.get(route.method).push(route);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unregisters a route with the router.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing.Route} route The route to unregister.
|
||||||
|
*/
|
||||||
|
unregisterRoute(route) {
|
||||||
|
if (!this._routes.has(route.method)) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('unregister-route-but-not-found-with-method', {
|
||||||
|
method: route.method
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const routeIndex = this._routes.get(route.method).indexOf(route);
|
||||||
|
if (routeIndex > -1) {
|
||||||
|
this._routes.get(route.method).splice(routeIndex, 1);
|
||||||
|
} else {
|
||||||
|
throw new WorkboxError_js.WorkboxError('unregister-route-route-not-registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2019 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
let defaultRouter;
|
||||||
|
/**
|
||||||
|
* Creates a new, singleton Router instance if one does not exist. If one
|
||||||
|
* does already exist, that instance is returned.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @return {Router}
|
||||||
|
*/
|
||||||
|
const getOrCreateDefaultRouter = () => {
|
||||||
|
if (!defaultRouter) {
|
||||||
|
defaultRouter = new Router();
|
||||||
|
// The helpers that use the default Router assume these listeners exist.
|
||||||
|
defaultRouter.addFetchListener();
|
||||||
|
defaultRouter.addCacheListener();
|
||||||
|
}
|
||||||
|
return defaultRouter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2019 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Easily register a RegExp, string, or function with a caching
|
||||||
|
* strategy to a singleton Router instance.
|
||||||
|
*
|
||||||
|
* This method will generate a Route for you if needed and
|
||||||
|
* call {@link workbox-routing.Router#registerRoute}.
|
||||||
|
*
|
||||||
|
* @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture
|
||||||
|
* If the capture param is a `Route`, all other arguments will be ignored.
|
||||||
|
* @param {workbox-routing~handlerCallback} [handler] A callback
|
||||||
|
* function that returns a Promise resulting in a Response. This parameter
|
||||||
|
* is required if `capture` is not a `Route` object.
|
||||||
|
* @param {string} [method='GET'] The HTTP method to match the Route
|
||||||
|
* against.
|
||||||
|
* @return {workbox-routing.Route} The generated `Route`.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
*/
|
||||||
|
function registerRoute(capture, handler, method) {
|
||||||
|
let route;
|
||||||
|
if (typeof capture === 'string') {
|
||||||
|
const captureUrl = new URL(capture, location.href);
|
||||||
|
{
|
||||||
|
if (!(capture.startsWith('/') || capture.startsWith('http'))) {
|
||||||
|
throw new WorkboxError_js.WorkboxError('invalid-string', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'capture'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// We want to check if Express-style wildcards are in the pathname only.
|
||||||
|
// TODO: Remove this log message in v4.
|
||||||
|
const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture;
|
||||||
|
// See https://github.com/pillarjs/path-to-regexp#parameters
|
||||||
|
const wildcards = '[*:?+]';
|
||||||
|
if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
|
||||||
|
logger_js.logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const matchCallback = ({
|
||||||
|
url
|
||||||
|
}) => {
|
||||||
|
{
|
||||||
|
if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
|
||||||
|
logger_js.logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.href === captureUrl.href;
|
||||||
|
};
|
||||||
|
// If `capture` is a string then `handler` and `method` must be present.
|
||||||
|
route = new Route(matchCallback, handler, method);
|
||||||
|
} else if (capture instanceof RegExp) {
|
||||||
|
// If `capture` is a `RegExp` then `handler` and `method` must be present.
|
||||||
|
route = new RegExpRoute(capture, handler, method);
|
||||||
|
} else if (typeof capture === 'function') {
|
||||||
|
// If `capture` is a function then `handler` and `method` must be present.
|
||||||
|
route = new Route(capture, handler, method);
|
||||||
|
} else if (capture instanceof Route) {
|
||||||
|
route = capture;
|
||||||
|
} else {
|
||||||
|
throw new WorkboxError_js.WorkboxError('unsupported-route-type', {
|
||||||
|
moduleName: 'workbox-routing',
|
||||||
|
funcName: 'registerRoute',
|
||||||
|
paramName: 'capture'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const defaultRouter = getOrCreateDefaultRouter();
|
||||||
|
defaultRouter.registerRoute(route);
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2019 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* If a Route throws an error while handling a request, this `handler`
|
||||||
|
* will be called and given a chance to provide a response.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
*/
|
||||||
|
function setCatchHandler(handler) {
|
||||||
|
const defaultRouter = getOrCreateDefaultRouter();
|
||||||
|
defaultRouter.setCatchHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2019 Google LLC
|
||||||
|
|
||||||
|
Use of this source code is governed by an MIT-style
|
||||||
|
license that can be found in the LICENSE file or at
|
||||||
|
https://opensource.org/licenses/MIT.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Define a default `handler` that's called when no routes explicitly
|
||||||
|
* match the incoming request.
|
||||||
|
*
|
||||||
|
* Without a default handler, unmatched requests will go against the
|
||||||
|
* network as if there were no service worker present.
|
||||||
|
*
|
||||||
|
* @param {workbox-routing~handlerCallback} handler A callback
|
||||||
|
* function that returns a Promise resulting in a Response.
|
||||||
|
*
|
||||||
|
* @memberof workbox-routing
|
||||||
|
*/
|
||||||
|
function setDefaultHandler(handler) {
|
||||||
|
const defaultRouter = getOrCreateDefaultRouter();
|
||||||
|
defaultRouter.setDefaultHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.NavigationRoute = NavigationRoute;
|
||||||
|
exports.RegExpRoute = RegExpRoute;
|
||||||
|
exports.Route = Route;
|
||||||
|
exports.Router = Router;
|
||||||
|
exports.registerRoute = registerRoute;
|
||||||
|
exports.setCatchHandler = setCatchHandler;
|
||||||
|
exports.setDefaultHandler = setDefaultHandler;
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
|
||||||
|
})({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-routing.dev.js.map
|
||||||
2
workbox/workbox-routing.prod.js
Normal file
2
workbox/workbox-routing.prod.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
this.workbox=this.workbox||{},this.workbox.routing=function(t,e){"use strict";try{self["workbox:routing:7.3.0"]&&_()}catch(t){}const s=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,r="GET"){this.handler=s(e),this.match=t,this.method=r}setCatchHandler(t){this.catchHandler=s(t)}}class n extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class i{constructor(){this.ft=new Map,this.dt=new Map}get routes(){return this.ft}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const r=s.origin===location.origin,{params:n,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:r,url:s});let o=i&&i.handler;const u=t.method;if(!o&&this.dt.has(u)&&(o=this.dt.get(u)),!o)return;let c;try{c=o.handle({url:s,request:t,event:e,params:n})}catch(t){c=Promise.reject(t)}const a=i&&i.catchHandler;return c instanceof Promise&&(this.wt||a)&&(c=c.catch((async r=>{if(a)try{return await a.handle({url:s,request:t,event:e,params:n})}catch(t){t instanceof Error&&(r=t)}if(this.wt)return this.wt.handle({url:s,request:t,event:e});throw r}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:r}){const n=this.ft.get(s.method)||[];for(const i of n){let n;const o=i.match({url:t,sameOrigin:e,request:s,event:r});if(o)return n=o,(Array.isArray(n)&&0===n.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(n=void 0),{route:i,params:n}}return{}}setDefaultHandler(t,e="GET"){this.dt.set(e,s(t))}setCatchHandler(t){this.wt=s(t)}registerRoute(t){this.ft.has(t.method)||this.ft.set(t.method,[]),this.ft.get(t.method).push(t)}unregisterRoute(t){if(!this.ft.has(t.method))throw new e.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const s=this.ft.get(t.method).indexOf(t);if(!(s>-1))throw new e.WorkboxError("unregister-route-route-not-registered");this.ft.get(t.method).splice(s,1)}}let o;const u=()=>(o||(o=new i,o.addFetchListener(),o.addCacheListener()),o);return t.NavigationRoute=class extends r{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super((t=>this.gt(t)),t),this.qt=e,this.yt=s}gt({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.yt)if(t.test(s))return!1;return!!this.qt.some((t=>t.test(s)))}},t.RegExpRoute=n,t.Route=r,t.Router=i,t.registerRoute=function(t,s,i){let o;if("string"==typeof t){const e=new URL(t,location.href);o=new r((({url:t})=>t.href===e.href),s,i)}else if(t instanceof RegExp)o=new n(t,s,i);else if("function"==typeof t)o=new r(t,s,i);else{if(!(t instanceof r))throw new e.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return u().registerRoute(o),o},t.setCatchHandler=function(t){u().setCatchHandler(t)},t.setDefaultHandler=function(t){u().setDefaultHandler(t)},t}({},workbox.core._private);
|
||||||
|
//# sourceMappingURL=workbox-routing.prod.js.map
|
||||||
1334
workbox/workbox-strategies.dev.js
Normal file
1334
workbox/workbox-strategies.dev.js
Normal file
File diff suppressed because it is too large
Load Diff
2
workbox/workbox-strategies.prod.js
Normal file
2
workbox/workbox-strategies.prod.js
Normal file
File diff suppressed because one or more lines are too long
2
workbox/workbox-sw.js
Normal file
2
workbox/workbox-sw.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
!function(){"use strict";try{self["workbox:sw:7.3.0"]&&_()}catch(t){}const t={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams",recipes:"recipes"};self.workbox=new class{constructor(){return this.v={},this.Pt={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.$t=this.Pt.debug?"dev":"prod",this.jt=!1,new Proxy(this,{get(e,s){if(e[s])return e[s];const o=t[s];return o&&e.loadModule(`workbox-${o}`),e[s]}})}setConfig(t={}){if(this.jt)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.Pt,t),this.$t=this.Pt.debug?"dev":"prod"}loadModule(t){const e=this.St(t);try{importScripts(e),this.jt=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}St(t){if(this.Pt.modulePathCb)return this.Pt.modulePathCb(t,this.Pt.debug);let e=["https://storage.googleapis.com/workbox-cdn/releases/7.3.0"];const s=`${t}.${this.$t}.js`,o=this.Pt.modulePathPrefix;return o&&(e=o.split("/"),""===e[e.length-1]&&e.splice(e.length-1,1)),e.push(s),e.join("/")}}}();
|
||||||
|
//# sourceMappingURL=workbox-sw.js.map
|
||||||
Loading…
Reference in New Issue
Block a user