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 b0b32bf..0000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +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/ - -# Install required extensions -RUN apt-get update && apt-get install -y \ - zip unzip libzip-dev \ - && docker-php-ext-install zip 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 83524df..0000000 --- a/custom-php.ini +++ /dev/null @@ -1,6 +0,0 @@ -; Increase max upload size -upload_max_filesize = 500M -post_max_size = 500M -memory_limit = 128M -; Enable required extensions -extension=zip diff --git a/designer/index.html b/designer/index.html new file mode 100644 index 0000000..7ed563e --- /dev/null +++ b/designer/index.html @@ -0,0 +1,140 @@ + + + + + + + Style Designer + + + + + + + + + +
+ + +
+ + +
+
+ +
+
+
+
+
+ +
+
+
+

CSS Editor

+
+ + +
+
+
+
+
+ +
+ + +
+
+
+ +
+
+
+
+
+ +
+
+
+
+

Theme Configuration

+ +
+
+
+
+ + +
Use only letters, numbers, hyphens or underscores.
+
Only letters, numbers, hyphens and underscores.
+
+
+ + +
Title is required.
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + 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

+
+
    +
  1. Edit the config.xml file before downloading the style.
  2. +
  3. Remember that "name" is an ID and cannot contain spaces or special characters.
  4. +
  5. If your work is based on another style, you must respect the license terms.
  6. +
  7. You can add any additional information about licenses, authorship, or anything else you need in the "description" field.
  8. +
  9. When you're finished, click "Download Style".
  10. +
+
+ + +
+
+ + + 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 - - - - - - - -
- Style Designer -
-
-

Download style

-
    -
  1. Edit the config.xml file before downloading the style.
  2. -
  3. Remember that "name" is an ID and cannot contain spaces or special characters.
  4. -
  5. If your work is based on another style, you must respect the license terms.
  6. -
  7. You can add any additional information about licenses, authorship, or anything else you need in the "description" field.
  8. -
  9. When you're finished, click "Download Style".
  10. -
-
- - -
- -
- - - - diff --git a/files/css/style-designer.css b/files/css/style-designer.css index 9ee718f..b52df36 100644 --- a/files/css/style-designer.css +++ b/files/css/style-designer.css @@ -1,36 +1,74 @@ 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; +} +#sdHeader .view-group{ + flex: 0 0 auto; } -#done { - float: right; - margin-left: .5em; +#sdHeader .config-group{ + flex: 0 0 auto; } -#_blank { - float: right; +#sdHeader .action-group{ + margin-left: auto; +} +#previewPanel iframe { + width: 100%; + height: calc(100vh - 64px); + border: none; + background-color: #fff; + flex: 1 1 auto; +} +.designer-page #configPanel{ + max-width: 720px; + margin: 0 auto; } .btn.mw { min-width: 100px; } -#_blank img{ +#openNewWindow img{ position: relative; top: -2px; } @@ -42,4 +80,125 @@ iframe { .btn-primary:focus { background-color: #107275; border-color: #107275; -} \ No newline at end of file +} +.preview-workspace{ + display: flex; + flex-direction: row; + align-items: stretch; + gap: 1rem; +} +.preview-workspace .css-editor{ + flex: 0 0 420px; + max-width: 45%; + min-width: 260px; + display: flex; + flex-direction: column; +} +.css-editor .card-body{ + display: flex; + flex-direction: column; + height: calc(100vh - 112px); + padding: 1rem; +} +.css-editor form{ + flex: 1 1 auto; + display: flex; + flex-direction: column; +} +#cssMessages{ + min-height: 1.5rem; +} +.code-editor{ + position: relative; + height: 100%; + border: 1px solid #ced4da; + border-radius: 0.375rem; + background-color: #101623; + overflow: hidden; + font-family: var(--bs-font-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-size: 0.875rem; + line-height: 1.45; +} +.code-editor:focus-within{ + border-color: #0ba1a1; + box-shadow: 0 0 0 0.2rem rgba(11, 161, 161, 0.25); +} +.code-editor__highlight{ + position: absolute; + inset: 0; + margin: 0; + padding: 0.75rem 0.875rem; + overflow: auto; + pointer-events: none; + white-space: pre-wrap; + word-break: normal; + overflow-wrap: normal; + tab-size: 4; + color: #e5e7eb; + background-color: transparent; + font-family: inherit; + font-size: inherit; + line-height: inherit; + box-sizing: border-box; +} +.code-editor__highlight::-webkit-scrollbar{ + display: none; +} +.code-editor__highlight{ + scrollbar-width: none; +} +.code-editor__input{ + position: absolute; + inset: 0; + width: 100%; + padding: 0.75rem 0.875rem; + border: 0; + background: transparent; + color: transparent; + caret-color: #0ba1a1; + resize: none; + font-size: inherit; + line-height: inherit; + tab-size: 4; + z-index: 1; + font-family: inherit; + box-sizing: border-box; +} +.code-editor__input:focus{ + outline: none; +} +.token-comment{ + color: #94a3b8; + font-style: italic; +} +.token-string{ + color: #fbbf24; +} +.token-property{ + color: #38bdf8; +} +.token-number{ + color: #f472b6; +} +.token-hex{ + color: #f87171; +} +.token-at{ + color: #34d399; +} +@media (max-width: 991.98px){ + .preview-workspace{ + flex-direction: column; + } + .preview-workspace .css-editor{ + max-width: 100%; + width: 100%; + } + .css-editor .card-body{ + height: auto; + min-height: 240px; + } + .code-editor{ + min-height: 240px; + } +} 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..d2a7a9a --- /dev/null +++ b/files/js/uploader.js @@ -0,0 +1,227 @@ +(() => { + 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: /\s*<\/script>/gi, + replacement: '' + }, + { + regex: //gi, + replacement: '' + }, + { + regex: /\s*<\/script>/gi, + replacement: '' + }, + { + regex: //gi, + replacement: '' + }, + { + regex: //gi, + replacement: '' + }, + { + 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}`); + } 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 isThemeAsset = relativePath.startsWith('theme/'); + const themeRelativePath = isThemeAsset ? relativePath.slice('theme/'.length) : null; + const normalizedThemePath = themeRelativePath === 'content.css' + ? 'style.css' + : themeRelativePath; + const storageRelativePath = isThemeAsset ? `theme/${normalizedThemePath}` : relativePath; + const destPath = basePath + storageRelativePath; + 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 (isThemeAsset) { + if (shouldTreatAsText(ext)) { + const themeContent = textContent ?? await zipEntry.async('string'); + // Compat: los paquetes antiguos usan theme/content.css; lo renombramos a theme/style.css para el editor. + await StyleStorage.saveText(`theme/${normalizedThemePath}`, themeContent); + } else { + const themeBinary = binaryContent ?? await zipEntry.async('uint8array'); + await StyleStorage.saveBinary(`theme/${normalizedThemePath}`, 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..7b9c74a --- /dev/null +++ b/files/js/viewer.js @@ -0,0 +1,750 @@ +(() => { + 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'); + const cssTab = document.getElementById('cssTab'); + const cssPanel = document.getElementById('cssPanel'); + const cssForm = document.getElementById('cssForm'); + const cssEditor = document.getElementById('cssEditor'); + const cssHighlight = document.getElementById('cssHighlight'); + const cssMessages = document.getElementById('cssMessages'); + const cssReloadButton = document.getElementById('reloadCss'); + const cssDownloadButton = document.getElementById('downloadCss'); + const saveCssButton = document.getElementById('saveCss'); + + if (!viewer || !viewSelector || !emptyState || !newWindowButton || !configPanel || !previewPanel || !configTab || !cssTab || !cssPanel || !cssForm || !cssEditor || !cssHighlight || !saveCssButton) return; + + const buttons = Array.from(viewSelector.querySelectorAll('[data-view]')); + const state = { + sources: {}, + activeView: null + }; + const cssState = { + lastSaved: '', + visible: false, + dirty: false, + applyTimer: null, + applying: false, + applyQueued: false + }; + const CSS_APPLY_DEBOUNCE = 800; + + 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 escapeHtmlLite(value = '') { + return value.replace(/[&<>'"]/g, (char) => { + switch (char) { + case '&': return '&'; + case '<': return '<'; + case '>': return '>'; + case '\'': return '''; + case '"': return '"'; + default: return char; + } + }); + } + + function highlightCssTokens(source = '') { + const normalized = source.replace(/\r\n?/g, '\n'); + + const tokenMatchers = [ + { + type: 'comment', + regex: /\/\*[\s\S]*?\*\//g + }, + { + type: 'string', + regex: /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g + }, + { + type: 'property', + regex: /(^|[{;]\s*)([a-z_-][\w-]*)\s*(?=:)/gi, + range(match) { + const prefixLength = match[1] ? match[1].length : 0; + const start = match.index + prefixLength; + return { start, end: start + match[2].length }; + } + }, + { + type: 'at', + regex: /@\w+/g + }, + { + type: 'hex', + regex: /#[0-9a-f]{3,8}\b/gi + }, + { + type: 'number', + regex: /\b\d*\.?\d+(?:px|em|rem|vh|vw|%)?/gi + } + ]; + + const tokens = []; + + function isOccupied(start, end) { + return tokens.some((token) => !(end <= token.start || start >= token.end)); + } + + tokenMatchers.forEach((matcher) => { + const regex = new RegExp(matcher.regex.source, matcher.regex.flags); + let match = regex.exec(normalized); + while (match) { + let start = match.index; + let end = regex.lastIndex; + + if (typeof matcher.range === 'function') { + const customRange = matcher.range(match); + start = customRange.start; + end = customRange.end; + } + + if (!isOccupied(start, end)) { + tokens.push({ + type: matcher.type, + start, + end + }); + } + + match = regex.exec(normalized); + } + }); + + if (!tokens.length) { + return escapeHtmlLite(normalized) || ' '; + } + + tokens.sort((a, b) => a.start - b.start); + + let cursor = 0; + let result = ''; + + tokens.forEach((token) => { + if (cursor < token.start) { + result += escapeHtmlLite(normalized.slice(cursor, token.start)); + } + const tokenText = normalized.slice(token.start, token.end); + result += `${escapeHtmlLite(tokenText)}`; + cursor = token.end; + }); + + if (cursor < normalized.length) { + result += escapeHtmlLite(normalized.slice(cursor)); + } + + return result || ' '; + } + + function updateCssHighlight() { + if (!cssHighlight || !cssEditor) return; + const content = cssEditor.value || ''; + const highlighted = highlightCssTokens(content); + cssHighlight.innerHTML = highlighted || ' '; + cssHighlight.scrollTop = cssEditor.scrollTop; + cssHighlight.scrollLeft = cssEditor.scrollLeft; + } + + function syncCssScroll() { + if (!cssHighlight || !cssEditor) return; + cssHighlight.scrollTop = cssEditor.scrollTop; + cssHighlight.scrollLeft = cssEditor.scrollLeft; + } + + function cancelScheduledCssApply() { + if (cssState.applyTimer) { + clearTimeout(cssState.applyTimer); + cssState.applyTimer = null; + } + } + + function scheduleCssAutoApply() { + if (!cssState.dirty) return; + cancelScheduledCssApply(); + cssState.applyTimer = setTimeout(() => { + cssState.applyTimer = null; + applyCssChanges(false); + }, CSS_APPLY_DEBOUNCE); + } + + function showCssMessage(type, text) { + if (!cssMessages) return; + if (!type || !text) { + cssMessages.innerHTML = ''; + return; + } + cssMessages.innerHTML = `
${text}
`; + } + + function showCssPanel(show) { + if (!cssPanel || !cssTab) return; + cssPanel.classList.toggle('d-none', !show); + cssTab.classList.toggle('active', show); + cssTab.setAttribute('aria-pressed', show ? 'true' : 'false'); + cssState.visible = show; + if (show && cssEditor) { + updateCssHighlight(); + cssEditor.focus(); + } + } + + function updateCssDirtyState() { + if (!cssEditor || !saveCssButton) return; + cssState.dirty = cssEditor.value !== cssState.lastSaved; + saveCssButton.disabled = !cssState.dirty; + if (cssState.dirty) { + showCssMessage('', ''); + } else { + cancelScheduledCssApply(); + } + } + + async function loadCssContent() { + if (!StyleStorageRef || !StyleStorageRef.readText) return ''; + try { + const text = await StyleStorageRef.readText('theme/style.css'); + if (typeof text === 'string') { + return text; + } + } catch (err) { + console.warn('Unable to read style.css:', err); + } + try { + const legacy = await StyleStorageRef.readText('theme/content.css'); + if (typeof legacy === 'string' && legacy) { + try { + await StyleStorageRef.saveText('theme/style.css', legacy); + } catch (saveErr) { + console.warn('Unable to normalize legacy content.css to style.css:', saveErr); + } + return legacy; + } + } catch (legacyErr) { + // Ignore if legacy file is missing. + } + return ''; + } + + function refreshActivePreview() { + const activeView = state.activeView; + if (!activeView || activeView === 'config') return; + setViewerSource(activeView); + } + + async function applyCssChanges(showSuccessMessage = true) { + if (!cssEditor || !StyleStorageRef || !StyleStorageRef.saveText) return; + if (!cssState.dirty && cssEditor.value === cssState.lastSaved) return; + + if (cssState.applying) { + cssState.applyQueued = true; + return; + } + + cancelScheduledCssApply(); + cssState.applying = true; + cssState.applyQueued = false; + + const content = cssEditor.value; + + try { + await StyleStorageRef.saveText('theme/style.css', content); + cssState.lastSaved = content; + updateCssDirtyState(); + if (showSuccessMessage) { + showCssMessage('success', 'CSS applied to preview.'); + } + refreshActivePreview(); + } catch (err) { + console.error(err); + showCssMessage('danger', `Unable to save CSS: ${err && err.message ? err.message : err}`); + } finally { + cssState.applying = false; + if (cssState.applyQueued || cssEditor.value !== cssState.lastSaved) { + cssState.applyQueued = false; + scheduleCssAutoApply(); + } + } + } + + async function handleCssSubmit(event) { + event.preventDefault(); + applyCssChanges(true); + } + + async function reloadCss(showMessage = true) { + if (!cssEditor) return; + const content = await loadCssContent(); + cssEditor.value = content; + cssState.lastSaved = content; + cssState.applyQueued = false; + cancelScheduledCssApply(); + updateCssDirtyState(); + updateCssHighlight(); + syncCssScroll(); + if (!showMessage) return; + if (content) { + showCssMessage('info', 'CSS reloaded from storage.'); + } else { + showCssMessage('info', 'style.css not found. Start editing to create it.'); + } + } + + function handleCssReload(event) { + if (event) event.preventDefault(); + reloadCss(true); + } + + function handleCssDownload(event) { + if (event) event.preventDefault(); + if (!cssEditor) return; + const blob = new Blob([cssEditor.value], { type: 'text/css' }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = 'style.css'; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + } + + async function setupCssSection() { + if (!cssForm || !cssEditor || !cssTab || !saveCssButton) return; + + await reloadCss(false); + showCssPanel(false); + cssTab.disabled = false; + cssTab.setAttribute('aria-pressed', 'false'); + cssTab.addEventListener('click', () => { + showCssPanel(!cssState.visible); + }); + + cssForm.addEventListener('submit', handleCssSubmit); + cssEditor.addEventListener('input', () => { + updateCssDirtyState(); + updateCssHighlight(); + scheduleCssAutoApply(); + }); + cssEditor.addEventListener('scroll', syncCssScroll); + + if (cssReloadButton) { + cssReloadButton.addEventListener('click', handleCssReload); + } + + if (cssDownloadButton) { + cssDownloadButton.addEventListener('click', handleCssDownload); + } + } + + function showConfigMessage(type, text) { + if (!configMessages) return; + configMessages.innerHTML = `
${text}
`; + } + + function setPreviewVisibility(show) { + if (show) { + previewPanel.classList.remove('d-none'); + configPanel.classList.add('d-none'); + deactivateConfigTab(); + cssTab.disabled = false; + } else { + previewPanel.classList.add('d-none'); + configPanel.classList.remove('d-none'); + showCssPanel(false); + cssTab.disabled = true; + } + } + + 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; + showCssPanel(false); + cssTab.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; + await setupCssSection(); + + 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 @@ + + + + + + + eXeLearning Style Designer + + + + + + +
+ +
+
+
+
+
+

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

+
    +
  1. Export the same project in the three supported formats without renaming the zip files.
  2. +
  3. Open the Upload page and drop the zips. We patch the HTML to load the theme from the browser cache.
  4. +
  5. Use the Designer to switch views, edit config.xml, and inspect live changes to style.css / style.js.
  6. +
  7. Grab the finished package from Download and test it back inside eXeLearning.
  8. +
+

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.
  • +
+
+
+
+
+
+
+ + diff --git a/index.php b/index.php deleted file mode 100644 index c58a785..0000000 --- a/index.php +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - Style Designer - - - - - - - - -
- -
- - - -
- Done - - - Upload - -
- - - -
-

Style Designer

-
- No contents available… -
-

Just click on "Upload" and follow the instructions.

-
- - - \ No newline at end of file diff --git a/style-sw.js b/style-sw.js new file mode 100644 index 0000000..85c4bcf --- /dev/null +++ b/style-sw.js @@ -0,0 +1,54 @@ +const CACHE_NAME = 'style-designer-cache-v1'; + +function shouldHandle(event) { + const requestUrl = new URL(event.request.url); + const scopeUrl = new URL(self.registration.scope); + + if (requestUrl.origin !== scopeUrl.origin) return false; + if (!requestUrl.pathname.startsWith(scopeUrl.pathname)) return false; + + const relativePath = requestUrl.pathname.slice(scopeUrl.pathname.length); + return relativePath.startsWith('contents/') || relativePath.startsWith('theme/'); +} + +function normalizeRequest(request) { + const url = new URL(request.url); + url.hash = ''; + url.search = ''; + return new Request(url.toString(), { + method: request.method, + headers: request.headers + }); +} + +self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('fetch', (event) => { + if (!shouldHandle(event)) return; + + event.respondWith((async () => { + const cache = await caches.open(CACHE_NAME); + let response = await cache.match(event.request); + if (response) return response; + + const normalized = normalizeRequest(event.request); + response = await cache.match(normalized); + if (response) return response; + + return new Response('Not found', { status: 404 }); + })()); +}); + +self.addEventListener('message', (event) => { + const { type } = event.data || {}; + + if (type === 'STYLE_CACHE_CLEAR') { + event.waitUntil(caches.delete(CACHE_NAME)); + } +}); diff --git a/upload/index.html b/upload/index.html new file mode 100644 index 0000000..fdd61a1 --- /dev/null +++ b/upload/index.html @@ -0,0 +1,43 @@ + + + + + + + Uploader | Style Designer + + + + + + + + + +
+ +
+
+

Upload .zip files

+
+
+
+ + +
Files must end with _page.zip, _scorm.zip or _web.zip.
+
+ +
+
    +
  1. Open a content file with eXeLearning 3 and export it in these formats: Website, Single page, SCORM 1.2. Do not change the default names that eXeLearning assigns (they end with _page.zip, _scorm.zip, and _web.zip).
  2. +
  3. Select the three .zip files and click on "Upload and process."
  4. +
  5. When the process is finished, go back to "Style Designer" to preview the different views.
  6. +
  7. You can edit the theme files and see the changes reflected immediately.
  8. +
+
+ + diff --git a/upload/index.php b/upload/index.php deleted file mode 100644 index 1e76511..0000000 --- a/upload/index.php +++ /dev/null @@ -1,158 +0,0 @@ - 'danger', 'text' => 'No files uploaded.']; - } else { - // Delete existing root-level contents and theme folders if they exist - if (is_dir($contentsDir)) { - exec("rm -rf " . escapeshellarg($contentsDir)); - } - if (is_dir($themeTargetDir)) { - exec("rm -rf " . escapeshellarg($themeTargetDir)); - } - - // Create fresh contents directory - if (!is_dir($contentsDir)) { - mkdir($contentsDir, 0777, true); - } - - // Process uploaded ZIP files - foreach ($_FILES['zipFiles']['name'] as $index => $fileName) { - $tmpPath = $_FILES['zipFiles']['tmp_name'][$index]; - - foreach ($allowedSuffixes as $key => $suffix) { - if (str_ends_with($fileName, $suffix . '.zip')) { - $targetFolder = $contentsDir . '/' . $folderNames[$key]; - - // Remove previous folder if exists (extra safety) - if (is_dir($targetFolder)) { - exec("rm -rf " . escapeshellarg($targetFolder)); - } - - // Create target folder only if it doesn't exist - if (!is_dir($targetFolder)) { - mkdir($targetFolder, 0777, true); - } - - // Extract ZIP - $zip = new ZipArchive; - if ($zip->open($tmpPath) === TRUE) { - $zip->extractTo($targetFolder); - $zip->close(); - unlink($tmpPath); // Delete uploaded zip - } else { - $messages[] = ['type' => 'danger', 'text' => 'Failed to unzip: ' . $fileName]; - } - } - } - } - - // Copy theme folder from contents/web to root - /** - * Recursively copy a directory - */ - function copyDir($src, $dst) { - $dir = opendir($src); - if (!is_dir($dst)) mkdir($dst, 0777, true); - while(false !== ($file = readdir($dir))) { - if ($file != '.' && $file != '..') { - $srcPath = $src . '/' . $file; - $dstPath = $dst . '/' . $file; - if (is_dir($srcPath)) { - copyDir($srcPath, $dstPath); - } else { - copy($srcPath, $dstPath); - } - } - } - closedir($dir); - } - $webThemeDir = $contentsDir . '/web/theme'; - if (is_dir($webThemeDir)) { - copyDir($webThemeDir, $themeTargetDir); - } - - // --- Replace strings in HTML files --- - $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($contentsDir)); - foreach ($iterator as $file) { - if ($file->isFile() && pathinfo($file, PATHINFO_EXTENSION) === 'html') { - $content = file_get_contents($file); - - $replacements = [ - '' => '', - '' => '', - '' => '', - '' => '', - ]; - - $content = str_replace(array_keys($replacements), array_values($replacements), $content); - - if (basename($file) === 'index.html') { - $content = str_replace('', '', $content); - } else { - $content = str_replace('', '', $content); - } - - file_put_contents($file, $content); - } - } - - $messages[] = ['type' => 'success', 'text' => 'Processing completed successfully.']; - } -} -?> - - - - - Uploader | Style Designer - - - - - - - - -
-

Upload .zip files

- - -
- -
- - -
-
- - -
Files must end with _page.zip, _scorm.zip or _web.zip
-
- -
-
    -
  1. Open a content file with eXeLearning 3 and export it in these formats: Website, Single page, SCORM 1.2.
    Do not change the default names that eXeLearning assigns (they end with _page.zip, _scorm.zip, and _web.zip).
  2. -
  3. Then select the 3 .zip files and click on "Upload and process."
  4. -
  5. When the process is finished, click on "Style Viewer." There you can see the different views and switch between them easily.
  6. -
  7. You can see the changes by editing the
  8. -
-
- -