Add offline play feature

This commit is contained in:
Christian Semmler 2025-07-17 18:21:25 -07:00
parent acbb75408f
commit 3b76e96113
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
22 changed files with 6498 additions and 1062 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
isle.wasm
isle.js
LEGO

436
app.js Normal file
View 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

File diff suppressed because it is too large Load Diff

BIN
install_off.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
install_on.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

762
style.css Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
uninstall_on.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
workbox/workbox-sw.js Normal file
View 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