From f8b0e34755d6d9f3f3c43e8b54cbba2c707304c2 Mon Sep 17 00:00:00 2001
From: Ernesto Serrano
Date: Sun, 2 Nov 2025 11:01:35 +0000
Subject: [PATCH 1/2] Reinvent the designer to use plain html
---
.dockerignore | 3 -
Dockerfile | 20 --
README.md | 58 ++---
custom-php.ini | 4 -
designer/index.html | 113 ++++++++++
docker-compose.yml | 8 -
download/index.html | 57 +++++
download/index.php | 150 -------------
files/css/style-designer.css | 73 ++++--
files/js/downloader.js | 166 ++++++++++++++
files/js/jszip.min.js | 13 ++
files/js/style-designer.js | 17 +-
files/js/style-storage.js | 184 +++++++++++++++
files/js/uploader.js | 212 ++++++++++++++++++
files/js/viewer.js | 423 +++++++++++++++++++++++++++++++++++
index.html | 63 ++++++
index.php | 58 -----
style-sw.js | 54 +++++
upload/index.html | 43 ++++
upload/index.php | 158 -------------
20 files changed, 1413 insertions(+), 464 deletions(-)
delete mode 100644 .dockerignore
delete mode 100644 Dockerfile
delete mode 100644 custom-php.ini
create mode 100644 designer/index.html
delete mode 100644 docker-compose.yml
create mode 100644 download/index.html
delete mode 100644 download/index.php
create mode 100644 files/js/downloader.js
create mode 100644 files/js/jszip.min.js
create mode 100644 files/js/style-storage.js
create mode 100644 files/js/uploader.js
create mode 100644 files/js/viewer.js
create mode 100644 index.html
delete mode 100644 index.php
create mode 100644 style-sw.js
create mode 100644 upload/index.html
delete mode 100644 upload/index.php
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index f9f0af9..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,3 +0,0 @@
-docker-compose.yml
-Dockerfile
-*.zip
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0eaab4b..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Use official PHP image with Apache
-FROM php:8.2-apache
-
-# Copy project files to Apache document root
-COPY . /var/www/html/
-
-# Set working directory
-WORKDIR /var/www/html/
-
-# Enable Apache mod_rewrite (if needed)
-RUN a2enmod rewrite
-
-# Give write permissions to the web server (if your scripts create files)
-RUN chown -R www-data:www-data /var/www/html
-
-# Expose port 80 inside the container
-EXPOSE 80
-
-# Copy custom PHP configuration
-COPY custom-php.ini /usr/local/etc/php/conf.d/
diff --git a/README.md b/README.md
index d092538..61f788e 100644
--- a/README.md
+++ b/README.md
@@ -37,51 +37,31 @@ It does not include the necessary security measures. This tool is intended solel
---
-## Installation
+## Serving the Tool
-### 1. Docker (Recommended)
+Any static server works; the examples below cover the most common options:
-This is the easiest way to run the application without installing PHP or Apache locally.
+- **Node.js (http-server)**
+ ```bash
+ npm install --global http-server # once
+ http-server -p 8080 # from the project root
+ ```
+ Navigate to `http://127.0.0.1:8080/`.
-1. Make sure **Docker Desktop** is installed and running.
-2. Clone or download this repository.
-3. Navigate to the project root in a terminal.
-4. Build and start the container:
+- **PHP built-in web server**
+ ```bash
+ php -S 127.0.0.1:8080
+ ```
-```bash
-docker-compose up --build
-```
+- **Python 3 (http.server)**
+ ```bash
+ python3 -m http.server 8080
+ ```
-5. Open your browser and visit:
+- **Any other static server**
+ Point Apache, Nginx, Caddy, etc. at the repository root; no rewrite rules are required.
-```
-http://localhost:8000/
-```
-
-> You can change the port mapping in `docker-compose.yml` if needed.
-
----
-
-### 2. Direct Installation on Apache
-
-If you prefer to run the application directly on a local Apache server:
-
-1. Make sure you have **PHP 8+** and Apache installed.
-2. Copy the project files to your Apache document root (e.g., `htdocs` or `www`).
-3. Ensure the `path-to-project/` folder is accessible via your browser.
-4. Adjust PHP settings if needed for large file uploads:
-
-```ini
-upload_max_filesize = 50M
-post_max_size = 50M
-memory_limit = 128M
-```
-
-5. Open your browser and visit:
-
-```
-http://localhost/path-to-project/index.php
-```
+> **Tip:** When you change code, do a hard refresh (`Ctrl`/`Cmd` + `Shift` + `R`) so the updated service worker and assets reload.
---
diff --git a/custom-php.ini b/custom-php.ini
deleted file mode 100644
index 9bd5efa..0000000
--- a/custom-php.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-; Increase max upload size
-upload_max_filesize = 500M
-post_max_size = 500M
-memory_limit = 128M
diff --git a/designer/index.html b/designer/index.html
new file mode 100644
index 0000000..db66f08
--- /dev/null
+++ b/designer/index.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+ Style Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Style Designer
+
+ No contents available…
+
+
Upload the exported packages in the Upload page and come back to preview them here.
+
+
+
+
+
+
Theme Configuration
+ Reset
+
+
+
+
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index c1ee1fb..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-services:
- php-web:
- build: .
- container_name: exelearning-style-designer
- ports:
- - "8000:80"
- volumes:
- - .:/var/www/html
diff --git a/download/index.html b/download/index.html
new file mode 100644
index 0000000..4582646
--- /dev/null
+++ b/download/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ Download | Style Designer
+
+
+
+
+
+
+
+
+
+
+
+
Download style
+
+
+ Edit the config.xml file before downloading the style.
+ Remember that "name" is an ID and cannot contain spaces or special characters.
+ If your work is based on another style, you must respect the license terms.
+ You can add any additional information about licenses, authorship, or anything else you need in the "description" field.
+ When you're finished, click "Download Style".
+
+
+ Download Style
+ Delete all files and create a new Style
+
+
+
+
+
+
+
+
If you click "Continue", all files will be permanently deleted , including the uploaded content and the Style you’ve been working on. This action cannot be undone.
+
+
+
+
+
+
+
diff --git a/download/index.php b/download/index.php
deleted file mode 100644
index b38be77..0000000
--- a/download/index.php
+++ /dev/null
@@ -1,150 +0,0 @@
-isDir()) {
- rmdir($item->getRealPath());
- } else {
- unlink($item->getRealPath());
- }
- }
- rmdir($dir);
- }
-
- // Paths to theme and contents folders
- $basePath = realpath(__DIR__ . '/..');
- $themePath = $basePath . '/theme';
- $contentsPath = $basePath . '/contents';
-
- // Delete both folders if they exist
- deleteDirectory($themePath);
- deleteDirectory($contentsPath);
-
- // Redirect to index.php located at the same level
- header("Location: ../index.php");
- exit;
-}
-?>
- from config.xml
- $xml = simplexml_load_file($configFile);
- $zipName = (string) $xml->name . '.zip';
-
- // Temporary path to create the ZIP
- $zipPath = __DIR__ . '/' . $zipName;
-
- $zip = new ZipArchive();
- if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
- die('Error: Cannot create ZIP file.');
- }
-
- // Add files recursively
- $files = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($themeFolder, RecursiveDirectoryIterator::SKIP_DOTS),
- RecursiveIteratorIterator::SELF_FIRST
- );
-
- foreach ($files as $file) {
- $filePath = $file->getRealPath();
- // Correct relative path inside ZIP
- $relativePath = str_replace('\\', '/', substr($filePath, strlen($themeFolder) + 1));
-
- if ($file->isDir()) {
- if (!empty($relativePath)) { // avoid adding empty root
- $zip->addEmptyDir($relativePath);
- }
- } else {
- $zip->addFile($filePath, $relativePath);
- }
- }
-
- $zip->close();
-
- // Force download
- header('Content-Type: application/zip');
- header('Content-Disposition: attachment; filename="' . basename($zipName) . '"');
- header('Content-Length: ' . filesize($zipPath));
- readfile($zipPath);
-
- // Delete temporary ZIP
- unlink($zipPath);
- exit;
-}
-?>
-
-
-
-
- Uploader | Style Designer
-
-
-
-
-
-
-
-
-
-
Download style
-
- Edit the config.xml file before downloading the style.
- Remember that "name" is an ID and cannot contain spaces or special characters.
- If your work is based on another style, you must respect the license terms.
- You can add any additional information about licenses, authorship, or anything else you need in the "description" field.
- When you're finished, click "Download Style".
-
-
- Download Style
- Delete all files and create a new Style
-
-
-
-
-
-
-
-
-
-
-
If you click "Continue", all files will be permanently deleted , including the uploaded content and the Style you’ve been working on. This action cannot be undone.
-
-
-
-
-
-
-
-
diff --git a/files/css/style-designer.css b/files/css/style-designer.css
index 9ee718f..be61dc0 100644
--- a/files/css/style-designer.css
+++ b/files/css/style-designer.css
@@ -1,36 +1,73 @@
body{
margin: 0;
- padding: 52px 0 0 0;
- height: 100vh;
- overflow-y: hidden;
+ padding: 64px 0 0 0;
+ min-height: 100vh;
+ background-color: #f9f9fb;
+}
+.designer-page .designer-main{
+ max-width: 1200px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+.designer-page #previewPanel{
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+}
+.page-content{
+ padding: 24px;
}
#sdHeader{
background: #2B2A33 url(../exelearning.png) no-repeat 20px 50%;
background-size: auto 32px;
border-bottom: 1px solid #0C0C0D;
- display: block;
width: 100%;
- text-align: center;
- padding: 10px;
- position: absolute;
+ padding: 10px 16px 10px 68px;
+ position: fixed;
top: 0;
- height: 52px;
+ left: 0;
+ z-index: 1000;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
}
-iframe {
- width: 100%;
- height: calc(100vh - 52px);
+#sdHeader .nav-group,
+#sdHeader .view-group,
+#sdHeader .config-group,
+#sdHeader .action-group{
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+#sdHeader .nav-group{
+ flex: 1 1 auto;
}
-#done {
- float: right;
- margin-left: .5em;
+#sdHeader .view-group{
+ flex: 0 0 auto;
}
-#_blank {
- float: right;
+#sdHeader .config-group{
+ flex: 0 0 auto;
+}
+#sdHeader .action-group{
+ margin-left: auto;
+}
+#previewPanel iframe {
+ width: 100%;
+ height: calc(100vh - 64px);
+ border: none;
+ background-color: #fff;
+}
+.designer-page #configPanel{
+ max-width: 720px;
+ margin: 0 auto;
}
.btn.mw {
min-width: 100px;
}
-#_blank img{
+#openNewWindow img{
position: relative;
top: -2px;
}
@@ -42,4 +79,4 @@ iframe {
.btn-primary:focus {
background-color: #107275;
border-color: #107275;
-}
\ No newline at end of file
+}
diff --git a/files/js/downloader.js b/files/js/downloader.js
new file mode 100644
index 0000000..4e15f17
--- /dev/null
+++ b/files/js/downloader.js
@@ -0,0 +1,166 @@
+(() => {
+ const messagesContainer = document.getElementById('downloadMessages');
+ const downloadButton = document.getElementById('downloadButton');
+ const confirmButton = document.getElementById('confirmDeleteAction');
+ const confirmModalElement = document.getElementById('confirmModal');
+
+ const TEXT_EXTENSIONS = new Set([
+ '.html', '.htm', '.css', '.js', '.json', '.txt', '.xml', '.xhtml', '.svg'
+ ]);
+
+ function getBootstrapModal() {
+ if (!confirmModalElement || typeof bootstrap === 'undefined') return null;
+ return bootstrap.Modal.getOrCreateInstance(confirmModalElement);
+ }
+
+ function clearMessages() {
+ if (messagesContainer) {
+ messagesContainer.innerHTML = '';
+ }
+ }
+
+ function showMessage(type, text) {
+ if (!messagesContainer) return;
+ const div = document.createElement('div');
+ div.className = `alert alert-${type}`;
+ div.role = 'status';
+ div.textContent = text;
+ messagesContainer.appendChild(div);
+ }
+
+ function getExtension(name) {
+ const match = name.toLowerCase().match(/\.[^.]+$/);
+ return match ? match[0] : '';
+ }
+
+ function isTextExtension(ext) {
+ return TEXT_EXTENSIONS.has(ext);
+ }
+
+ async function handleDownload() {
+ clearMessages();
+
+ if (!window.StyleStorage) {
+ showMessage('danger', 'Storage is not available.');
+ return;
+ }
+
+ if (typeof JSZip === 'undefined') {
+ showMessage('danger', 'JSZip library is missing.');
+ return;
+ }
+
+ try {
+ await StyleStorage.init();
+ } catch (err) {
+ console.error(err);
+ showMessage('danger', `Unable to initialise storage. ${err && err.message ? err.message : ''}`);
+ return;
+ }
+
+ const themePaths = await StyleStorage.list('theme/');
+ if (!themePaths.length) {
+ showMessage('warning', 'No theme files found. Upload content first.');
+ return;
+ }
+
+ const zip = new JSZip();
+ let themeName = 'style';
+
+ try {
+ const configContent = await StyleStorage.readText('theme/config.xml');
+ if (configContent) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(configContent, 'application/xml');
+ const nameNode = doc.querySelector('name');
+ if (nameNode && nameNode.textContent) {
+ const sanitized = nameNode.textContent.trim();
+ if (sanitized) themeName = sanitized;
+ }
+ } else {
+ showMessage('warning', 'config.xml not found. Using default name.');
+ }
+ } catch (err) {
+ console.warn('Unable to read config.xml', err);
+ showMessage('warning', 'Could not read config.xml. Using default name.');
+ }
+
+ for (const path of themePaths) {
+ const relative = path.replace(/^theme\//, '');
+ const ext = getExtension(relative);
+ const fullPath = `theme/${relative}`;
+
+ if (isTextExtension(ext)) {
+ const content = await StyleStorage.readText(fullPath);
+ if (content != null) {
+ zip.file(relative, content);
+ }
+ } else {
+ const data = await StyleStorage.readBinary(fullPath);
+ if (data != null) {
+ zip.file(relative, data, { binary: true });
+ }
+ }
+ }
+
+ const blob = await zip.generateAsync({ type: 'blob' });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `${themeName}.zip`;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ URL.revokeObjectURL(url);
+
+ showMessage('success', `Downloading ${themeName}.zip…`);
+ }
+
+ async function handleDelete() {
+ clearMessages();
+
+ if (!window.StyleStorage) {
+ showMessage('danger', 'Storage is not available.');
+ return;
+ }
+
+ try {
+ await StyleStorage.init();
+ await StyleStorage.clear();
+ localStorage.removeItem('style-designer-default-entry');
+ showMessage('success', 'All files removed. You can upload a new style now.');
+ } catch (err) {
+ console.error(err);
+ showMessage('danger', `Unable to delete files: ${err && err.message ? err.message : err}`);
+ return;
+ }
+
+ const modal = getBootstrapModal();
+ if (modal) {
+ modal.hide();
+ }
+ }
+
+ function init() {
+ if (!downloadButton) return;
+
+ downloadButton.addEventListener('click', () => {
+ downloadButton.disabled = true;
+ handleDownload().finally(() => {
+ downloadButton.disabled = false;
+ });
+ });
+
+ if (confirmButton) {
+ confirmButton.addEventListener('click', () => {
+ const button = confirmButton;
+ button.disabled = true;
+ handleDelete().finally(() => {
+ button.disabled = false;
+ });
+ });
+ }
+ }
+
+ document.addEventListener('DOMContentLoaded', init, { once: true });
+})();
diff --git a/files/js/jszip.min.js b/files/js/jszip.min.js
new file mode 100644
index 0000000..ff4cfd5
--- /dev/null
+++ b/files/js/jszip.min.js
@@ -0,0 +1,13 @@
+/*!
+
+JSZip v3.10.1 - A JavaScript class for generating and reading zip files
+
+
+(c) 2009-2016 Stuart Knightley
+Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
+
+JSZip uses the library pako released under the MIT license :
+https://github.com/nodeca/pako/blob/main/LICENSE
+*/
+
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r {
+ const sd = window.sd || {
+ init() {
+ // Placeholder for future hooks
+ }
+ };
+ if (typeof sd.init === 'function') {
+ sd.init();
}
-}
-sd.init();
\ No newline at end of file
+
+ window.sd = sd;
+})();
diff --git a/files/js/style-storage.js b/files/js/style-storage.js
new file mode 100644
index 0000000..6b3790e
--- /dev/null
+++ b/files/js/style-storage.js
@@ -0,0 +1,184 @@
+const StyleStorage = (() => {
+ const script = document.currentScript;
+ const scriptUrl = script ? new URL(script.src) : new URL(window.location.href);
+ const basePath = scriptUrl.pathname.replace(/files\/js\/style-storage\.js(?:\?.*)?$/, '');
+ const scopeHref = `${scriptUrl.origin}${basePath}`;
+ const swUrl = new URL('style-sw.js', scopeHref).href;
+
+ const CACHE_NAME = 'style-designer-cache-v1';
+ let registrationPromise;
+
+ const MIME_TYPES = {
+ '.html': 'text/html',
+ '.css': 'text/css',
+ '.js': 'application/javascript',
+ '.json': 'application/json',
+ '.xml': 'application/xml',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.gif': 'image/gif',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.zip': 'application/zip',
+ '.webm': 'video/webm',
+ '.ogg': 'audio/ogg',
+ '.wav': 'audio/wav',
+ '.m4a': 'audio/mp4',
+ '.woff': 'font/woff',
+ '.woff2': 'font/woff2',
+ '.ttf': 'font/ttf',
+ '.otf': 'font/otf',
+ '.mp3': 'audio/mpeg',
+ '.mp4': 'video/mp4',
+ '.webp': 'image/webp'
+ };
+
+ function getExtension(path) {
+ const match = path.match(/\.[^.]+$/);
+ return match ? match[0].toLowerCase() : '';
+ }
+
+ function inferMime(path, fallback = 'application/octet-stream') {
+ const ext = getExtension(path);
+ return MIME_TYPES[ext] || fallback;
+ }
+
+ async function ensureRegistration() {
+ if (!('serviceWorker' in navigator)) {
+ throw new Error('Service workers are required.');
+ }
+
+ const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(window.location.hostname);
+ if (!window.isSecureContext && !isLocalhost) {
+ throw new Error('A secure context (https or localhost) is required to use StyleStorage.');
+ }
+
+ if (!registrationPromise) {
+ const scopeOption = basePath || '/';
+ registrationPromise = navigator.serviceWorker.register(swUrl, {
+ scope: scopeOption
+ }).catch((err) => {
+ console.error('StyleStorage: service worker registration failed', err);
+ throw err;
+ }).then(() => navigator.serviceWorker.ready.catch((err) => {
+ console.error('StyleStorage: waiting for service worker readiness failed', err);
+ throw err;
+ }));
+ }
+
+ return registrationPromise;
+ }
+
+ async function getCache() {
+ await ensureRegistration();
+ return caches.open(CACHE_NAME);
+ }
+
+ function toAbsoluteURL(path) {
+ const normalized = path.replace(/^\//, '');
+ return `${scopeHref}${normalized}`;
+ }
+
+ async function clear() {
+ await ensureRegistration();
+
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage({ type: 'STYLE_CACHE_CLEAR' });
+ }
+
+ await caches.delete(CACHE_NAME);
+ }
+
+ async function save(path, body, options = {}) {
+ const url = toAbsoluteURL(path);
+ const cache = await getCache();
+
+ const headers = new Headers();
+ const mime = options.contentType || inferMime(path, options.defaultType);
+
+ if (mime) {
+ const isTextType = /^text\/|\/json$|\/javascript$|\/xml$/.test(mime);
+ headers.set('Content-Type', mime + (isTextType && !/charset=/i.test(mime) ? '; charset=utf-8' : ''));
+ }
+
+ const request = new Request(url);
+ const response = new Response(body, {
+ status: 200,
+ headers
+ });
+
+ await cache.put(request, response);
+ }
+
+ async function saveText(path, content) {
+ await save(path, content, { defaultType: 'text/plain' });
+ }
+
+ async function saveBinary(path, data, contentType) {
+ let body = data;
+ if (data instanceof ArrayBuffer) {
+ body = data;
+ } else if (ArrayBuffer.isView(data)) {
+ body = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
+ } else if (!(data instanceof Blob)) {
+ body = new Blob([data], { type: contentType || inferMime(path) });
+ }
+
+ await save(path, body, { contentType: contentType || inferMime(path) });
+ }
+
+ async function list(prefix = '') {
+ const cache = await getCache();
+ const requests = await cache.keys();
+ const result = new Set();
+ const prefixNormalized = prefix.replace(/^\//, '');
+
+ for (const request of requests) {
+ const url = new URL(request.url);
+ let relativePath = url.pathname;
+
+ if (relativePath.startsWith(basePath)) {
+ relativePath = relativePath.slice(basePath.length);
+ }
+
+ if (!relativePath) continue;
+ if (relativePath.startsWith(prefixNormalized)) {
+ result.add(relativePath);
+ }
+ }
+
+ return Array.from(result);
+ }
+
+ async function readText(path) {
+ const cache = await getCache();
+ const url = toAbsoluteURL(path);
+ const response = await cache.match(url) || await cache.match(url.split('?')[0]);
+ if (!response) return null;
+ return response.text();
+ }
+
+ async function readBinary(path) {
+ const cache = await getCache();
+ const url = toAbsoluteURL(path);
+ const response = await cache.match(url) || await cache.match(url.split('?')[0]);
+ if (!response) return null;
+ return response.arrayBuffer();
+ }
+
+ return {
+ init: ensureRegistration,
+ clear,
+ saveText,
+ saveBinary,
+ list,
+ readText,
+ readBinary,
+ get scope() {
+ return `${basePath}`;
+ }
+ };
+})();
+
+window.StyleStorage = StyleStorage;
diff --git a/files/js/uploader.js b/files/js/uploader.js
new file mode 100644
index 0000000..0e2cd13
--- /dev/null
+++ b/files/js/uploader.js
@@ -0,0 +1,212 @@
+(() => {
+ const SUFFIX_TO_VIEW = {
+ '_web.zip': 'web',
+ '_page.zip': 'page',
+ '_scorm.zip': 'scorm'
+ };
+
+ const TEXT_EXTENSIONS = new Set([
+ '.html', '.htm', '.css', '.js', '.json', '.txt', '.xml', '.xhtml', '.svg'
+ ]);
+
+ const messagesContainer = document.getElementById('messages');
+ const uploadForm = document.getElementById('uploadForm');
+ const fileInput = document.getElementById('zipFiles');
+
+ const replacements = [
+ {
+ regex: /'
+ },
+ {
+ regex: / /gi,
+ replacement: ''
+ },
+ {
+ regex: /'
+ },
+ {
+ regex: / /gi,
+ replacement: ''
+ }
+ ];
+
+ function clearMessages() {
+ if (messagesContainer) {
+ messagesContainer.innerHTML = '';
+ }
+ }
+
+ function showMessage(type, text) {
+ if (!messagesContainer) return;
+ const div = document.createElement('div');
+ div.className = `alert alert-${type}`;
+ div.role = 'status';
+ div.textContent = text;
+ messagesContainer.appendChild(div);
+ }
+
+ function getExtension(name) {
+ const match = name.toLowerCase().match(/\.[^.]+$/);
+ return match ? match[0] : '';
+ }
+
+ function categorizeFiles(fileList) {
+ const map = new Map();
+
+ for (const file of fileList) {
+ const fileName = file.name.toLowerCase();
+ for (const suffix of Object.keys(SUFFIX_TO_VIEW)) {
+ if (fileName.endsWith(suffix)) {
+ map.set(SUFFIX_TO_VIEW[suffix], file);
+ break;
+ }
+ }
+ }
+
+ return map;
+ }
+
+ function patchHtml(content, isIndex) {
+ let result = content;
+ for (const { regex, replacement } of replacements) {
+ result = result.replace(regex, replacement);
+ }
+
+ const injection = isIndex
+ ? ''
+ : '';
+
+ if (!/files\/js\/style-designer\.js/.test(result)) {
+ if (/<\/body>/i.test(result)) {
+ result = result.replace(/<\/body>/i, `${injection}
+
+
+
+
+
+
eXeLearning Style Designer
+
A lightweight tool to preview, tweak, and package custom styles for eXeLearning 3—locally or directly on GitHub Pages.
+
Bring the three export flavours (Website , Single page , SCORM 1.2 ) of your project, upload them, and iterate visually on the generated theme assets.
+
+ Heads up: this tool skips authentication and any server-side validation. Keep it for design workflows only — never deploy it in production.
+
+
Workflow
+
+ Export the same project in the three supported formats without renaming the zip files.
+ Open the Upload page and drop the zips. We patch the HTML to load the theme from the browser cache.
+ Use the Designer to switch views, edit config.xml, and inspect live changes to style.css / style.js.
+ Grab the finished package from Download and test it back inside eXeLearning.
+
+
Need more details? Check the full README inside the repository.
+
+
+
+
+
Quick links
+
+
+
Remember to:
+
+ Enable every project property during export so you can style them.
+ Respect the license of the base style if you are extending one.
+ Validate the generated style inside eXeLearning before distribution.
+
+
+
+
+
+
+
+ `);
+ } else {
+ result += injection;
+ }
+ }
+
+ return result;
+ }
+
+ function shouldTreatAsText(ext) {
+ return TEXT_EXTENSIONS.has(ext);
+ }
+
+ async function processArchive(file, viewId, metadata) {
+ const zip = await JSZip.loadAsync(file);
+ const tasks = [];
+ const basePath = `contents/${viewId}/`;
+
+ zip.forEach((relativePath, zipEntry) => {
+ if (zipEntry.dir) return;
+ if (relativePath.startsWith('__MACOSX')) return;
+ if (relativePath.includes('..')) return;
+
+ const destPath = basePath + relativePath;
+ tasks.push((async () => {
+ const ext = getExtension(relativePath);
+ const fileName = relativePath.split('/').pop() || '';
+ let textContent = null;
+ let binaryContent = null;
+
+ if (ext === '.html') {
+ textContent = await zipEntry.async('string');
+ const patched = patchHtml(textContent, fileName === 'index.html');
+ await StyleStorage.saveText(destPath, patched);
+
+ if (viewId === 'web' && relativePath.startsWith('html/') && fileName !== 'index.html' && !metadata.defaultEntry) {
+ metadata.defaultEntry = fileName;
+ }
+ } else if (shouldTreatAsText(ext)) {
+ textContent = await zipEntry.async('string');
+ await StyleStorage.saveText(destPath, textContent);
+ } else {
+ binaryContent = await zipEntry.async('uint8array');
+ await StyleStorage.saveBinary(destPath, binaryContent);
+ }
+
+ if (relativePath.startsWith('theme/')) {
+ if (shouldTreatAsText(ext)) {
+ const themeContent = textContent ?? await zipEntry.async('string');
+ await StyleStorage.saveText(`theme/${relativePath.slice('theme/'.length)}`, themeContent);
+ } else {
+ const themeBinary = binaryContent ?? await zipEntry.async('uint8array');
+ await StyleStorage.saveBinary(`theme/${relativePath.slice('theme/'.length)}`, themeBinary);
+ }
+ }
+ })());
+ });
+
+ await Promise.all(tasks);
+ }
+
+ async function handleSubmit(event) {
+ event.preventDefault();
+ clearMessages();
+
+ if (!fileInput || !fileInput.files || !fileInput.files.length) {
+ showMessage('danger', 'No files selected.');
+ return;
+ }
+
+ const mapping = categorizeFiles(fileInput.files);
+ const missingViews = Object.values(SUFFIX_TO_VIEW).filter((view) => !mapping.has(view));
+
+ if (missingViews.length) {
+ showMessage('danger', `Missing files for: ${missingViews.join(', ')}.`);
+ return;
+ }
+
+ if (uploadForm) uploadForm.classList.add('is-processing');
+
+ try {
+ await StyleStorage.init();
+ await StyleStorage.clear();
+
+ const metadata = { defaultEntry: null };
+
+ for (const view of Object.values(SUFFIX_TO_VIEW)) {
+ const file = mapping.get(view);
+ if (file) {
+ await processArchive(file, view, metadata);
+ }
+ }
+
+ if (metadata.defaultEntry) {
+ localStorage.setItem('style-designer-default-entry', metadata.defaultEntry);
+ } else {
+ localStorage.removeItem('style-designer-default-entry');
+ }
+
+ showMessage('success', 'Processing completed successfully.');
+ } catch (err) {
+ console.error(err);
+ showMessage('danger', `Error processing files: ${err.message || err}`);
+ } finally {
+ if (uploadForm) uploadForm.classList.remove('is-processing');
+ if (fileInput) fileInput.value = '';
+ }
+ }
+
+ async function init() {
+ if (typeof JSZip === 'undefined') {
+ showMessage('danger', 'JSZip library is missing.');
+ return;
+ }
+
+ try {
+ await StyleStorage.init();
+ } catch (err) {
+ console.error(err);
+ showMessage('danger', `Your browser does not support the required features. ${err && err.message ? err.message : ''}`);
+ return;
+ }
+
+ if (uploadForm) {
+ uploadForm.addEventListener('submit', handleSubmit);
+ }
+ }
+
+ document.addEventListener('DOMContentLoaded', init, { once: true });
+})();
diff --git a/files/js/viewer.js b/files/js/viewer.js
new file mode 100644
index 0000000..ed70594
--- /dev/null
+++ b/files/js/viewer.js
@@ -0,0 +1,423 @@
+(() => {
+ const config = window.styleDesignerConfig || {};
+ const defaultEntryRaw = typeof config.defaultEntry === 'string' ? config.defaultEntry.trim() : '';
+ const defaultEntry = defaultEntryRaw || 'index.html';
+
+ const StyleStorageRef = window.StyleStorage;
+ const viewer = document.getElementById('viewer');
+ const viewSelector = document.getElementById('viewSelector');
+ const emptyState = document.getElementById('emptyState');
+ const newWindowButton = document.getElementById('openNewWindow');
+ const configForm = document.getElementById('configForm');
+ const configMessages = document.getElementById('configMessages');
+ const resetConfigButton = document.getElementById('resetConfig');
+ const downloadConfigButton = document.getElementById('downloadConfig');
+ const configPanel = document.getElementById('configPanel');
+ const previewPanel = document.getElementById('previewPanel');
+ const configTab = document.getElementById('configTab');
+
+ if (!viewer || !viewSelector || !emptyState || !newWindowButton || !configPanel || !previewPanel || !configTab) return;
+
+ const buttons = Array.from(viewSelector.querySelectorAll('[data-view]'));
+ const state = {
+ sources: {},
+ activeView: null
+ };
+
+ const rootPath = (() => {
+ if (StyleStorageRef && typeof StyleStorageRef.scope === 'string') {
+ const scope = StyleStorageRef.scope || '/';
+ return scope.endsWith('/') ? scope : `${scope}/`;
+ }
+ return '/';
+ })();
+
+ const defaultConfigValues = {
+ name: 'base',
+ title: 'Default',
+ version: '2025',
+ compatibility: '3.0',
+ author: 'eXeLearning.net',
+ license: 'Creative Commons by-sa',
+ 'license-url': 'http://creativecommons.org/licenses/by-sa/3.0/',
+ description: 'Minimally-styled, feature rich responsive style for eXe.\n\niDevice icons by Francisco Javier Pulido Cuadrado.',
+ downloadable: '0'
+ };
+
+ const configFields = [
+ 'name',
+ 'title',
+ 'version',
+ 'compatibility',
+ 'author',
+ 'license',
+ 'license-url',
+ 'description',
+ 'downloadable'
+ ];
+
+ function showConfigMessage(type, text) {
+ if (!configMessages) return;
+ configMessages.innerHTML = `
`;
+ }
+
+ function setPreviewVisibility(show) {
+ if (show) {
+ previewPanel.classList.remove('d-none');
+ configPanel.classList.add('d-none');
+ configTab.classList.remove('active');
+ configTab.setAttribute('aria-pressed', 'false');
+ } else {
+ previewPanel.classList.add('d-none');
+ configPanel.classList.remove('d-none');
+ }
+ }
+
+ function activateConfigTab() {
+ configTab.classList.add('active');
+ configTab.setAttribute('aria-pressed', 'true');
+ }
+
+ function deactivateConfigTab() {
+ configTab.classList.remove('active');
+ configTab.setAttribute('aria-pressed', 'false');
+ }
+
+ function escapeXml(value) {
+ return value.replace(/[<>&'"]/g, (char) => {
+ switch (char) {
+ case '<': return '<';
+ case '>': return '>';
+ case '&': return '&';
+ case '\'': return ''';
+ case '"': return '"';
+ default: return char;
+ }
+ });
+ }
+
+ function sanitizeValue(value = '') {
+ return String(value || '').trim();
+ }
+
+ function buildConfigXml(values) {
+ const sanitized = { ...defaultConfigValues, ...values };
+ const description = sanitizeValue(sanitized.description)
+ .replace(/\r?\n/g, '\n ');
+
+ return [
+ '',
+ '',
+ ` ${escapeXml(sanitizeValue(sanitized.name))} `,
+ ` ${escapeXml(sanitizeValue(sanitized.title))} `,
+ ` ${escapeXml(sanitizeValue(sanitized.version))} `,
+ ` ${escapeXml(sanitizeValue(sanitized.compatibility))} `,
+ ` ${escapeXml(sanitizeValue(sanitized.author))} `,
+ ` ${escapeXml(sanitizeValue(sanitized.license))} `,
+ ` ${escapeXml(sanitizeValue(sanitized['license-url']))} `,
+ description
+ ? ` ${escapeXml(description)} `
+ : ' ',
+ ` ${sanitizeValue(sanitized.downloadable) === '1' ? '1' : '0'} `,
+ ' ',
+ ''
+ ].join('\n');
+ }
+
+ function extractConfigValues(doc) {
+ const values = { ...defaultConfigValues };
+ configFields.forEach((field) => {
+ const node = doc.querySelector(field);
+ if (node && typeof node.textContent === 'string') {
+ values[field] = node.textContent.trim();
+ }
+ });
+ values.downloadable = values.downloadable === '1' ? '1' : '0';
+ return values;
+ }
+
+ function populateConfigForm(values) {
+ if (!configForm) return;
+ configFields.forEach((field) => {
+ const element = configForm.elements.namedItem(field);
+ if (!element) return;
+ if (field === 'description') {
+ element.value = values[field] || '';
+ } else {
+ element.value = typeof values[field] === 'string' ? values[field] : '';
+ }
+ });
+ }
+
+ async function loadConfig() {
+ if (!configForm || !StyleStorageRef || !StyleStorageRef.readText) return;
+ let xml = null;
+ try {
+ xml = await StyleStorageRef.readText('theme/config.xml');
+ } catch (err) {
+ console.warn('Unable to read config.xml:', err);
+ }
+
+ let values = defaultConfigValues;
+ if (xml) {
+ try {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(xml, 'application/xml');
+ if (!doc.querySelector('parsererror')) {
+ values = extractConfigValues(doc);
+ } else {
+ showConfigMessage('warning', 'config.xml is malformed. Showing defaults.');
+ }
+ } catch (err) {
+ console.warn('Unable to parse config.xml:', err);
+ showConfigMessage('warning', 'config.xml could not be parsed. Showing defaults.');
+ }
+ }
+ populateConfigForm(values);
+ if (configForm) {
+ configForm.classList.remove('was-validated');
+ }
+ }
+
+ function gatherConfigFormValues() {
+ if (!configForm) return { ...defaultConfigValues };
+ const formData = new FormData(configForm);
+ const values = {};
+ configFields.forEach((field) => {
+ const raw = formData.get(field);
+ values[field] = raw == null ? '' : sanitizeValue(raw.toString());
+ });
+ values.name = values.name.replace(/\s+/g, '');
+ values.downloadable = values.downloadable === '1' ? '1' : '0';
+ return values;
+ }
+
+ async function handleConfigSubmit(event) {
+ event.preventDefault();
+ if (!configForm || !StyleStorageRef || !StyleStorageRef.saveText) return;
+
+ configForm.classList.add('was-validated');
+ if (!configForm.checkValidity()) {
+ configForm.reportValidity();
+ return;
+ }
+
+ const values = gatherConfigFormValues();
+ const xml = buildConfigXml(values);
+
+ try {
+ await StyleStorageRef.saveText('theme/config.xml', xml);
+ showConfigMessage('success', 'config.xml saved successfully.');
+ } catch (err) {
+ console.error(err);
+ showConfigMessage('danger', `Unable to save config.xml: ${err && err.message ? err.message : err}`);
+ }
+ }
+
+ function handleConfigReset() {
+ populateConfigForm(defaultConfigValues);
+ configForm.classList.remove('was-validated');
+ showConfigMessage('info', 'Default values restored. Remember to save to apply changes.');
+ }
+
+ function handleConfigDownload() {
+ if (!configForm) return;
+ configForm.classList.add('was-validated');
+ if (!configForm.checkValidity()) {
+ configForm.reportValidity();
+ return;
+ }
+ const values = gatherConfigFormValues();
+ const xml = buildConfigXml(values);
+ const blob = new Blob([xml], { type: 'application/xml' });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `${sanitizeValue(values.name) || 'style'}.config.xml`;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ URL.revokeObjectURL(url);
+ }
+
+ async function setupConfigSection() {
+ if (!configForm) return;
+ await loadConfig();
+ configForm.addEventListener('submit', handleConfigSubmit);
+ if (resetConfigButton) {
+ resetConfigButton.addEventListener('click', handleConfigReset);
+ }
+ if (downloadConfigButton) {
+ downloadConfigButton.addEventListener('click', handleConfigDownload);
+ }
+ configTab.setAttribute('aria-pressed', 'false');
+ configTab.addEventListener('click', () => {
+ activateConfigView();
+ });
+ }
+
+ function buildCandidates(viewId) {
+ const base = `${rootPath}contents/${viewId}`;
+ const candidates = [];
+
+ if (viewId === 'page') {
+ candidates.push(`${base}/index.html`);
+ candidates.push(`${base}/html/${defaultEntry}`);
+ candidates.push(`${base}/html/index.html`);
+ } else {
+ if (defaultEntry !== 'index.html') {
+ candidates.push(`${base}/html/${defaultEntry}`);
+ }
+ candidates.push(`${base}/index.html`);
+ candidates.push(`${base}/html/index.html`);
+ }
+
+ return candidates;
+ }
+
+ function withCacheBuster(url) {
+ const separator = url.includes('?') ? '&' : '?';
+ return `${url}${separator}v=${Date.now()}`;
+ }
+
+ async function resourceExists(url) {
+ const target = withCacheBuster(url);
+ try {
+ const response = await fetch(target, { method: 'GET', cache: 'no-store' });
+ return response.ok;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ function updateActiveButton(activeViewId) {
+ buttons.forEach((btn) => {
+ const isActive = btn.dataset.view === activeViewId;
+ btn.classList.toggle('active', isActive);
+ btn.setAttribute('aria-pressed', String(isActive));
+ });
+ if (activeViewId === 'config') {
+ activateConfigTab();
+ } else {
+ deactivateConfigTab();
+ }
+ }
+
+ function setViewerSource(viewId) {
+ const baseSrc = state.sources[viewId];
+ if (!baseSrc) return;
+
+ const src = withCacheBuster(baseSrc);
+ viewer.src = src;
+ viewer.hidden = false;
+ setPreviewVisibility(true);
+ state.activeView = viewId;
+
+ updateActiveButton(viewId);
+ newWindowButton.disabled = false;
+ }
+
+ function activateConfigView() {
+ setPreviewVisibility(false);
+ updateActiveButton('config');
+ state.activeView = 'config';
+ newWindowButton.disabled = true;
+ }
+
+ async function prepareViews() {
+ const availableViews = [];
+
+ for (const button of buttons) {
+ const viewId = button.dataset.view;
+ const candidates = buildCandidates(viewId);
+
+ for (const candidate of candidates) {
+ // skip duplicate checks for the same path
+ if (state.sources[viewId] === candidate) continue;
+ if (await resourceExists(candidate)) {
+ state.sources[viewId] = candidate;
+ availableViews.push(viewId);
+ break;
+ }
+ }
+
+ if (!state.sources[viewId]) {
+ button.classList.add('d-none');
+ }
+ }
+
+ return availableViews;
+ }
+
+ function selectInitialView(availableViews) {
+ const preferredOrder = ['web', 'page', 'scorm'];
+ return preferredOrder.find((view) => availableViews.includes(view)) || availableViews[0];
+ }
+
+ function displayStorageError(details) {
+ if (emptyState) {
+ emptyState.hidden = false;
+ emptyState.innerHTML = `
+
+ Unable to initialise the preview storage. Please ensure your browser supports Service Workers and reload the page.${details ? `${details} ` : ''}
+
+ `;
+ }
+ if (viewSelector) viewSelector.hidden = true;
+ if (viewer) viewer.hidden = true;
+ if (newWindowButton) newWindowButton.disabled = true;
+ configPanel.classList.add('d-none');
+ configTab.disabled = true;
+ }
+
+ async function init() {
+ if (!StyleStorageRef || !StyleStorageRef.init) {
+ displayStorageError('Missing StyleStorage helper.');
+ return;
+ }
+
+ try {
+ await StyleStorageRef.init();
+ } catch (err) {
+ console.error(err);
+ displayStorageError(err && err.message ? err.message : '');
+ return;
+ }
+
+ await setupConfigSection();
+ configTab.disabled = false;
+
+ const availableViews = await prepareViews();
+
+ if (!availableViews.length) {
+ activateConfigView();
+ viewer.hidden = true;
+ viewSelector.hidden = true;
+ if (emptyState) emptyState.hidden = true;
+ showConfigMessage('info', 'No preview content available yet. Upload your exported packages from the Upload page.');
+ return;
+ }
+
+ emptyState.hidden = true;
+ viewSelector.hidden = false;
+
+ buttons
+ .filter((btn) => !btn.classList.contains('d-none'))
+ .forEach((btn) => {
+ btn.addEventListener('click', () => setViewerSource(btn.dataset.view));
+ });
+
+ newWindowButton.addEventListener('click', () => {
+ if (state.activeView && state.activeView !== 'config') {
+ window.open(viewer.src, '_blank', 'noopener,noreferrer');
+ }
+ });
+
+ const initialView = selectInitialView(availableViews);
+ if (initialView) {
+ setViewerSource(initialView);
+ }
+ }
+
+ document.addEventListener('DOMContentLoaded', init, { once: true });
+})();
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6b38629
--- /dev/null
+++ b/index.html
@@ -0,0 +1,63 @@
+
+
+