diff --git a/Dockerfile b/Dockerfile index 6ee767d35ed5115de1d0d46b77704c176976a2cf..3ee3a1a706bb92c2055eadb6953ad931116ebb3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,48 +1,44 @@ -# FROM ubuntu:20.04 - -# RUN apt-get update && apt-get install -y --no-install-recommends \ -# build-essential \ -# ca-certificates \ -# curl \ -# git \ -# libgl1-mesa-glx \ -# libglib2.0-0 \ -# libsm6 \ -# libxext6 \ -# libxrender-dev \ -# python3-dev \ -# python3-pip \ -# python3-setuptools \ -# python3-wheel \ -# sudo \ -# unzip \ -# vim \ -# wget \ -# && rm -rf /var/lib/apt/lists/* - -# RUN useradd -m huggingface - -# # RUN wget https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.5.24/Easy-Diffusion-Linux.zip && \ -# # unzip Easy-Diffusion-Linux.zip && \ -# # rm Easy-Diffusion-Linux.zip - -# RUN mkdir easy-diffusion -# # copy the local repo to the container -# COPY . /home/huggingface/easy-diffusion - -# EXPOSE 9000 -# # EXPOSE $PORT - -# RUN chown -R huggingface /home/huggingface/easy-diffusion -# RUN chmod -R u+x /home/huggingface/easy-diffusion - -# USER huggingface +FROM ubuntu:20.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + git \ + libgl1-mesa-glx \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender-dev \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + sudo \ + unzip \ + vim \ + wget \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m huggingface + +# RUN wget https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.5.24/Easy-Diffusion-Linux.zip && \ +# unzip Easy-Diffusion-Linux.zip && \ +# rm Easy-Diffusion-Linux.zip + +RUN mkdir easy-diffusion +# copy the local repo to the container +COPY . /home/huggingface/easy-diffusion -# WORKDIR /home/huggingface +EXPOSE 9000 +# EXPOSE $PORT -# CMD [ "easy-diffusion/start.sh" ] +RUN chown -R huggingface /home/huggingface/easy-diffusion +RUN chmod -R u+x /home/huggingface/easy-diffusion + +USER huggingface -FROM we2app/easy-diffusiom +WORKDIR /home/huggingface -EXPOSE 9000 +CMD [ "easy-diffusion/start.sh" ] \ No newline at end of file diff --git a/developer_console.sh b/developer_console.sh new file mode 100644 index 0000000000000000000000000000000000000000..73972568d0d330a1aa837f6acfee58789798e3d3 --- /dev/null +++ b/developer_console.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +if [ "$0" == "bash" ]; then + echo "Opening Stable Diffusion UI - Developer Console.." + echo "" + + # set legacy and new installer's PATH, if they exist + if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi + if [ -e "installer_files/env" ]; then export PATH="$(pwd)/installer_files/env/bin:$PATH"; fi + + # activate the installer env + CONDA_BASEPATH=$(conda info --base) + source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # avoids the 'shell not initialized' error + + conda activate + + # test the environment + echo "Environment Info:" + which git + git --version + + which conda + conda --version + + echo "" + + # activate the legacy environment (if present) and set PYTHONPATH + if [ -e "installer_files/env" ]; then + export PYTHONPATH="$(pwd)/installer_files/env/lib/python3.8/site-packages" + fi + if [ -e "stable-diffusion/env" ]; then + CONDA_BASEPATH=$(conda info --base) + source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # otherwise conda complains about 'shell not initialized' (needed when running in a script) + + conda activate ./stable-diffusion/env + + export PYTHONPATH="$(pwd)/stable-diffusion/env/lib/python3.8/site-packages" + fi + + which python + python --version + + echo "PYTHONPATH=$PYTHONPATH" + + # done + + echo "" +else + file_name=$(basename "${BASH_SOURCE[0]}") + bash --init-file "$file_name" +fi diff --git a/models/gfpgan/GFPGANv1.3.pth b/models/gfpgan/GFPGANv1.3.pth new file mode 100644 index 0000000000000000000000000000000000000000..1da748a3ef84ff85dd2c77c836f222aae22b007e --- /dev/null +++ b/models/gfpgan/GFPGANv1.3.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c953a88f2727c85c3d9ae72e2bd4846bbaf59fe6972ad94130e23e7017524a70 +size 348632874 diff --git a/models/gfpgan/Place your gfpgan model files here.txt b/models/gfpgan/Place your gfpgan model files here.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a894e572e51de59bf8482a6dcdc3f9945affee5 --- /dev/null +++ b/models/gfpgan/Place your gfpgan model files here.txt @@ -0,0 +1 @@ +Supported extensions: .pth \ No newline at end of file diff --git a/models/hypernetwork/Place your hypernetwork model files here.txt b/models/hypernetwork/Place your hypernetwork model files here.txt new file mode 100644 index 0000000000000000000000000000000000000000..1b206cbd1a40418e1d5cd65f319c7a3e816ab41c --- /dev/null +++ b/models/hypernetwork/Place your hypernetwork model files here.txt @@ -0,0 +1 @@ +Supported extensions: .pt or .safetensors \ No newline at end of file diff --git a/models/realesrgan/Place your realesrgan model files here.txt b/models/realesrgan/Place your realesrgan model files here.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a894e572e51de59bf8482a6dcdc3f9945affee5 --- /dev/null +++ b/models/realesrgan/Place your realesrgan model files here.txt @@ -0,0 +1 @@ +Supported extensions: .pth \ No newline at end of file diff --git a/models/realesrgan/RealESRGAN_x4plus.pth b/models/realesrgan/RealESRGAN_x4plus.pth new file mode 100644 index 0000000000000000000000000000000000000000..9ddced536d07803300536317fef662bb499bca71 --- /dev/null +++ b/models/realesrgan/RealESRGAN_x4plus.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fa0d38905f75ac06eb49a7951b426670021be3018265fd191d2125df9d682f1 +size 67040989 diff --git a/models/realesrgan/RealESRGAN_x4plus_anime_6B.pth b/models/realesrgan/RealESRGAN_x4plus_anime_6B.pth new file mode 100644 index 0000000000000000000000000000000000000000..1f04b81349b49d9c8ebd211d5baa9728d30ee798 --- /dev/null +++ b/models/realesrgan/RealESRGAN_x4plus_anime_6B.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f872d837d3c90ed2e05227bed711af5671a6fd1c9f7d7e91c911a61f155e99da +size 17938799 diff --git a/models/stable-diffusion/Place your stable-diffusion model files here.txt b/models/stable-diffusion/Place your stable-diffusion model files here.txt new file mode 100644 index 0000000000000000000000000000000000000000..33df8feaeb88bcda45c9809652dc462b30647718 --- /dev/null +++ b/models/stable-diffusion/Place your stable-diffusion model files here.txt @@ -0,0 +1 @@ +Supported extensions: .ckpt or .safetensors \ No newline at end of file diff --git a/models/stable-diffusion/sd-v1-4.ckpt b/models/stable-diffusion/sd-v1-4.ckpt new file mode 100644 index 0000000000000000000000000000000000000000..f57e82f9b6539b56f00dc6139747fceea8de367c --- /dev/null +++ b/models/stable-diffusion/sd-v1-4.ckpt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe4efff1e174c627256e44ec2991ba279b3816e364b49f9be2abc0b3ff3f8556 +size 4265380512 diff --git a/models/vae/Place your vae model files here.txt b/models/vae/Place your vae model files here.txt new file mode 100644 index 0000000000000000000000000000000000000000..527281083dac40fa9ba90e19953034e9123afd27 --- /dev/null +++ b/models/vae/Place your vae model files here.txt @@ -0,0 +1 @@ +Supported extensions: .vae.pt or .ckpt or .safetensors \ No newline at end of file diff --git a/models/vae/vae-ft-mse-840000-ema-pruned.ckpt b/models/vae/vae-ft-mse-840000-ema-pruned.ckpt new file mode 100644 index 0000000000000000000000000000000000000000..7322202939e53e60602bfed9b6374b566a367737 --- /dev/null +++ b/models/vae/vae-ft-mse-840000-ema-pruned.ckpt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6a580b13a5bc05a5e16e4dbb80608ff2ec251a162311590c1f34c013d7f3dab +size 334695179 diff --git a/node-shark/Dockerfile b/node-shark/Dockerfile deleted file mode 100644 index a1f51136a8f27384337bfbbd91f4d672713e30e7..0000000000000000000000000000000000000000 --- a/node-shark/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM ubuntu:20.04 - -LABEL version="1.0" - -RUN apt-get update -RUN apt-get install -y curl sudo -RUN curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - -RUN apt-get install -y nodejs - -# set working directory to /app -WORKDIR /app - -# copy index.js from current directory into the container at /app -COPY . /app - -# install need packages specified in package.json -RUN npm install - -# This allows Heroku bind its PORT the Apps port -# since Heroku needs to use its own PORT before the App can be made accessible to the World -EXPOSE $PORT - -# run app when container launches -CMD ["node", "app.js"] \ No newline at end of file diff --git a/node-shark/app.js b/node-shark/app.js deleted file mode 100644 index ea8e64a54fd87b052a5752a098f72707a56b6df4..0000000000000000000000000000000000000000 --- a/node-shark/app.js +++ /dev/null @@ -1,49 +0,0 @@ -// const http = require('http'); - -// const hostname = process.env.HOST || '0.0.0.0'; -// const port = process.env.PORT || 7860; - -// const server = http.createServer((req, res) => { -// res.statusCode = 200; -// res.setHeader('Content-Type', 'text/plain'); -// res.end('Hello World'); -// }); - -// server.listen(port, hostname, () => { -// console.log(`Server running at http://${hostname}:${port}/`); -// }); - -var express = require("express"); -var app = express(); -var router = express.Router(); - -var path = __dirname + '/views/'; - -// Constants -const PORT = process.env.PORT || 7860; -const HOST = '0.0.0.0'; - -router.use(function (req,res,next) { - console.log("/" + req.method); - next(); -}); - -// Hello world api -router.get("/",function(req,res){ - res.send("Hello world!"); -}); - -router.get("/home",function(req,res){ - res.sendFile(path + "index.html"); -}); - -router.get("/sharks",function(req,res){ - res.sendFile(path + "sharks.html"); -}); - -app.use(express.static(path)); -app.use("/", router); - -app.listen(PORT, function () { - console.log(`Example app listening on port ${PORT}`) -}) \ No newline at end of file diff --git a/node-shark/package-lock.json b/node-shark/package-lock.json deleted file mode 100644 index fb0674422c434f602aa1752f2c1caed0b99a8a51..0000000000000000000000000000000000000000 --- a/node-shark/package-lock.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "name": "nodejs-image-demo", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - } - } -} diff --git a/node-shark/package.json b/node-shark/package.json deleted file mode 100644 index 1e4ee965fe9111c361dc6438cef75ad77c8fb224..0000000000000000000000000000000000000000 --- a/node-shark/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "nodejs-image-demo", - "version": "1.0.0", - "description": "nodejs image demo", - "author": "Sammy the Shark ", - "license": "MIT", - "main": "app.js", - "scripts": { - "start": "node app.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "nodejs", - "bootstrap", - "express" - ], - "dependencies": { - "express": "^4.16.4", - "http": "0.0.0" - } -} diff --git a/node-shark/views/css/styles.css b/node-shark/views/css/styles.css deleted file mode 100644 index 3051412ba5ee2ada931ef5952675f7ffc63a30db..0000000000000000000000000000000000000000 --- a/node-shark/views/css/styles.css +++ /dev/null @@ -1,45 +0,0 @@ -.navbar { - margin-bottom: 0; -} - -body { - background: #020A1B; - color: #ffffff; - font-family: 'Merriweather', sans-serif; -} -h1, -h2 { - font-weight: bold; -} -p { - font-size: 16px; - color: #ffffff; -} - - -.jumbotron { - background: #0048CD; - color: white; - text-align: center; -} -.jumbotron p { - color: white; - font-size: 26px; -} - -.btn-primary { - color: #fff; - text-color: #000000; - border-color: white; - margin-bottom: 5px; -} - -img, video, audio { - margin-top: 20px; - max-width: 80%; -} - -div.caption: { - float: left; - clear: both; -} \ No newline at end of file diff --git a/node-shark/views/index.html b/node-shark/views/index.html deleted file mode 100644 index bbf2239113002ce0e20138e14eed9c40680cfb8f..0000000000000000000000000000000000000000 --- a/node-shark/views/index.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - About Sharks - - - - - - - - - - -
-
-

Want to Learn About Sharks?

-

Are you ready to learn about sharks?

-
-

Get Shark Info

-
-
-
-
-
-

Not all sharks are alike

-

Though some are dangerous, sharks generally do not attack humans. Out of the 500 species known to researchers, only 30 have been known to attack humans.

-
-
-

Sharks are ancient

-

There is evidence to suggest that sharks lived up to 400 million years ago.

-
-
-
- - \ No newline at end of file diff --git a/node-shark/views/sharks.html b/node-shark/views/sharks.html deleted file mode 100644 index 81d74e4bbb1ad33cf4168dd0d169de0674ff764e..0000000000000000000000000000000000000000 --- a/node-shark/views/sharks.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - About Sharks - - - - - - - - - -
-

Shark Info

-
-
-
-
-

-

Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans.
- Sawshark -

-
-
-

-

Other sharks are known to be friendly and welcoming!
- Sammy the Shark -

-
-
-
- - \ No newline at end of file diff --git a/scripts/check_modules.py b/scripts/check_modules.py new file mode 100644 index 0000000000000000000000000000000000000000..416ad85121c0709e83138ac8db67b94d17eac9c2 --- /dev/null +++ b/scripts/check_modules.py @@ -0,0 +1,13 @@ +''' +This script checks if the given modules exist +''' + +import sys +import pkgutil + +modules = sys.argv[1:] +missing_modules = [] +for m in modules: + if pkgutil.find_loader(m) is None: + print('module', m, 'not found') + exit(1) diff --git a/scripts/functions.sh b/scripts/functions.sh index 5b1be7f4bd4b7325b7257cff665ad44493ead4c1..495e99500f4be59c569cd4ec91b4820070f9a0b4 100644 --- a/scripts/functions.sh +++ b/scripts/functions.sh @@ -31,7 +31,7 @@ EOF filesize() { case "$(uname -s)" in Linux*) stat -c "%s" $1;; - Darwin*) stat -f "%z" $1;; + Darwin*) /usr/bin/stat -f "%z" $1;; *) echo "Unknown OS: $OS_NAME! This script runs only on Linux or Mac" && exit esac } diff --git a/scripts/install_status.txt b/scripts/install_status.txt index 8b137891791fe96927ad78e64b0aad7bded08bdc..22983a242aa9a7fd667d149c38c609b61473de8a 100644 --- a/scripts/install_status.txt +++ b/scripts/install_status.txt @@ -1 +1,2 @@ +sd_ui_git_cloned diff --git a/scripts/on_sd_start.sh b/scripts/on_sd_start.sh new file mode 100644 index 0000000000000000000000000000000000000000..b62a2fc200459014ce7ab3e95dd9d8baa216d5ff --- /dev/null +++ b/scripts/on_sd_start.sh @@ -0,0 +1,323 @@ +#!/bin/bash + +cp sd-ui-files/scripts/functions.sh scripts/ +cp sd-ui-files/scripts/on_env_start.sh scripts/ +cp sd-ui-files/scripts/bootstrap.sh scripts/ +cp sd-ui-files/scripts/check_modules.py scripts/ + +source ./scripts/functions.sh + +# activate the installer env +CONDA_BASEPATH=$(conda info --base) +source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # avoids the 'shell not initialized' error + +conda activate || fail "Failed to activate conda" + +# remove the old version of the dev console script, if it's still present +if [ -e "open_dev_console.sh" ]; then + rm "open_dev_console.sh" +fi + +python -c "import os; import shutil; frm = 'sd-ui-files/ui/hotfix/9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'; dst = os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'transformers', '9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'); shutil.copyfile(frm, dst) if os.path.exists(dst) else print(''); print('Hotfixed broken JSON file from OpenAI');" + +# Caution, this file will make your eyes and brain bleed. It's such an unholy mess. +# Note to self: Please rewrite this in Python. For the sake of your own sanity. + +# set the correct installer path (current vs legacy) +if [ -e "installer_files/env" ]; then + export INSTALL_ENV_DIR="$(pwd)/installer_files/env" +fi +if [ -e "stable-diffusion/env" ]; then + export INSTALL_ENV_DIR="$(pwd)/stable-diffusion/env" +fi + +# create the stable-diffusion folder, to work with legacy installations +if [ ! -e "stable-diffusion" ]; then mkdir stable-diffusion; fi +cd stable-diffusion + +# activate the old stable-diffusion env, if it exists +if [ -e "env" ]; then + conda activate ./env || fail "conda activate failed" +fi + +# disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan) +if [ -e "src" ]; then mv src src-old; fi +if [ -e "ldm" ]; then mv ldm ldm-old; fi + +mkdir -p "../models/stable-diffusion" +mkdir -p "../models/gfpgan" +mkdir -p "../models/realesrgan" +mkdir -p "../models/vae" + +# migrate the legacy models to the correct path (if already downloaded) +if [ -e "sd-v1-4.ckpt" ]; then mv sd-v1-4.ckpt ../models/stable-diffusion/; fi +if [ -e "custom-model.ckpt" ]; then mv custom-model.ckpt ../models/stable-diffusion/; fi +if [ -e "GFPGANv1.3.pth" ]; then mv GFPGANv1.3.pth ../models/gfpgan/; fi +if [ -e "RealESRGAN_x4plus.pth" ]; then mv RealESRGAN_x4plus.pth ../models/realesrgan/; fi +if [ -e "RealESRGAN_x4plus_anime_6B.pth" ]; then mv RealESRGAN_x4plus_anime_6B.pth ../models/realesrgan/; fi + +OS_NAME=$(uname -s) +case "${OS_NAME}" in + Linux*) OS_NAME="linux";; + Darwin*) OS_NAME="macos";; + *) echo "Unknown OS: $OS_NAME! This script runs only on Linux or Mac" && exit +esac + +# install torch and torchvision +if python ../scripts/check_modules.py torch torchvision; then + # temp fix for installations that installed torch 2.0 by mistake + if [ "$OS_NAME" == "linux" ]; then + python -m pip install --upgrade torch==1.13.1+cu116 torchvision==0.14.1+cu116 --extra-index-url https://download.pytorch.org/whl/cu116 -q + elif [ "$OS_NAME" == "macos" ]; then + python -m pip install --upgrade torch==1.13.1 torchvision==0.14.1 -q + fi + + echo "torch and torchvision have already been installed." +else + echo "Installing torch and torchvision.." + + export PYTHONNOUSERSITE=1 + export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" + + if [ "$OS_NAME" == "linux" ]; then + if python -m pip install --upgrade torch==1.13.1+cu116 torchvision==0.14.1+cu116 --extra-index-url https://download.pytorch.org/whl/cu116 ; then + echo "Installed." + else + fail "torch install failed" + fi + elif [ "$OS_NAME" == "macos" ]; then + if python -m pip install --upgrade torch==1.13.1 torchvision==0.14.1 ; then + echo "Installed." + else + fail "torch install failed" + fi + fi +fi + +# install/upgrade sdkit +if python ../scripts/check_modules.py sdkit sdkit.models ldm transformers numpy antlr4 gfpgan realesrgan ; then + echo "sdkit is already installed." + + # skip sdkit upgrade if in developer-mode + if [ ! -e "../src/sdkit" ]; then + export PYTHONNOUSERSITE=1 + export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" + + python -m pip install --upgrade sdkit==1.0.48 -q + fi +else + echo "Installing sdkit: https://pypi.org/project/sdkit/" + + export PYTHONNOUSERSITE=1 + export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" + + if python -m pip install sdkit==1.0.48 ; then + echo "Installed." + else + fail "sdkit install failed" + fi +fi + +python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))" + +# upgrade stable-diffusion-sdkit +python -m pip install --upgrade stable-diffusion-sdkit==2.1.4 -q +python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))" + +# install rich +if python ../scripts/check_modules.py rich; then + echo "rich has already been installed." +else + echo "Installing rich.." + + export PYTHONNOUSERSITE=1 + export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" + + if python -m pip install rich ; then + echo "Installed." + else + fail "Install failed for rich" + fi +fi + +if python ../scripts/check_modules.py uvicorn fastapi ; then + echo "Packages necessary for Easy Diffusion were already installed" +else + printf "\n\nDownloading packages necessary for Easy Diffusion..\n\n" + + export PYTHONNOUSERSITE=1 + export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" + + if conda install -c conda-forge -y uvicorn fastapi ; then + echo "Installed. Testing.." + else + fail "'conda install uvicorn' failed" + fi + + if ! command -v uvicorn &> /dev/null; then + fail "UI packages not found!" + fi +fi + +if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then + model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"` + + if [ "$model_size" -eq "4265380512" ] || [ "$model_size" -eq "7703807346" ] || [ "$model_size" -eq "7703810927" ]; then + echo "Data files (weights) necessary for Stable Diffusion were already downloaded" + else + printf "\n\nThe model file present at models/stable-diffusion/sd-v1-4.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.." + rm ../models/stable-diffusion/sd-v1-4.ckpt + fi +fi + +if [ ! -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then + echo "Downloading data files (weights) for Stable Diffusion.." + + curl -L -k https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt > ../models/stable-diffusion/sd-v1-4.ckpt + + if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then + model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"` + if [ ! "$model_size" == "4265380512" ]; then + fail "The downloaded model file was invalid! Bytes downloaded: $model_size" + fi + else + fail "Error downloading the data files (weights) for Stable Diffusion" + fi +fi + + +if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then + model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"` + + if [ "$model_size" -eq "348632874" ]; then + echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded" + else + printf "\n\nThe model file present at models/gfpgan/GFPGANv1.3.pth is invalid. It is only $model_size bytes in size. Re-downloading.." + rm ../models/gfpgan/GFPGANv1.3.pth + fi +fi + +if [ ! -f "../models/gfpgan/GFPGANv1.3.pth" ]; then + echo "Downloading data files (weights) for GFPGAN (Face Correction).." + + curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > ../models/gfpgan/GFPGANv1.3.pth + + if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then + model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"` + if [ ! "$model_size" -eq "348632874" ]; then + fail "The downloaded GFPGAN model file was invalid! Bytes downloaded: $model_size" + fi + else + fail "Error downloading the data files (weights) for GFPGAN (Face Correction)." + fi +fi + + +if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then + model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"` + + if [ "$model_size" -eq "67040989" ]; then + echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded" + else + printf "\n\nThe model file present at models/realesrgan/RealESRGAN_x4plus.pth is invalid. It is only $model_size bytes in size. Re-downloading.." + rm ../models/realesrgan/RealESRGAN_x4plus.pth + fi +fi + +if [ ! -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then + echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus.." + + curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > ../models/realesrgan/RealESRGAN_x4plus.pth + + if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then + model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"` + if [ ! "$model_size" -eq "67040989" ]; then + fail "The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: $model_size" + fi + else + fail "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus" + fi +fi + + +if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then + model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"` + + if [ "$model_size" -eq "17938799" ]; then + echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded" + else + printf "\n\nThe model file present at models/realesrgan/RealESRGAN_x4plus_anime_6B.pth is invalid. It is only $model_size bytes in size. Re-downloading.." + rm ../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth + fi +fi + +if [ ! -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then + echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime.." + + curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > ../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth + + if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then + model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"` + if [ ! "$model_size" -eq "17938799" ]; then + fail "The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: $model_size" + fi + else + fail "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime." + fi +fi + + +if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then + model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"` + + if [ "$model_size" -eq "334695179" ]; then + echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded" + else + printf "\n\nThe model file present at models/vae/vae-ft-mse-840000-ema-pruned.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.." + rm ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt + fi +fi + +if [ ! -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then + echo "Downloading data files (weights) for the default VAE (sd-vae-ft-mse-original).." + + curl -L -k https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt > ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt + + if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then + model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"` + if [ ! "$model_size" -eq "334695179" ]; then + printf "\n\nError: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: $model_size\n\n" + printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n" + read -p "Press any key to continue" + exit + fi + else + printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n" + read -p "Press any key to continue" + exit + fi +fi + +if [ `grep -c sd_install_complete ../scripts/install_status.txt` -gt "0" ]; then + echo sd_weights_downloaded >> ../scripts/install_status.txt + echo sd_install_complete >> ../scripts/install_status.txt +fi + +printf "\n\nEasy Diffusion installation complete, starting the server!\n\n" + +SD_PATH=`pwd` + +export PYTORCH_ENABLE_MPS_FALLBACK=1 +export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" +echo "PYTHONPATH=$PYTHONPATH" + +which python +python --version + +cd .. +export SD_UI_PATH=`pwd`/ui +cd stable-diffusion + +uvicorn main:server_api --app-dir "$SD_UI_PATH" --port ${SD_UI_BIND_PORT:-9000} --host ${SD_UI_BIND_IP:-0.0.0.0} --log-level error + +read -p "Press any key to continue" diff --git a/ui/__pycache__/main.cpython-38.pyc b/ui/__pycache__/main.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbef3092302fb8addf6f7960933bde7db8deda0d Binary files /dev/null and b/ui/__pycache__/main.cpython-38.pyc differ diff --git a/ui/easydiffusion/__init__.py b/ui/easydiffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ui/easydiffusion/__pycache__/__init__.cpython-38.pyc b/ui/easydiffusion/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1052ff8a1e7cdd7616be3b6f322f806eef129a96 Binary files /dev/null and b/ui/easydiffusion/__pycache__/__init__.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/app.cpython-38.pyc b/ui/easydiffusion/__pycache__/app.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..244f56cf590598ae84f231ad066a700c47ce4347 Binary files /dev/null and b/ui/easydiffusion/__pycache__/app.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/device_manager.cpython-38.pyc b/ui/easydiffusion/__pycache__/device_manager.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b11d8853d5ec4ada81720466e2d014076961b095 Binary files /dev/null and b/ui/easydiffusion/__pycache__/device_manager.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/model_manager.cpython-38.pyc b/ui/easydiffusion/__pycache__/model_manager.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bb568386ed74c0601d48a5d20558d83542de071 Binary files /dev/null and b/ui/easydiffusion/__pycache__/model_manager.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/renderer.cpython-38.pyc b/ui/easydiffusion/__pycache__/renderer.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b17cfb658a8323dda09a38c39f4eab593cf63a97 Binary files /dev/null and b/ui/easydiffusion/__pycache__/renderer.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/server.cpython-38.pyc b/ui/easydiffusion/__pycache__/server.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6475eb17c083f8fb4c504bc3067e2e3e06ddac9 Binary files /dev/null and b/ui/easydiffusion/__pycache__/server.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/task_manager.cpython-38.pyc b/ui/easydiffusion/__pycache__/task_manager.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98690ffb8ac267a126d3529a2346cf1cba03671e Binary files /dev/null and b/ui/easydiffusion/__pycache__/task_manager.cpython-38.pyc differ diff --git a/ui/easydiffusion/__pycache__/types.cpython-38.pyc b/ui/easydiffusion/__pycache__/types.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e68993788d7b89d78f615be59b308bf199e883bd Binary files /dev/null and b/ui/easydiffusion/__pycache__/types.cpython-38.pyc differ diff --git a/ui/easydiffusion/app.py b/ui/easydiffusion/app.py new file mode 100644 index 0000000000000000000000000000000000000000..83bb08c181b01510496d41c0267da2266341317e --- /dev/null +++ b/ui/easydiffusion/app.py @@ -0,0 +1,328 @@ +import os +import socket +import sys +import json +import traceback +import logging +import shlex +import urllib +from rich.logging import RichHandler + +from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config + +from easydiffusion import task_manager +from easydiffusion.utils import log + +# Remove all handlers associated with the root logger object. +for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + +LOG_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s %(threadName)s %(message)s" +logging.basicConfig( + level=logging.INFO, + format=LOG_FORMAT, + datefmt="%X", + handlers=[RichHandler(markup=True, rich_tracebacks=False, show_time=False, show_level=False)], +) + +SD_DIR = os.getcwd() + +SD_UI_DIR = os.getenv("SD_UI_PATH", None) + +CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts")) +MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models")) + +USER_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "plugins")) +CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins")) + +USER_UI_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "ui") +CORE_UI_PLUGINS_DIR = os.path.join(CORE_PLUGINS_DIR, "ui") +USER_SERVER_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "server") +UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, "core"), (USER_UI_PLUGINS_DIR, "user")) + +sys.path.append(os.path.dirname(SD_UI_DIR)) +sys.path.append(USER_SERVER_PLUGINS_DIR) + +OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder +PRESERVE_CONFIG_VARS = ["FORCE_FULL_PRECISION"] +TASK_TTL = 15 * 60 # Discard last session's task timeout +APP_CONFIG_DEFAULTS = { + # auto: selects the cuda device with the most free memory, cuda: use the currently active cuda device. + "render_devices": "auto", # valid entries: 'auto', 'cpu' or 'cuda:N' (where N is a GPU index) + "update_branch": "main", + "ui": { + "open_browser_on_start": True, + }, +} + +IMAGE_EXTENSIONS = [".png", ".apng", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".jxl", ".gif", ".webp", ".avif", ".svg"] +CUSTOM_MODIFIERS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "modifiers")) +CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS=[".portrait", "_portrait", " portrait", "-portrait"] +CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS=[".landscape", "_landscape", " landscape", "-landscape"] + +def init(): + os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True) + os.makedirs(USER_SERVER_PLUGINS_DIR, exist_ok=True) + + load_server_plugins() + + update_render_threads() + + +def getConfig(default_val=APP_CONFIG_DEFAULTS): + try: + config_json_path = os.path.join(CONFIG_DIR, "config.json") + if not os.path.exists(config_json_path): + config = default_val + else: + with open(config_json_path, "r", encoding="utf-8") as f: + config = json.load(f) + if "net" not in config: + config["net"] = {} + if os.getenv("SD_UI_BIND_PORT") is not None: + config["net"]["listen_port"] = int(os.getenv("SD_UI_BIND_PORT")) + else: + config["net"]["listen_port"] = 9000 + if os.getenv("SD_UI_BIND_IP") is not None: + config["net"]["listen_to_network"] = os.getenv("SD_UI_BIND_IP") == "0.0.0.0" + else: + config["net"]["listen_to_network"] = True + return config + except Exception as e: + log.warn(traceback.format_exc()) + return default_val + + +def setConfig(config): + try: # config.json + config_json_path = os.path.join(CONFIG_DIR, "config.json") + with open(config_json_path, "w", encoding="utf-8") as f: + json.dump(config, f) + except: + log.error(traceback.format_exc()) + + try: # config.bat + config_bat_path = os.path.join(CONFIG_DIR, "config.bat") + config_bat = [] + + if "update_branch" in config: + config_bat.append(f"@set update_branch={config['update_branch']}") + + config_bat.append(f"@set SD_UI_BIND_PORT={config['net']['listen_port']}") + bind_ip = "0.0.0.0" if config["net"]["listen_to_network"] else "127.0.0.1" + config_bat.append(f"@set SD_UI_BIND_IP={bind_ip}") + + # Preserve these variables if they are set + for var in PRESERVE_CONFIG_VARS: + if os.getenv(var) is not None: + config_bat.append(f"@set {var}={os.getenv(var)}") + + if len(config_bat) > 0: + with open(config_bat_path, "w", encoding="utf-8") as f: + f.write("\n".join(config_bat)) + except: + log.error(traceback.format_exc()) + + try: # config.sh + config_sh_path = os.path.join(CONFIG_DIR, "config.sh") + config_sh = ["#!/bin/bash"] + + if "update_branch" in config: + config_sh.append(f"export update_branch={config['update_branch']}") + + config_sh.append(f"export SD_UI_BIND_PORT={config['net']['listen_port']}") + bind_ip = "0.0.0.0" if config["net"]["listen_to_network"] else "127.0.0.1" + config_sh.append(f"export SD_UI_BIND_IP={bind_ip}") + + # Preserve these variables if they are set + for var in PRESERVE_CONFIG_VARS: + if os.getenv(var) is not None: + config_bat.append(f'export {var}="{shlex.quote(os.getenv(var))}"') + + if len(config_sh) > 1: + with open(config_sh_path, "w", encoding="utf-8") as f: + f.write("\n".join(config_sh)) + except: + log.error(traceback.format_exc()) + + +def save_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name, vram_usage_level): + config = getConfig() + if "model" not in config: + config["model"] = {} + + config["model"]["stable-diffusion"] = ckpt_model_name + config["model"]["vae"] = vae_model_name + config["model"]["hypernetwork"] = hypernetwork_model_name + + if vae_model_name is None or vae_model_name == "": + del config["model"]["vae"] + if hypernetwork_model_name is None or hypernetwork_model_name == "": + del config["model"]["hypernetwork"] + + config["vram_usage_level"] = vram_usage_level + + setConfig(config) + + +def update_render_threads(): + config = getConfig() + render_devices = config.get("render_devices", "auto") + active_devices = task_manager.get_devices()["active"].keys() + + log.debug(f"requesting for render_devices: {render_devices}") + task_manager.update_render_threads(render_devices, active_devices) + + +def getUIPlugins(): + plugins = [] + + for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES: + for file in os.listdir(plugins_dir): + if file.endswith(".plugin.js"): + plugins.append(f"/plugins/{dir_prefix}/{file}") + + return plugins + + +def load_server_plugins(): + if not os.path.exists(USER_SERVER_PLUGINS_DIR): + return + + import importlib + + def load_plugin(file): + mod_path = file.replace(".py", "") + return importlib.import_module(mod_path) + + def apply_plugin(file, plugin): + if hasattr(plugin, "get_cond_and_uncond"): + import sdkit.generate.image_generator + + sdkit.generate.image_generator.get_cond_and_uncond = plugin.get_cond_and_uncond + log.info(f"Overridden get_cond_and_uncond with the one in the server plugin: {file}") + + for file in os.listdir(USER_SERVER_PLUGINS_DIR): + file_path = os.path.join(USER_SERVER_PLUGINS_DIR, file) + if (not os.path.isdir(file_path) and not file_path.endswith("_plugin.py")) or ( + os.path.isdir(file_path) and not file_path.endswith("_plugin") + ): + continue + + try: + log.info(f"Loading server plugin: {file}") + mod = load_plugin(file) + + log.info(f"Applying server plugin: {file}") + apply_plugin(file, mod) + except: + log.warn(f"Error while loading a server plugin") + log.warn(traceback.format_exc()) + + +def getIPConfig(): + try: + ips = socket.gethostbyname_ex(socket.gethostname()) + ips[2].append(ips[0]) + return ips[2] + except Exception as e: + log.exception(e) + return [] + + +def open_browser(): + config = getConfig() + ui = config.get("ui", {}) + net = config.get("net", {"listen_port": 9000}) + port = net.get("listen_port", 9000) + if ui.get("open_browser_on_start", True): + import webbrowser + + webbrowser.open(f"http://localhost:{port}") + +def get_image_modifiers(): + modifiers_json_path = os.path.join(SD_UI_DIR, "modifiers.json") + + modifier_categories = {} + original_category_order=[] + with open(modifiers_json_path, "r", encoding="utf-8") as f: + modifiers_file = json.load(f) + + # The trailing slash is needed to support symlinks + if not os.path.isdir(f"{CUSTOM_MODIFIERS_DIR}/"): + return modifiers_file + + # convert modifiers from a list of objects to a dict of dicts + for category_item in modifiers_file: + category_name = category_item['category'] + original_category_order.append(category_name) + category = {} + for modifier_item in category_item['modifiers']: + modifier = {} + for preview_item in modifier_item['previews']: + modifier[preview_item['name']] = preview_item['path'] + category[modifier_item['modifier']] = modifier + modifier_categories[category_name] = category + + def scan_directory(directory_path: str, category_name="Modifiers"): + for entry in os.scandir(directory_path): + if entry.is_file(): + file_extension = list(filter(lambda e: entry.name.endswith(e), IMAGE_EXTENSIONS)) + if len(file_extension) == 0: + continue + + modifier_name = entry.name[: -len(file_extension[0])] + modifier_path = f"custom/{entry.path[len(CUSTOM_MODIFIERS_DIR) + 1:]}" + # URL encode path segments + modifier_path = "/".join(map(lambda segment: urllib.parse.quote(segment), modifier_path.split("/"))) + is_portrait = True + is_landscape = True + + portrait_extension = list(filter(lambda e: modifier_name.lower().endswith(e), CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS)) + landscape_extension = list(filter(lambda e: modifier_name.lower().endswith(e), CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS)) + + if len(portrait_extension) > 0: + is_landscape = False + modifier_name = modifier_name[: -len(portrait_extension[0])] + elif len(landscape_extension) > 0: + is_portrait = False + modifier_name = modifier_name[: -len(landscape_extension[0])] + + if (category_name not in modifier_categories): + modifier_categories[category_name] = {} + + category = modifier_categories[category_name] + + if (modifier_name not in category): + category[modifier_name] = {} + + if (is_portrait or "portrait" not in category[modifier_name]): + category[modifier_name]["portrait"] = modifier_path + + if (is_landscape or "landscape" not in category[modifier_name]): + category[modifier_name]["landscape"] = modifier_path + elif entry.is_dir(): + scan_directory( + entry.path, + entry.name if directory_path==CUSTOM_MODIFIERS_DIR else f"{category_name}/{entry.name}", + ) + + scan_directory(CUSTOM_MODIFIERS_DIR) + + custom_categories = sorted( + [cn for cn in modifier_categories.keys() if cn not in original_category_order], + key=str.casefold, + ) + + # convert the modifiers back into a list of objects + modifier_categories_list = [] + for category_name in [*original_category_order, *custom_categories]: + category = { 'category': category_name, 'modifiers': [] } + for modifier_name in sorted(modifier_categories[category_name].keys(), key=str.casefold): + modifier = { 'modifier': modifier_name, 'previews': [] } + for preview_name, preview_path in modifier_categories[category_name][modifier_name].items(): + modifier['previews'].append({ 'name': preview_name, 'path': preview_path }) + category['modifiers'].append(modifier) + modifier_categories_list.append(category) + + return modifier_categories_list diff --git a/ui/easydiffusion/device_manager.py b/ui/easydiffusion/device_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..18069a82b41421904bd01bf9c70c82744af67b85 --- /dev/null +++ b/ui/easydiffusion/device_manager.py @@ -0,0 +1,253 @@ +import os +import platform +import torch +import traceback +import re + +from easydiffusion.utils import log + +""" +Set `FORCE_FULL_PRECISION` in the environment variables, or in `config.bat`/`config.sh` to set full precision (i.e. float32). +Otherwise the models will load at half-precision (i.e. float16). + +Half-precision is fine most of the time. Full precision is only needed for working around GPU bugs (like NVIDIA 16xx GPUs). +""" + +COMPARABLE_GPU_PERCENTILE = ( + 0.65 # if a GPU's free_mem is within this % of the GPU with the most free_mem, it will be picked +) + +mem_free_threshold = 0 + + +def get_device_delta(render_devices, active_devices): + """ + render_devices: 'cpu', or 'auto', or 'mps' or ['cuda:N'...] + active_devices: ['cpu', 'mps', 'cuda:N'...] + """ + + if render_devices in ("cpu", "auto", "mps"): + render_devices = [render_devices] + elif render_devices is not None: + if isinstance(render_devices, str): + render_devices = [render_devices] + if isinstance(render_devices, list) and len(render_devices) > 0: + render_devices = list(filter(lambda x: x.startswith("cuda:") or x == "mps", render_devices)) + if len(render_devices) == 0: + raise Exception( + 'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "mps"} or {"render_devices": "auto"}' + ) + + render_devices = list(filter(lambda x: is_device_compatible(x), render_devices)) + if len(render_devices) == 0: + raise Exception( + "Sorry, none of the render_devices configured in config.json are compatible with Stable Diffusion" + ) + else: + raise Exception( + 'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}' + ) + else: + render_devices = ["auto"] + + if "auto" in render_devices: + render_devices = auto_pick_devices(active_devices) + if "cpu" in render_devices: + log.warn("WARNING: Could not find a compatible GPU. Using the CPU, but this will be very slow!") + + active_devices = set(active_devices) + render_devices = set(render_devices) + + devices_to_start = render_devices - active_devices + devices_to_stop = active_devices - render_devices + + return devices_to_start, devices_to_stop + + +def is_mps_available(): + return ( + platform.system() == "Darwin" + and hasattr(torch.backends, "mps") + and torch.backends.mps.is_available() + and torch.backends.mps.is_built() + ) + + +def is_cuda_available(): + return torch.cuda.is_available() + + +def auto_pick_devices(currently_active_devices): + global mem_free_threshold + + if is_mps_available(): + return ["mps"] + + if not is_cuda_available(): + return ["cpu"] + + device_count = torch.cuda.device_count() + if device_count == 1: + return ["cuda:0"] if is_device_compatible("cuda:0") else ["cpu"] + + log.debug("Autoselecting GPU. Using most free memory.") + devices = [] + for device in range(device_count): + device = f"cuda:{device}" + if not is_device_compatible(device): + continue + + mem_free, mem_total = torch.cuda.mem_get_info(device) + mem_free /= float(10**9) + mem_total /= float(10**9) + device_name = torch.cuda.get_device_name(device) + log.debug( + f"{device} detected: {device_name} - Memory (free/total): {round(mem_free, 2)}Gb / {round(mem_total, 2)}Gb" + ) + devices.append({"device": device, "device_name": device_name, "mem_free": mem_free}) + + devices.sort(key=lambda x: x["mem_free"], reverse=True) + max_mem_free = devices[0]["mem_free"] + curr_mem_free_threshold = COMPARABLE_GPU_PERCENTILE * max_mem_free + mem_free_threshold = max(curr_mem_free_threshold, mem_free_threshold) + + # Auto-pick algorithm: + # 1. Pick the top 75 percentile of the GPUs, sorted by free_mem. + # 2. Also include already-running devices (GPU-only), otherwise their free_mem will + # always be very low (since their VRAM contains the model). + # These already-running devices probably aren't terrible, since they were picked in the past. + # Worst case, the user can restart the program and that'll get rid of them. + devices = list( + filter((lambda x: x["mem_free"] > mem_free_threshold or x["device"] in currently_active_devices), devices) + ) + devices = list(map(lambda x: x["device"], devices)) + return devices + + +def device_init(context, device): + """ + This function assumes the 'device' has already been verified to be compatible. + `get_device_delta()` has already filtered out incompatible devices. + """ + + validate_device_id(device, log_prefix="device_init") + + if "cuda" not in device: + context.device = device + context.device_name = get_processor_name() + context.half_precision = False + log.debug(f"Render device available as {context.device_name}") + return + + context.device_name = torch.cuda.get_device_name(device) + context.device = device + + # Force full precision on 1660 and 1650 NVIDIA cards to avoid creating green images + if needs_to_force_full_precision(context): + log.warn(f"forcing full precision on this GPU, to avoid green images. GPU detected: {context.device_name}") + # Apply force_full_precision now before models are loaded. + context.half_precision = False + + log.info(f'Setting {device} as active, with precision: {"half" if context.half_precision else "full"}') + torch.cuda.device(device) + + +def needs_to_force_full_precision(context): + if "FORCE_FULL_PRECISION" in os.environ: + return True + + device_name = context.device_name.lower() + return ( + ("nvidia" in device_name or "geforce" in device_name or "quadro" in device_name) + and ( + " 1660" in device_name + or " 1650" in device_name + or " t400" in device_name + or " t550" in device_name + or " t600" in device_name + or " t1000" in device_name + or " t1200" in device_name + or " t2000" in device_name + ) + ) or ("tesla k40m" in device_name) + + +def get_max_vram_usage_level(device): + if "cuda" in device: + _, mem_total = torch.cuda.mem_get_info(device) + else: + return "high" + + mem_total /= float(10**9) + if mem_total < 4.5: + return "low" + elif mem_total < 6.5: + return "balanced" + + return "high" + + +def validate_device_id(device, log_prefix=""): + def is_valid(): + if not isinstance(device, str): + return False + if device == "cpu" or device == "mps": + return True + if not device.startswith("cuda:") or not device[5:].isnumeric(): + return False + return True + + if not is_valid(): + raise EnvironmentError( + f"{log_prefix}: device id should be 'cpu', 'mps', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}" + ) + + +def is_device_compatible(device): + """ + Returns True/False, and prints any compatibility errors + """ + # static variable "history". + is_device_compatible.history = getattr(is_device_compatible, "history", {}) + try: + validate_device_id(device, log_prefix="is_device_compatible") + except: + log.error(str(e)) + return False + + if device in ("cpu", "mps"): + return True + # Memory check + try: + _, mem_total = torch.cuda.mem_get_info(device) + mem_total /= float(10**9) + if mem_total < 3.0: + if is_device_compatible.history.get(device) == None: + log.warn(f"GPU {device} with less than 3 GB of VRAM is not compatible with Stable Diffusion") + is_device_compatible.history[device] = 1 + return False + except RuntimeError as e: + log.error(str(e)) + return False + return True + + +def get_processor_name(): + try: + import subprocess + + if platform.system() == "Windows": + return platform.processor() + elif platform.system() == "Darwin": + os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin" + command = "sysctl -n machdep.cpu.brand_string" + return subprocess.check_output(command, shell=True).decode().strip() + elif platform.system() == "Linux": + command = "cat /proc/cpuinfo" + all_info = subprocess.check_output(command, shell=True).decode().strip() + for line in all_info.split("\n"): + if "model name" in line: + return re.sub(".*model name.*:", "", line, 1).strip() + except: + log.error(traceback.format_exc()) + return "cpu" diff --git a/ui/easydiffusion/model_manager.py b/ui/easydiffusion/model_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..116edf33500f385740ac7b48983eecd9174815a4 --- /dev/null +++ b/ui/easydiffusion/model_manager.py @@ -0,0 +1,255 @@ +import os + +from easydiffusion import app +from easydiffusion.types import TaskData +from easydiffusion.utils import log + +from sdkit import Context +from sdkit.models import load_model, unload_model, scan_model + +KNOWN_MODEL_TYPES = ["stable-diffusion", "vae", "hypernetwork", "gfpgan", "realesrgan"] +MODEL_EXTENSIONS = { + "stable-diffusion": [".ckpt", ".safetensors"], + "vae": [".vae.pt", ".ckpt", ".safetensors"], + "hypernetwork": [".pt", ".safetensors"], + "gfpgan": [".pth"], + "realesrgan": [".pth"], +} +DEFAULT_MODELS = { + "stable-diffusion": [ # needed to support the legacy installations + "custom-model", # only one custom model file was supported initially, creatively named 'custom-model' + "sd-v1-4", # Default fallback. + ], + "gfpgan": ["GFPGANv1.3"], + "realesrgan": ["RealESRGAN_x4plus"], +} +MODELS_TO_LOAD_ON_START = ["stable-diffusion", "vae", "hypernetwork"] + +known_models = {} + + +def init(): + make_model_folders() + getModels() # run this once, to cache the picklescan results + + +def load_default_models(context: Context): + set_vram_optimizations(context) + + # init default model paths + for model_type in MODELS_TO_LOAD_ON_START: + context.model_paths[model_type] = resolve_model_to_use(model_type=model_type) + try: + load_model(context, model_type) + except Exception as e: + log.error(f"[red]Error while loading {model_type} model: {context.model_paths[model_type]}[/red]") + log.error(f"[red]Error: {e}[/red]") + log.error(f"[red]Consider removing the model from the model folder.[red]") + + +def unload_all(context: Context): + for model_type in KNOWN_MODEL_TYPES: + unload_model(context, model_type) + + +def resolve_model_to_use(model_name: str = None, model_type: str = None): + model_extensions = MODEL_EXTENSIONS.get(model_type, []) + default_models = DEFAULT_MODELS.get(model_type, []) + config = app.getConfig() + + model_dirs = [os.path.join(app.MODELS_DIR, model_type), app.SD_DIR] + if not model_name: # When None try user configured model. + # config = getConfig() + if "model" in config and model_type in config["model"]: + model_name = config["model"][model_type] + + if model_name: + # Check models directory + models_dir_path = os.path.join(app.MODELS_DIR, model_type, model_name) + for model_extension in model_extensions: + if os.path.exists(models_dir_path + model_extension): + return models_dir_path + model_extension + if os.path.exists(model_name + model_extension): + return os.path.abspath(model_name + model_extension) + + # Default locations + if model_name in default_models: + default_model_path = os.path.join(app.SD_DIR, model_name) + for model_extension in model_extensions: + if os.path.exists(default_model_path + model_extension): + return default_model_path + model_extension + + # Can't find requested model, check the default paths. + for default_model in default_models: + for model_dir in model_dirs: + default_model_path = os.path.join(model_dir, default_model) + for model_extension in model_extensions: + if os.path.exists(default_model_path + model_extension): + if model_name is not None: + log.warn( + f"Could not find the configured custom model {model_name}{model_extension}. Using the default one: {default_model_path}{model_extension}" + ) + return default_model_path + model_extension + + return None + + +def reload_models_if_necessary(context: Context, task_data: TaskData): + model_paths_in_req = { + "stable-diffusion": task_data.use_stable_diffusion_model, + "vae": task_data.use_vae_model, + "hypernetwork": task_data.use_hypernetwork_model, + "gfpgan": task_data.use_face_correction, + "realesrgan": task_data.use_upscale, + "nsfw_checker": True if task_data.block_nsfw else None, + } + models_to_reload = { + model_type: path + for model_type, path in model_paths_in_req.items() + if context.model_paths.get(model_type) != path + } + + if set_vram_optimizations(context): # reload SD + models_to_reload["stable-diffusion"] = model_paths_in_req["stable-diffusion"] + + for model_type, model_path_in_req in models_to_reload.items(): + context.model_paths[model_type] = model_path_in_req + + action_fn = unload_model if context.model_paths[model_type] is None else load_model + action_fn(context, model_type, scan_model=False) # we've scanned them already + + +def resolve_model_paths(task_data: TaskData): + task_data.use_stable_diffusion_model = resolve_model_to_use( + task_data.use_stable_diffusion_model, model_type="stable-diffusion" + ) + task_data.use_vae_model = resolve_model_to_use(task_data.use_vae_model, model_type="vae") + task_data.use_hypernetwork_model = resolve_model_to_use(task_data.use_hypernetwork_model, model_type="hypernetwork") + + if task_data.use_face_correction: + task_data.use_face_correction = resolve_model_to_use(task_data.use_face_correction, "gfpgan") + if task_data.use_upscale: + task_data.use_upscale = resolve_model_to_use(task_data.use_upscale, "realesrgan") + + +def set_vram_optimizations(context: Context): + config = app.getConfig() + vram_usage_level = config.get("vram_usage_level", "balanced") + + if vram_usage_level != context.vram_usage_level: + context.vram_usage_level = vram_usage_level + return True + + return False + + +def make_model_folders(): + for model_type in KNOWN_MODEL_TYPES: + model_dir_path = os.path.join(app.MODELS_DIR, model_type) + + os.makedirs(model_dir_path, exist_ok=True) + + help_file_name = f"Place your {model_type} model files here.txt" + help_file_contents = f'Supported extensions: {" or ".join(MODEL_EXTENSIONS.get(model_type))}' + + with open(os.path.join(model_dir_path, help_file_name), "w", encoding="utf-8") as f: + f.write(help_file_contents) + + +def is_malicious_model(file_path): + try: + if file_path.endswith(".safetensors"): + return False + scan_result = scan_model(file_path) + if scan_result.issues_count > 0 or scan_result.infected_files > 0: + log.warn( + ":warning: [bold red]Scan %s: %d scanned, %d issue, %d infected.[/bold red]" + % (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files) + ) + return True + else: + log.debug( + "Scan %s: [green]%d scanned, %d issue, %d infected.[/green]" + % (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files) + ) + return False + except Exception as e: + log.error(f"error while scanning: {file_path}, error: {e}") + return False + + +def getModels(): + models = { + "active": { + "stable-diffusion": "sd-v1-4", + "vae": "", + "hypernetwork": "", + }, + "options": { + "stable-diffusion": ["sd-v1-4"], + "vae": [], + "hypernetwork": [], + }, + } + + models_scanned = 0 + + class MaliciousModelException(Exception): + "Raised when picklescan reports a problem with a model" + pass + + def scan_directory(directory, suffixes, directoriesFirst: bool = True): + nonlocal models_scanned + tree = [] + for entry in sorted( + os.scandir(directory), key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower()) + ): + if entry.is_file(): + matching_suffix = list(filter(lambda s: entry.name.endswith(s), suffixes)) + if len(matching_suffix) == 0: + continue + matching_suffix = matching_suffix[0] + + mtime = entry.stat().st_mtime + mod_time = known_models[entry.path] if entry.path in known_models else -1 + if mod_time != mtime: + models_scanned += 1 + if is_malicious_model(entry.path): + raise MaliciousModelException(entry.path) + known_models[entry.path] = mtime + tree.append(entry.name[: -len(matching_suffix)]) + elif entry.is_dir(): + scan = scan_directory(entry.path, suffixes, directoriesFirst=False) + + if len(scan) != 0: + tree.append((entry.name, scan)) + return tree + + def listModels(model_type): + nonlocal models_scanned + + model_extensions = MODEL_EXTENSIONS.get(model_type, []) + models_dir = os.path.join(app.MODELS_DIR, model_type) + if not os.path.exists(models_dir): + os.makedirs(models_dir) + + try: + models["options"][model_type] = scan_directory(models_dir, model_extensions) + except MaliciousModelException as e: + models["scan-error"] = e + + # custom models + listModels(model_type="stable-diffusion") + listModels(model_type="vae") + listModels(model_type="hypernetwork") + listModels(model_type="gfpgan") + + if models_scanned > 0: + log.info(f"[green]Scanned {models_scanned} models. Nothing infected[/]") + + # legacy + custom_weight_path = os.path.join(app.SD_DIR, "custom-model.ckpt") + if os.path.exists(custom_weight_path): + models["options"]["stable-diffusion"].append("custom-model") + + return models diff --git a/ui/easydiffusion/renderer.py b/ui/easydiffusion/renderer.py new file mode 100644 index 0000000000000000000000000000000000000000..c5dc88b44de0e7c34b1dcab09bd0897c8e8ded6b --- /dev/null +++ b/ui/easydiffusion/renderer.py @@ -0,0 +1,180 @@ +import queue +import time +import json +import pprint + +from easydiffusion import device_manager +from easydiffusion.types import TaskData, Response, Image as ResponseImage, UserInitiatedStop, GenerateImageRequest +from easydiffusion.utils import get_printable_request, save_images_to_disk, log + +from sdkit import Context +from sdkit.generate import generate_images +from sdkit.filter import apply_filters +from sdkit.utils import img_to_buffer, img_to_base64_str, latent_samples_to_images, gc + +context = Context() # thread-local +""" +runtime data (bound locally to this thread), for e.g. device, references to loaded models, optimization flags etc +""" + + +def init(device): + """ + Initializes the fields that will be bound to this runtime's context, and sets the current torch device + """ + context.stop_processing = False + context.temp_images = {} + context.partial_x_samples = None + + device_manager.device_init(context, device) + + +def make_images( + req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback +): + context.stop_processing = False + print_task_info(req, task_data) + + images, seeds = make_images_internal(req, task_data, data_queue, task_temp_images, step_callback) + + res = Response(req, task_data, images=construct_response(images, seeds, task_data, base_seed=req.seed)) + res = res.json() + data_queue.put(json.dumps(res)) + log.info("Task completed") + + return res + + +def print_task_info(req: GenerateImageRequest, task_data: TaskData): + req_str = pprint.pformat(get_printable_request(req)).replace("[", "\[") + task_str = pprint.pformat(task_data.dict()).replace("[", "\[") + log.info(f"request: {req_str}") + log.info(f"task data: {task_str}") + + +def make_images_internal( + req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback +): + + images, user_stopped = generate_images_internal( + req, task_data, data_queue, task_temp_images, step_callback, task_data.stream_image_progress, task_data.stream_image_progress_interval + ) + filtered_images = filter_images(task_data, images, user_stopped) + + if task_data.save_to_disk_path is not None: + save_images_to_disk(images, filtered_images, req, task_data) + + seeds = [*range(req.seed, req.seed + len(images))] + if task_data.show_only_filtered_image or filtered_images is images: + return filtered_images, seeds + else: + return images + filtered_images, seeds + seeds + + +def generate_images_internal( + req: GenerateImageRequest, + task_data: TaskData, + data_queue: queue.Queue, + task_temp_images: list, + step_callback, + stream_image_progress: bool, + stream_image_progress_interval: int, +): + context.temp_images.clear() + + callback = make_step_callback(req, task_data, data_queue, task_temp_images, step_callback, stream_image_progress, stream_image_progress_interval) + + try: + if req.init_image is not None: + req.sampler_name = "ddim" + + images = generate_images(context, callback=callback, **req.dict()) + user_stopped = False + except UserInitiatedStop: + images = [] + user_stopped = True + if context.partial_x_samples is not None: + images = latent_samples_to_images(context, context.partial_x_samples) + finally: + if hasattr(context, "partial_x_samples") and context.partial_x_samples is not None: + del context.partial_x_samples + context.partial_x_samples = None + + return images, user_stopped + + +def filter_images(task_data: TaskData, images: list, user_stopped): + if user_stopped: + return images + + filters_to_apply = [] + if task_data.block_nsfw: + filters_to_apply.append("nsfw_checker") + if task_data.use_face_correction and "gfpgan" in task_data.use_face_correction.lower(): + filters_to_apply.append("gfpgan") + if task_data.use_upscale and "realesrgan" in task_data.use_upscale.lower(): + filters_to_apply.append("realesrgan") + + if len(filters_to_apply) == 0: + return images + + return apply_filters(context, filters_to_apply, images, scale=task_data.upscale_amount) + + +def construct_response(images: list, seeds: list, task_data: TaskData, base_seed: int): + return [ + ResponseImage( + data=img_to_base64_str(img, task_data.output_format, task_data.output_quality), + seed=seed, + ) + for img, seed in zip(images, seeds) + ] + + +def make_step_callback( + req: GenerateImageRequest, + task_data: TaskData, + data_queue: queue.Queue, + task_temp_images: list, + step_callback, + stream_image_progress: bool, + stream_image_progress_interval: int, +): + n_steps = req.num_inference_steps if req.init_image is None else int(req.num_inference_steps * req.prompt_strength) + last_callback_time = -1 + + def update_temp_img(x_samples, task_temp_images: list): + partial_images = [] + images = latent_samples_to_images(context, x_samples) + if task_data.block_nsfw: + images = apply_filters(context, "nsfw_checker", images) + + for i, img in enumerate(images): + buf = img_to_buffer(img, output_format="JPEG") + + context.temp_images[f"{task_data.request_id}/{i}"] = buf + task_temp_images[i] = buf + partial_images.append({"path": f"/image/tmp/{task_data.request_id}/{i}"}) + del images + return partial_images + + def on_image_step(x_samples, i): + nonlocal last_callback_time + + context.partial_x_samples = x_samples + step_time = time.time() - last_callback_time if last_callback_time != -1 else -1 + last_callback_time = time.time() + + progress = {"step": i, "step_time": step_time, "total_steps": n_steps} + + if stream_image_progress and stream_image_progress_interval > 0 and i % stream_image_progress_interval == 0: + progress["output"] = update_temp_img(x_samples, task_temp_images) + + data_queue.put(json.dumps(progress)) + + step_callback() + + if context.stop_processing: + raise UserInitiatedStop("User requested that we stop processing") + + return on_image_step diff --git a/ui/easydiffusion/server.py b/ui/easydiffusion/server.py new file mode 100644 index 0000000000000000000000000000000000000000..1d05a1f030cebac9cc41c37d3e80d34cacb0af75 --- /dev/null +++ b/ui/easydiffusion/server.py @@ -0,0 +1,304 @@ +"""server.py: FastAPI SD-UI Web Host. +Notes: + async endpoints always run on the main thread. Without they run on the thread pool. +""" +import os +import traceback +import datetime +from typing import List, Union + +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from starlette.responses import FileResponse, JSONResponse, StreamingResponse +from pydantic import BaseModel + +from easydiffusion import app, model_manager, task_manager +from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest +from easydiffusion.utils import log + +import mimetypes + +log.info(f"started in {app.SD_DIR}") +log.info(f"started at {datetime.datetime.now():%x %X}") + +server_api = FastAPI() + +NOCACHE_HEADERS = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"} + + +class NoCacheStaticFiles(StaticFiles): + def __init__(self, directory: str): + # follow_symlink is only available on fastapi >= 0.92.0 + if (os.path.islink(directory)): + super().__init__(directory = os.path.realpath(directory)) + else: + super().__init__(directory = directory) + + def is_not_modified(self, response_headers, request_headers) -> bool: + if "content-type" in response_headers and ( + "javascript" in response_headers["content-type"] or "css" in response_headers["content-type"] + ): + response_headers.update(NOCACHE_HEADERS) + return False + + return super().is_not_modified(response_headers, request_headers) + + +class SetAppConfigRequest(BaseModel): + update_branch: str = None + render_devices: Union[List[str], List[int], str, int] = None + model_vae: str = None + ui_open_browser_on_start: bool = None + listen_to_network: bool = None + listen_port: int = None + + +def init(): + mimetypes.init() + mimetypes.add_type('text/css', '.css') + + if os.path.isdir(app.CUSTOM_MODIFIERS_DIR): + server_api.mount( + "/media/modifier-thumbnails/custom", + NoCacheStaticFiles(directory=app.CUSTOM_MODIFIERS_DIR), + name="custom-thumbnails", + ) + + server_api.mount("/media", NoCacheStaticFiles(directory=os.path.join(app.SD_UI_DIR, "media")), name="media") + + for plugins_dir, dir_prefix in app.UI_PLUGINS_SOURCES: + server_api.mount( + f"/plugins/{dir_prefix}", NoCacheStaticFiles(directory=plugins_dir), name=f"plugins-{dir_prefix}" + ) + + @server_api.post("/app_config") + async def set_app_config(req: SetAppConfigRequest): + return set_app_config_internal(req) + + @server_api.get("/get/{key:path}") + def read_web_data(key: str = None): + return read_web_data_internal(key) + + @server_api.get("/ping") # Get server and optionally session status. + def ping(session_id: str = None): + return ping_internal(session_id) + + @server_api.post("/render") + def render(req: dict): + return render_internal(req) + + @server_api.post("/model/merge") + def model_merge(req: dict): + print(req) + return model_merge_internal(req) + + @server_api.get("/image/stream/{task_id:int}") + def stream(task_id: int): + return stream_internal(task_id) + + @server_api.get("/image/stop") + def stop(task: int): + return stop_internal(task) + + @server_api.get("/image/tmp/{task_id:int}/{img_id:int}") + def get_image(task_id: int, img_id: int): + return get_image_internal(task_id, img_id) + + @server_api.get("/") + def read_root(): + return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS) + + @server_api.on_event("shutdown") + def shutdown_event(): # Signal render thread to close on shutdown + task_manager.current_state_error = SystemExit("Application shutting down.") + + +# API implementations +def set_app_config_internal(req: SetAppConfigRequest): + config = app.getConfig() + if req.update_branch is not None: + config["update_branch"] = req.update_branch + if req.render_devices is not None: + update_render_devices_in_config(config, req.render_devices) + if req.ui_open_browser_on_start is not None: + if "ui" not in config: + config["ui"] = {} + config["ui"]["open_browser_on_start"] = req.ui_open_browser_on_start + if req.listen_to_network is not None: + if "net" not in config: + config["net"] = {} + config["net"]["listen_to_network"] = bool(req.listen_to_network) + if req.listen_port is not None: + if "net" not in config: + config["net"] = {} + config["net"]["listen_port"] = int(req.listen_port) + try: + app.setConfig(config) + + if req.render_devices: + app.update_render_threads() + + return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS) + except Exception as e: + log.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + + +def update_render_devices_in_config(config, render_devices): + if render_devices not in ("cpu", "auto") and not render_devices.startswith("cuda:"): + raise HTTPException(status_code=400, detail=f"Invalid render device requested: {render_devices}") + + if render_devices.startswith("cuda:"): + render_devices = render_devices.split(",") + + config["render_devices"] = render_devices + + +def read_web_data_internal(key: str = None): + if not key: # /get without parameters, stable-diffusion easter egg. + raise HTTPException(status_code=418, detail="StableDiffusion is drawing a teapot!") # HTTP418 I'm a teapot + elif key == "app_config": + return JSONResponse(app.getConfig(), headers=NOCACHE_HEADERS) + elif key == "system_info": + config = app.getConfig() + + output_dir = config.get("force_save_path", os.path.join(os.path.expanduser("~"), app.OUTPUT_DIRNAME)) + + system_info = { + "devices": task_manager.get_devices(), + "hosts": app.getIPConfig(), + "default_output_dir": output_dir, + "enforce_output_dir": ("force_save_path" in config), + } + system_info["devices"]["config"] = config.get("render_devices", "auto") + return JSONResponse(system_info, headers=NOCACHE_HEADERS) + elif key == "models": + return JSONResponse(model_manager.getModels(), headers=NOCACHE_HEADERS) + elif key == "modifiers": + return JSONResponse(app.get_image_modifiers(), headers=NOCACHE_HEADERS) + elif key == "ui_plugins": + return JSONResponse(app.getUIPlugins(), headers=NOCACHE_HEADERS) + else: + raise HTTPException(status_code=404, detail=f"Request for unknown {key}") # HTTP404 Not Found + + +def ping_internal(session_id: str = None): + if task_manager.is_alive() <= 0: # Check that render threads are alive. + if task_manager.current_state_error: + raise HTTPException(status_code=500, detail=str(task_manager.current_state_error)) + raise HTTPException(status_code=500, detail="Render thread is dead.") + if task_manager.current_state_error and not isinstance(task_manager.current_state_error, StopAsyncIteration): + raise HTTPException(status_code=500, detail=str(task_manager.current_state_error)) + # Alive + response = {"status": str(task_manager.current_state)} + if session_id: + session = task_manager.get_cached_session(session_id, update_ttl=True) + response["tasks"] = {id(t): t.status for t in session.tasks} + response["devices"] = task_manager.get_devices() + return JSONResponse(response, headers=NOCACHE_HEADERS) + + +def render_internal(req: dict): + try: + # separate out the request data into rendering and task-specific data + render_req: GenerateImageRequest = GenerateImageRequest.parse_obj(req) + task_data: TaskData = TaskData.parse_obj(req) + + # Overwrite user specified save path + config = app.getConfig() + if "force_save_path" in config: + task_data.save_to_disk_path = config["force_save_path"] + + render_req.init_image_mask = req.get("mask") # hack: will rename this in the HTTP API in a future revision + + app.save_to_config( + task_data.use_stable_diffusion_model, + task_data.use_vae_model, + task_data.use_hypernetwork_model, + task_data.vram_usage_level, + ) + + # enqueue the task + new_task = task_manager.render(render_req, task_data) + response = { + "status": str(task_manager.current_state), + "queue": len(task_manager.tasks_queue), + "stream": f"/image/stream/{id(new_task)}", + "task": id(new_task), + } + return JSONResponse(response, headers=NOCACHE_HEADERS) + except ChildProcessError as e: # Render thread is dead + raise HTTPException(status_code=500, detail=f"Rendering thread has died.") # HTTP500 Internal Server Error + except ConnectionRefusedError as e: # Unstarted task pending limit reached, deny queueing too many. + raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable + except Exception as e: + log.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + + +def model_merge_internal(req: dict): + try: + from sdkit.train import merge_models + from easydiffusion.utils.save_utils import filename_regex + + mergeReq: MergeRequest = MergeRequest.parse_obj(req) + + merge_models( + model_manager.resolve_model_to_use(mergeReq.model0, "stable-diffusion"), + model_manager.resolve_model_to_use(mergeReq.model1, "stable-diffusion"), + mergeReq.ratio, + os.path.join(app.MODELS_DIR, "stable-diffusion", filename_regex.sub("_", mergeReq.out_path)), + mergeReq.use_fp16, + ) + return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS) + except Exception as e: + log.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + + +def stream_internal(task_id: int): + # TODO Move to WebSockets ?? + task = task_manager.get_cached_task(task_id, update_ttl=True) + if not task: + raise HTTPException(status_code=404, detail=f"Request {task_id} not found.") # HTTP404 NotFound + # if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict + if task.buffer_queue.empty() and not task.lock.locked(): + if task.response: + # log.info(f'Session {session_id} sending cached response') + return JSONResponse(task.response, headers=NOCACHE_HEADERS) + raise HTTPException(status_code=425, detail="Too Early, task not started yet.") # HTTP425 Too Early + # log.info(f'Session {session_id} opened live render stream {id(task.buffer_queue)}') + return StreamingResponse(task.read_buffer_generator(), media_type="application/json") + + +def stop_internal(task: int): + if not task: + if ( + task_manager.current_state == task_manager.ServerStates.Online + or task_manager.current_state == task_manager.ServerStates.Unavailable + ): + raise HTTPException(status_code=409, detail="Not currently running any tasks.") # HTTP409 Conflict + task_manager.current_state_error = StopAsyncIteration("") + return {"OK"} + task_id = task + task = task_manager.get_cached_task(task_id, update_ttl=False) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} was not found.") # HTTP404 Not Found + if isinstance(task.error, StopAsyncIteration): + raise HTTPException(status_code=409, detail=f"Task {task_id} is already stopped.") # HTTP409 Conflict + task.error = StopAsyncIteration(f"Task {task_id} stop requested.") + return {"OK"} + + +def get_image_internal(task_id: int, img_id: int): + task = task_manager.get_cached_task(task_id, update_ttl=True) + if not task: + raise HTTPException(status_code=410, detail=f"Task {task_id} could not be found.") # HTTP404 NotFound + if not task.temp_images[img_id]: + raise HTTPException(status_code=425, detail="Too Early, task data is not available yet.") # HTTP425 Too Early + try: + img_data = task.temp_images[img_id] + img_data.seek(0) + return StreamingResponse(img_data, media_type="image/jpeg") + except KeyError as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/ui/easydiffusion/task_manager.py b/ui/easydiffusion/task_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..31fdaa6f822b0da8a361ffe3dac9729d041f58bb --- /dev/null +++ b/ui/easydiffusion/task_manager.py @@ -0,0 +1,565 @@ +"""task_manager.py: manage tasks dispatching and render threads. +Notes: + render_threads should be the only hard reference held by the manager to the threads. + Use weak_thread_data to store all other data using weak keys. + This will allow for garbage collection after the thread dies. +""" +import json +import traceback + +TASK_TTL = 15 * 60 # seconds, Discard last session's task timeout + +import torch +import queue, threading, time, weakref +from typing import Any, Hashable + +from easydiffusion import device_manager +from easydiffusion.types import TaskData, GenerateImageRequest +from easydiffusion.utils import log + +from sdkit.utils import gc + +THREAD_NAME_PREFIX = "" +ERR_LOCK_FAILED = " failed to acquire lock within timeout." +LOCK_TIMEOUT = 15 # Maximum locking time in seconds before failing a task. +# It's better to get an exception than a deadlock... ALWAYS use timeout in critical paths. + +DEVICE_START_TIMEOUT = 60 # seconds - Maximum time to wait for a render device to init. + + +class SymbolClass(type): # Print nicely formatted Symbol names. + def __repr__(self): + return self.__qualname__ + + def __str__(self): + return self.__name__ + + +class Symbol(metaclass=SymbolClass): + pass + + +class ServerStates: + class Init(Symbol): + pass + + class LoadingModel(Symbol): + pass + + class Online(Symbol): + pass + + class Rendering(Symbol): + pass + + class Unavailable(Symbol): + pass + + +class RenderTask: # Task with output queue and completion lock. + def __init__(self, req: GenerateImageRequest, task_data: TaskData): + task_data.request_id = id(self) + self.render_request: GenerateImageRequest = req # Initial Request + self.task_data: TaskData = task_data + self.response: Any = None # Copy of the last reponse + self.render_device = None # Select the task affinity. (Not used to change active devices). + self.temp_images: list = [None] * req.num_outputs * (1 if task_data.show_only_filtered_image else 2) + self.error: Exception = None + self.lock: threading.Lock = threading.Lock() # Locks at task start and unlocks when task is completed + self.buffer_queue: queue.Queue = queue.Queue() # Queue of JSON string segments + + async def read_buffer_generator(self): + try: + while not self.buffer_queue.empty(): + res = self.buffer_queue.get(block=False) + self.buffer_queue.task_done() + yield res + except queue.Empty as e: + yield + + @property + def status(self): + if self.lock.locked(): + return "running" + if isinstance(self.error, StopAsyncIteration): + return "stopped" + if self.error: + return "error" + if not self.buffer_queue.empty(): + return "buffer" + if self.response: + return "completed" + return "pending" + + @property + def is_pending(self): + return bool(not self.response and not self.error) + + +# Temporary cache to allow to query tasks results for a short time after they are completed. +class DataCache: + def __init__(self): + self._base = dict() + self._lock: threading.Lock = threading.Lock() + + def _get_ttl_time(self, ttl: int) -> int: + return int(time.time()) + ttl + + def _is_expired(self, timestamp: int) -> bool: + return int(time.time()) >= timestamp + + def clean(self) -> None: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.clean" + ERR_LOCK_FAILED) + try: + # Create a list of expired keys to delete + to_delete = [] + for key in self._base: + ttl, _ = self._base[key] + if self._is_expired(ttl): + to_delete.append(key) + # Remove Items + for key in to_delete: + (_, val) = self._base[key] + if isinstance(val, RenderTask): + log.debug(f"RenderTask {key} expired. Data removed.") + elif isinstance(val, SessionState): + log.debug(f"Session {key} expired. Data removed.") + else: + log.debug(f"Key {key} expired. Data removed.") + del self._base[key] + finally: + self._lock.release() + + def clear(self) -> None: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.clear" + ERR_LOCK_FAILED) + try: + self._base.clear() + finally: + self._lock.release() + + def delete(self, key: Hashable) -> bool: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.delete" + ERR_LOCK_FAILED) + try: + if key not in self._base: + return False + del self._base[key] + return True + finally: + self._lock.release() + + def keep(self, key: Hashable, ttl: int) -> bool: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.keep" + ERR_LOCK_FAILED) + try: + if key in self._base: + _, value = self._base.get(key) + self._base[key] = (self._get_ttl_time(ttl), value) + return True + return False + finally: + self._lock.release() + + def put(self, key: Hashable, value: Any, ttl: int) -> bool: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.put" + ERR_LOCK_FAILED) + try: + self._base[key] = (self._get_ttl_time(ttl), value) + except Exception as e: + log.error(traceback.format_exc()) + return False + else: + return True + finally: + self._lock.release() + + def tryGet(self, key: Hashable) -> Any: + if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("DataCache.tryGet" + ERR_LOCK_FAILED) + try: + ttl, value = self._base.get(key, (None, None)) + if ttl is not None and self._is_expired(ttl): + log.debug(f"Session {key} expired. Discarding data.") + del self._base[key] + return None + return value + finally: + self._lock.release() + + +manager_lock = threading.RLock() +render_threads = [] +current_state = ServerStates.Init +current_state_error: Exception = None +tasks_queue = [] +session_cache = DataCache() +task_cache = DataCache() +weak_thread_data = weakref.WeakKeyDictionary() +idle_event: threading.Event = threading.Event() + + +class SessionState: + def __init__(self, id: str): + self._id = id + self._tasks_ids = [] + + @property + def id(self): + return self._id + + @property + def tasks(self): + tasks = [] + for task_id in self._tasks_ids: + task = task_cache.tryGet(task_id) + if task: + tasks.append(task) + return tasks + + def put(self, task, ttl=TASK_TTL): + task_id = id(task) + self._tasks_ids.append(task_id) + if not task_cache.put(task_id, task, ttl): + return False + while len(self._tasks_ids) > len(render_threads) * 2: + self._tasks_ids.pop(0) + return True + + +def thread_get_next_task(): + from easydiffusion import renderer + + if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + log.warn(f"Render thread on device: {renderer.context.device} failed to acquire manager lock.") + return None + if len(tasks_queue) <= 0: + manager_lock.release() + return None + task = None + try: # Select a render task. + for queued_task in tasks_queue: + if queued_task.render_device and renderer.context.device != queued_task.render_device: + # Is asking for a specific render device. + if is_alive(queued_task.render_device) > 0: + continue # requested device alive, skip current one. + else: + # Requested device is not active, return error to UI. + queued_task.error = Exception(queued_task.render_device + " is not currently active.") + task = queued_task + break + if not queued_task.render_device and renderer.context.device == "cpu" and is_alive() > 1: + # not asking for any specific devices, cpu want to grab task but other render devices are alive. + continue # Skip Tasks, don't run on CPU unless there is nothing else or user asked for it. + task = queued_task + break + if task is not None: + del tasks_queue[tasks_queue.index(task)] + return task + finally: + manager_lock.release() + + +def thread_render(device): + global current_state, current_state_error + + from easydiffusion import renderer, model_manager + + try: + renderer.init(device) + + weak_thread_data[threading.current_thread()] = { + "device": renderer.context.device, + "device_name": renderer.context.device_name, + "alive": True, + } + + current_state = ServerStates.LoadingModel + model_manager.load_default_models(renderer.context) + + current_state = ServerStates.Online + except Exception as e: + log.error(traceback.format_exc()) + weak_thread_data[threading.current_thread()] = {"error": e, "alive": False} + return + + while True: + session_cache.clean() + task_cache.clean() + if not weak_thread_data[threading.current_thread()]["alive"]: + log.info(f"Shutting down thread for device {renderer.context.device}") + model_manager.unload_all(renderer.context) + return + if isinstance(current_state_error, SystemExit): + current_state = ServerStates.Unavailable + return + task = thread_get_next_task() + if task is None: + idle_event.clear() + idle_event.wait(timeout=1) + continue + if task.error is not None: + log.error(task.error) + task.response = {"status": "failed", "detail": str(task.error)} + task.buffer_queue.put(json.dumps(task.response)) + continue + if current_state_error: + task.error = current_state_error + task.response = {"status": "failed", "detail": str(task.error)} + task.buffer_queue.put(json.dumps(task.response)) + continue + log.info(f"Session {task.task_data.session_id} starting task {id(task)} on {renderer.context.device_name}") + if not task.lock.acquire(blocking=False): + raise Exception("Got locked task from queue.") + try: + + def step_callback(): + global current_state_error + + if ( + isinstance(current_state_error, SystemExit) + or isinstance(current_state_error, StopAsyncIteration) + or isinstance(task.error, StopAsyncIteration) + ): + renderer.context.stop_processing = True + if isinstance(current_state_error, StopAsyncIteration): + task.error = current_state_error + current_state_error = None + log.info(f"Session {task.task_data.session_id} sent cancel signal for task {id(task)}") + + current_state = ServerStates.LoadingModel + model_manager.resolve_model_paths(task.task_data) + model_manager.reload_models_if_necessary(renderer.context, task.task_data) + + current_state = ServerStates.Rendering + task.response = renderer.make_images( + task.render_request, task.task_data, task.buffer_queue, task.temp_images, step_callback + ) + # Before looping back to the generator, mark cache as still alive. + task_cache.keep(id(task), TASK_TTL) + session_cache.keep(task.task_data.session_id, TASK_TTL) + except Exception as e: + task.error = str(e) + task.response = {"status": "failed", "detail": str(task.error)} + task.buffer_queue.put(json.dumps(task.response)) + log.error(traceback.format_exc()) + finally: + gc(renderer.context) + task.lock.release() + task_cache.keep(id(task), TASK_TTL) + session_cache.keep(task.task_data.session_id, TASK_TTL) + if isinstance(task.error, StopAsyncIteration): + log.info(f"Session {task.task_data.session_id} task {id(task)} cancelled!") + elif task.error is not None: + log.info(f"Session {task.task_data.session_id} task {id(task)} failed!") + else: + log.info( + f"Session {task.task_data.session_id} task {id(task)} completed by {renderer.context.device_name}." + ) + current_state = ServerStates.Online + + +def get_cached_task(task_id: str, update_ttl: bool = False): + # By calling keep before tryGet, wont discard if was expired. + if update_ttl and not task_cache.keep(task_id, TASK_TTL): + # Failed to keep task, already gone. + return None + return task_cache.tryGet(task_id) + + +def get_cached_session(session_id: str, update_ttl: bool = False): + if update_ttl: + session_cache.keep(session_id, TASK_TTL) + session = session_cache.tryGet(session_id) + if not session: + session = SessionState(session_id) + session_cache.put(session_id, session, TASK_TTL) + return session + + +def get_devices(): + devices = { + "all": {}, + "active": {}, + } + + def get_device_info(device): + if device in ("cpu", "mps"): + return {"name": device_manager.get_processor_name()} + + mem_free, mem_total = torch.cuda.mem_get_info(device) + mem_free /= float(10**9) + mem_total /= float(10**9) + + return { + "name": torch.cuda.get_device_name(device), + "mem_free": mem_free, + "mem_total": mem_total, + "max_vram_usage_level": device_manager.get_max_vram_usage_level(device), + } + + # list the compatible devices + cuda_count = torch.cuda.device_count() + for device in range(cuda_count): + device = f"cuda:{device}" + if not device_manager.is_device_compatible(device): + continue + + devices["all"].update({device: get_device_info(device)}) + + if device_manager.is_mps_available(): + devices["all"].update({"mps": get_device_info("mps")}) + + devices["all"].update({"cpu": get_device_info("cpu")}) + + # list the activated devices + if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("get_devices" + ERR_LOCK_FAILED) + try: + for rthread in render_threads: + if not rthread.is_alive(): + continue + weak_data = weak_thread_data.get(rthread) + if not weak_data or not "device" in weak_data or not "device_name" in weak_data: + continue + device = weak_data["device"] + devices["active"].update({device: get_device_info(device)}) + finally: + manager_lock.release() + + return devices + + +def is_alive(device=None): + if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("is_alive" + ERR_LOCK_FAILED) + nbr_alive = 0 + try: + for rthread in render_threads: + if device is not None: + weak_data = weak_thread_data.get(rthread) + if weak_data is None or not "device" in weak_data or weak_data["device"] is None: + continue + thread_device = weak_data["device"] + if thread_device != device: + continue + if rthread.is_alive(): + nbr_alive += 1 + return nbr_alive + finally: + manager_lock.release() + + +def start_render_thread(device): + if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("start_render_thread" + ERR_LOCK_FAILED) + log.info(f"Start new Rendering Thread on device: {device}") + try: + rthread = threading.Thread(target=thread_render, kwargs={"device": device}) + rthread.daemon = True + rthread.name = THREAD_NAME_PREFIX + device + rthread.start() + render_threads.append(rthread) + finally: + manager_lock.release() + timeout = DEVICE_START_TIMEOUT + while not rthread.is_alive() or not rthread in weak_thread_data or not "device" in weak_thread_data[rthread]: + if rthread in weak_thread_data and "error" in weak_thread_data[rthread]: + log.error(f"{rthread}, {device}, error: {weak_thread_data[rthread]['error']}") + return False + if timeout <= 0: + return False + timeout -= 1 + time.sleep(1) + return True + + +def stop_render_thread(device): + try: + device_manager.validate_device_id(device, log_prefix="stop_render_thread") + except: + log.error(traceback.format_exc()) + return False + + if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): + raise Exception("stop_render_thread" + ERR_LOCK_FAILED) + log.info(f"Stopping Rendering Thread on device: {device}") + + try: + thread_to_remove = None + for rthread in render_threads: + weak_data = weak_thread_data.get(rthread) + if weak_data is None or not "device" in weak_data or weak_data["device"] is None: + continue + thread_device = weak_data["device"] + if thread_device == device: + weak_data["alive"] = False + thread_to_remove = rthread + break + if thread_to_remove is not None: + render_threads.remove(rthread) + return True + finally: + manager_lock.release() + + return False + + +def update_render_threads(render_devices, active_devices): + devices_to_start, devices_to_stop = device_manager.get_device_delta(render_devices, active_devices) + log.debug(f"devices_to_start: {devices_to_start}") + log.debug(f"devices_to_stop: {devices_to_stop}") + + for device in devices_to_stop: + if is_alive(device) <= 0: + log.debug(f"{device} is not alive") + continue + if not stop_render_thread(device): + log.warn(f"{device} could not stop render thread") + + for device in devices_to_start: + if is_alive(device) >= 1: + log.debug(f"{device} already registered.") + continue + if not start_render_thread(device): + log.warn(f"{device} failed to start.") + + if is_alive() <= 0: # No running devices, probably invalid user config. + raise EnvironmentError( + 'ERROR: No active render devices! Please verify the "render_devices" value in config.json' + ) + + log.debug(f"active devices: {get_devices()['active']}") + + +def shutdown_event(): # Signal render thread to close on shutdown + global current_state_error + current_state_error = SystemExit("Application shutting down.") + + +def render(render_req: GenerateImageRequest, task_data: TaskData): + current_thread_count = is_alive() + if current_thread_count <= 0: # Render thread is dead + raise ChildProcessError("Rendering thread has died.") + + # Alive, check if task in cache + session = get_cached_session(task_data.session_id, update_ttl=True) + pending_tasks = list(filter(lambda t: t.is_pending, session.tasks)) + if current_thread_count < len(pending_tasks): + raise ConnectionRefusedError( + f"Session {task_data.session_id} already has {len(pending_tasks)} pending tasks out of {current_thread_count}." + ) + + new_task = RenderTask(render_req, task_data) + if session.put(new_task, TASK_TTL): + # Use twice the normal timeout for adding user requests. + # Tries to force session.put to fail before tasks_queue.put would. + if manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT * 2): + try: + tasks_queue.append(new_task) + idle_event.set() + return new_task + finally: + manager_lock.release() + raise RuntimeError("Failed to add task to cache.") diff --git a/ui/easydiffusion/types.py b/ui/easydiffusion/types.py new file mode 100644 index 0000000000000000000000000000000000000000..8e7044f3d729d9d7ea0b7e5a790be7012f21f8ab --- /dev/null +++ b/ui/easydiffusion/types.py @@ -0,0 +1,103 @@ +from pydantic import BaseModel +from typing import Any + + +class GenerateImageRequest(BaseModel): + prompt: str = "" + negative_prompt: str = "" + + seed: int = 42 + width: int = 512 + height: int = 512 + + num_outputs: int = 1 + num_inference_steps: int = 50 + guidance_scale: float = 7.5 + + init_image: Any = None + init_image_mask: Any = None + prompt_strength: float = 0.8 + preserve_init_image_color_profile = False + + sampler_name: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms" + hypernetwork_strength: float = 0 + + +class TaskData(BaseModel): + request_id: str = None + session_id: str = "session" + save_to_disk_path: str = None + vram_usage_level: str = "balanced" # or "low" or "medium" + + use_face_correction: str = None # or "GFPGANv1.3" + use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B" + upscale_amount: int = 4 # or 2 + use_stable_diffusion_model: str = "sd-v1-4" + # use_stable_diffusion_config: str = "v1-inference" + use_vae_model: str = None + use_hypernetwork_model: str = None + + show_only_filtered_image: bool = False + block_nsfw: bool = False + output_format: str = "jpeg" # or "png" or "webp" + output_quality: int = 75 + metadata_output_format: str = "txt" # or "json" + stream_image_progress: bool = False + stream_image_progress_interval: int = 5 + + +class MergeRequest(BaseModel): + model0: str = None + model1: str = None + ratio: float = None + out_path: str = "mix" + use_fp16 = True + + +class Image: + data: str # base64 + seed: int + is_nsfw: bool + path_abs: str = None + + def __init__(self, data, seed): + self.data = data + self.seed = seed + + def json(self): + return { + "data": self.data, + "seed": self.seed, + "path_abs": self.path_abs, + } + + +class Response: + render_request: GenerateImageRequest + task_data: TaskData + images: list + + def __init__(self, render_request: GenerateImageRequest, task_data: TaskData, images: list): + self.render_request = render_request + self.task_data = task_data + self.images = images + + def json(self): + del self.render_request.init_image + del self.render_request.init_image_mask + + res = { + "status": "succeeded", + "render_request": self.render_request.dict(), + "task_data": self.task_data.dict(), + "output": [], + } + + for image in self.images: + res["output"].append(image.json()) + + return res + + +class UserInitiatedStop(Exception): + pass diff --git a/ui/easydiffusion/utils/__init__.py b/ui/easydiffusion/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b9c5e21ac840a325a440847ba24087dd232393cb --- /dev/null +++ b/ui/easydiffusion/utils/__init__.py @@ -0,0 +1,8 @@ +import logging + +log = logging.getLogger("easydiffusion") + +from .save_utils import ( + save_images_to_disk, + get_printable_request, +) diff --git a/ui/easydiffusion/utils/__pycache__/__init__.cpython-38.pyc b/ui/easydiffusion/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63befe90eed235721001cd465e5cbaadc108da59 Binary files /dev/null and b/ui/easydiffusion/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/ui/easydiffusion/utils/__pycache__/save_utils.cpython-38.pyc b/ui/easydiffusion/utils/__pycache__/save_utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c399d0174bbdcfd53450ea55a5f9a25b1faf09a Binary files /dev/null and b/ui/easydiffusion/utils/__pycache__/save_utils.cpython-38.pyc differ diff --git a/ui/easydiffusion/utils/save_utils.py b/ui/easydiffusion/utils/save_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6012bc44219cf33189503fab5de73fa3b821780f --- /dev/null +++ b/ui/easydiffusion/utils/save_utils.py @@ -0,0 +1,131 @@ +import os +import time +import re + +from easydiffusion.types import TaskData, GenerateImageRequest + +from sdkit.utils import save_images, save_dicts +from numpy import base_repr + +filename_regex = re.compile("[^a-zA-Z0-9._-]") + +# keep in sync with `ui/media/js/dnd.js` +TASK_TEXT_MAPPING = { + "prompt": "Prompt", + "width": "Width", + "height": "Height", + "seed": "Seed", + "num_inference_steps": "Steps", + "guidance_scale": "Guidance Scale", + "prompt_strength": "Prompt Strength", + "use_face_correction": "Use Face Correction", + "use_upscale": "Use Upscaling", + "upscale_amount": "Upscale By", + "sampler_name": "Sampler", + "negative_prompt": "Negative Prompt", + "use_stable_diffusion_model": "Stable Diffusion model", + "use_vae_model": "VAE model", + "use_hypernetwork_model": "Hypernetwork model", + "hypernetwork_strength": "Hypernetwork Strength", +} + + +def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData): + now = time.time() + save_dir_path = os.path.join(task_data.save_to_disk_path, filename_regex.sub("_", task_data.session_id)) + metadata_entries = get_metadata_entries_for_request(req, task_data) + make_filename = make_filename_callback(req, now=now) + + if task_data.show_only_filtered_image or filtered_images is images: + save_images( + filtered_images, + save_dir_path, + file_name=make_filename, + output_format=task_data.output_format, + output_quality=task_data.output_quality, + ) + if task_data.metadata_output_format.lower() in ["json", "txt", "embed"]: + save_dicts( + metadata_entries, + save_dir_path, + file_name=make_filename, + output_format=task_data.metadata_output_format, + file_format=task_data.output_format, + ) + else: + make_filter_filename = make_filename_callback(req, now=now, suffix="filtered") + + save_images( + images, + save_dir_path, + file_name=make_filename, + output_format=task_data.output_format, + output_quality=task_data.output_quality, + ) + save_images( + filtered_images, + save_dir_path, + file_name=make_filter_filename, + output_format=task_data.output_format, + output_quality=task_data.output_quality, + ) + if task_data.metadata_output_format.lower() in ["json", "txt", "embed"]: + save_dicts( + metadata_entries, + save_dir_path, + file_name=make_filter_filename, + output_format=task_data.metadata_output_format, + file_format=task_data.output_format, + ) + + +def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskData): + metadata = get_printable_request(req) + metadata.update( + { + "use_stable_diffusion_model": task_data.use_stable_diffusion_model, + "use_vae_model": task_data.use_vae_model, + "use_hypernetwork_model": task_data.use_hypernetwork_model, + "use_face_correction": task_data.use_face_correction, + "use_upscale": task_data.use_upscale, + } + ) + if metadata["use_upscale"] is not None: + metadata["upscale_amount"] = task_data.upscale_amount + if task_data.use_hypernetwork_model is None: + del metadata["hypernetwork_strength"] + + # if text, format it in the text format expected by the UI + is_txt_format = task_data.metadata_output_format.lower() == "txt" + if is_txt_format: + metadata = {TASK_TEXT_MAPPING[key]: val for key, val in metadata.items() if key in TASK_TEXT_MAPPING} + + entries = [metadata.copy() for _ in range(req.num_outputs)] + for i, entry in enumerate(entries): + entry["Seed" if is_txt_format else "seed"] = req.seed + i + + return entries + + +def get_printable_request(req: GenerateImageRequest): + metadata = req.dict() + del metadata["init_image"] + del metadata["init_image_mask"] + if req.init_image is None: + del metadata["prompt_strength"] + return metadata + + +def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None): + if now is None: + now = time.time() + + def make_filename(i): + img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(int(i),36) # Base 36 conversion, 0-9, A-Z + + prompt_flattened = filename_regex.sub("_", req.prompt)[:50] + name = f"{prompt_flattened}_{img_id}" + name = name if suffix is None else f"{name}_{suffix}" + return name + + return make_filename diff --git a/ui/hotfix/9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142 b/ui/hotfix/9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142 new file mode 100644 index 0000000000000000000000000000000000000000..2c19f6666e0e163c7954df66cb901353fcad088e --- /dev/null +++ b/ui/hotfix/9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142 @@ -0,0 +1,171 @@ +{ + "_name_or_path": "clip-vit-large-patch14/", + "architectures": [ + "CLIPModel" + ], + "initializer_factor": 1.0, + "logit_scale_init_value": 2.6592, + "model_type": "clip", + "projection_dim": 768, + "text_config": { + "_name_or_path": "", + "add_cross_attention": false, + "architectures": null, + "attention_dropout": 0.0, + "bad_words_ids": null, + "bos_token_id": 0, + "chunk_size_feed_forward": 0, + "cross_attention_hidden_size": null, + "decoder_start_token_id": null, + "diversity_penalty": 0.0, + "do_sample": false, + "dropout": 0.0, + "early_stopping": false, + "encoder_no_repeat_ngram_size": 0, + "eos_token_id": 2, + "finetuning_task": null, + "forced_bos_token_id": null, + "forced_eos_token_id": null, + "hidden_act": "quick_gelu", + "hidden_size": 768, + "id2label": { + "0": "LABEL_0", + "1": "LABEL_1" + }, + "initializer_factor": 1.0, + "initializer_range": 0.02, + "intermediate_size": 3072, + "is_decoder": false, + "is_encoder_decoder": false, + "label2id": { + "LABEL_0": 0, + "LABEL_1": 1 + }, + "layer_norm_eps": 1e-05, + "length_penalty": 1.0, + "max_length": 20, + "max_position_embeddings": 77, + "min_length": 0, + "model_type": "clip_text_model", + "no_repeat_ngram_size": 0, + "num_attention_heads": 12, + "num_beam_groups": 1, + "num_beams": 1, + "num_hidden_layers": 12, + "num_return_sequences": 1, + "output_attentions": false, + "output_hidden_states": false, + "output_scores": false, + "pad_token_id": 1, + "prefix": null, + "problem_type": null, + "projection_dim" : 768, + "pruned_heads": {}, + "remove_invalid_values": false, + "repetition_penalty": 1.0, + "return_dict": true, + "return_dict_in_generate": false, + "sep_token_id": null, + "task_specific_params": null, + "temperature": 1.0, + "tie_encoder_decoder": false, + "tie_word_embeddings": true, + "tokenizer_class": null, + "top_k": 50, + "top_p": 1.0, + "torch_dtype": null, + "torchscript": false, + "transformers_version": "4.16.0.dev0", + "use_bfloat16": false, + "vocab_size": 49408 + }, + "text_config_dict": { + "hidden_size": 768, + "intermediate_size": 3072, + "num_attention_heads": 12, + "num_hidden_layers": 12, + "projection_dim": 768 + }, + "torch_dtype": "float32", + "transformers_version": null, + "vision_config": { + "_name_or_path": "", + "add_cross_attention": false, + "architectures": null, + "attention_dropout": 0.0, + "bad_words_ids": null, + "bos_token_id": null, + "chunk_size_feed_forward": 0, + "cross_attention_hidden_size": null, + "decoder_start_token_id": null, + "diversity_penalty": 0.0, + "do_sample": false, + "dropout": 0.0, + "early_stopping": false, + "encoder_no_repeat_ngram_size": 0, + "eos_token_id": null, + "finetuning_task": null, + "forced_bos_token_id": null, + "forced_eos_token_id": null, + "hidden_act": "quick_gelu", + "hidden_size": 1024, + "id2label": { + "0": "LABEL_0", + "1": "LABEL_1" + }, + "image_size": 224, + "initializer_factor": 1.0, + "initializer_range": 0.02, + "intermediate_size": 4096, + "is_decoder": false, + "is_encoder_decoder": false, + "label2id": { + "LABEL_0": 0, + "LABEL_1": 1 + }, + "layer_norm_eps": 1e-05, + "length_penalty": 1.0, + "max_length": 20, + "min_length": 0, + "model_type": "clip_vision_model", + "no_repeat_ngram_size": 0, + "num_attention_heads": 16, + "num_beam_groups": 1, + "num_beams": 1, + "num_hidden_layers": 24, + "num_return_sequences": 1, + "output_attentions": false, + "output_hidden_states": false, + "output_scores": false, + "pad_token_id": null, + "patch_size": 14, + "prefix": null, + "problem_type": null, + "projection_dim" : 768, + "pruned_heads": {}, + "remove_invalid_values": false, + "repetition_penalty": 1.0, + "return_dict": true, + "return_dict_in_generate": false, + "sep_token_id": null, + "task_specific_params": null, + "temperature": 1.0, + "tie_encoder_decoder": false, + "tie_word_embeddings": true, + "tokenizer_class": null, + "top_k": 50, + "top_p": 1.0, + "torch_dtype": null, + "torchscript": false, + "transformers_version": "4.16.0.dev0", + "use_bfloat16": false + }, + "vision_config_dict": { + "hidden_size": 1024, + "intermediate_size": 4096, + "num_attention_heads": 16, + "num_hidden_layers": 24, + "patch_size": 14, + "projection_dim": 768 + } +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000000000000000000000000000000000000..d2b836b187c4eac4807ae19400b992c00f51a9ea --- /dev/null +++ b/ui/index.html @@ -0,0 +1,514 @@ + + + + Easy Diffusion + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ Stable Diffusion is starting.. +
+
+ + Generate + + + Settings + + + Help & Community + +
+
+ +
+
+
+
+
+ or + + + +
+ +
+
+ +
+ + +
+
+ + + +
+
+
+ + Browse + +
+
+ + Draw +
+
+
+ + Inpaint +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ + +
+ + + +
+
+ + + +
+

+ Image Settings + + + Reset Image Settings + + +

+
+
+ Image Settings + + + + + + + + + + + + + + + + + +
+ + + Click to learn more about custom models +
+ + Click to learn more about VAEs +
+ + Click to learn more about samplers +
+ + + + +
Small image sizes can cause bad image quality
+

+ +

+ +
+ +
+ +
    +
  • Render Settings
  • +
  • +
  • +
  • + + + with + +
  • +
  • +
+
+
+ +
+

+ Image Modifiers (art styles, tags etc) + + + Add Custom Modifiers + + +

+
+
+ + +   + + +
+
+
+
+ +
+
+ Type a prompt and press the "Make Image" button.

You can set an "Initial Image" if you want to guide the AI.

+ You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section + and selecting the desired modifiers.

+ Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.

Enjoy! :) +
+ +
+
+ + +
+ + + + +
+
+
+
+
+
+ +
+
+

System Settings

+
+
+ +

+
+

System Info

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

Help

+ +
+ + +
+
+
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + diff --git a/ui/main.py b/ui/main.py new file mode 100644 index 0000000000000000000000000000000000000000..77def4a5499a78f9b73c7eedaaa4cfdfedf4ae94 --- /dev/null +++ b/ui/main.py @@ -0,0 +1,10 @@ +from easydiffusion import model_manager, app, server +from easydiffusion.server import server_api # required for uvicorn + +# Init the app +model_manager.init() +app.init() +server.init() + +# start the browser ui +app.open_browser() diff --git a/ui/media/css/auto-save.css b/ui/media/css/auto-save.css new file mode 100644 index 0000000000000000000000000000000000000000..80aa48d88dccc99e6f3fdd2e5511c515f19fdea2 --- /dev/null +++ b/ui/media/css/auto-save.css @@ -0,0 +1,81 @@ +/* Auto-Settings Styling */ +#auto_save_settings ~ button { + margin: 5px; +} +#auto_save_settings:not(:checked) ~ button { + display: none; +} + +.form-table { + margin: auto; +} + +.form-table th { + padding-top: 15px; + padding-bottom: 5px; +} + +.form-table td:first-child > *, +.form-table th:first-child > * { + float: right; + white-space: nowrap; +} + +.form-table td:last-child > *, +.form-table th:last-child > * { + float: left; +} + + +.parameters-table { + display: flex; + flex-direction: column; + gap: 1px; +} + +.parameters-table > div { + background: var(--background-color2); + display: flex; + padding: 0px 4px; +} + +.parameters-table > div > div { + padding: 10px; + display: flex; + align-items: center; + justify-content: center; +} + +.parameters-table small { + color: rgb(153, 153, 153); +} + +.parameters-table > div > div:nth-child(1) { + font-size: 20px; + width: 45px; +} + +.parameters-table > div > div:nth-child(2) { + flex: 1; + flex-direction: column; + text-align: left; + justify-content: center; + align-items: start; + gap: 4px; +} + +.parameters-table > div > div:nth-child(3) { + text-align: right; +} + +.parameters-table > div:first-child { + border-radius: 12px 12px 0px 0px; +} + +.parameters-table > div:last-child { + border-radius: 0px 0px 12px 12px; +} + +.parameters-table .fa-fire { + color: #F7630C; +} \ No newline at end of file diff --git a/ui/media/css/fontawesome-all.min.css b/ui/media/css/fontawesome-all.min.css new file mode 100644 index 0000000000000000000000000000000000000000..d32fa927333e63740ff8cf5cdb3c5ebd0310b4e1 --- /dev/null +++ b/ui/media/css/fontawesome-all.min.css @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-child-rifle:before{content:"\e4e0"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-arrow-turn-right:before,.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"}.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(/media/fonts/fa-brands-400.woff2) format("woff2"),url(/media/fonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(/media/fonts/fa-regular-400.woff2) format("woff2"),url(/media/fonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(/media/fonts/fa-solid-900.woff2) format("woff2"),url(/media/fonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(/media/fonts/fa-brands-400.woff2) format("woff2"),url(/media/fonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(/media/fonts/fa-solid-900.woff2) format("woff2"),url(/media/fonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(/media/fonts/fa-regular-400.woff2) format("woff2"),url(/media/fonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(/media/fonts/fa-solid-900.woff2) format("woff2"),url(/media/fonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(/media/fonts/fa-brands-400.woff2) format("woff2"),url(/media/fonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(/media/fonts/fa-regular-400.woff2) format("woff2"),url(/media/fonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(/media/fonts/fa-v4compatibility.woff2) format("woff2"),url(/media/fonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} \ No newline at end of file diff --git a/ui/media/css/fonts.css b/ui/media/css/fonts.css new file mode 100644 index 0000000000000000000000000000000000000000..8bd4f5d5f27373041f74f98fdcce22219b68c6f1 --- /dev/null +++ b/ui/media/css/fonts.css @@ -0,0 +1,40 @@ +/* work-sans-regular - latin */ +@font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 400; + src: local(''), + url('/media/fonts/work-sans-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('/media/fonts/work-sans-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* work-sans-600 - latin */ + @font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 600; + src: local(''), + url('/media/fonts/work-sans-v18-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('/media/fonts/work-sans-v18-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* work-sans-700 - latin */ + @font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 700; + src: local(''), + url('/media/fonts/work-sans-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('/media/fonts/work-sans-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* work-sans-800 - latin */ + @font-face { + font-family: 'Work Sans'; + font-style: normal; + font-weight: 800; + src: local(''), + url('/media/fonts/work-sans-v18-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('/media/fonts/work-sans-v18-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + \ No newline at end of file diff --git a/ui/media/css/image-editor.css b/ui/media/css/image-editor.css new file mode 100644 index 0000000000000000000000000000000000000000..ea8112e38e87bfb73a0252ed9e8e9f75108c61ca --- /dev/null +++ b/ui/media/css/image-editor.css @@ -0,0 +1,228 @@ +.editor-controls-left { + padding-left: 32px; + text-align: left; + padding-bottom: 20px; + max-width: min-content; +} + +.editor-options-container { + display: flex; + row-gap: 10px; +} + +.editor-options-container > * { + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.editor-options-container > * > * { + position: inherit; + width: 32px; + height: 32px; + border-radius: 16px; + background: var(--background-color3); + cursor: pointer; + transition: opacity 0.25s; +} +.editor-options-container > * > *:hover { + opacity: 0.75; +} + +.editor-options-container > * > *.active { + border: 1px solid #3584e4; +} + +.image_editor_opacity .editor-options-container > * > *:not(.active) { + border: 1px solid var(--background-color3); +} + +.image_editor_color .editor-options-container { + flex-wrap: wrap; +} +.image_editor_color .editor-options-container > * { + flex: 20%; +} +.image_editor_color .editor-options-container > * > * { + position: relative; +} +.image_editor_color .editor-options-container > * > *.active::before { + content: "\f00c"; + display: var(--fa-display,inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(125%); + color: black; +} +.image_editor_color .editor-options-container > *:first-child { + flex: 100%; +} +.image_editor_color .editor-options-container > *:first-child > * { + width: 100%; +} +.image_editor_color .editor-options-container > *:first-child > * > input { + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} +.image_editor_color .editor-options-container > *:first-child > * > span { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + opacity: 0.5; +} +.image_editor_color .editor-options-container > *:first-child > *.active > span { + opacity: 0; +} + +.image_editor_tool .editor-options-container { + flex-wrap: wrap; +} + +.image_editor_tool .editor-options-container > * { + padding: 2px; + flex: 50%; +} + +.editor-controls-center { + /* background: var(--background-color2); */ + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.editor-controls-center > div { + position: relative; + background: black; +} + +.editor-controls-center canvas { + position: absolute; + left: 0; + top: 0; +} + +.editor-controls-right { + padding: 32px; + display: flex; + flex-direction: column; +} + + +.editor-controls-right > div:last-child { + flex: 1; + display: flex; + flex-direction: column; + min-width: 200px; + gap: 5px; + justify-content: end; +} + +.image-editor-button { + width: 100%; + height: 32px; + border-radius: 16px; + background: var(--background-color3); +} + +.editor-controls-right .image-editor-button { + margin-bottom: 4px; +} + +#init_image_button_inpaint .input-toggle { + position: absolute; + left: 16px; +} + +#init_image_button_inpaint .input-toggle input:not(:checked) ~ label { + pointer-events: none; +} + +.image-editor-popup { + --popup-margin: 16px; + --popup-padding: 24px; +} + +@media screen and (min-width: 700px) { + .image-editor-popup { + overflow-y: auto; + } +} + +.image-editor-popup > div { + margin: var(--popup-margin); + padding: var(--popup-padding); + min-height: calc(99h - (2 * var(--popup-margin))); + max-width: none; + min-width: fit-content; +} + +.image-editor-popup h1 { + position: absolute; + top: 32px; + left: 50%; + transform: translateX(-50%); +} + + +@media screen and (max-width: 700px) { + .image-editor-popup > div { + margin: 0px; + padding: 0px; + } + + .image-editor-popup h1 { + position: relative; + transform: none; + left: auto; + } +} + + +.image-editor-popup > div > div { + min-height: calc(99vh - (2 * var(--popup-margin)) - (2 * var(--popup-padding))); +} + +.inpainter .image_editor_color { + display: none; +} + +.inpainter .editor-canvas-background { + opacity: 0.75; +} + +#init_image_preview_container .button { + display: flex; + padding: 6px; + height: 24px; + box-shadow: 2px 2px 1px 1px #00000088; +} + +#init_image_preview_container .button:hover { + background: var(--background-color4) +} + +.image-editor-popup .button { + display: flex; +} +.image-editor-popup h4 { + text-align: left; +} + +.image-editor-popup .load_mask { + display: none; +} +.inpainter .load_mask { + display: flex; +} \ No newline at end of file diff --git a/ui/media/css/image-modal.css b/ui/media/css/image-modal.css new file mode 100644 index 0000000000000000000000000000000000000000..e3d70f72717ea6ed9e551f0d791ffb244c75529c --- /dev/null +++ b/ui/media/css/image-modal.css @@ -0,0 +1,84 @@ +#viewFullSizeImgModal { + --popup-padding: 24px; + position: sticky; + padding: var(--popup-padding); + pointer-events: none; + width: 100vw; + height: 100vh; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + z-index: 1001; +} + +#viewFullSizeImgModal > * { + pointer-events: auto; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +#viewFullSizeImgModal .backdrop { + max-width: unset; + width: 100%; + max-height: unset; + height: 100%; + inset: 0; + position: absolute; + top: 0; + left: 0; + z-index: 1001; + opacity: .5; + border: none; + box-shadow: none; + overflow: hidden; +} + +#viewFullSizeImgModal .content { + min-height: initial; + max-height: calc(100vh - (var(--popup-padding) * 2)); + height: fit-content; + min-width: initial; + max-width: calc(100vw - (var(--popup-padding) * 2)); + width: fit-content; + z-index: 1003; + overflow: visible; +} + +#viewFullSizeImgModal .image-wrapper { + min-height: initial; + max-height: calc(100vh - (var(--popup-padding) * 2)); + height: fit-content; + min-width: initial; + max-width: calc(100vw - (var(--popup-padding) * 2)); + width: fit-content; + box-sizing: border-box; + pointer-events: auto; + margin: 0; + padding: 0; + overflow: auto; +} + +#viewFullSizeImgModal img.natural-zoom { + max-width: calc(100vh - (var(--popup-padding) * 2) - 4px); + max-height: calc(100vh - (var(--popup-padding) * 2) - 4px); +} + +#viewFullSizeImgModal .content > div::-webkit-scrollbar-track, #viewFullSizeImgModal .content > div::-webkit-scrollbar-corner { + background: rgba(0, 0, 0, .5) +} + +#viewFullSizeImgModal .menu-bar { + position: absolute; + top: 0; + right: 0; + padding-right: var(--scrollbar-width); +} + +#viewFullSizeImgModal .menu-bar .tertiaryButton { + font-size: 1.2em; + margin: 12px 12px 0 0; + cursor: pointer; +} diff --git a/ui/media/css/jquery-confirm.min.css b/ui/media/css/jquery-confirm.min.css new file mode 100644 index 0000000000000000000000000000000000000000..400f0b8d2577ccc26b8c6dd3ca4a9c53fee3415a --- /dev/null +++ b/ui/media/css/jquery-confirm.min.css @@ -0,0 +1,9 @@ +/*! + * jquery-confirm v3.3.2 (http://craftpip.github.io/jquery-confirm/) + * Author: boniface pereira + * Website: www.craftpip.com + * Contact: hey@craftpip.com + * + * Copyright 2013-2017 jquery-confirm + * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE) + */@-webkit-keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}body[class*=jconfirm-no-scroll-]{overflow:hidden!important}.jconfirm{position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999999;font-family:inherit;overflow:hidden}.jconfirm .jconfirm-bg{position:fixed;top:0;left:0;right:0;bottom:0;-webkit-transition:opacity .4s;transition:opacity .4s}.jconfirm .jconfirm-bg.jconfirm-bg-h{opacity:0!important}.jconfirm .jconfirm-scrollpane{-webkit-perspective:500px;perspective:500px;-webkit-perspective-origin:center;perspective-origin:center;display:table;width:100%;height:100%}.jconfirm .jconfirm-row{display:table-row;width:100%}.jconfirm .jconfirm-cell{display:table-cell;vertical-align:middle}.jconfirm .jconfirm-holder{max-height:100%;padding:50px 0}.jconfirm .jconfirm-box-container{-webkit-transition:-webkit-transform;transition:-webkit-transform;transition:transform;transition:transform,-webkit-transform}.jconfirm .jconfirm-box-container.jconfirm-no-transition{-webkit-transition:none!important;transition:none!important}.jconfirm .jconfirm-box{background:white;border-radius:4px;position:relative;outline:0;padding:15px 15px 0;overflow:hidden;margin-left:auto;margin-right:auto}@-webkit-keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@-webkit-keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@-webkit-keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@-webkit-keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@-webkit-keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@-webkit-keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}@keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}.jconfirm .jconfirm-box.jconfirm-type-animated{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.jconfirm .jconfirm-box.jconfirm-type-blue{border-top:solid 7px #3498db;-webkit-animation-name:type-blue;animation-name:type-blue}.jconfirm .jconfirm-box.jconfirm-type-green{border-top:solid 7px #2ecc71;-webkit-animation-name:type-green;animation-name:type-green}.jconfirm .jconfirm-box.jconfirm-type-red{border-top:solid 7px #e74c3c;-webkit-animation-name:type-red;animation-name:type-red}.jconfirm .jconfirm-box.jconfirm-type-orange{border-top:solid 7px #f1c40f;-webkit-animation-name:type-orange;animation-name:type-orange}.jconfirm .jconfirm-box.jconfirm-type-purple{border-top:solid 7px #9b59b6;-webkit-animation-name:type-purple;animation-name:type-purple}.jconfirm .jconfirm-box.jconfirm-type-dark{border-top:solid 7px #34495e;-webkit-animation-name:type-dark;animation-name:type-dark}.jconfirm .jconfirm-box.loading{height:120px}.jconfirm .jconfirm-box.loading:before{content:'';position:absolute;left:0;background:white;right:0;top:0;bottom:0;border-radius:10px;z-index:1}.jconfirm .jconfirm-box.loading:after{opacity:.6;content:'';height:30px;width:30px;border:solid 3px transparent;position:absolute;left:50%;margin-left:-15px;border-radius:50%;-webkit-animation:jconfirm-spin 1s infinite linear;animation:jconfirm-spin 1s infinite linear;border-bottom-color:dodgerblue;top:50%;margin-top:-15px;z-index:2}.jconfirm .jconfirm-box div.jconfirm-closeIcon{height:20px;width:20px;position:absolute;top:10px;right:10px;cursor:pointer;opacity:.6;text-align:center;font-size:27px!important;line-height:14px!important;display:none;z-index:1}.jconfirm .jconfirm-box div.jconfirm-closeIcon:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-closeIcon .fa{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .glyphicon{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .zmdi{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon:hover{opacity:1}.jconfirm .jconfirm-box div.jconfirm-title-c{display:block;font-size:22px;line-height:20px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;padding-bottom:15px}.jconfirm .jconfirm-box div.jconfirm-title-c.jconfirm-hand{cursor:move}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{font-size:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c i{vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:inherit;font-family:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-content-pane{margin-bottom:15px;height:auto;-webkit-transition:height .4s ease-in;transition:height .4s ease-in;display:inline-block;width:100%;position:relative;overflow-x:hidden;overflow-y:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane.no-scroll{overflow-y:hidden}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar{width:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-track{background:rgba(0,0,0,0.1)}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-thumb{background:#666;border-radius:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content{overflow:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content img{max-width:100%;height:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content:empty{display:none}.jconfirm .jconfirm-box .jconfirm-buttons{padding-bottom:11px}.jconfirm .jconfirm-box .jconfirm-buttons>button{margin-bottom:4px;margin-left:2px;margin-right:2px}.jconfirm .jconfirm-box .jconfirm-buttons button{display:inline-block;padding:6px 12px;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-radius:4px;min-height:1em;-webkit-transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease;-webkit-tap-highlight-color:transparent;border:0;background-image:none}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue{background-color:#3498db;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue:hover{background-color:#2980b9;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green{background-color:#2ecc71;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green:hover{background-color:#27ae60;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red{background-color:#e74c3c;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red:hover{background-color:#c0392b;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange{background-color:#f1c40f;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange:hover{background-color:#f39c12;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default{background-color:#ecf0f1;color:#000;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default:hover{background-color:#bdc3c7;color:#000}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple{background-color:#9b59b6;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple:hover{background-color:#8e44ad;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark{background-color:#34495e;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark:hover{background-color:#2c3e50;color:#FFF}.jconfirm .jconfirm-box.jconfirm-type-red .jconfirm-title-c .jconfirm-icon-c{color:#e74c3c!important}.jconfirm .jconfirm-box.jconfirm-type-blue .jconfirm-title-c .jconfirm-icon-c{color:#3498db!important}.jconfirm .jconfirm-box.jconfirm-type-green .jconfirm-title-c .jconfirm-icon-c{color:#2ecc71!important}.jconfirm .jconfirm-box.jconfirm-type-purple .jconfirm-title-c .jconfirm-icon-c{color:#9b59b6!important}.jconfirm .jconfirm-box.jconfirm-type-orange .jconfirm-title-c .jconfirm-icon-c{color:#f1c40f!important}.jconfirm .jconfirm-box.jconfirm-type-dark .jconfirm-title-c .jconfirm-icon-c{color:#34495e!important}.jconfirm .jconfirm-clear{clear:both}.jconfirm.jconfirm-rtl{direction:rtl}.jconfirm.jconfirm-rtl div.jconfirm-closeIcon{left:5px;right:auto}.jconfirm.jconfirm-white .jconfirm-bg,.jconfirm.jconfirm-light .jconfirm-bg{background-color:#444;opacity:.2}.jconfirm.jconfirm-white .jconfirm-box,.jconfirm.jconfirm-light .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);border-radius:5px}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#333}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#ddd}.jconfirm.jconfirm-white.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-black .jconfirm-bg,.jconfirm.jconfirm-dark .jconfirm-bg{background-color:darkslategray;opacity:.4}.jconfirm.jconfirm-black .jconfirm-box,.jconfirm.jconfirm-dark .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);background:#444;border-radius:5px;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button{border:0;background-image:none;text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none;-webkit-transition:background .1s;transition:background .1s;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#fff;background:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#666}.jconfirm.jconfirm-black.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-shake{-webkit-animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-glow{-webkit-animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@-webkit-keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}@keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}.jconfirm{-webkit-perspective:400px;perspective:400px}.jconfirm .jconfirm-box{opacity:1;-webkit-transition-property:all;transition-property:all}.jconfirm .jconfirm-box.jconfirm-animation-top,.jconfirm .jconfirm-box.jconfirm-animation-left,.jconfirm .jconfirm-box.jconfirm-animation-right,.jconfirm .jconfirm-box.jconfirm-animation-bottom,.jconfirm .jconfirm-box.jconfirm-animation-opacity,.jconfirm .jconfirm-box.jconfirm-animation-zoom,.jconfirm .jconfirm-box.jconfirm-animation-scale,.jconfirm .jconfirm-box.jconfirm-animation-none,.jconfirm .jconfirm-box.jconfirm-animation-rotate,.jconfirm .jconfirm-box.jconfirm-animation-rotatex,.jconfirm .jconfirm-box.jconfirm-animation-rotatey,.jconfirm .jconfirm-box.jconfirm-animation-scaley,.jconfirm .jconfirm-box.jconfirm-animation-scalex{opacity:0}.jconfirm .jconfirm-box.jconfirm-animation-rotate{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.jconfirm .jconfirm-box.jconfirm-animation-rotatex{-webkit-transform:rotateX(90deg);transform:rotateX(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatexr{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatey{-webkit-transform:rotatey(90deg);transform:rotatey(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotateyr{-webkit-transform:rotatey(-90deg);transform:rotatey(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scaley{-webkit-transform:scaley(1.5);transform:scaley(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scalex{-webkit-transform:scalex(1.5);transform:scalex(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-top{-webkit-transform:translate(0px,-100px);transform:translate(0px,-100px)}.jconfirm .jconfirm-box.jconfirm-animation-left{-webkit-transform:translate(-100px,0px);transform:translate(-100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-right{-webkit-transform:translate(100px,0px);transform:translate(100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-bottom{-webkit-transform:translate(0px,100px);transform:translate(0px,100px)}.jconfirm .jconfirm-box.jconfirm-animation-zoom{-webkit-transform:scale(1.2);transform:scale(1.2)}.jconfirm .jconfirm-box.jconfirm-animation-scale{-webkit-transform:scale(0.5);transform:scale(0.5)}.jconfirm .jconfirm-box.jconfirm-animation-none{visibility:hidden}.jconfirm.jconfirm-supervan .jconfirm-bg{background-color:rgba(54,70,93,0.95)}.jconfirm.jconfirm-supervan .jconfirm-box{background-color:transparent}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-blue{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-green{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-red{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-orange{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-purple{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-dark{border:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-closeIcon{color:white}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c{text-align:center;color:white;font-size:28px;font-weight:normal}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c>*{padding-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content-pane{margin-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content{text-align:center;color:white}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons button{font-size:16px;border-radius:2px;background:#303f53;text-shadow:none;border:0;color:white;padding:10px;min-width:100px}.jconfirm.jconfirm-supervan.jconfirm-rtl .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-material .jconfirm-bg{background-color:rgba(0,0,0,0.67)}.jconfirm.jconfirm-material .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 25px 10px 25px}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons{text-align:right}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-weight:500}.jconfirm.jconfirm-material.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-bootstrap .jconfirm-bg{background-color:rgba(0,0,0,0.21)}.jconfirm.jconfirm-bootstrap .jconfirm-box{background-color:white;-webkit-box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);border:solid 1px rgba(0,0,0,0.4);padding:15px 0 0}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold;padding-left:15px;padding-right:15px}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87);padding:0 15px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons{text-align:right;padding:10px;margin:-5px 0 0;border-top:solid 1px #ddd;overflow:hidden;border-radius:0 0 4px 4px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons button{font-weight:500}.jconfirm.jconfirm-bootstrap.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-modern .jconfirm-bg{background-color:slategray;opacity:.6}.jconfirm.jconfirm-modern .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 30px 15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87);top:15px;right:15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:24px;font-weight:bold;text-align:center;margin-bottom:10px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s;-webkit-transform:scale(0);transform:scale(0);display:block;margin-right:0;margin-left:0;margin-bottom:10px;font-size:69px;color:#aaa}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-content{text-align:center;font-size:15px;color:#777;margin-bottom:25px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button{font-weight:bold;text-transform:uppercase;-webkit-transition:background .1s;transition:background .1s;padding:10px 20px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button+button{margin-left:4px}.jconfirm.jconfirm-modern.jconfirm-open .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{-webkit-transform:scale(1);transform:scale(1)} \ No newline at end of file diff --git a/ui/media/css/main.css b/ui/media/css/main.css new file mode 100644 index 0000000000000000000000000000000000000000..c5e7c251745c34ed3596dc4dd835fa5dedf3651c --- /dev/null +++ b/ui/media/css/main.css @@ -0,0 +1,1298 @@ +* { + font-family: Work Sans, Verdana, Geneva, sans-serif; + box-sizing: border-box; +} + +html { + position: relative; +} + +body { + margin: 0; + font-size: 11pt; + background-color: var(--background-color1); + color: var(--text-color); +} +a { + color: rgb(0, 102, 204); +} +a:visited { + color: rgb(0, 102, 204); +} +label { + font-size: 10pt; +} +code { + background: var(--background-color4); + padding: 2px 4px; + border-radius: 4px; +} +#logo_img { + width: 32px; + height: 32px; + transform: translateY(4px); +} +#prompt { + width: 100%; + height: 65pt; + font-size: 14px; + margin-bottom: 6px; + margin-top: 5px; + display: block; + border: 2px solid var(--background-color2); +} +#negative_prompt { + width: 100%; + height: 50pt; + font-size: 13px; + margin-bottom: 5px; + margin-top: 5px; + display: block; +} +.image_clear_btn { + position: absolute; + transform: translate(30%, -30%); + background: black; + color: white; + border: 2pt solid #ccc; + padding: 0; + cursor: pointer; + outline: inherit; + border-radius: 8pt; + width: 16pt; + height: 16pt; + font-family: Verdana; + font-size: 8pt; + top: 0px; + right: 0px; +} +.image_clear_btn:active { + position: absolute; + top: 0px; + left: auto; +} +.settings-box ul { + font-size: 9pt; + margin-bottom: 5px; + padding-left: 10px; + list-style-type: none; +} +.settings-box li { + padding-bottom: 4pt; +} +.editor-slider { + vertical-align: middle; +} +.outputMsg { + font-size: small; + padding-bottom: 3pt; +} +#footer { + font-size: small; + padding: 10pt; + background: none; +} +#footer-legal { + font-size: 8pt; +} +#footer-spacer { + flex: 0.7 +} +.imgInfoLabel { + font-size: 0.8em; + background-color: var(--background-color2); +} +.imgSeedLabel { + padding: 5px; + border-radius: 0px 3px 3px 0px; +} +.imgExpandBtn { + border-radius: 3px 0px 0px 3px; + border-right: 1px solid var(--tertiary-border-color); + padding: 5px 5px 5px; + padding-left: 7px; + cursor: pointer; +} +.imgExpandBtn:hover { + background-color: var(--accent-color); +} +.imgItem { + display: inline-block; + margin-top: 1em; + margin-right: 1em; +} +.imgContainer { + display: flex; + justify-content: flex-end; + position: relative; +} +.imgItemInfo { + padding-bottom: 0.5em; + display: flex; + align-items: flex-end; + flex-direction: column; + position: absolute; + padding-right: 5pt; + padding-top: 6pt; + opacity: 0; + transition: 0.1s all; +} +.imgPreviewItemClearBtn { + opacity: 0; +} +.imgContainer .img_bottom_label { + opacity: 0; +} +.imgPreviewItemClearBtn:hover { + background: rgb(177, 27, 0); +} +.imgContainer:hover > .imgItemInfo { + opacity: 1; +} +.imgContainer:hover > .imgPreviewItemClearBtn { + opacity: 1; +} +.imgContainer:hover > .img_bottom_label { + opacity: 60%; +} +.imgItemInfo > * { + margin-bottom: 7px; +} +.imgItemInfo .tasksBtns { + margin-left: 5pt; +} +.imgItem .image_clear_btn { + transform: translate(40%, -50%); +} +#container { + min-height: 100vh; + width: 100%; + margin: 0px; + display: flex; + flex-direction: column; +} +#logo small { + font-size: 11pt; +} +#editor { + background: var(--background-color1); + padding: 16px; + display: flex; + flex-direction: column; + flex: 0 0 380pt; +} +#editor label { + font-weight: normal; +} +#editor h4 { + margin: 0px; + white-space: nowrap; +} +#editor .collapsible-content { + width: 100%; +} +.settings-box label small { + color: rgb(153, 153, 153); + margin-right: 10px; +} +#preview { + padding: 8px; + background: var(--background-color1); +} +#preview .collapsible-content { + padding: 0px 15px; +} +#editor-inputs-prompt { + flex: 1; +} +#editor-inputs .row { + padding-bottom: 10px; +} +#makeImage { + border-radius: 6px; +} +#editor-modifiers h5 { + padding: 5pt 0; + margin: 0; +} +#makeImage { + flex: 0 0 70px; + background: var(--accent-color); + border: var(--primary-button-border); + color: var(--accent-text-color); + width: 100%; + height: 30pt; +} +#makeImage:hover { + background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%)); +} +#stopImage { + flex: 0 0 70px; + background: rgb(132, 8, 0); + border: 2px solid rgb(122, 29, 0); + color: rgb(255, 221, 255); + height: 30pt; + border-radius: 6px; + flex-grow: 2; +} +#stopImage:hover { + background: rgb(177, 27, 0); +} + +div#render-buttons { + gap: 3px; + margin-top: 4px; + display: none; +} +button#pause { + flex-grow: 1; + background: var(--accent-color); +} +button#resume { + flex-grow: 1; + background: var(--accent-color); + display: none; +} + +.flex-container { + display: flex; + width: 100%; +} +.col-free { + flex: 1; +} +.collapsible { + cursor: pointer; +} +.collapsible-content { + display: block; + padding-left: 10px; +} +.collapsible-content h5 { + padding: 5pt 0pt; + margin: 0; + font-size: 10pt; +} +.collapsible-handle { + color: white; + padding-right: 5px; +} +.collapsible:not(.active) ~ .collapsible-content { + display: none !important; +} +#editor-modifiers { + overflow-y: auto; + overflow-x: hidden; +} +#editor-modifiers .editor-modifiers-leaf { + padding-top: 10pt; + padding-bottom: 10pt; +} +img { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15); +} +div.img-preview img { + width:100%; + height: 100%; + max-height: 70vh; + cursor: pointer; +} +.line-separator { + background: var(--background-color3); + height: 1pt; + margin: 16px 0px; +} +#editor-inputs-tags-container { + margin-top: 5pt; + display: none; +} +#server-status { + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + text-align: right; +} +#server-status-color { + font-size: 14pt; + color: rgb(200, 139, 0); + display: inline; +} +#server-status-msg { + color: rgb(200, 139, 0); + padding-left: 2pt; + font-size: 10pt; +} +.preview-prompt { + font-size: 13pt; + display: inline; +} +#coffeeButton { + height: 23px; + transform: translateY(25%); +} + +#top-nav { + position: relative; + background: var(--background-color4); + display: flex; +} +.tab .icon { + padding-right: 4pt; + font-size: 14pt; + transform: translateY(1pt); +} +#logo { + display: inline; + padding: 12px; + padding-top: 8px; + white-space: nowrap; +} +#logo h1 { + display: inline; +} +#top-nav-items { + list-style-type: none; + display: inline; + float: right; +} +#top-nav-items > li { + float: left; + display: inline; + padding-left: 20pt; +} +#top-nav-items > li:first-child { + cursor: default; +} +#initial-text { + padding-top: 15pt; + padding-left: 4pt; +} +.settings-subheader { + font-size: 10pt; + font-weight: bold; +} +.pl-5 { + padding-left: 5pt; +} +#community-links { + display: inline-block; + list-style-type: none; + text-align: left; + margin: auto; + padding: 0px; +} +#community-links li { + padding-bottom: 12pt; + display: block; + font-size: 10pt; +} +#community-links li .fa-fw { + padding-right: 2pt; +} +#community-links li a { + color: var(--text-color); + text-decoration: none; +} +.float-child h1 { + border-bottom: var(--button-border); +} +#help-links { + display: inline-block; + list-style-type: none; + text-align: left; + margin: auto; + padding: 0px; +} +#help-links li { + padding-bottom: 12pt; + display: block; + font-size: 10pt; +} +#help-links li .fa-fw { + padding-right: 2pt; +} +#help-links li a { + color: var(--text-color); + text-decoration: none; +} +#help-links li ul { + padding-inline-start: 10px; + margin-top: 8px; +} +.help-section { + font-size: 130%; +} +.dropdown { + overflow: hidden; +} +.dropdown-content { + display: none; + position: absolute; + z-index: 2; + border-radius: 7px; + padding: 0px; + margin-bottom: 15px; + box-shadow: 0 20px 28px 0 rgba(0, 0, 0, 0.75), 0 6px 20px 0 rgba(0, 0, 0, 0.75); +} +.dropdown:hover .dropdown-content { + display: block; +} +.dropdown:hover + .dropdown-content { + display: block; +} +.dropdown-content:hover { + display: block; +} + +.display-settings { + float: right; + position: relative; +} + +.display-settings .dropdown-content { + right: 0px; + top: 12pt; +} + +.dropdown-item { + padding: 4px; + background: var(--background-color1); + border: 2px solid var(--background-color4); +} + +.dropdown-item:first-child { + border-radius: 7px 7px 0px 0px; +} + +.dropdown-item:last-child { + border-radius: 0px 0px 7px 7px; +} + +.imageTaskContainer { + border: 1px solid var(--background-color2); + margin-bottom: 10pt; + padding: 5pt; + border-radius: 5pt; + box-shadow: 0 20px 28px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15); +} +.imageTaskContainer > div > .collapsible-handle { + display: none; +} +.dropTargetBefore::before{ + content: ""; + border: 1px solid #fff; + margin-bottom: -2px; + display: block; + box-shadow: 0 0 5px #fff; + transform: translate(0px, -14px); +} +.dropTargetAfter::after{ + content: ""; + border: 1px solid #fff; + margin-bottom: -2px; + display: block; + box-shadow: 0 0 5px #fff; + transform: translate(0px, 14px); +} +.drag-handle { + margin-right: 6px; + cursor: move; +} +.taskStatusLabel { + font-size: 8pt; + background:var(--background-color2); + border: 1px solid rgb(61, 62, 66); + padding: 2pt 4pt; + border-radius: 2pt; + margin-right: 5pt; + display: inline; +} +.activeTaskLabel { + background:rgb(0, 90, 30); + border: 1px solid rgb(0, 75, 19); + color:rgb(222, 253, 230) +} +.waitingTaskLabel { + background:rgb(128, 89, 0); + border: 1px solid rgb(107, 75, 0); + color:rgb(255, 242, 211) +} +.primaryButton { + flex: 0 0 70px; + background: var(--accent-color); + border: var(--primary-button-border); + color: rgb(255, 221, 255); + padding: 3pt 6pt; +} +.secondaryButton { + background: rgb(132, 8, 0); + border: 1px solid rgb(122, 29, 0); + color: rgb(255, 221, 255); + padding: 3pt 6pt; + border-radius: 5px; +} +.secondaryButton:hover { + background: rgb(177, 27, 0); +} +.tertiaryButton { + background: var(--tertiary-background-color); + color: var(--tertiary-color); + border: 1px solid var(--tertiary-border-color); + padding: 3pt 6pt; + border-radius: 5px; +} +.tertiaryButton:hover { + background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%)); + color: var(--accent-text-color); +} +.tertiaryButton.pressed { + border-style: inset; + background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%)); + color: var(--accent-text-color); +} +.useSettings { + margin-right: 6pt; + float: right; +} +.stopTask { + float: right; +} +#preview-tools { + display: none; + padding: 4pt; +} +#preview-tools .display-settings .dropdown-content { + right: -6px; + top: 20px; + box-shadow: none; + width: max-content; +} +.taskConfig { + font-size: 10pt; + color: #aaa; + margin-bottom: 5pt; + margin-top: 5pt; +} +.img-batch { + display: inline; +} +#prompt_from_file { + display: none; +} + +#init_image_preview_container { + display: flex; + margin-top: 6px; + margin-bottom: 8px; +} + +#init_image_preview_container:not(.has-image) #init_image_wrapper, +#init_image_preview_container:not(.has-image) #inpaint_button_container { + display: none; +} + + +#init_image_buttons { + display: flex; + gap: 8px; +} + +#init_image_preview_container.has-image #init_image_buttons { + flex-direction: column; + padding-left: 8px; +} + +#init_image_buttons .button { + position: relative; + height: 32px; + width: 150px; +} + +#init_image_buttons .button > input { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + opacity: 0; +} + +#inpaint_button_container { + display: flex; + align-items: center; + gap: 8px; +} + +#init_image_wrapper { + grid-row: span 3; + position: relative; + width: fit-content; + max-height: 150px; +} + +#init_image_preview { + max-height: 150px; + height: 100%; + width: 100%; + object-fit: contain; + border-radius: 6px; + transition: all 1s ease-in-out; +} +/* +#init_image_preview:hover { + max-width: 500px; + max-height: 1000px; + + transition: all 1s 0.5s ease-in-out; +} */ + +#init_image_size_box { + border-radius: 6px 0px; +} +.img_bottom_label { + position: absolute; + right: 0px; + bottom: 0px; + padding: 3px; + background: black; + color: white; + text-shadow: 0px 0px 4px black; + opacity: 60%; + font-size: 12px; +} + +#editor-settings { + min-width: 350px; +} + +#editor-settings-entries { + display: flex; + flex-direction: column; +} + +#editor-settings-entries > div { + margin-top: 15px; +} + +#editor-settings-entries ul { + padding: 0px; +} + +#editor-settings-entries table td { + padding: 0px; + line-height: 28px; +} + +#editor-settings-entries table td:first-child { + float: right; + padding-right: 4px; + white-space: nowrap; +} + +#negative_prompt { + width: 100%; +} + +/* INPUTS STYLING */ +button, +input[type="file"], +input[type="checkbox"], +select, +option { + cursor: pointer; +} + +input[type="file"] * { + cursor: pointer; +} + +input, +select, +textarea { + border-radius: var(--input-border-radius); + padding: 4px; + accent-color: var(--accent-color); + background: var(--input-background-color); + border: var(--input-border-size) solid var(--input-border-color); + color: var(--input-text-color); + font-size: 9pt; +} + +input:hover { + accent-color: var(--accent-color-hover); +} + +input { + padding: 4px 6px; +} + +input:focus, +select:focus, +textarea:focus { + outline: 2px solid var(--accent-color); +} + +input[disabled], +select[disabled], +textarea[disabled] { + opacity: 0.5; +} + +input[type="file"] { + width: 100%; + padding: 2px; +} + +button, +input::file-selector-button, +.button { + padding: 2px 4px; + border-radius: var(--input-border-radius); + background: var(--button-color); + color: var(--button-text-color); + border: var(--button-border); + align-items: center; + justify-content: center; + cursor: pointer; +} + +.button i { + margin-right: 8px; +} + +button:hover, +.button:hover { + transition-duration: 0.1s; + background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%)); +} + +input::file-selector-button { + padding: 0px 4px; + height: 19px; +} + + +.input-toggle { + display: inline-block; + position: relative; + vertical-align: middle; + width: calc(var(--input-height) * 2); + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + margin-right: 4px; +} +.input-toggle > input { + position: absolute; + opacity: 0; + pointer-events: none; +} +.input-toggle > label { + display: block; + overflow: hidden; + cursor: pointer; + height: var(--input-height); + padding: 0; + line-height: var(--input-height); + border: var(--input-border-size) solid var(--input-border-color); + border-radius: var(--input-height); + background: var(--input-background-color); + transition: background 0.2s ease-in; +} +.input-toggle > label:before { + content: ""; + display: block; + width: calc(var(--input-height) - ((var(--input-border-size) + var(--input-switch-padding)) * 2)); + margin: 0px; + background: var(--input-text-color); + position: absolute; + top: calc(var(--input-border-size) + var(--input-switch-padding)); + bottom: calc(var(--input-border-size) + var(--input-switch-padding)); + right: calc(var(--input-border-size) + var(--input-switch-padding) + var(--input-height)); + border-radius: calc(var(--input-height) - ((var(--input-border-size) + var(--input-switch-padding)) * 2)); + transition: all 0.2s ease-in 0s; + opacity: 0.8; +} +.input-toggle > input:checked + label { + background: var(--accent-color); +} +.input-toggle > input:checked + label:before { + right: calc(var(--input-border-size) + var(--input-switch-padding)); + opacity: 1; +} +.model-filter { + width: 90%; + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Small screens */ +@media screen and (max-width: 1365px) { + #top-nav { + flex-direction: column; + } +} + +/* MOBILE SUPPORT */ +@media screen and (max-width: 700px) { + body { + margin: 0px; + } + #container { + margin: 0px; + } + .flex-container { + flex-direction: column; + } + #preview { + margin: 0px; + padding: 0px; + } + #preview .collapsible-content { + padding: 0px; + } + #preview .collapsible-content { + padding: 0px; + } + .imgItem { + margin-right: 0px; + } + .imgItem img { + height: 100%; + width: 100%; + object-fit: contain; + } + #editor { + padding: 16px 8px; + } + #editor-settings { + min-width: 0px; + } + .tab-content-inner { + margin: 0px; + } + .tab { + font-size: 0; + } + .tab .icon { + padding-right: 0px; + } + #server-status { + top: 75%; + } + .popup > div { + padding-left: 5px !important; + padding-right: 5px !important; + } + .popup > div input, .popup > div select { + max-width: 40vw; + } + .popup .close-button { + padding: 0px !important; + margin: 24px !important; + } + .simple-tooltip { + display: none; + } + #preview-tools button { + font-size: 0px; + } + #preview-tools button .icon { + font-size: 12pt; + } +} + +@media screen and (max-width: 500px) { + #server-status #server-status-msg { + display: none; + } + #server-status:hover #server-status-msg { + display: inline; + } +} + +@media (min-width: 700px) { + /* #editor { + max-width: 480px; + }*/ + .float-container { + padding: 20px; + } + .float-child { + width: 50%; + float: left; + padding: 20px; + } +} + +.help-btn { + position: relative; +} + +#promptsFromFileBtn { + font-size: 9pt; + display: inline; + padding: 2pt; +} + +.section-button { + position: relative; + transform: translateY(-13%); +} +.collapsible:not(.active) #copy-image-settings { + display: none; +} + +.section-button { + cursor: pointer; + float: right; + padding: 8px; + opacity: 1; + transition: opacity 0.5; +} + +.section-button { + cursor: pointer; + float: right; + padding: 8px; + opacity: 1; + transition: opacity 0.5; +} + +.collapsible:not(.active) .section-button { + display: none; +} + +/* SIMPLE TOOTIP */ +.simple-tooltip { + border-radius: 3px; + font-weight: bold; + font-size: 12px; + background-color: var(--background-color3); + + visibility: hidden; + opacity: 0; + position: absolute; + width: max-content; + max-width: 300px; + padding: 8px 12px; + transition: 0.3s all; + z-index: 1000; + + pointer-events: none; +} + +@media (hover: hover) { + :hover > .simple-tooltip { + opacity: 1; + visibility: visible; + } +} +.simple-tooltip.right { + right: 0px; + top: 50%; + transform: translate(100%, -50%); +} +:hover > .simple-tooltip.right { + transform: translate(100%, -50%); +} + +.simple-tooltip.top { + top: 0px; + left: 50%; + transform: translate(-50%, calc(-100% + 15%)); +} +:hover > .simple-tooltip.top { + transform: translate(-50%, -100%); +} + +.simple-tooltip.left { + left: 0px; + top: 50%; + transform: translate(calc(-100% + 15%), -50%); +} +:hover > .simple-tooltip.left { + transform: translate(-100%, -50%); +} + +.simple-tooltip.bottom { + bottom: 0px; + left: 50%; + transform: translate(-50%, calc(100% - 15%)); +} +:hover > .simple-tooltip.bottom { + transform: translate(-50%, 100%); +} + +.simple-tooltip.top-left { + top: 0px; + left: 0px; + transform: translate(calc(-100% + 15%), calc(-100% + 15%)); +} +:hover > .simple-tooltip.top-left { + transform: translate(-80%, -100%); +} + +/* PROGRESS BAR */ +.progress-bar { + background: var(--background-color3); + border-radius: 4px; + border: 2px solid var(--background-color3); + height: 16px; + position: relative; + transition: 0.25s 1s border, 0.25s 1s height; + clear: both; +} +.progress-bar > div { + background: var(--accent-color); + border-radius: 4px; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + transition: width 1s ease-in-out; +} +.progress-bar.active { + background: repeating-linear-gradient(-65deg, + var(--background-color2), + var(--background-color2) 4px, + var(--background-color3) 5px, + var(--background-color3) 9px, + var(--background-color2) 10px); + background-size: 200% auto; + background-position: 0 100%; + animation: progress-anim 2s infinite; + animation-fill-mode: forwards; + animation-timing-function: linear; +} + +@keyframes progress-anim { + 0% { background-position: -55px 0; } + 100% { background-position: 0 0; } +} + +/* POPUPS */ +.popup:not(.active) { + visibility: hidden; + opacity: 0; +} + +.popup { + position: absolute; + background: rgba(32, 33, 36, 50%); + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + z-index: 1000; + opacity: 1; + transition: 0s visibility, 0.3s opacity; +} + +@media only screen and (min-height: 1050px) { + .popup { + position: fixed; + } +} + +.popup > div { + position: relative; + background: var(--background-color2); + border: solid 1px var(--background-color3); + max-width: 700px; + margin: auto; + margin-top: 50px; + border-radius: 6px; + padding: 30px; + text-align: center; + box-shadow: 0px 0px 30px black; +} + +.popup .close-button { + position: absolute; + right: 0px; + top: 0px; + transform: scale(150%); + cursor: pointer; + padding: 24px; +} + +/* TABS */ +.tab-container { + display: flex; + align-items: flex-end; +} + +.tab { + padding: 8px 16px; + border-radius: 4px 4px 0px 0px; + margin-left: 8px; + cursor: pointer; + background: var(--background-color1); + opacity: 50%; + transition: opacity 0.25s; +} + +.tab:hover { + opacity: 75%; +} + +.tab.active { + opacity: 100%; +} + +.tab-content:not(.active) { + display: none; +} + +#tab-content-wrapper > * { + padding-top: 8px; +} + +.tab-content-inner { + margin: auto; + max-width: 600px; + text-align: center; + padding: 20px 10px; +} + +.panel-box { + background: var(--background-color2); + border: 1px solid var(--background-color3); + border-radius: 7px; + padding: 7px; + margin-bottom: 15px; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15); +} + +i.active { + background: var(--accent-color); +} +.primaryButton.active { + background: hsl(var(--accent-hue), 100%, 50%); +} +#system-info { + max-width: 800px; + font-size: 10pt; +} +#system-info .value { + text-align: left; + padding-left: 10pt; +} +#system-info label { + float: right; + font-weight: bold; +} + +button:active { + transition-duration: 0.1s; + background-color: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 24%)); + position: relative; + top: 1px; + left: 1px; +} + +div.task-initimg > img { + margin-right: 6px; + display: block; +} +div.task-fs-initimage { + display: none; + position: absolute; +} +div.task-initimg:hover div.task-fs-initimage { + display: block; + position: absolute; + z-index: 9999; + box-shadow: 0 0 30px #000; + margin-top:-64px; + max-width: 75vw; + max-height: 75vh; +} +div.top-right { + position: absolute; + top: 8px; + right: 8px; +} + +#small_image_warning { + font-size: smaller; + color: var(--status-orange); +} + +button#save-system-settings-btn { + padding: 4pt 8pt; +} +#ip-info a { + color:var(--text-color) +} +#ip-info div { + line-height: 200%; +} + +#download-images-popup .parameters-table > div { + background: var(--background-color1); +} + +/* SCROLLBARS */ +:root { + --scrollbar-width: 14px; + --scrollbar-radius: 10px; +} + +.scrollbar-editor::-webkit-scrollbar { + width: 8px; +} + +.scrollbar-editor::-webkit-scrollbar-track { + border-radius: 10px; +} + +.scrollbar-editor::-webkit-scrollbar-thumb { + background: --background-color2; + border-radius: 10px; +} + +::-webkit-scrollbar { + width: var(--scrollbar-width); +} + +::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px var(--input-border-color); + border-radius: var(--input-border-radius); +} + +::-webkit-scrollbar-thumb { + background: var(--background-color2); + border-radius: var(--scrollbar-radius); +} + +body.pause { + border: solid 12px var(--accent-color); +} + +body.wait-pause { + animation: blinker 2s linear infinite; +} + +@keyframes blinker { + 0% { border: solid 12px var(--accent-color); } + 50% { border: solid 12px var(--background-color1); } + 100% { border: solid 12px var(--accent-color); } +} + +.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c { + color: var(--button-text-color); +} +.jconfirm.jconfirm-modern .jconfirm-box { + background-color: var(--background-color1); +} + +.displayNone { + display:none !important; +} diff --git a/ui/media/css/modifier-thumbnails.css b/ui/media/css/modifier-thumbnails.css new file mode 100644 index 0000000000000000000000000000000000000000..9b462e57a7a0ab25714f24141955f7df5e040927 --- /dev/null +++ b/ui/media/css/modifier-thumbnails.css @@ -0,0 +1,223 @@ +.modifier-card { + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.1s; + border-radius: 7px; + margin: 3pt 3pt; + float: left; + width: 8em; + height: 11.5em; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 8em 3.5em; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: + "modifier-card-image-container" + "modifier-card-container"; + border: 2px solid rgba(255, 255, 255, .05); + cursor: pointer; +} +.modifier-card-size_5 { + width: 18em; + grid-template-rows: 18em 3.5em; + height: 21.5em; +} +.modifier-card-size_5 .modifier-card-image-overlay { + font-size: 8em; +} +.modifier-card-size_4 { + width: 14em; + grid-template-rows: 14em 3.5em; + height: 17.5em; +} +.modifier-card-size_4 .modifier-card-image-overlay { + font-size: 7em; +} +.modifier-card-size_3 { + width: 11em; + grid-template-rows: 11em 3.5em; + height: 14.5em; +} +.modifier-card-size_3 .modifier-card-image-overlay { + font-size: 6em; +} +.modifier-card-size_2 { + width: 10em; + grid-template-rows: 10em 3.5em; + height: 13.5em; +} +.modifier-card-size_2 .modifier-card-image-overlay { + font-size: 6em; +} +.modifier-card-size_1 { + width: 9em; + grid-template-rows: 9em 3.5em; + height: 12.5em; +} +.modifier-card-size_1 .modifier-card-image-overlay { + font-size: 5em; +} +.modifier-card-size_-1 { + width: 7em; + grid-template-rows: 7em 3.5em; + height: 10.5em; +} +.modifier-card-size_-1 .modifier-card-image-overlay { + font-size: 4em; +} +.modifier-card-size_-2 { + width: 6em; + grid-template-rows: 6em 3.5em; + height: 9.5em; +} +.modifier-card-size_-2 .modifier-card-image-overlay { + font-size: 3em; +} +.modifier-card-size_-3 { + width: 5em; + grid-template-rows: 5em 3.5em; + height: 8.5em; +} +.modifier-card-size_-3 .modifier-card-image-overlay { + font-size: 3em; +} +.modifier-card-size_-3 .modifier-card-label { + font-size: 0.8em; +} +.modifier-card-tiny { + width: 6em; + height: 9.5em; + grid-template-rows: 6em 3.5em; +} +.modifier-card-tiny .modifier-card-image-overlay { + font-size: 4em; +} +.modifier-card:hover { + transform: scale(1.05); + box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.25); +} +.modifier-card-image-container { + border-radius: 5px 5px 0 0; + width: inherit; + height: 100%; + background-color: rgba(0, 0, 0, .2); + grid-area: modifier-card-image-container; + position: relative; + display: flex; + align-items: center; + justify-content: center; + color: rgb(255 255 255 / 8%); +} +.modifier-card-image-container img { + width: inherit; + height: 100%; + border-radius: 5px 5px 0 0; +} +.modifier-card-image-container * { + position: absolute; +} +.modifier-card-container { + text-align: center; + background-color: rgba(0,0,0,0.5); + border-radius: 0 0 5px 5px; + display: flex; + justify-content: center; + align-items: center; + grid-area: modifier-card-container; + font-weight: 100; + font-size: .9em; + width: inherit; +} +.modifier-card-label { + padding: 4px; + word-break: break-word; +} +.modifier-card-image-overlay { + width: inherit; + height: inherit; + background-color: rgb(0 0 0 / 50%); + z-index: 2; + position: absolute; + border-radius: 5px 5px 0 0; + opacity: 0; + font-size: 5em; + font-weight: 900; + color: rgb(255 255 255 / 50%); + display: flex; + align-items: center; + justify-content: center; +} +.modifier-card-overlay { + width: inherit; + height: inherit; + position: absolute; + z-index: 3; +} +.modifier-card:hover > .modifier-card-image-container .modifier-card-image-overlay { + opacity: 1; +} +.modifier-card:hover > .modifier-card-image-container img { + filter: blur(.1em); +} +.modifier-card:active { + transform: scale(0.95); + box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.5); +} +#preview-image { + margin-top: 0.5em; + margin-bottom: 0.5em; +} +.modifier-card-active { + border: 2px solid rgb(179 82 255 / 94%); + box-shadow: 0 0px 10px 0 rgb(170 0 229 / 58%); +} +.tooltip { + position: relative; + display: inline-block; +} +.tooltip .tooltip-text { + visibility: hidden; + width: 120px; + background: rgb(101,97,181); + background: linear-gradient(180deg, rgba(101,97,181,1) 0%, rgba(47,45,85,1) 100%); + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + top: 105%; + left: 39%; + margin-left: -60px; + opacity: 0; + transition: opacity 0.3s; + border: 2px solid rgb(90 100 177 / 94%); + box-shadow: 0px 10px 20px 5px rgb(11 0 58 / 55%); + width: 10em; +} +.tooltip .tooltip-text::after { + content: ""; + position: absolute; + top: -0.9em; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent rgb(90 100 177 / 94%) transparent; +} +.tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} +#modifier-card-size-slider { + width: 6em; + margin-bottom: 0.5em; + vertical-align: middle; +} +#modifier-settings-btn { + float: right; +} +#modifier-settings-config textarea { + width: 90%; + height: 150px; +} \ No newline at end of file diff --git a/ui/media/css/searchable-models.css b/ui/media/css/searchable-models.css new file mode 100644 index 0000000000000000000000000000000000000000..06d24acbec5c84029c632893195deb91ce826286 --- /dev/null +++ b/ui/media/css/searchable-models.css @@ -0,0 +1,99 @@ +.model-list { + position: absolute; + margin-block-start: 2px; + display: none; + padding-inline-start: 0; + max-height: 200px; + overflow: auto; + background: var(--input-background-color); + border: var(--input-border-size) solid var(--input-border-color); + border-radius: var(--input-border-radius); + color: var(--input-text-color); + z-index: 1; + line-height: normal; +} + +.model-list ul { + padding-right: 20px; + padding-inline-start: 0; + margin-top: 3pt; +} + +.model-list li { + padding-top: 3px; + padding-bottom: 3px; +} + +.model-list .icon { + padding-right: 3pt; +} + +.model-result { + list-style: none; +} + +.model-no-result { + color: var(--text-color); + list-style: none; + padding: 3px 6px 3px 6px; + font-size: 9pt; + font-style: italic; + display: none; +} + +.model-list li.model-folder { + color: var(--text-color); + list-style: none; + padding: 6px 6px 6px 6px; + font-size: 9pt; + font-weight: bold; + border-top: 1px solid var(--background-color1); +} + +.model-list li.model-file { + color: var(--input-text-color); + list-style: none; + padding-left: 12px; + padding-right:20px; + font-size: 10pt; + font-weight: normal; + transition: none; + transition:property: none; + cursor: default; +} + +.model-list li.model-file.in-root-folder { + padding-left: 6px; +} + +.model-list li.model-file.selected { + background: grey; +} + +.model-selector { + cursor: pointer; +} + +.model-selector-arrow { + position: absolute; + width: 17px; + margin: 5px -17px; + padding-top: 3px; + cursor: pointer; + font-size: 8pt; + transition: none; +} + +.model-input { + white-space: nowrap; +} + +.reloadModels { + background: var(--background-color2); + border: none; + padding: 0px 0px; +} + +#reload-models.secondaryButton:hover { + background: var(--background-color2); +} diff --git a/ui/media/css/themes.css b/ui/media/css/themes.css new file mode 100644 index 0000000000000000000000000000000000000000..053199f8a2bc225b0ece8d079229ff2a4c2480b3 --- /dev/null +++ b/ui/media/css/themes.css @@ -0,0 +1,186 @@ +:root { + --main-hue: 222; + --main-saturation: 4%; + --value-base: 13%; + --value-step: 5%; + --background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base)); + --background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1 * var(--value-step)))); + --background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.5 * var(--value-step)))); + --background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1.5 * var(--value-step)))); + + --accent-hue: 267; + --accent-lightness: 36%; + --accent-lightness-hover: 40%; + + --text-color: #eee; + + --input-text-color: #eee; + --input-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.7 * var(--value-step)))); + --input-border-color: var(--background-color4); + + --button-text-color: var(--input-text-color); + --button-color: var(--input-background-color); + --button-border: none; + + /* other */ + --input-border-radius: 4px; + --input-border-size: 1px; + --accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness)); + --accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover)); + --accent-text-color: rgb(255, 221, 255); + --primary-button-border: none; + --input-switch-padding: 1px; + --input-height: 18px; + --tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (2 * var(--value-step)))); + --tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3 * var(--value-step)))); + --tertiary-color: var(--input-text-color) + + /* Main theme color, hex color fallback. */ + --theme-color-fallback: #673AB6; + --status-orange: rgb(200, 139, 0); + --status-green: green; + --status-red: red; +} + +.theme-light { + --background-color1: white; + --background-color2: #ececec; + --background-color3: #e7e9eb; + --background-color4: #cccccc; + + --text-color: black; + + --input-text-color: black; + --input-background-color: #f8f9fa; + --input-border-color: grey; + + --theme-color-fallback: #aaaaaa; + + --tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (16.8 * var(--value-step)))); + --tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (12 * var(--value-step)))); + + --accent-text-color: white; +} + +.theme-discord { + --background-color1: #36393f; + --background-color2: #2f3136; + --background-color3: #292b2f; + --background-color4: #202225; + + --accent-hue: 235; + --accent-lightness: 65%; + + --input-border-size: 2px; + --input-background-color: #202225; + --input-border-color: var(--input-background-color); + + --theme-color-fallback: #202225; + + --tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step)))); + --tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step)))); + --accent-text-color: white; +} + +.theme-cool-blue { + --main-hue: 222; + --main-saturation: 18%; + --value-base: 18%; + --value-step: 3%; + --background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base)); + --background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1 * var(--value-step)))); + --background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step)))); + --background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step)))); + + --input-background-color: var(--background-color3); + + --accent-hue: 212; + + --theme-color-fallback: #0056b8; + + --tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step)))); + --tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step)))); + --accent-text-color: #f7fbff; +} + + +.theme-blurple { + --main-hue: 235; + --main-saturation: 18%; + --value-base: 16%; + --value-step: 3%; + --background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base)); + --background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1 * var(--value-step)))); + --background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step)))); + --background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step)))); + + --input-background-color: var(--background-color3); + + --theme-color-fallback: #5300b8; + + --tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step)))); + --tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step)))); +} + +.theme-super-dark { + --main-hue: 222; + --main-saturation: 18%; + --value-base: 5%; + --value-step: 5%; + --background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base)); + --background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1 * var(--value-step)))); + --background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (2 * var(--value-step)))); + --background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1.4 * var(--value-step)))); + + --input-background-color: var(--background-color3); + --input-border-size: 0px; + + --theme-color-fallback: #000000; +} + +.theme-wild { + --main-hue: 128; + --main-saturation: 18%; + --value-base: 20%; + --value-step: 5%; + --background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base)); + --background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1 * var(--value-step)))); + --background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step)))); + --background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step)))); + + --accent-hue: 212; + + --input-border-size: 1px; + --input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step)))); + --input-text-color: #FF0000; + --input-border-color: #005E05; + + --tertiary-color: white; + --accent-text-color: #f7fbff; +} + + +.theme-gnomie { + --background-color1: #242424; + --background-color2: #353535; + --background-color3: #494949; + --background-color4: #000000; + + --accent-hue: 213; + --accent-lightness: 55%; + --accent-color: #2168bf; + + --input-border-radius: 6px; + --input-text-color: #ffffff; + --input-background-color: #2a2a2a; + --input-border-size: 0px; + --input-border-color: var(--input-background-color); + + --theme-color-fallback: #2168bf; +} + +.theme-gnomie .panel-box { + border: none; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25); + border-radius: 10px; +} diff --git a/ui/media/ding.mp3 b/ui/media/ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f2843b06cec845d487e3a23076dddaff619cc36c Binary files /dev/null and b/ui/media/ding.mp3 differ diff --git a/ui/media/fonts/fa-brands-400.ttf b/ui/media/fonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..502f3621e7f97d1d94ce05794e47a6c6a56e94e7 Binary files /dev/null and b/ui/media/fonts/fa-brands-400.ttf differ diff --git a/ui/media/fonts/fa-brands-400.woff2 b/ui/media/fonts/fa-brands-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d801b51f665b890e58113fd9d12f8bb893b26a08 Binary files /dev/null and b/ui/media/fonts/fa-brands-400.woff2 differ diff --git a/ui/media/fonts/fa-regular-400.ttf b/ui/media/fonts/fa-regular-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e0abe2710fd807665b9e6bccb50c1470b0d0067b Binary files /dev/null and b/ui/media/fonts/fa-regular-400.ttf differ diff --git a/ui/media/fonts/fa-regular-400.woff2 b/ui/media/fonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d736e4b24cb29e94ed89dd498952911ee9955220 Binary files /dev/null and b/ui/media/fonts/fa-regular-400.woff2 differ diff --git a/ui/media/fonts/fa-solid-900.ttf b/ui/media/fonts/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13c9489771fd4e60ed0ea9303e882f94f640ec6d Binary files /dev/null and b/ui/media/fonts/fa-solid-900.ttf differ diff --git a/ui/media/fonts/fa-solid-900.woff2 b/ui/media/fonts/fa-solid-900.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3516fdbe330ef1cd0775dbb5bdee41a4ce2cc010 Binary files /dev/null and b/ui/media/fonts/fa-solid-900.woff2 differ diff --git a/ui/media/fonts/fa-v4compatibility.ttf b/ui/media/fonts/fa-v4compatibility.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dc2981941df8e153515fcc8718292d5e83b3d594 Binary files /dev/null and b/ui/media/fonts/fa-v4compatibility.ttf differ diff --git a/ui/media/fonts/fa-v4compatibility.woff2 b/ui/media/fonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..28d46b15ace8f3e5056c6de1e387062b92c8dae3 Binary files /dev/null and b/ui/media/fonts/fa-v4compatibility.woff2 differ diff --git a/ui/media/fonts/work-sans-v18-latin-600.woff b/ui/media/fonts/work-sans-v18-latin-600.woff new file mode 100644 index 0000000000000000000000000000000000000000..eb93c921163a6544c72771f9b7ac9224490a9402 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-600.woff differ diff --git a/ui/media/fonts/work-sans-v18-latin-600.woff2 b/ui/media/fonts/work-sans-v18-latin-600.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..03a0ade1c5ad64150431dd9bb670f186579132d1 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-600.woff2 differ diff --git a/ui/media/fonts/work-sans-v18-latin-700.woff b/ui/media/fonts/work-sans-v18-latin-700.woff new file mode 100644 index 0000000000000000000000000000000000000000..3e824cb594270053e862bfe59dafe900e4a821d4 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-700.woff differ diff --git a/ui/media/fonts/work-sans-v18-latin-700.woff2 b/ui/media/fonts/work-sans-v18-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..26dc08ec26a5c3e750e7258c10ff288a0971489c Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-700.woff2 differ diff --git a/ui/media/fonts/work-sans-v18-latin-800.woff b/ui/media/fonts/work-sans-v18-latin-800.woff new file mode 100644 index 0000000000000000000000000000000000000000..b2b0e8f021133bbadda967db9fe8ec61e197e3d4 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-800.woff differ diff --git a/ui/media/fonts/work-sans-v18-latin-800.woff2 b/ui/media/fonts/work-sans-v18-latin-800.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..34c79825c3c6b308937f842c5c8fdd87437006e4 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-800.woff2 differ diff --git a/ui/media/fonts/work-sans-v18-latin-regular.woff b/ui/media/fonts/work-sans-v18-latin-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..8f9653a2e8e54135cd8d90cb6321e40edc998ac2 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-regular.woff differ diff --git a/ui/media/fonts/work-sans-v18-latin-regular.woff2 b/ui/media/fonts/work-sans-v18-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..536e79b19483d574802956afaf4ca35af87075f4 Binary files /dev/null and b/ui/media/fonts/work-sans-v18-latin-regular.woff2 differ diff --git a/ui/media/images/fa-eraser.png b/ui/media/images/fa-eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..3176c9ec87953f50e2ebeebe1b87b42086fdde1a Binary files /dev/null and b/ui/media/images/fa-eraser.png differ diff --git a/ui/media/images/fa-eraser.svg b/ui/media/images/fa-eraser.svg new file mode 100644 index 0000000000000000000000000000000000000000..6acb1d5a3f5297d303bf07905642102d4a024bd4 --- /dev/null +++ b/ui/media/images/fa-eraser.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/media/images/fa-eye-dropper.png b/ui/media/images/fa-eye-dropper.png new file mode 100644 index 0000000000000000000000000000000000000000..bbad9a12aec5f9de053776e2e7753e1a02e82463 Binary files /dev/null and b/ui/media/images/fa-eye-dropper.png differ diff --git a/ui/media/images/fa-eye-dropper.svg b/ui/media/images/fa-eye-dropper.svg new file mode 100644 index 0000000000000000000000000000000000000000..25894e6329376312ea6931d694f713e3dc44329e --- /dev/null +++ b/ui/media/images/fa-eye-dropper.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/media/images/fa-fill.svg b/ui/media/images/fa-fill.svg new file mode 100644 index 0000000000000000000000000000000000000000..af41281ae8bad12f71aef212fe5b9a993dd1dd96 --- /dev/null +++ b/ui/media/images/fa-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/media/images/fa-pencil.png b/ui/media/images/fa-pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6de9dadecebeb9eeb5d5de49d63bf08e8079cf Binary files /dev/null and b/ui/media/images/fa-pencil.png differ diff --git a/ui/media/images/fa-pencil.svg b/ui/media/images/fa-pencil.svg new file mode 100644 index 0000000000000000000000000000000000000000..ec76b9fe545cb224d4b0c82ebb5107cf8e9deecf --- /dev/null +++ b/ui/media/images/fa-pencil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/media/images/favicon-16x16.png b/ui/media/images/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..26ad0ae6f5b3299b7a1c321a5b8fc06b6d1ddd70 Binary files /dev/null and b/ui/media/images/favicon-16x16.png differ diff --git a/ui/media/images/favicon-32x32.png b/ui/media/images/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f58fbc2447b1478adfd6546442b943b4ea870aca Binary files /dev/null and b/ui/media/images/favicon-32x32.png differ diff --git a/ui/media/images/icon-512x512.png b/ui/media/images/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..b75baa8de1c4d8cb8ba96a2ca528e7c64f994e65 Binary files /dev/null and b/ui/media/images/icon-512x512.png differ diff --git a/ui/media/images/kofi.png b/ui/media/images/kofi.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdebbaacc3c9fee0deed20eeed8847efd9c4f41 Binary files /dev/null and b/ui/media/images/kofi.png differ diff --git a/ui/media/js/FileSaver.min.js b/ui/media/js/FileSaver.min.js new file mode 100644 index 0000000000000000000000000000000000000000..78f46d2c76de47283b6fbb1d6be4d9dd248f0773 --- /dev/null +++ b/ui/media/js/FileSaver.min.js @@ -0,0 +1,2 @@ +(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); + diff --git a/ui/media/js/auto-save.js b/ui/media/js/auto-save.js new file mode 100644 index 0000000000000000000000000000000000000000..4179263e463708c33002598182f2ba3ff8d9db2a --- /dev/null +++ b/ui/media/js/auto-save.js @@ -0,0 +1,324 @@ +// Saving settings +let saveSettingsConfigTable = document.getElementById("save-settings-config-table") +let saveSettingsConfigOverlay = document.getElementById("save-settings-config") +let resetImageSettingsButton = document.getElementById("reset-image-settings") + +const SETTINGS_KEY = "user_settings_v2" + +const SETTINGS = {} // key=id. dict initialized in initSettings. { element, default, value, ignore } +const SETTINGS_IDS_LIST = [ + "prompt", + "seed", + "random_seed", + "num_outputs_total", + "num_outputs_parallel", + "stable_diffusion_model", + "vae_model", + "hypernetwork_model", + "sampler_name", + "width", + "height", + "num_inference_steps", + "guidance_scale", + "prompt_strength", + "hypernetwork_strength", + "output_format", + "output_quality", + "negative_prompt", + "stream_image_progress", + "use_face_correction", + "gfpgan_model", + "use_upscale", + "upscale_amount", + "block_nsfw", + "show_only_filtered_image", + "upscale_model", + "preview-image", + "modifier-card-size-slider", + "theme", + "save_to_disk", + "diskPath", + "sound_toggle", + "vram_usage_level", + "confirm_dangerous_actions", + "metadata_output_format", + "auto_save_settings", + "apply_color_correction", + "process_order_toggle", + "thumbnail_size", + "auto_scroll", + "zip_toggle", + "tree_toggle", + "json_toggle" +] + +const IGNORE_BY_DEFAULT = [ + "prompt" +] + +const SETTINGS_SECTIONS = [ // gets the "keys" property filled in with an ordered list of settings in this section via initSettings + { id: "editor-inputs", name: "Prompt" }, + { id: "editor-settings", name: "Image Settings" }, + { id: "system-settings", name: "System Settings" }, + { id: "container", name: "Other" } +] + +async function initSettings() { + SETTINGS_IDS_LIST.forEach(id => { + var element = document.getElementById(id) + if (!element) { + console.error(`Missing settings element ${id}`) + } + if (id in SETTINGS) { // don't create it again + return + } + SETTINGS[id] = { + key: id, + element: element, + label: getSettingLabel(element), + default: getSetting(element), + value: getSetting(element), + ignore: IGNORE_BY_DEFAULT.includes(id) + } + element.addEventListener("input", settingChangeHandler) + element.addEventListener("change", settingChangeHandler) + }) + var unsorted_settings_ids = [...SETTINGS_IDS_LIST] + SETTINGS_SECTIONS.forEach(section => { + var name = section.name + var element = document.getElementById(section.id) + var unsorted_ids = unsorted_settings_ids.map(id => `#${id}`).join(",") + var children = unsorted_ids == "" ? [] : Array.from(element.querySelectorAll(unsorted_ids)); + section.keys = [] + children.forEach(e => { + section.keys.push(e.id) + }) + unsorted_settings_ids = unsorted_settings_ids.filter(id => children.find(e => e.id == id) == undefined) + }) + loadSettings() +} + +function getSetting(element) { + if (element.dataset && 'path' in element.dataset) { + return element.dataset.path + } + if (typeof element === "string" || element instanceof String) { + element = SETTINGS[element].element + } + if (element.type == "checkbox") { + return element.checked + } + return element.value +} +function setSetting(element, value) { + if (element.dataset && 'path' in element.dataset) { + element.dataset.path = value + return // no need to dispatch any event here because the models are not loaded yet + } + if (typeof element === "string" || element instanceof String) { + element = SETTINGS[element].element + } + SETTINGS[element.id].value = value + if (getSetting(element) == value) { + return // no setting necessary + } + if (element.type == "checkbox") { + element.checked = value + } + else { + element.value = value + } + element.dispatchEvent(new Event("input")) + element.dispatchEvent(new Event("change")) +} + +function saveSettings() { + var saved_settings = Object.values(SETTINGS).map(setting => { + return { + key: setting.key, + value: setting.value, + ignore: setting.ignore + } + }) + localStorage.setItem(SETTINGS_KEY, JSON.stringify(saved_settings)) +} + +var CURRENTLY_LOADING_SETTINGS = false +function loadSettings() { + var saved_settings_text = localStorage.getItem(SETTINGS_KEY) + if (saved_settings_text) { + var saved_settings = JSON.parse(saved_settings_text) + if (saved_settings.find(s => s.key == "auto_save_settings")?.value == false) { + setSetting("auto_save_settings", false) + return + } + CURRENTLY_LOADING_SETTINGS = true + saved_settings.forEach(saved_setting => { + var setting = SETTINGS[saved_setting.key] + if (!setting) { + console.warn(`Attempted to load setting ${saved_setting.key}, but no setting found`); + return null; + } + setting.ignore = saved_setting.ignore + if (!setting.ignore) { + setting.value = saved_setting.value + setSetting(setting.element, setting.value) + } + }) + CURRENTLY_LOADING_SETTINGS = false + } + else { + CURRENTLY_LOADING_SETTINGS = true + tryLoadOldSettings(); + CURRENTLY_LOADING_SETTINGS = false + saveSettings() + } +} + +function loadDefaultSettingsSection(section_id) { + CURRENTLY_LOADING_SETTINGS = true + var section = SETTINGS_SECTIONS.find(s => s.id == section_id); + section.keys.forEach(key => { + var setting = SETTINGS[key]; + setting.value = setting.default + setSetting(setting.element, setting.value) + }) + CURRENTLY_LOADING_SETTINGS = false + saveSettings() +} + +function settingChangeHandler(event) { + if (!CURRENTLY_LOADING_SETTINGS) { + var element = event.target + var value = getSetting(element) + if (value != SETTINGS[element.id].value) { + SETTINGS[element.id].value = value + saveSettings() + } + } +} + +function getSettingLabel(element) { + var labelElement = document.querySelector(`label[for='${element.id}']`) + var label = labelElement?.innerText || element.id + var truncate_length = 30 + if (label.includes(" (")) { + label = label.substring(0, label.indexOf(" (")) + } + if (label.length > truncate_length) { + label = label.substring(0, truncate_length - 3) + "..." + } + label = label.replace("➕", "") + label = label.replace("➖", "") + return label +} + +function fillSaveSettingsConfigTable() { + saveSettingsConfigTable.textContent = "" + SETTINGS_SECTIONS.forEach(section => { + var section_row = `${section.name}` + saveSettingsConfigTable.insertAdjacentHTML("beforeend", section_row) + section.keys.forEach(key => { + var setting = SETTINGS[key] + var element = setting.element + var checkbox_id = `shouldsave_${element.id}` + var is_checked = setting.ignore ? "" : "checked" + var value = setting.value + var value_truncate_length = 30 + if ((typeof value === "string" || value instanceof String) && value.length > value_truncate_length) { + value = value.substring(0, value_truncate_length - 3) + "..." + } + var newrow = `(${value})` + saveSettingsConfigTable.insertAdjacentHTML("beforeend", newrow) + var checkbox = document.getElementById(checkbox_id) + checkbox.addEventListener("input", event => { + setting.ignore = !checkbox.checked + saveSettings() + }) + }) + }) + prettifyInputs(saveSettingsConfigTable) +} + +// configureSettingsSaveBtn + + + + +var autoSaveSettings = document.getElementById("auto_save_settings") +var configSettingsButton = document.createElement("button") +configSettingsButton.textContent = "Configure" +configSettingsButton.style.margin = "0px 5px" +autoSaveSettings.insertAdjacentElement("beforebegin", configSettingsButton) +autoSaveSettings.addEventListener("change", () => { + configSettingsButton.style.display = autoSaveSettings.checked ? "block" : "none" +}) +configSettingsButton.addEventListener('click', () => { + fillSaveSettingsConfigTable() + saveSettingsConfigOverlay.classList.add("active") +}) +resetImageSettingsButton.addEventListener('click', event => { + loadDefaultSettingsSection("editor-settings"); + event.stopPropagation() +}) + + +function tryLoadOldSettings() { + console.log("Loading old user settings") + // load v1 auto-save.js settings + var old_map = { + "guidance_scale_slider": "guidance_scale", + "prompt_strength_slider": "prompt_strength" + } + var settings_key_v1 = "user_settings" + var saved_settings_text = localStorage.getItem(settings_key_v1) + if (saved_settings_text) { + var saved_settings = JSON.parse(saved_settings_text) + Object.keys(saved_settings.should_save).forEach(key => { + key = key in old_map ? old_map[key] : key + if (!(key in SETTINGS)) return + SETTINGS[key].ignore = !saved_settings.should_save[key] + }); + Object.keys(saved_settings.values).forEach(key => { + key = key in old_map ? old_map[key] : key + if (!(key in SETTINGS)) return + var setting = SETTINGS[key] + if (!setting.ignore) { + setting.value = saved_settings.values[key] + setSetting(setting.element, setting.value) + } + }); + localStorage.removeItem(settings_key_v1) + } + + // load old individually stored items + var individual_settings_map = { // maps old localStorage-key to new SETTINGS-key + "soundEnabled": "sound_toggle", + "saveToDisk": "save_to_disk", + "useCPU": "use_cpu", + "diskPath": "diskPath", + "useFaceCorrection": "use_face_correction", + "useUpscaling": "use_upscale", + "showOnlyFilteredImage": "show_only_filtered_image", + "streamImageProgress": "stream_image_progress", + "outputFormat": "output_format", + "autoSaveSettings": "auto_save_settings", + }; + Object.keys(individual_settings_map).forEach(localStorageKey => { + var localStorageValue = localStorage.getItem(localStorageKey); + if (localStorageValue !== null) { + let key = individual_settings_map[localStorageKey] + var setting = SETTINGS[key] + if (!setting) { + console.warn(`Attempted to map old setting ${key}, but no setting found`); + return null; + } + if (setting.element.type == "checkbox" && (typeof localStorageValue === "string" || localStorageValue instanceof String)) { + localStorageValue = localStorageValue == "true" + } + setting.value = localStorageValue + setSetting(setting.element, setting.value) + localStorage.removeItem(localStorageKey); + } + }) +} diff --git a/ui/media/js/dnd.js b/ui/media/js/dnd.js new file mode 100644 index 0000000000000000000000000000000000000000..f1f009117237be41d5a4a96920aefabec35d6be3 --- /dev/null +++ b/ui/media/js/dnd.js @@ -0,0 +1,620 @@ +"use strict" // Opt in to a restricted variant of JavaScript + +const EXT_REGEX = /(?:\.([^.]+))?$/ +const TEXT_EXTENSIONS = ['txt', 'json'] +const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga', 'webp'] + +function parseBoolean(stringValue) { + if (typeof stringValue === 'boolean') { + return stringValue + } + if (typeof stringValue === 'number') { + return stringValue !== 0 + } + if (typeof stringValue !== 'string') { + return false + } + switch(stringValue?.toLowerCase()?.trim()) { + case "true": + case "yes": + case "on": + case "1": + return true; + + case "false": + case "no": + case "off": + case "0": + case "none": + case null: + case undefined: + return false; + } + try { + return Boolean(JSON.parse(stringValue)); + } catch { + return Boolean(stringValue) + } +} + +const TASK_MAPPING = { + prompt: { name: 'Prompt', + setUI: (prompt) => { + promptField.value = prompt + }, + readUI: () => promptField.value, + parse: (val) => val + }, + negative_prompt: { name: 'Negative Prompt', + setUI: (negative_prompt) => { + negativePromptField.value = negative_prompt + }, + readUI: () => negativePromptField.value, + parse: (val) => val + }, + active_tags: { name: "Image Modifiers", + setUI: (active_tags) => { + refreshModifiersState(active_tags) + }, + readUI: () => activeTags.map(x => x.name), + parse: (val) => val + }, + inactive_tags: { name: "Inactive Image Modifiers", + setUI: (inactive_tags) => { + refreshInactiveTags(inactive_tags) + }, + readUI: () => activeTags.filter(tag => tag.inactive === true).map(x => x.name), + parse: (val) => val + }, + width: { name: 'Width', + setUI: (width) => { + const oldVal = widthField.value + widthField.value = width + if (!widthField.value) { + widthField.value = oldVal + } + }, + readUI: () => parseInt(widthField.value), + parse: (val) => parseInt(val) + }, + height: { name: 'Height', + setUI: (height) => { + const oldVal = heightField.value + heightField.value = height + if (!heightField.value) { + heightField.value = oldVal + } + }, + readUI: () => parseInt(heightField.value), + parse: (val) => parseInt(val) + }, + seed: { name: 'Seed', + setUI: (seed) => { + if (!seed) { + randomSeedField.checked = true + seedField.disabled = true + seedField.value = 0 + return + } + randomSeedField.checked = false + seedField.disabled = false + seedField.value = seed + }, + readUI: () => parseInt(seedField.value), // just return the value the user is seeing in the UI + parse: (val) => parseInt(val) + }, + num_inference_steps: { name: 'Steps', + setUI: (num_inference_steps) => { + numInferenceStepsField.value = num_inference_steps + }, + readUI: () => parseInt(numInferenceStepsField.value), + parse: (val) => parseInt(val) + }, + guidance_scale: { name: 'Guidance Scale', + setUI: (guidance_scale) => { + guidanceScaleField.value = guidance_scale + updateGuidanceScaleSlider() + }, + readUI: () => parseFloat(guidanceScaleField.value), + parse: (val) => parseFloat(val) + }, + prompt_strength: { name: 'Prompt Strength', + setUI: (prompt_strength) => { + promptStrengthField.value = prompt_strength + updatePromptStrengthSlider() + }, + readUI: () => parseFloat(promptStrengthField.value), + parse: (val) => parseFloat(val) + }, + + init_image: { name: 'Initial Image', + setUI: (init_image) => { + initImagePreview.src = init_image + }, + readUI: () => initImagePreview.src, + parse: (val) => val + }, + mask: { name: 'Mask', + setUI: (mask) => { + setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter) + imageInpainter.setImg(mask) + }, 250) + maskSetting.checked = Boolean(mask) + }, + readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined), + parse: (val) => val + }, + preserve_init_image_color_profile: { name: 'Preserve Color Profile', + setUI: (preserve_init_image_color_profile) => { + applyColorCorrectionField.checked = parseBoolean(preserve_init_image_color_profile) + }, + readUI: () => applyColorCorrectionField.checked, + parse: (val) => parseBoolean(val) + }, + + use_face_correction: { name: 'Use Face Correction', + setUI: (use_face_correction) => { + const oldVal = gfpganModelField.value + gfpganModelField.value = getModelPath(use_face_correction, ['.pth']) + if (gfpganModelField.value) { // Is a valid value for the field. + useFaceCorrectionField.checked = true + gfpganModelField.disabled = false + } else { // Not a valid value, restore the old value and disable the filter. + gfpganModelField.disabled = true + gfpganModelField.value = oldVal + useFaceCorrectionField.checked = false + } + + //useFaceCorrectionField.checked = parseBoolean(use_face_correction) + }, + readUI: () => (useFaceCorrectionField.checked ? gfpganModelField.value : undefined), + parse: (val) => val + }, + use_upscale: { name: 'Use Upscaling', + setUI: (use_upscale) => { + const oldVal = upscaleModelField.value + upscaleModelField.value = getModelPath(use_upscale, ['.pth']) + if (upscaleModelField.value) { // Is a valid value for the field. + useUpscalingField.checked = true + upscaleModelField.disabled = false + upscaleAmountField.disabled = false + } else { // Not a valid value, restore the old value and disable the filter. + upscaleModelField.disabled = true + upscaleAmountField.disabled = true + upscaleModelField.value = oldVal + useUpscalingField.checked = false + } + }, + readUI: () => (useUpscalingField.checked ? upscaleModelField.value : undefined), + parse: (val) => val + }, + upscale_amount: { name: 'Upscale By', + setUI: (upscale_amount) => { + upscaleAmountField.value = upscale_amount + }, + readUI: () => upscaleAmountField.value, + parse: (val) => val + }, + sampler_name: { name: 'Sampler', + setUI: (sampler_name) => { + samplerField.value = sampler_name + }, + readUI: () => samplerField.value, + parse: (val) => val + }, + use_stable_diffusion_model: { name: 'Stable Diffusion model', + setUI: (use_stable_diffusion_model) => { + const oldVal = stableDiffusionModelField.value + + use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, ['.ckpt', '.safetensors']) + stableDiffusionModelField.value = use_stable_diffusion_model + + if (!stableDiffusionModelField.value) { + stableDiffusionModelField.value = oldVal + } + }, + readUI: () => stableDiffusionModelField.value, + parse: (val) => val + }, + use_vae_model: { name: 'VAE model', + setUI: (use_vae_model) => { + const oldVal = vaeModelField.value + use_vae_model = (use_vae_model === undefined || use_vae_model === null || use_vae_model === 'None' ? '' : use_vae_model) + + if (use_vae_model !== '') { + use_vae_model = getModelPath(use_vae_model, ['.vae.pt', '.ckpt']) + use_vae_model = use_vae_model !== '' ? use_vae_model : oldVal + } + vaeModelField.value = use_vae_model + }, + readUI: () => vaeModelField.value, + parse: (val) => val + }, + use_hypernetwork_model: { name: 'Hypernetwork model', + setUI: (use_hypernetwork_model) => { + const oldVal = hypernetworkModelField.value + use_hypernetwork_model = (use_hypernetwork_model === undefined || use_hypernetwork_model === null || use_hypernetwork_model === 'None' ? '' : use_hypernetwork_model) + + if (use_hypernetwork_model !== '') { + use_hypernetwork_model = getModelPath(use_hypernetwork_model, ['.pt']) + use_hypernetwork_model = use_hypernetwork_model !== '' ? use_hypernetwork_model : oldVal + } + hypernetworkModelField.value = use_hypernetwork_model + hypernetworkModelField.dispatchEvent(new Event('change')) + }, + readUI: () => hypernetworkModelField.value, + parse: (val) => val + }, + hypernetwork_strength: { name: 'Hypernetwork Strength', + setUI: (hypernetwork_strength) => { + hypernetworkStrengthField.value = hypernetwork_strength + updateHypernetworkStrengthSlider() + }, + readUI: () => parseFloat(hypernetworkStrengthField.value), + parse: (val) => parseFloat(val) + }, + + num_outputs: { name: 'Parallel Images', + setUI: (num_outputs) => { + numOutputsParallelField.value = num_outputs + }, + readUI: () => parseInt(numOutputsParallelField.value), + parse: (val) => val + }, + + use_cpu: { name: 'Use CPU', + setUI: (use_cpu) => { + useCPUField.checked = use_cpu + }, + readUI: () => useCPUField.checked, + parse: (val) => val + }, + + stream_image_progress: { name: 'Stream Image Progress', + setUI: (stream_image_progress) => { + streamImageProgressField.checked = (parseInt(numOutputsTotalField.value) > 50 ? false : stream_image_progress) + }, + readUI: () => streamImageProgressField.checked, + parse: (val) => Boolean(val) + }, + show_only_filtered_image: { name: 'Show only the corrected/upscaled image', + setUI: (show_only_filtered_image) => { + showOnlyFilteredImageField.checked = show_only_filtered_image + }, + readUI: () => showOnlyFilteredImageField.checked, + parse: (val) => Boolean(val) + }, + output_format: { name: 'Output Format', + setUI: (output_format) => { + outputFormatField.value = output_format + }, + readUI: () => outputFormatField.value, + parse: (val) => val + }, + save_to_disk_path: { name: 'Save to disk path', + setUI: (save_to_disk_path) => { + saveToDiskField.checked = Boolean(save_to_disk_path) + diskPathField.value = save_to_disk_path + }, + readUI: () => diskPathField.value, + parse: (val) => val + } +} + +function restoreTaskToUI(task, fieldsToSkip) { + fieldsToSkip = fieldsToSkip || [] + + if ('numOutputsTotal' in task) { + numOutputsTotalField.value = task.numOutputsTotal + } + if ('seed' in task) { + randomSeedField.checked = false + seedField.value = task.seed + } + if (!('reqBody' in task)) { + return + } + for (const key in TASK_MAPPING) { + if (key in task.reqBody && !fieldsToSkip.includes(key)) { + TASK_MAPPING[key].setUI(task.reqBody[key]) + } + } + + // properly reset fields not present in the task + if (!('use_hypernetwork_model' in task.reqBody)) { + hypernetworkModelField.value = "" + hypernetworkModelField.dispatchEvent(new Event("change")) + } + + // restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d) + promptField.value = task.reqBody.original_prompt + if (!('original_prompt' in task.reqBody)) { + promptField.value = task.reqBody.prompt + } + + // properly reset checkboxes + if (!('use_face_correction' in task.reqBody)) { + useFaceCorrectionField.checked = false + gfpganModelField.disabled = true + } + if (!('use_upscale' in task.reqBody)) { + useUpscalingField.checked = false + } + if (!('mask' in task.reqBody) && maskSetting.checked) { + maskSetting.checked = false + maskSetting.dispatchEvent(new Event("click")) + } + upscaleModelField.disabled = !useUpscalingField.checked + upscaleAmountField.disabled = !useUpscalingField.checked + + // hide/show source picture as needed + if (IMAGE_REGEX.test(initImagePreview.src) && task.reqBody.init_image == undefined) { + // hide source image + initImageClearBtn.dispatchEvent(new Event("click")) + } + else if (task.reqBody.init_image !== undefined) { + // listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpainter) + initImagePreview.addEventListener('load', function() { + if (Boolean(task.reqBody.mask)) { + imageInpainter.setImg(task.reqBody.mask) + maskSetting.checked = true + } + }, { once: true }) + initImagePreview.src = task.reqBody.init_image + } +} +function readUI() { + const reqBody = {} + for (const key in TASK_MAPPING) { + reqBody[key] = TASK_MAPPING[key].readUI() + } + return { + 'numOutputsTotal': parseInt(numOutputsTotalField.value), + 'seed': TASK_MAPPING['seed'].readUI(), + 'reqBody': reqBody + } +} +function getModelPath(filename, extensions) +{ + if (typeof filename !== "string") { + return + } + + let pathIdx + if (filename.includes('/models/stable-diffusion/')) { + pathIdx = filename.indexOf('/models/stable-diffusion/') + 25 // Linux, Mac paths + } + else if (filename.includes('\\models\\stable-diffusion\\')) { + pathIdx = filename.indexOf('\\models\\stable-diffusion\\') + 25 // Linux, Mac paths + } + if (pathIdx >= 0) { + filename = filename.slice(pathIdx) + } + extensions.forEach(ext => { + if (filename.endsWith(ext)) { + filename = filename.slice(0, filename.length - ext.length) + } + }) + return filename +} + +const TASK_TEXT_MAPPING = { + prompt: 'Prompt', + width: 'Width', + height: 'Height', + seed: 'Seed', + num_inference_steps: 'Steps', + guidance_scale: 'Guidance Scale', + prompt_strength: 'Prompt Strength', + use_face_correction: 'Use Face Correction', + use_upscale: 'Use Upscaling', + upscale_amount: 'Upscale By', + sampler_name: 'Sampler', + negative_prompt: 'Negative Prompt', + use_stable_diffusion_model: 'Stable Diffusion model', + use_hypernetwork_model: 'Hypernetwork model', + hypernetwork_strength: 'Hypernetwork Strength' +} +function parseTaskFromText(str) { + const taskReqBody = {} + + const lines = str.split('\n') + if (lines.length === 0) { + return + } + + // Prompt + let knownKeyOnFirstLine = false + for (let key in TASK_TEXT_MAPPING) { + if (lines[0].startsWith(TASK_TEXT_MAPPING[key] + ':')) { + knownKeyOnFirstLine = true + break + } + } + if (!knownKeyOnFirstLine) { + taskReqBody.prompt = lines[0] + console.log('Prompt:', taskReqBody.prompt) + } + + for (const key in TASK_TEXT_MAPPING) { + if (key in taskReqBody) { + continue + } + + const name = TASK_TEXT_MAPPING[key]; + let val = undefined + + const reName = new RegExp(`${name}\\ *:\\ *(.*)(?:\\r\\n|\\r|\\n)*`, 'igm') + const match = reName.exec(str); + if (match) { + str = str.slice(0, match.index) + str.slice(match.index + match[0].length) + val = match[1] + } + if (val !== undefined) { + taskReqBody[key] = TASK_MAPPING[key].parse(val.trim()) + console.log(TASK_MAPPING[key].name + ':', taskReqBody[key]) + if (!str) { + break + } + } + } + if (Object.keys(taskReqBody).length <= 0) { + return undefined + } + const task = { reqBody: taskReqBody } + if ('seed' in taskReqBody) { + task.seed = taskReqBody.seed + } + return task +} + +async function parseContent(text) { + text = text.trim(); + if (text.startsWith('{') && text.endsWith('}')) { + try { + const task = JSON.parse(text) + if (!('reqBody' in task)) { // support the format saved to the disk, by the UI + task.reqBody = Object.assign({}, task) + } + restoreTaskToUI(task) + return true + } catch (e) { + console.warn(`JSON text content couldn't be parsed.`, e) + } + return false + } + // Normal txt file. + const task = parseTaskFromText(text) + if (text.toLowerCase().includes('seed:') && task) { // only parse valid task content + restoreTaskToUI(task) + return true + } else { + console.warn(`Raw text content couldn't be parsed.`) + promptField.value = text + return false + } +} + +async function readFile(file, i) { + console.log(`Event %o reading file[${i}]:${file.name}...`) + const fileContent = (await file.text()).trim() + return await parseContent(fileContent) +} + +function dropHandler(ev) { + console.log('Content dropped...') + let items = [] + + if (ev?.dataTransfer?.items) { // Use DataTransferItemList interface + items = Array.from(ev.dataTransfer.items) + items = items.filter(item => item.kind === 'file') + items = items.map(item => item.getAsFile()) + } else if (ev?.dataTransfer?.files) { // Use DataTransfer interface + items = Array.from(ev.dataTransfer.files) + } + + items.forEach(item => {item.file_ext = EXT_REGEX.exec(item.name.toLowerCase())[1]}) + + let text_items = items.filter(item => TEXT_EXTENSIONS.includes(item.file_ext)) + let image_items = items.filter(item => IMAGE_EXTENSIONS.includes(item.file_ext)) + + if (image_items.length > 0 && ev.target == initImageSelector) { + return // let the event bubble up, so that the Init Image filepicker can receive this + } + + ev.preventDefault() // Prevent default behavior (Prevent file/content from being opened) + text_items.forEach(readFile) +} +function dragOverHandler(ev) { + console.log('Content in drop zone') + + // Prevent default behavior (Prevent file/content from being opened) + ev.preventDefault() + + ev.dataTransfer.dropEffect = "copy" + + let img = new Image() + img.src = '//' + location.host + '/media/images/favicon-32x32.png' + ev.dataTransfer.setDragImage(img, 16, 16) +} + +document.addEventListener("drop", dropHandler) +document.addEventListener("dragover", dragOverHandler) + +const TASK_REQ_NO_EXPORT = [ + "use_cpu", + "save_to_disk_path" +] +const resetSettings = document.getElementById('reset-image-settings') + +function checkReadTextClipboardPermission (result) { + if (result.state != "granted" && result.state != "prompt") { + return + } + // PASTE ICON + const pasteIcon = document.createElement('i') + pasteIcon.className = 'fa-solid fa-paste section-button' + pasteIcon.innerHTML = `Paste Image Settings` + pasteIcon.addEventListener('click', async (event) => { + event.stopPropagation() + // Add css class 'active' + pasteIcon.classList.add('active') + // In 350 ms remove the 'active' class + asyncDelay(350).then(() => pasteIcon.classList.remove('active')) + + // Retrieve clipboard content and try to parse it + const text = await navigator.clipboard.readText(); + await parseContent(text) + }) + resetSettings.parentNode.insertBefore(pasteIcon, resetSettings) +} +navigator.permissions.query({ name: "clipboard-read" }).then(checkReadTextClipboardPermission, (reason) => console.log('clipboard-read is not available. %o', reason)) + +document.addEventListener('paste', async (event) => { + if (event.target) { + const targetTag = event.target.tagName.toLowerCase() + // Disable when targeting input elements. + if (targetTag === 'input' || targetTag === 'textarea') { + return + } + } + const paste = (event.clipboardData || window.clipboardData).getData('text') + const selection = window.getSelection() + if (selection.toString().trim().length <= 0 && await parseContent(paste)) { + event.preventDefault() + return + } +}) + +// Adds a copy and a paste icon if the browser grants permission to write to clipboard. +function checkWriteToClipboardPermission (result) { + if (result.state != "granted" && result.state != "prompt") { + return + } + // COPY ICON + const copyIcon = document.createElement('i') + copyIcon.className = 'fa-solid fa-clipboard section-button' + copyIcon.innerHTML = `Copy Image Settings` + copyIcon.addEventListener('click', (event) => { + event.stopPropagation() + // Add css class 'active' + copyIcon.classList.add('active') + // In 350 ms remove the 'active' class + asyncDelay(350).then(() => copyIcon.classList.remove('active')) + const uiState = readUI() + TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key]) + if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) { + delete uiState.reqBody.init_image + delete uiState.reqBody.prompt_strength + } + navigator.clipboard.writeText(JSON.stringify(uiState, undefined, 4)) + }) + resetSettings.parentNode.insertBefore(copyIcon, resetSettings) +} +// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS. +navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => { + if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === 'function') { + // Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373 + checkWriteToClipboardPermission({state:"granted"}) + } +}) diff --git a/ui/media/js/engine.js b/ui/media/js/engine.js new file mode 100644 index 0000000000000000000000000000000000000000..ae24e5b8a6cb43f089c91ebf2064a050ffd75a5d --- /dev/null +++ b/ui/media/js/engine.js @@ -0,0 +1,1311 @@ +/** SD-UI Backend control and classes. + */ +(function () { "use strict"; + const RETRY_DELAY_IF_BUFFER_IS_EMPTY = 1000 // ms + const RETRY_DELAY_IF_SERVER_IS_BUSY = 30 * 1000 // ms, status_code 503, already a task running + const RETRY_DELAY_ON_ERROR = 4000 // ms + const TASK_STATE_SERVER_UPDATE_DELAY = 1500 // ms + const SERVER_STATE_VALIDITY_DURATION = 90 * 1000 // ms - 90 seconds to allow ping to timeout more than once before killing tasks. + const HEALTH_PING_INTERVAL = 5000 // ms + const IDLE_COOLDOWN = 2500 // ms + const CONCURRENT_TASK_INTERVAL = 100 // ms + + /** Connects to an endpoint and resumes connection after reaching end of stream until all data is received. + * Allows closing the connection while the server buffers more data. + */ + class ChunkedStreamReader { + #bufferedString = '' // Data received waiting to be read. + #url + #fetchOptions + #response + + constructor(url, initialContent='', options={}) { + if (typeof url !== 'string' && !(url instanceof String)) { + throw new Error('url is not a string.') + } + if (typeof initialContent !== 'undefined' && typeof initialContent !== 'string') { + throw new Error('initialContent is not a string.') + } + this.#bufferedString = initialContent + this.#url = url + this.#fetchOptions = Object.assign({ + headers: { + 'Content-Type': 'application/json' + } + }, options) + this.onNext = undefined + } + + get url() { + if (this.#response.redirected) { + return this.#response.url + } + return this.#url + } + get bufferedString() { + return this.#bufferedString + } + get status() { + this.#response?.status + } + get statusText() { + this.#response?.statusText + } + + parse(value) { + if (typeof value === 'undefined') { + return + } + if (!isArrayOrTypedArray(value)) { + return [value] + } + if (value.length === 0) { + return value + } + if (typeof this.textDecoder === 'undefined') { + this.textDecoder = new TextDecoder() + } + return [this.textDecoder.decode(value)] + } + onComplete(value) { + return value + } + onError(response) { + throw new Error(response.statusText) + } + onNext({value, done}, response) { + return {value, done} + } + + async *[Symbol.asyncIterator]() { + return this.open() + } + async *open() { + let value = undefined + let done = undefined + do { + if (this.#response) { + await asyncDelay(RETRY_DELAY_IF_BUFFER_IS_EMPTY) + } + this.#response = await fetch(this.#url, this.#fetchOptions) + if (!this.#response.ok) { + if (this.#response.status === 425) { + continue + } + // Request status indicate failure + console.warn('Stream %o stopped unexpectedly.', this.#response) + value = await Promise.resolve(this.onError(this.#response)) + if (typeof value === 'boolean' && value) { + continue + } + return value + } + const reader = this.#response.body.getReader() + done = false + do { + const readState = await reader.read() + value = this.parse(readState.value) + if (value) { + for(let sVal of value) { + ({value: sVal, done} = await Promise.resolve(this.onNext({value:sVal, done:readState.done}))) + yield sVal + if (done) { + return this.onComplete(sVal) + } + } + } + if (done) { + return + } + } while(value && !done) + } while (!done && (this.#response.ok || this.#response.status === 425)) + } + *readStreamAsJSON(jsonStr, throwOnError) { + if (typeof jsonStr !== 'string') { + throw new Error('jsonStr is not a string.') + } + do { + if (this.#bufferedString.length > 0) { + // Append new data when required + if (jsonStr.length > 0) { + jsonStr = this.#bufferedString + jsonStr + } else { + jsonStr = this.#bufferedString + } + this.#bufferedString = '' + } + if (!jsonStr) { + return + } + // Find next delimiter + let lastChunkIdx = jsonStr.indexOf('}{') + if (lastChunkIdx >= 0) { + this.#bufferedString = jsonStr.substring(0, lastChunkIdx + 1) + jsonStr = jsonStr.substring(lastChunkIdx + 1) + } else { + this.#bufferedString = jsonStr + jsonStr = '' + } + if (this.#bufferedString.length <= 0) { + return + } + // hack for a middleman buffering all the streaming updates, and unleashing them on the poor browser in one shot. + // this results in having to parse JSON like {"step": 1}{"step": 2}{"step": 3}{"ste... + // which is obviously invalid and can happen at any point while rendering. + // So we need to extract only the next {} section + try { // Try to parse + const jsonObj = JSON.parse(this.#bufferedString) + this.#bufferedString = jsonStr + jsonStr = '' + yield jsonObj + } catch (e) { + if (throwOnError) { + console.error(`Parsing: "${this.#bufferedString}", Buffer: "${jsonStr}"`) + } + this.#bufferedString += jsonStr + if (e instanceof SyntaxError && !throwOnError) { + return + } + throw e + } + } while (this.#bufferedString.length > 0 && this.#bufferedString.indexOf('}') >= 0) + } + } + + const EVENT_IDLE = 'idle' + const EVENT_STATUS_CHANGED = 'statusChange' + const EVENT_UNHANDLED_REJECTION = 'unhandledRejection' + const EVENT_TASK_QUEUED = 'taskQueued' + const EVENT_TASK_START = 'taskStart' + const EVENT_TASK_END = 'taskEnd' + const EVENT_TASK_ERROR = 'task_error' + const EVENT_UNEXPECTED_RESPONSE = 'unexpectedResponse' + const EVENTS_TYPES = [ + EVENT_IDLE, + EVENT_STATUS_CHANGED, + EVENT_UNHANDLED_REJECTION, + + EVENT_TASK_QUEUED, + EVENT_TASK_START, + EVENT_TASK_END, + EVENT_TASK_ERROR, + + EVENT_UNEXPECTED_RESPONSE, + ] + Object.freeze(EVENTS_TYPES) + const eventSource = new GenericEventSource(EVENTS_TYPES) + + function setServerStatus(msgType, msg) { + return eventSource.fireEvent(EVENT_STATUS_CHANGED, {type: msgType, message: msg}) + } + + const ServerStates = { + init: 'Init', + loadingModel: 'LoadingModel', + online: 'Online', + rendering: 'Rendering', + unavailable: 'Unavailable', + } + Object.freeze(ServerStates) + + let sessionId = Date.now() + let serverState = {'status': ServerStates.unavailable, 'time': Date.now()} + + async function healthCheck() { + if (Date.now() < serverState.time + (HEALTH_PING_INTERVAL / 2) && isServerAvailable()) { + // Ping confirmed online less than half of HEALTH_PING_INTERVAL ago. + return true + } + if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { + console.warn('WARNING! SERVER_STATE_VALIDITY_DURATION has elapsed since the last Ping completed.') + } + try { + let res = undefined + if (typeof sessionId !== 'undefined') { + res = await fetch('/ping?session_id=' + sessionId) + } else { + res = await fetch('/ping') + } + serverState = await res.json() + if (typeof serverState !== 'object' || typeof serverState.status !== 'string') { + console.error(`Server reply didn't contain a state value.`) + serverState = {'status': ServerStates.unavailable, 'time': Date.now()} + setServerStatus('error', 'offline') + return false + } + // Set status + switch(serverState.status) { + case ServerStates.init: + // Wait for init to complete before updating status. + break + case ServerStates.online: + setServerStatus('online', 'ready') + break + case ServerStates.loadingModel: + setServerStatus('busy', 'loading..') + break + case ServerStates.rendering: + setServerStatus('busy', 'rendering..') + break + default: // Unavailable + console.error('Ping received an unexpected server status. Status: %s', serverState.status) + setServerStatus('error', serverState.status.toLowerCase()) + break + } + serverState.time = Date.now() + return true + } catch (e) { + console.error(e) + serverState = {'status': ServerStates.unavailable, 'time': Date.now()} + setServerStatus('error', 'offline') + } + return false + } + + function isServerAvailable() { + if (typeof serverState !== 'object') { + console.error('serverState not set to a value. Connection to server could be lost...') + return false + } + if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { + console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connection to server could be lost...') + return false + } + switch (serverState.status) { + case ServerStates.loadingModel: + case ServerStates.rendering: + case ServerStates.online: + return true + default: + console.warn('Unexpected server status. Server could be unavailable... Status: %s', serverState.status) + return false + } + } + + async function waitUntil(isReadyFn, delay, timeout) { + if (typeof delay === 'number') { + const msDelay = delay + delay = () => asyncDelay(msDelay) + } + if (typeof delay !== 'function') { + throw new Error('delay is not a number or a function.') + } + if (typeof timeout !== 'undefined' && typeof timeout !== 'number') { + throw new Error('timeout is not a number.') + } + if (typeof timeout === 'undefined' || timeout < 0) { + timeout = Number.MAX_SAFE_INTEGER + } + timeout = Date.now() + timeout + while (timeout > Date.now() + && Date.now() < serverState.time + SERVER_STATE_VALIDITY_DURATION + && !Boolean(await Promise.resolve(isReadyFn())) + ) { + await delay() + if (!isServerAvailable()) { // Can fail if ping got frozen/suspended... + if (await healthCheck() && isServerAvailable()) { // Force a recheck of server status before failure... + continue // Continue waiting if last healthCheck confirmed the server is still alive. + } + throw new Error('Connection with server lost.') + } + } + if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) { + console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Released waitUntil on stale server state.') + } + } + + const TaskStatus = { + init: 'init', + pending: 'pending', // Queued locally, not yet posted to server + waiting: 'waiting', // Waiting to run on server + processing: 'processing', + stopped: 'stopped', + completed: 'completed', + failed: 'failed', + } + Object.freeze(TaskStatus) + + const TASK_STATUS_ORDER = [ + TaskStatus.init, + TaskStatus.pending, + TaskStatus.waiting, + TaskStatus.processing, + //Don't add status that are final. + ] + + const task_queue = new Map() + const concurrent_generators = new Map() + const weak_results = new WeakMap() + + class Task { + // Private properties... + _reqBody = {} // request body of this task. + #reader = undefined + #status = TaskStatus.init + #id = undefined + #exception = undefined + + constructor(options={}) { + this._reqBody = Object.assign({}, options) + if (typeof this._reqBody.session_id === 'undefined') { + this._reqBody.session_id = sessionId + } else if (this._reqBody.session_id !== SD.sessionId && String(this._reqBody.session_id) !== String(SD.sessionId)) { + throw new Error('Use SD.sessionId to set the request session_id.') + } + this._reqBody.session_id = String(this._reqBody.session_id) + } + + get id() { + return this.#id + } + _setId(id) { + if (typeof this.#id !== 'undefined') { + throw new Error('The task ID can only be set once.') + } + this.#id = id + } + + get exception() { + return this.#exception + } + async abort(exception) { + if (this.isCompleted || this.isStopped || this.hasFailed) { + return + } + if (typeof exception !== 'undefined') { + if (typeof exception === 'string') { + exception = new Error(exception) + } + if (typeof exception !== 'object') { + throw new Error('exception is not an object.') + } + if (!(exception instanceof Error)) { + throw new Error('exception is not an Error or a string.') + } + } + const res = await fetch('/image/stop?task=' + this.id) + if (!res.ok) { + console.log('Stop response:', res) + throw new Error(res.statusText) + } + task_queue.delete(this) + this.#exception = exception + this.#status = (exception ? TaskStatus.failed : TaskStatus.stopped) + } + + get reqBody() { + if (this.#status === TaskStatus.init) { + return this._reqBody + } + console.warn('Task reqBody cannot be changed after the init state.') + return Object.assign({}, this._reqBody) + } + + get isPending() { + return TASK_STATUS_ORDER.indexOf(this.#status) >= 0 + } + get isCompleted() { + return this.#status === TaskStatus.completed + } + get hasFailed() { + return this.#status === TaskStatus.failed + } + get isStopped() { + return this.#status === TaskStatus.stopped + } + get status() { + return this.#status + } + _setStatus(status) { + if (status === this.#status) { + return + } + const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status) + if (currentIdx < 0) { + throw Error(`The task status ${this.#status} is final and can't be changed.`) + } + const newIdx = TASK_STATUS_ORDER.indexOf(status) + if (newIdx >= 0 && newIdx < currentIdx) { + throw Error(`The task status ${status} can't replace ${this.#status}.`) + } + this.#status = status + } + + /** Send current task to server. + * @param {*} [timeout=-1] Optional timeout value in ms + * @returns the response from the render request. + * @memberof Task + */ + async post(url, timeout=-1) { + if(this.status !== TaskStatus.init && this.status !== TaskStatus.pending) { + throw new Error(`Task status ${this.status} is not valid for post.`) + } + this._setStatus(TaskStatus.pending) + Object.freeze(this._reqBody) + + const abortSignal = (timeout >= 0 ? AbortSignal.timeout(timeout) : undefined) + let res = undefined + try { + this.checkReqBody() + do { + abortSignal?.throwIfAborted() + res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(this._reqBody), + signal: abortSignal + }) + // status_code 503, already a task running. + } while (res.status === 503 && await asyncDelay(RETRY_DELAY_IF_SERVER_IS_BUSY)) + } catch (err) { + this.abort(err) + throw err + } + if (!res.ok) { + const err = new Error(`Unexpected response HTTP${res.status}. Details: ${res.statusText}`) + this.abort(err) + throw err + } + return await res.json() + } + + static getReader(url) { + const reader = new ChunkedStreamReader(url) + const parseToString = reader.parse + reader.parse = function(value) { + value = parseToString.call(this, value) + if (!value || value.length <= 0) { + return + } + return reader.readStreamAsJSON(value.join('')) + } + reader.onNext = function({done, value}) { + // By default is completed when the return value has a status defined. + if (typeof value === 'object' && 'status' in value) { + done = true + } + return {done, value} + } + return reader + } + _setReader(reader) { + if (typeof this.#reader !== 'undefined') { + throw new Error('The task reader can only be set once.') + } + this.#reader = reader + } + get reader() { + if (this.#reader) { + return this.#reader + } + if (!this.streamUrl) { + throw new Error('The task has no stream Url defined.') + } + this.#reader = Task.getReader(this.streamUrl) + const task = this + const onNext = this.#reader.onNext + this.#reader.onNext = function({done, value}) { + if (value && typeof value === 'object') { + if (task.status === TaskStatus.init + || task.status === TaskStatus.pending + || task.status === TaskStatus.waiting + ) { + task._setStatus(TaskStatus.processing) + } + if ('step' in value && 'total_steps' in value) { + task.step = value.step + task.total_steps = value.total_steps + } + } + return onNext.call(this, {done, value}) + } + this.#reader.onComplete = function(value) { + task.result = value + if (task.isPending) { + task._setStatus(TaskStatus.completed) + } + return value + } + this.#reader.onError = function(response) { + const err = new Error(response.statusText) + task.abort(err) + throw err + } + return this.#reader + } + + async waitUntil({timeout=-1, callback, status, signal}) { + const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status) + if (currentIdx <= 0) { + return false + } + const stIdx = (status ? TASK_STATUS_ORDER.indexOf(status) : currentIdx + 1) + if (stIdx >= 0 && stIdx <= currentIdx) { + return true + } + if (stIdx < 0 && currentIdx < 0) { + return this.#status === (status || TaskStatus.completed) + } + if (signal?.aborted) { + return false + } + const task = this + switch(this.#status) { + case TaskStatus.pending: + case TaskStatus.waiting: + // Wait for server status to include this task. + await waitUntil( + async () => { + if (task.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) { + return true + } + if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { + return true + } + }, + TASK_STATE_SERVER_UPDATE_DELAY, + timeout, + ) + if (this.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) { + this._setStatus(TaskStatus.waiting) + } + if (await Promise.resolve(callback?.call(this)) || signal?.aborted) { + return false + } + if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.waiting)) { + return true + } + // Wait for task to start on server. + await waitUntil( + async () => { + if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'pending') { + return true + } + if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { + return true + } + }, + TASK_STATE_SERVER_UPDATE_DELAY, + timeout, + ) + const state = (typeof serverState.tasks === 'object' ? serverState.tasks[String(task.#id)] : undefined) + if (state === 'running' || state === 'buffer' || state === 'completed') { + this._setStatus(TaskStatus.processing) + } + if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { + return false + } + if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.processing)) { + return true + } + case TaskStatus.processing: + await waitUntil( + async () => { + if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'running') { + return true + } + if (await Promise.resolve(callback?.call(task)) || signal?.aborted) { + return true + } + }, + TASK_STATE_SERVER_UPDATE_DELAY, + timeout, + ) + await Promise.resolve(callback?.call(this)) + default: + return this.#status === (status || TaskStatus.completed) + } + } + + async enqueue(promiseGenerator, ...args) { + if (this.status !== TaskStatus.init) { + throw new Error(`Task is in an invalid status ${this.status} to add to queue.`) + } + this._setStatus(TaskStatus.pending) + task_queue.set(this, promiseGenerator) + await eventSource.fireEvent(EVENT_TASK_QUEUED, {task:this}) + await Task.enqueue(promiseGenerator, ...args) + await this.waitUntil({status: TaskStatus.completed}) + if (this.exception) { + throw this.exception + } + return this.result + } + static async enqueue(promiseGenerator, ...args) { + if (typeof promiseGenerator === 'undefined') { + throw new Error('To enqueue a concurrent task, a *Promise Generator is needed but undefined was found.') + } + //if (Symbol.asyncIterator in result || Symbol.iterator in result) { + //concurrent_generators.set(result, Promise.resolve(args)) + if (typeof promiseGenerator === 'function') { + concurrent_generators.set(asGenerator({callback: promiseGenerator}), Promise.resolve(args)) + } else { + concurrent_generators.set(promiseGenerator, Promise.resolve(args)) + } + await waitUntil(() => !concurrent_generators.has(promiseGenerator), CONCURRENT_TASK_INTERVAL) + return weak_results.get(promiseGenerator) + } + static enqueueNew(task, classCtor, progressCallback) { + if (task.status !== TaskStatus.init) { + throw new Error('Task has an invalid status to add to queue.') + } + if (!(task instanceof classCtor)) { + throw new Error('Task is not a instance of classCtor.') + } + let promiseGenerator = undefined + if (typeof progressCallback === 'undefined') { + promiseGenerator = classCtor.start(task) + } else if (typeof progressCallback === 'function') { + promiseGenerator = classCtor.start(task, progressCallback) + } else { + throw new Error('progressCallback is not a function.') + } + return Task.prototype.enqueue.call(task, promiseGenerator) + } + + static async run(promiseGenerator, {callback, signal, timeout=-1}={}) { + let value = undefined + let done = undefined + if (timeout < 0) { + timeout = Number.MAX_SAFE_INTEGER + } + timeout = Date.now() + timeout + do { + ({value, done} = await Promise.resolve(promiseGenerator.next(value))) + if (value instanceof Promise) { + value = await value + } + if (callback) { + ({value, done} = await Promise.resolve(callback.call(promiseGenerator, {value, done}))) + } + if (value instanceof Promise) { + value = await value + } + } while(!done && !signal?.aborted && timeout > Date.now()) + return value + } + static async *asGenerator({callback, generator, signal, timeout=-1}={}) { + let value = undefined + let done = undefined + if (timeout < 0) { + timeout = Number.MAX_SAFE_INTEGER + } + timeout = Date.now() + timeout + do { + ({value, done} = await Promise.resolve(generator.next(value))) + if (value instanceof Promise) { + value = await value + } + if (callback) { + ({value, done} = await Promise.resolve(callback.call(generator, {value, done}))) + if (value instanceof Promise) { + value = await value + } + } + value = yield value + } while(!done && !signal?.aborted && timeout > Date.now()) + return value + } + } + + const TASK_REQUIRED = { + "session_id": 'string', + "prompt": 'string', + "negative_prompt": 'string', + "width": 'number', + "height": 'number', + "seed": 'number', + + "sampler_name": 'string', + "use_stable_diffusion_model": 'string', + "num_inference_steps": 'number', + "guidance_scale": 'number', + + "num_outputs": 'number', + "stream_progress_updates": 'boolean', + "stream_image_progress": 'boolean', + "show_only_filtered_image": 'boolean', + "output_format": 'string', + "output_quality": 'number', + } + const TASK_DEFAULTS = { + "sampler_name": "plms", + "use_stable_diffusion_model": "sd-v1-4", + "num_inference_steps": 50, + "guidance_scale": 7.5, + "negative_prompt": "", + + "num_outputs": 1, + "stream_progress_updates": true, + "stream_image_progress": true, + "show_only_filtered_image": true, + "block_nsfw": false, + "output_format": "png", + "output_quality": 75, + } + const TASK_OPTIONAL = { + "device": 'string', + "init_image": 'string', + "mask": 'string', + "save_to_disk_path": 'string', + "use_face_correction": 'string', + "use_upscale": 'string', + "use_vae_model": 'string', + "use_hypernetwork_model": 'string', + "hypernetwork_strength": 'number', + } + + // Higer values will result in... + // pytorch_lightning/utilities/seed.py:60: UserWarning: X is not in bounds, numpy accepts from 0 to 4294967295 + const MAX_SEED_VALUE = 4294967295 + + class RenderTask extends Task { + constructor(options={}) { + super(options) + if (typeof this._reqBody.seed === 'undefined') { + this._reqBody.seed = Math.floor(Math.random() * (MAX_SEED_VALUE + 1)) + } + if (typeof typeof this._reqBody.seed === 'number' && (this._reqBody.seed > MAX_SEED_VALUE || this._reqBody.seed < 0)) { + throw new Error(`seed must be in range 0 to ${MAX_SEED_VALUE}.`) + } + + if ('use_cpu' in this._reqBody) { + if (this._reqBody.use_cpu) { + this._reqBody.device = 'cpu' + } + delete this._reqBody.use_cpu + } + if (this._reqBody.init_image) { + if (typeof this._reqBody.prompt_strength === 'undefined') { + this._reqBody.prompt_strength = 0.8 + } else if (typeof this._reqBody.prompt_strength !== 'number') { + throw new Error(`prompt_strength need to be of type number but ${typeof this._reqBody.prompt_strength} was found.`) + } + } + if ('modifiers' in this._reqBody) { + if (Array.isArray(this._reqBody.modifiers) && this._reqBody.modifiers.length > 0) { + this._reqBody.modifiers = this._reqBody.modifiers.filter((val) => val.trim()) + if (this._reqBody.modifiers.length > 0) { + this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers.join(', ')}` + } + } + if (typeof this._reqBody.modifiers === 'string' && this._reqBody.modifiers.length > 0) { + this._reqBody.modifiers = this._reqBody.modifiers.trim() + if (this._reqBody.modifiers.length > 0) { + this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers}` + } + } + delete this._reqBody.modifiers + } + this.checkReqBody() + } + + checkReqBody() { + for (const key in TASK_DEFAULTS) { + if (typeof this._reqBody[key] === 'undefined') { + this._reqBody[key] = TASK_DEFAULTS[key] + } + } + for (const key in TASK_REQUIRED) { + if (typeof this._reqBody[key] !== TASK_REQUIRED[key]) { + throw new Error(`${key} need to be of type ${TASK_REQUIRED[key]} but ${typeof this._reqBody[key]} was found.`) + } + } + for (const key in this._reqBody) { + if (key in TASK_REQUIRED) { + continue + } + if (key in TASK_OPTIONAL) { + if (typeof this._reqBody[key] == "undefined") { + delete this._reqBody[key] + console.warn(`reqBody[${key}] was set to undefined. Removing optional key without value...`) + continue + } + if (typeof this._reqBody[key] !== TASK_OPTIONAL[key]) { + throw new Error(`${key} need to be of type ${TASK_OPTIONAL[key]} but ${typeof this._reqBody[key]} was found.`) + } + } + } + } + + /** Send current task to server. + * @param {*} [timeout=-1] Optional timeout value in ms + * @returns the response from the render request. + * @memberof Task + */ + async post(timeout=-1) { + performance.mark('make-render-request') + if (performance.getEntriesByName('click-makeImage', 'mark').length > 0) { + performance.measure('diff', 'click-makeImage', 'make-render-request') + console.log('delay between clicking and making the server request:', performance.getEntriesByName('diff', 'measure')[0].duration + ' ms') + } + + let jsonResponse = await super.post('/render', timeout) + if (typeof jsonResponse?.task !== 'number') { + console.warn('Endpoint error response: ', jsonResponse) + const event = Object.assign({task:this}, jsonResponse) + await eventSource.fireEvent(EVENT_UNEXPECTED_RESPONSE, event) + if ('continueWith' in event) { + jsonResponse = await Promise.resolve(event.continueWith) + } + if (typeof jsonResponse?.task !== 'number') { + const err = new Error(jsonResponse?.detail || 'Endpoint response does not contains a task ID.') + this.abort(err) + throw err + } + } + this._setId(jsonResponse.task) + if (jsonResponse.stream) { + this.streamUrl = jsonResponse.stream + } + this._setStatus(TaskStatus.waiting) + return jsonResponse + } + + enqueue(progressCallback) { + return Task.enqueueNew(this, RenderTask, progressCallback) + } + *start(progressCallback) { + if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') { + throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback) + } + if (this.isStopped) { + return + } + + this._setStatus(TaskStatus.pending) + progressCallback?.call(this, {reqBody: this._reqBody}) + Object.freeze(this._reqBody) + + // Post task request to backend + let renderRequest = undefined + try { + renderRequest = yield this.post() + yield progressCallback?.call(this, {renderResponse: renderRequest}) + } catch (e) { + yield progressCallback?.call(this, { detail: e.message }) + throw e + } + try { // Wait for task to start on server. + yield this.waitUntil({ + callback: function() { return progressCallback?.call(this, {}) }, + status: TaskStatus.processing, + }) + } catch (e) { + this.abort(err) + throw e + } + // Update class status and callback. + const taskState = (typeof serverState.tasks === 'object' ? serverState.tasks[String(this.id)] : undefined) + switch(taskState) { + case 'pending': // Session has pending tasks. + console.error('Server %o render request %o is still waiting.', serverState, renderRequest) + //Only update status if not already set by waitUntil + if (this.status === TaskStatus.init + || this.status === TaskStatus.pending + ) { + // Set status as Waiting in backend. + this._setStatus(TaskStatus.waiting) + } + break + case 'running': + case 'buffer': + // Normal expected messages. + this._setStatus(TaskStatus.processing) + break + case 'completed': + if (this.isPending) { + // Set state to processing until we read the reply + this._setStatus(TaskStatus.processing) + } + console.warn('Server %o render request %o completed unexpectedly', serverState, renderRequest) + break // Continue anyway to try to read cached result. + case 'error': + this._setStatus(TaskStatus.failed) + console.error('Server %o render request %o has failed', serverState, renderRequest) + break // Still valid, Update UI with error message + case 'stopped': + this._setStatus(TaskStatus.stopped) + console.log('Server %o render request %o was stopped', serverState, renderRequest) + return false + default: + if (!progressCallback) { + const err = new Error('Unexpected server task state: ' + taskState || 'Undefined') + this.abort(err) + throw err + } + const response = yield progressCallback.call(this, {}) + if (response instanceof Error) { + this.abort(response) + throw response + } + if (!response) { + return false + } + } + + // Task started! + // Open the reader. + const reader = this.reader + const task = this + reader.onError = function(response) { + if (progressCallback) { + task.abort(new Error(response.statusText)) + return progressCallback.call(task, { response, reader }) + } + return Task.prototype.onError.call(task, response) + } + yield progressCallback?.call(this, { reader }) + + //Start streaming the results. + const streamGenerator = reader.open() + let value = undefined + let done = undefined + yield progressCallback?.call(this, { stream: streamGenerator }) + do { + ({value, done} = yield streamGenerator.next()) + if (typeof value !== 'object') { + continue + } + yield progressCallback?.call(this, { update: value }) + } while(!done) + return value + } + static start(task, progressCallback) { + if (typeof task !== 'object') { + throw new Error ('task is not an object. task type: ' + typeof task) + } + if (!(task instanceof Task)) { + if (task.reqBody) { + task = new RenderTask(task.reqBody) + } else { + task = new RenderTask(task) + } + } + return task.start(progressCallback) + } + static run(task, progressCallback) { + const promiseGenerator = RenderTask.start(task, progressCallback) + return Task.run(promiseGenerator) + } + } + class FilterTask extends Task { + constructor(options={}) { + } + /** Send current task to server. + * @param {*} [timeout=-1] Optional timeout value in ms + * @returns the response from the render request. + * @memberof Task + */ + async post(timeout=-1) { + let jsonResponse = await super.post('/filter', timeout) + //this._setId(jsonResponse.task) + this._setStatus(TaskStatus.waiting) + } + enqueue(progressCallback) { + return Task.enqueueNew(this, FilterTask, progressCallback) + } + *start(progressCallback) { + if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') { + throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback) + } + if (this.isStopped) { + return + } + } + static start(task, progressCallback) { + if (typeof task !== 'object') { + throw new Error ('task is not an object. task type: ' + typeof task) + } + if (!(task instanceof Task)) { + if (task.reqBody) { + task = new FilterTask(task.reqBody) + } else { + task = new FilterTask(task) + } + } + return task.start(progressCallback) + } + static run(task, progressCallback) { + const promiseGenerator = FilterTask.start(task, progressCallback) + return Task.run(promiseGenerator) + } + } + + const getSystemInfo = debounce(async function() { + let systemInfo = { + devices: { + all: {}, + active: {}, + }, + hosts: [] + } + try { + const res = await fetch('/get/system_info') + if (!res.ok) { + console.error('Invalid response fetching devices', res.statusText) + return systemInfo + } + systemInfo = await res.json() + } catch (e) { + console.error('error fetching system info', e) + } + return systemInfo + }, 250, true) + async function getDevices() { + let systemInfo = getSystemInfo() + return systemInfo.devices + } + async function getHosts() { + let systemInfo = getSystemInfo() + return systemInfo.hosts + } + + async function getModels() { + let models = { + 'stable-diffusion': [], + 'vae': [], + } + try { + const res = await fetch('/get/models') + if (!res.ok) { + console.error('Invalid response fetching models', res.statusText) + return models + } + models = await res.json() + console.log('get models response', models) + } catch (e) { + console.log('get models error', e) + } + return models + } + + function getServerCapacity() { + let activeDevicesCount = Object.keys(serverState?.devices?.active || {}).length + if (typeof window === "object" && window.document.visibilityState === 'hidden') { + activeDevicesCount = 1 + activeDevicesCount + } + return activeDevicesCount + } + + let idleEventPromise = undefined + function continueTasks() { + if (typeof navigator?.scheduling?.isInputPending === 'function') { + const inputPendingOptions = { + // Report mouse/pointer move events when queue is empty. + // Delay idle after mouse moves stops. + includeContinuous: Boolean(task_queue.size <= 0 && concurrent_generators.size <= 0) + } + if (navigator.scheduling.isInputPending(inputPendingOptions)) { + // Browser/User still active. + return asyncDelay(CONCURRENT_TASK_INTERVAL) + } + } + const serverCapacity = getServerCapacity() + if (task_queue.size <= 0 && concurrent_generators.size <= 0) { + if (!idleEventPromise?.isPending) { + idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity, idle: true})) + } + // Calling idle could result in task being added to queue. + // if (task_queue.size <= 0 && concurrent_generators.size <= 0) { + // return asyncDelay(IDLE_COOLDOWN).then(() => idleEventPromise) + // } + } + if (task_queue.size < serverCapacity) { + if (!idleEventPromise?.isPending) { + idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity - task_queue.size})) + } + } + const completedTasks = [] + for (let [generator, promise] of concurrent_generators.entries()) { + if (promise.isPending) { + continue + } + let value = promise.resolvedValue?.value || promise.resolvedValue + if (promise.isRejected) { + console.error(promise.rejectReason) + const event = {generator, reason: promise.rejectReason} + eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, event) + if ('continueWith' in event) { + value = Promise.resolve(event.continueWith) + } else { + concurrent_generators.delete(generator) + completedTasks.push({generator, promise}) + continue + } + } + if (value instanceof Promise) { + promise = makeQuerablePromise(value.then((val) => ({done: promise.resolvedValue?.done, value: val}))) + concurrent_generators.set(generator, promise) + continue + } + weak_results.set(generator, value) + if (promise.resolvedValue?.done) { + concurrent_generators.delete(generator) + completedTasks.push({generator, promise}) + continue + } + + promise = generator.next(value) + if (!(promise instanceof Promise)) { + promise = Promise.resolve(promise) + } + promise = makeQuerablePromise(promise) + concurrent_generators.set(generator, promise) + } + + for (let [task, generator] of task_queue.entries()) { + const cTsk = completedTasks.find((item) => item.generator === generator) + if (cTsk?.promise?.rejectReason || task.hasFailed) { + eventSource.fireEvent(EVENT_TASK_ERROR, {task, generator, reason: cTsk?.promise?.rejectReason || task.exception }) + task_queue.delete(task) + continue + } + if (task.isCompleted || task.isStopped || cTsk) { + const eventEndArgs = {task, generator} + if (task.isStopped) { + eventEndArgs.stopped = true + } + eventSource.fireEvent(EVENT_TASK_END, eventEndArgs) + task_queue.delete(task) + continue + } + if (concurrent_generators.size > serverCapacity) { + break + } + if (!generator) { + if (typeof task.start === 'function') { + generator = task.start() + } + } else if (concurrent_generators.has(generator)) { + continue + } + const event = {task, generator}; + const beforeStart = eventSource.fireEvent(EVENT_TASK_START, event) // optional beforeStart promise to wait on before starting task. + const promise = makeQuerablePromise(beforeStart.then(() => Promise.resolve(event.beforeStart))) + concurrent_generators.set(event.generator, promise) + task_queue.set(task, event.generator) + } + const promises = Array.from(concurrent_generators.values()) + if (promises.length <= 0) { + return asyncDelay(CONCURRENT_TASK_INTERVAL) + } + return Promise.race(promises).finally(continueTasks) + } + let taskPromise = undefined + function startCheck() { + if (taskPromise?.isPending) { + return + } + do { + if (taskPromise?.resolvedValue instanceof Promise) { + taskPromise = makeQuerablePromise(taskPromise.resolvedValue) + continue + } + if (typeof navigator?.scheduling?.isInputPending === 'function' && navigator.scheduling.isInputPending()) { + return + } + const continuePromise = continueTasks().catch(async function(err) { + console.error(err) + await eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, {reason: err}) + await asyncDelay(RETRY_DELAY_ON_ERROR) + }) + taskPromise = makeQuerablePromise(continuePromise) + } while(taskPromise?.isResolved) + } + + const SD = { + ChunkedStreamReader, + ServerStates, + TaskStatus, + Task, + RenderTask, + FilterTask, + + Events: EVENTS_TYPES, + init: async function(options={}) { + if ('events' in options) { + for (const key in options.events) { + eventSource.addEventListener(key, options.events[key]) + } + } + await healthCheck() + setInterval(healthCheck, HEALTH_PING_INTERVAL) + setInterval(startCheck, CONCURRENT_TASK_INTERVAL) + }, + + /** Add a new event listener + */ + addEventListener: (...args) => eventSource.addEventListener(...args), + /** Remove the event listener + */ + removeEventListener: (...args) => eventSource.removeEventListener(...args), + + isServerAvailable, + getServerCapacity, + + getSystemInfo, + getDevices, + getHosts, + + getModels, + + render: (...args) => RenderTask.run(...args), + filter: (...args) => FilterTask.run(...args), + waitUntil, + }; + + Object.defineProperties(SD, { + serverState: { + configurable: false, + get: () => serverState, + }, + isAvailable: { + configurable: false, + get: () => isServerAvailable(), + }, + serverCapacity: { + configurable: false, + get: () => getServerCapacity(), + }, + sessionId: { + configurable: false, + get: () => sessionId, + set: (val) => { + if (typeof val === 'undefined') { + throw new Error("Can't set sessionId to undefined.") + } + sessionId = val + }, + }, + MAX_SEED_VALUE: { + configurable: false, + get: () => MAX_SEED_VALUE, + }, + activeTasks: { + configurable: false, + get: () => task_queue, + }, + }) + Object.defineProperties(getGlobal(), { + SD: { + configurable: false, + get: () => SD, + }, + sessionId: { //TODO Remove in the future in favor of SD.sessionId + configurable: false, + get: () => { + console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.') + console.trace() + return SD.sessionId + }, + set: (val) => { + console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.') + console.trace() + SD.sessionId = val + } + } + }) +})() diff --git a/ui/media/js/image-editor.js b/ui/media/js/image-editor.js new file mode 100644 index 0000000000000000000000000000000000000000..4597e983240d9b51875a236a3d71864b07429190 --- /dev/null +++ b/ui/media/js/image-editor.js @@ -0,0 +1,876 @@ +var editorControlsLeft = document.getElementById("image-editor-controls-left") + +const IMAGE_EDITOR_MAX_SIZE = 800 + +const IMAGE_EDITOR_BUTTONS = [ + { + name: "Cancel", + icon: "fa-regular fa-circle-xmark", + handler: editor => { + editor.hide() + } + }, + { + name: "Save", + icon: "fa-solid fa-floppy-disk", + handler: editor => { + editor.saveImage() + } + } +] + +const defaultToolBegin = (editor, ctx, x, y, is_overlay = false) => { + ctx.beginPath() + ctx.moveTo(x, y) +} +const defaultToolMove = (editor, ctx, x, y, is_overlay = false) => { + ctx.lineTo(x, y) + if (is_overlay) { + ctx.clearRect(0, 0, editor.width, editor.height) + ctx.stroke() + } +} +const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => { + ctx.stroke() + if (is_overlay) { + ctx.clearRect(0, 0, editor.width, editor.height) + } +} +const toolDoNothing = (editor, ctx, x, y, is_overlay = false) => {} + +const IMAGE_EDITOR_TOOLS = [ + { + id: "draw", + name: "Draw", + icon: "fa-solid fa-pencil", + cursor: "url(/media/images/fa-pencil.svg) 0 24, pointer", + begin: defaultToolBegin, + move: defaultToolMove, + end: defaultToolEnd + }, + { + id: "erase", + name: "Erase", + icon: "fa-solid fa-eraser", + cursor: "url(/media/images/fa-eraser.svg) 0 14, pointer", + begin: defaultToolBegin, + move: (editor, ctx, x, y, is_overlay = false) => { + ctx.lineTo(x, y) + if (is_overlay) { + ctx.clearRect(0, 0, editor.width, editor.height) + ctx.globalCompositeOperation = "source-over" + ctx.globalAlpha = 1 + ctx.filter = "none" + ctx.drawImage(editor.canvas_current, 0, 0) + editor.setBrush(editor.layers.overlay) + ctx.stroke() + editor.canvas_current.style.opacity = 0 + } + }, + end: (editor, ctx, x, y, is_overlay = false) => { + ctx.stroke() + if (is_overlay) { + ctx.clearRect(0, 0, editor.width, editor.height) + editor.canvas_current.style.opacity = "" + } + }, + setBrush: (editor, layer) => { + layer.ctx.globalCompositeOperation = "destination-out" + } + }, + { + id: "fill", + name: "Fill", + icon: "fa-solid fa-fill", + cursor: "url(/media/images/fa-fill.svg) 20 6, pointer", + begin: (editor, ctx, x, y, is_overlay = false) => { + if (!is_overlay) { + var color = hexToRgb(ctx.fillStyle) + color.a = parseInt(ctx.globalAlpha * 255) // layer.ctx.globalAlpha + flood_fill(editor, ctx, parseInt(x), parseInt(y), color) + } + }, + move: toolDoNothing, + end: toolDoNothing + }, + { + id: "colorpicker", + name: "Picker", + icon: "fa-solid fa-eye-dropper", + cursor: "url(/media/images/fa-eye-dropper.svg) 0 24, pointer", + begin: (editor, ctx, x, y, is_overlay = false) => { + if (!is_overlay) { + var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data + var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data + var drawn_opacity = drawn_rgb[3] / 255 + editor.custom_color_input.value = rgbToHex({ + r: (drawn_rgb[0] * drawn_opacity) + (img_rgb[0] * (1 - drawn_opacity)), + g: (drawn_rgb[1] * drawn_opacity) + (img_rgb[1] * (1 - drawn_opacity)), + b: (drawn_rgb[2] * drawn_opacity) + (img_rgb[2] * (1 - drawn_opacity)), + }) + editor.custom_color_input.dispatchEvent(new Event("change")) + } + }, + move: toolDoNothing, + end: toolDoNothing + } +] + +const IMAGE_EDITOR_ACTIONS = [ + { + id: "load_mask", + name: "Load mask from file", + className: "load_mask", + icon: "fa-regular fa-folder-open", + handler: (editor) => { + let el = document.createElement('input') + el.setAttribute("type", "file") + el.addEventListener("change", function() { + if (this.files.length === 0) { + return + } + + let reader = new FileReader() + let file = this.files[0] + + reader.addEventListener('load', function(event) { + let maskData = reader.result + + editor.layers.drawing.ctx.clearRect(0, 0, editor.width, editor.height) + var image = new Image() + image.onload = () => { + editor.layers.drawing.ctx.drawImage(image, 0, 0, editor.width, editor.height) + } + image.src = maskData + }) + + if (file) { + reader.readAsDataURL(file) + } + }) + + el.click() + }, + trackHistory: true + }, + { + id: "fill_all", + name: "Fill all", + icon: "fa-solid fa-paint-roller", + handler: (editor) => { + editor.ctx_current.globalCompositeOperation = "source-over" + editor.ctx_current.rect(0, 0, editor.width, editor.height) + editor.ctx_current.fill() + editor.setBrush() + }, + trackHistory: true + }, + { + id: "clear", + name: "Clear", + icon: "fa-solid fa-xmark", + handler: (editor) => { + editor.ctx_current.clearRect(0, 0, editor.width, editor.height) + }, + trackHistory: true + }, + { + id: "undo", + name: "Undo", + icon: "fa-solid fa-rotate-left", + handler: (editor) => { + editor.history.undo() + }, + trackHistory: false + }, + { + id: "redo", + name: "Redo", + icon: "fa-solid fa-rotate-right", + handler: (editor) => { + editor.history.redo() + }, + trackHistory: false + } +] + +var IMAGE_EDITOR_SECTIONS = [ + { + name: "tool", + title: "Tool", + default: "draw", + options: Array.from(IMAGE_EDITOR_TOOLS.map(t => t.id)), + initElement: (element, option) => { + var tool_info = IMAGE_EDITOR_TOOLS.find(t => t.id == option) + element.className = "image-editor-button button" + var sub_element = document.createElement("div") + var icon = document.createElement("i") + tool_info.icon.split(" ").forEach(c => icon.classList.add(c)) + sub_element.appendChild(icon) + sub_element.append(tool_info.name) + element.appendChild(sub_element) + } + }, + { + name: "color", + title: "Color", + default: "#f1c232", + options: [ + "custom", + "#ea9999", "#e06666", "#cc0000", "#990000", "#660000", + "#f9cb9c", "#f6b26b", "#e69138", "#b45f06", "#783f04", + "#ffe599", "#ffd966", "#f1c232", "#bf9000", "#7f6000", + "#b6d7a8", "#93c47d", "#6aa84f", "#38761d", "#274e13", + "#a4c2f4", "#6d9eeb", "#3c78d8", "#1155cc", "#1c4587", + "#b4a7d6", "#8e7cc3", "#674ea7", "#351c75", "#20124d", + "#d5a6bd", "#c27ba0", "#a64d79", "#741b47", "#4c1130", + "#ffffff", "#c0c0c0", "#838383", "#525252", "#000000", + ], + initElement: (element, option) => { + if (option == "custom") { + var input = document.createElement("input") + input.type = "color" + element.appendChild(input) + var span = document.createElement("span") + span.textContent = "Custom" + span.onclick = function(e) { + input.click() + } + element.appendChild(span) + } + else { + element.style.background = option + } + }, + getCustom: editor => { + var input = editor.popup.querySelector(".image_editor_color input") + return input.value + } + }, + { + name: "brush_size", + title: "Brush Size", + default: 48, + options: [ 6, 12, 16, 24, 30, 40, 48, 64 ], + initElement: (element, option) => { + element.parentElement.style.flex = option + element.style.width = option + "px" + element.style.height = option + "px" + element.style['margin-right'] = '2px' + element.style["border-radius"] = (option / 2).toFixed() + "px" + } + }, + { + name: "opacity", + title: "Opacity", + default: 0, + options: [ 0, 0.2, 0.4, 0.6, 0.8 ], + initElement: (element, option) => { + element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px` + } + }, + { + name: "sharpness", + title: "Sharpness", + default: 0, + options: [ 0, 0.05, 0.1, 0.2, 0.3 ], + initElement: (element, option) => { + var size = 32 + var blur_amount = parseInt(option * size) + var sub_element = document.createElement("div") + sub_element.style.background = `var(--background-color3)` + sub_element.style.filter = `blur(${blur_amount}px)` + sub_element.style.width = `${size - 2}px` + sub_element.style.height = `${size - 2}px` + sub_element.style['border-radius'] = `${size}px` + element.style.background = "none" + element.appendChild(sub_element) + } + } +] + +class EditorHistory { + constructor(editor) { + this.editor = editor + this.events = [] // stack of all events (actions/edits) + this.current_edit = null + this.rewind_index = 0 // how many events back into the history we've rewound to. (current state is just after event at index 'length - this.rewind_index - 1') + } + push(event) { + // probably add something here eventually to save state every x events + if (this.rewind_index != 0) { + this.events = this.events.slice(0, 0 - this.rewind_index) + this.rewind_index = 0 + } + var snapshot_frequency = 20 // (every x edits, take a snapshot of the current drawing state, for faster rewinding) + if (this.events.length > 0 && this.events.length % snapshot_frequency == 0) { + event.snapshot = this.editor.layers.drawing.ctx.getImageData(0, 0, this.editor.width, this.editor.height) + } + this.events.push(event) + } + pushAction(action) { + this.push({ + type: "action", + id: action + }); + } + editBegin(x, y) { + this.current_edit = { + type: "edit", + id: this.editor.getOptionValue("tool"), + options: Object.assign({}, this.editor.options), + points: [ { x: x, y: y } ] + } + } + editMove(x, y) { + if (this.current_edit) { + this.current_edit.points.push({ x: x, y: y }) + } + } + editEnd(x, y) { + if (this.current_edit) { + this.push(this.current_edit) + this.current_edit = null + } + } + clear() { + this.events = [] + } + undo() { + this.rewindTo(this.rewind_index + 1) + } + redo() { + this.rewindTo(this.rewind_index - 1) + } + rewindTo(new_rewind_index) { + if (new_rewind_index < 0 || new_rewind_index > this.events.length) { + return; // do nothing if target index is out of bounds + } + + var ctx = this.editor.layers.drawing.ctx + ctx.clearRect(0, 0, this.editor.width, this.editor.height) + + var target_index = this.events.length - 1 - new_rewind_index + var snapshot_index = target_index + while (snapshot_index > -1) { + if (this.events[snapshot_index].snapshot) { + break + } + snapshot_index-- + } + + if (snapshot_index != -1) { + ctx.putImageData(this.events[snapshot_index].snapshot, 0, 0); + } + + for (var i = (snapshot_index + 1); i <= target_index; i++) { + var event = this.events[i] + if (event.type == "action") { + var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == event.id) + action.handler(this.editor) + } + else if (event.type == "edit") { + var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == event.id) + this.editor.setBrush(this.editor.layers.drawing, event.options) + + var first_point = event.points[0] + tool.begin(this.editor, ctx, first_point.x, first_point.y) + for (var point_i = 1; point_i < event.points.length; point_i++) { + tool.move(this.editor, ctx, event.points[point_i].x, event.points[point_i].y) + } + var last_point = event.points[event.points.length - 1] + tool.end(this.editor, ctx, last_point.x, last_point.y) + } + } + + // re-set brush to current settings + this.editor.setBrush(this.editor.layers.drawing) + + this.rewind_index = new_rewind_index + } +} + +class ImageEditor { + constructor(popup, inpainter = false) { + this.inpainter = inpainter + this.popup = popup + this.history = new EditorHistory(this) + if (inpainter) { + this.popup.classList.add("inpainter") + } + this.drawing = false + this.temp_previous_tool = null // used for the ctrl-colorpicker functionality + this.container = popup.querySelector(".editor-controls-center > div") + this.layers = {} + var layer_names = [ + "background", + "drawing", + "overlay" + ] + layer_names.forEach(name => { + let canvas = document.createElement("canvas") + canvas.className = `editor-canvas-${name}` + this.container.appendChild(canvas) + this.layers[name] = { + name: name, + canvas: canvas, + ctx: canvas.getContext("2d") + } + }) + + // add mouse handlers + this.container.addEventListener("mousedown", this.mouseHandler.bind(this)) + this.container.addEventListener("mouseup", this.mouseHandler.bind(this)) + this.container.addEventListener("mousemove", this.mouseHandler.bind(this)) + this.container.addEventListener("mouseout", this.mouseHandler.bind(this)) + this.container.addEventListener("mouseenter", this.mouseHandler.bind(this)) + + this.container.addEventListener("touchstart", this.mouseHandler.bind(this)) + this.container.addEventListener("touchmove", this.mouseHandler.bind(this)) + this.container.addEventListener("touchcancel", this.mouseHandler.bind(this)) + this.container.addEventListener("touchend", this.mouseHandler.bind(this)) + + // initialize editor controls + this.options = {} + this.optionElements = {} + IMAGE_EDITOR_SECTIONS.forEach(section => { + section.id = `image_editor_${section.name}` + var sectionElement = document.createElement("div") + sectionElement.className = section.id + + var title = document.createElement("h4") + title.innerText = section.title + sectionElement.appendChild(title) + + var optionsContainer = document.createElement("div") + optionsContainer.classList.add("editor-options-container") + + this.optionElements[section.name] = [] + section.options.forEach((option, index) => { + var optionHolder = document.createElement("div") + var optionElement = document.createElement("div") + optionHolder.appendChild(optionElement) + section.initElement(optionElement, option) + optionElement.addEventListener("click", target => this.selectOption(section.name, index)) + optionsContainer.appendChild(optionHolder) + this.optionElements[section.name].push(optionElement) + }) + this.selectOption(section.name, section.options.indexOf(section.default)) + + sectionElement.appendChild(optionsContainer) + + this.popup.querySelector(".editor-controls-left").appendChild(sectionElement) + }) + + this.custom_color_input = this.popup.querySelector(`input[type="color"]`) + this.custom_color_input.addEventListener("change", () => { + this.custom_color_input.parentElement.style.background = this.custom_color_input.value + this.selectOption("color", 0) + }) + + if (this.inpainter) { + this.selectOption("color", IMAGE_EDITOR_SECTIONS.find(s => s.name == "color").options.indexOf("#ffffff")) + this.selectOption("opacity", IMAGE_EDITOR_SECTIONS.find(s => s.name == "opacity").options.indexOf(0.4)) + } + + // initialize the right-side controls + var buttonContainer = document.createElement("div") + IMAGE_EDITOR_BUTTONS.forEach(button => { + var element = document.createElement("div") + var icon = document.createElement("i") + element.className = "image-editor-button button" + icon.className = button.icon + element.appendChild(icon) + element.append(button.name) + buttonContainer.appendChild(element) + element.addEventListener("click", event => button.handler(this)) + }) + var actionsContainer = document.createElement("div") + var actionsTitle = document.createElement("h4") + actionsTitle.textContent = "Actions" + actionsContainer.appendChild(actionsTitle); + IMAGE_EDITOR_ACTIONS.forEach(action => { + var element = document.createElement("div") + var icon = document.createElement("i") + element.className = "image-editor-button button" + if (action.className) { + element.className += " " + action.className + } + icon.className = action.icon + element.appendChild(icon) + element.append(action.name) + actionsContainer.appendChild(element) + element.addEventListener("click", event => this.runAction(action.id)) + }) + this.popup.querySelector(".editor-controls-right").appendChild(actionsContainer) + this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer) + + this.keyHandlerBound = this.keyHandler.bind(this) + + this.setSize(512, 512) + } + show() { + this.popup.classList.add("active") + document.addEventListener("keydown", this.keyHandlerBound) + document.addEventListener("keyup", this.keyHandlerBound) + } + hide() { + this.popup.classList.remove("active") + document.removeEventListener("keydown", this.keyHandlerBound) + document.removeEventListener("keyup", this.keyHandlerBound) + } + setSize(width, height) { + if (width == this.width && height == this.height) { + return + } + + if (width > height) { + var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768) + var multiplier = max_size / width + width = (multiplier * width).toFixed() + height = (multiplier * height).toFixed() + } + else { + var max_size = Math.min(parseInt(window.innerHeight * 0.9), height, 768) + var multiplier = max_size / height + width = (multiplier * width).toFixed() + height = (multiplier * height).toFixed() + } + this.width = parseInt(width) + this.height = parseInt(height) + + this.container.style.width = width + "px" + this.container.style.height = height + "px" + + Object.values(this.layers).forEach(layer => { + layer.canvas.width = width + layer.canvas.height = height + }) + + if (this.inpainter) { + this.saveImage() // We've reset the size of the image so inpainting is different + } + this.setBrush() + this.history.clear() + } + get tool() { + var tool_id = this.getOptionValue("tool") + return IMAGE_EDITOR_TOOLS.find(t => t.id == tool_id); + } + loadTool() { + this.drawing = false + this.container.style.cursor = this.tool.cursor; + } + setImage(url, width, height) { + this.setSize(width, height) + this.layers.background.ctx.clearRect(0, 0, this.width, this.height) + if (!(url && this.inpainter)) { + this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height) + } + if (url) { + var image = new Image() + image.onload = () => { + this.layers.background.ctx.drawImage(image, 0, 0, this.width, this.height) + } + image.src = url + } + else { + this.layers.background.ctx.fillStyle = "#ffffff" + this.layers.background.ctx.beginPath() + this.layers.background.ctx.rect(0, 0, this.width, this.height) + this.layers.background.ctx.fill() + } + this.history.clear() + } + saveImage() { + if (!this.inpainter) { + // This is not an inpainter, so save the image as the new img2img input + this.layers.background.ctx.drawImage(this.layers.drawing.canvas, 0, 0, this.width, this.height) + var base64 = this.layers.background.canvas.toDataURL() + initImagePreview.src = base64 // this will trigger the rest of the app to use it + } + else { + // This is an inpainter, so make sure the toggle is set accordingly + var is_blank = !this.layers.drawing.ctx + .getImageData(0, 0, this.width, this.height).data + .some(channel => channel !== 0) + maskSetting.checked = !is_blank + } + this.hide() + } + getImg() { // a drop-in replacement of the drawingboard version + return this.layers.drawing.canvas.toDataURL() + } + setImg(dataUrl) { // a drop-in replacement of the drawingboard version + var image = new Image() + image.onload = () => { + var ctx = this.layers.drawing.ctx; + ctx.clearRect(0, 0, this.width, this.height) + ctx.globalCompositeOperation = "source-over" + ctx.globalAlpha = 1 + ctx.filter = "none" + ctx.drawImage(image, 0, 0, this.width, this.height) + this.setBrush(this.layers.drawing) + } + image.src = dataUrl + } + runAction(action_id) { + var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == action_id) + if (action.trackHistory) { + this.history.pushAction(action_id) + } + action.handler(this) + } + setBrush(layer = null, options = null) { + if (options == null) { + options = this.options + } + if (layer) { + layer.ctx.lineCap = "round" + layer.ctx.lineJoin = "round" + layer.ctx.lineWidth = options.brush_size + layer.ctx.fillStyle = options.color + layer.ctx.strokeStyle = options.color + var sharpness = parseInt(options.sharpness * options.brush_size) + layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)` + layer.ctx.globalAlpha = (1 - options.opacity) + layer.ctx.globalCompositeOperation = "source-over" + var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == options.tool) + if (tool && tool.setBrush) { + tool.setBrush(editor, layer) + } + } + else { + Object.values([ "drawing", "overlay" ]).map(name => this.layers[name]).forEach(l => { + this.setBrush(l) + }) + } + } + get ctx_overlay() { + return this.layers.overlay.ctx + } + get ctx_current() { // the idea is this will help support having custom layers and editing each one + return this.layers.drawing.ctx + } + get canvas_current() { + return this.layers.drawing.canvas + } + keyHandler(event) { // handles keybinds like ctrl+z, ctrl+y + if (!this.popup.classList.contains("active")) { + document.removeEventListener("keydown", this.keyHandlerBound) + document.removeEventListener("keyup", this.keyHandlerBound) + return // this catches if something else closes the window but doesnt properly unbind the key handler + } + + // keybindings + if (event.type == "keydown") { + if ((event.key == "z" || event.key == "Z") && event.ctrlKey) { + if (!event.shiftKey) { + this.history.undo() + } + else { + this.history.redo() + } + } + if (event.key == "y" && event.ctrlKey) { + this.history.redo() + } + if (event.key === "Escape") { + this.hide() + } + } + + // dropper ctrl holding handler stuff + var dropper_active = this.temp_previous_tool != null; + if (dropper_active && !event.ctrlKey) { + this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == this.temp_previous_tool)) + this.temp_previous_tool = null + } + else if (!dropper_active && event.ctrlKey) { + this.temp_previous_tool = this.getOptionValue("tool") + this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == "colorpicker")) + } + } + mouseHandler(event) { + var bbox = this.layers.overlay.canvas.getBoundingClientRect() + var x = (event.clientX || 0) - bbox.left + var y = (event.clientY || 0) - bbox.top + var type = event.type; + var touchmap = { + touchstart: "mousedown", + touchmove: "mousemove", + touchend: "mouseup", + touchcancel: "mouseup" + } + if (type in touchmap) { + type = touchmap[type] + if (event.touches && event.touches[0]) { + var touch = event.touches[0] + var x = (touch.clientX || 0) - bbox.left + var y = (touch.clientY || 0) - bbox.top + } + } + event.preventDefault() + // do drawing-related stuff + if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) { + this.drawing = true + this.tool.begin(this, this.ctx_current, x, y) + this.tool.begin(this, this.ctx_overlay, x, y, true) + this.history.editBegin(x, y) + } + if (type == "mouseup" || type == "mousemove") { + if (this.drawing) { + if (x > 0 && y > 0) { + this.tool.move(this, this.ctx_current, x, y) + this.tool.move(this, this.ctx_overlay, x, y, true) + this.history.editMove(x, y) + } + } + } + if (type == "mouseup" || type == "mouseout") { + if (this.drawing) { + this.drawing = false + this.tool.end(this, this.ctx_current, x, y) + this.tool.end(this, this.ctx_overlay, x, y, true) + this.history.editEnd(x, y) + } + } + } + getOptionValue(section_name) { + var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name) + return this.options && section_name in this.options ? this.options[section_name] : section.default + } + selectOption(section_name, option_index) { + var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name) + var value = section.options[option_index] + this.options[section_name] = value == "custom" ? section.getCustom(this) : value + + this.optionElements[section_name].forEach(element => element.classList.remove("active")) + this.optionElements[section_name][option_index].classList.add("active") + + // change the editor + this.setBrush() + if (section.name == "tool") { + this.loadTool() + } + } +} + +const imageEditor = new ImageEditor(document.getElementById("image-editor")) +const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true) + +imageEditor.setImage(null, 512, 512) +imageInpainter.setImage(null, 512, 512) + +document.getElementById("init_image_button_draw").addEventListener("click", () => { + imageEditor.show() +}) +document.getElementById("init_image_button_inpaint").addEventListener("click", () => { + imageInpainter.show() +}) + +img2imgUnload() // no init image when the app starts + + +function rgbToHex(rgb) { + function componentToHex(c) { + var hex = parseInt(c).toString(16) + return hex.length == 1 ? "0" + hex : hex + } + return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b) +} + +function hexToRgb(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function pixelCompare(int1, int2) { + return Math.abs(int1 - int2) < 4 +} + +// adapted from https://ben.akrin.com/canvas_fill/fill_04.html +function flood_fill(editor, the_canvas_context, x, y, color) { + pixel_stack = [{x:x, y:y}] ; + pixels = the_canvas_context.getImageData( 0, 0, editor.width, editor.height ) ; + var linear_cords = ( y * editor.width + x ) * 4 ; + var original_color = {r:pixels.data[linear_cords], + g:pixels.data[linear_cords+1], + b:pixels.data[linear_cords+2], + a:pixels.data[linear_cords+3]} ; + + var opacity = color.a / 255; + var new_color = { + r: parseInt((color.r * opacity) + (original_color.r * (1 - opacity))), + g: parseInt((color.g * opacity) + (original_color.g * (1 - opacity))), + b: parseInt((color.b * opacity) + (original_color.b * (1 - opacity))) + } + + if ((pixelCompare(new_color.r, original_color.r) && + pixelCompare(new_color.g, original_color.g) && + pixelCompare(new_color.b, original_color.b))) + { + return; // This color is already the color we want, so do nothing + } + var max_stack_size = editor.width * editor.height; + while( pixel_stack.length > 0 && pixel_stack.length < max_stack_size ) { + new_pixel = pixel_stack.shift() ; + x = new_pixel.x ; + y = new_pixel.y ; + + linear_cords = ( y * editor.width + x ) * 4 ; + while( y-->=0 && + (pixelCompare(pixels.data[linear_cords], original_color.r) && + pixelCompare(pixels.data[linear_cords+1], original_color.g) && + pixelCompare(pixels.data[linear_cords+2], original_color.b))) { + linear_cords -= editor.width * 4 ; + } + linear_cords += editor.width * 4 ; + y++ ; + + var reached_left = false ; + var reached_right = false ; + while( y++0 ) { + if( pixelCompare(pixels.data[linear_cords-4], original_color.r) && + pixelCompare(pixels.data[linear_cords-4+1], original_color.g) && + pixelCompare(pixels.data[linear_cords-4+2], original_color.b)) { + if( !reached_left ) { + pixel_stack.push( {x:x-1, y:y} ) ; + reached_left = true ; + } + } else if( reached_left ) { + reached_left = false ; + } + } + + if( x { + const img = imageContainer.querySelector('img') + + if (value) { + zoomElem.classList.remove('fa-magnifying-glass-plus') + zoomElem.classList.add('fa-magnifying-glass-minus') + if (img) { + img.classList.remove('natural-zoom') + + let zoomLevel = typeof value === 'number' ? value : img.dataset.zoomLevel + if (!zoomLevel) { + zoomLevel = 100 + } + + img.dataset.zoomLevel = zoomLevel + img.width = img.naturalWidth * (+zoomLevel / 100) + img.height = img.naturalHeight * (+zoomLevel / 100) + } + } else { + zoomElem.classList.remove('fa-magnifying-glass-minus') + zoomElem.classList.add('fa-magnifying-glass-plus') + if (img) { + img.classList.add('natural-zoom') + img.removeAttribute('width') + img.removeAttribute('height') + } + } + } + + zoomElem.addEventListener( + 'click', + () => setZoomLevel(imageContainer.querySelector('img')?.classList?.contains('natural-zoom')), + ) + + const close = () => { + imageContainer.innerHTML = '' + modalElem.classList.remove('active') + document.body.style.overflow = 'initial' + } + + window.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modalElem.classList.contains('active')) { + close() + } + }) + window.addEventListener('click', (e) => { + if (modalElem.classList.contains('active')) { + if (e.target === backdrop || e.target === closeElem) { + close() + } + + e.stopPropagation() + e.stopImmediatePropagation() + e.preventDefault() + } + }) + + return (optionsFactory) => { + const options = typeof optionsFactory === 'function' ? optionsFactory() : optionsFactory + const src = typeof options === 'string' ? options : options.src + + // TODO center it if < window size + const imgElem = createElement('img', { src }, 'natural-zoom') + imageContainer.appendChild(imgElem) + modalElem.classList.add('active') + document.body.style.overflow = 'hidden' + setZoomLevel(false) + } +})() diff --git a/ui/media/js/image-modifiers.js b/ui/media/js/image-modifiers.js new file mode 100644 index 0000000000000000000000000000000000000000..a7a030ff1d2d57cb6dabf239278b324cb96d4e73 --- /dev/null +++ b/ui/media/js/image-modifiers.js @@ -0,0 +1,385 @@ +let activeTags = [] +let modifiers = [] +let customModifiersGroupElement = undefined +let customModifiersInitialContent + +let editorModifierEntries = document.querySelector('#editor-modifiers-entries') +let editorModifierTagsList = document.querySelector('#editor-inputs-tags-list') +let editorTagsContainer = document.querySelector('#editor-inputs-tags-container') +let modifierCardSizeSlider = document.querySelector('#modifier-card-size-slider') +let previewImageField = document.querySelector('#preview-image') +let modifierSettingsBtn = document.querySelector('#modifier-settings-btn') +let modifierSettingsOverlay = document.querySelector('#modifier-settings-config') +let customModifiersTextBox = document.querySelector('#custom-modifiers-input') +let customModifierEntriesToolbar = document.querySelector('#editor-modifiers-entries-toolbar') + +const modifierThumbnailPath = 'media/modifier-thumbnails' +const activeCardClass = 'modifier-card-active' +const CUSTOM_MODIFIERS_KEY = "customModifiers" + +function createModifierCard(name, previews, removeBy) { + const modifierCard = document.createElement('div') + let style = previewImageField.value + let styleIndex = (style=='portrait') ? 0 : 1 + + modifierCard.className = 'modifier-card' + modifierCard.innerHTML = ` +
+
+
+
+

+ Modifier Image +
+
+

+
` + + const image = modifierCard.querySelector('.modifier-card-image') + const errorText = modifierCard.querySelector('.modifier-card-error-label') + const label = modifierCard.querySelector('.modifier-card-label') + + errorText.innerText = 'No Image' + + if (typeof previews == 'object') { + image.src = previews[styleIndex]; // portrait + image.setAttribute('preview-type', style) + } else { + image.remove() + } + + const maxLabelLength = 30 + const cardLabel = removeBy ? name.replace('by ', '') : name + + if(cardLabel.length <= maxLabelLength) { + label.querySelector('p').innerText = cardLabel + } else { + const tooltipText = document.createElement('span') + tooltipText.className = 'tooltip-text' + tooltipText.innerText = name + + label.classList.add('tooltip') + label.appendChild(tooltipText) + + label.querySelector('p').innerText = cardLabel.substring(0, maxLabelLength) + '...' + } + label.querySelector('p').dataset.fullName = name // preserve the full name + + return modifierCard +} + +function createModifierGroup(modifierGroup, initiallyExpanded, removeBy) { + const title = modifierGroup.category + const modifiers = modifierGroup.modifiers + + const titleEl = document.createElement('h5') + titleEl.className = 'collapsible' + titleEl.innerText = title + + const modifiersEl = document.createElement('div') + modifiersEl.classList.add('collapsible-content', 'editor-modifiers-leaf') + + if (initiallyExpanded === true) { + titleEl.className += ' active' + } + + modifiers.forEach(modObj => { + const modifierName = modObj.modifier + const modifierPreviews = modObj?.previews?.map(preview => `${IMAGE_REGEX.test(preview.image) ? preview.image : modifierThumbnailPath + '/' + preview.path}`) + + const modifierCard = createModifierCard(modifierName, modifierPreviews, removeBy) + + if(typeof modifierCard == 'object') { + modifiersEl.appendChild(modifierCard) + const trimmedName = trimModifiers(modifierName) + + modifierCard.addEventListener('click', () => { + if (activeTags.map(x => trimModifiers(x.name)).includes(trimmedName)) { + // remove modifier from active array + activeTags = activeTags.filter(x => trimModifiers(x.name) != trimmedName) + toggleCardState(trimmedName, false) + } else { + // add modifier to active array + activeTags.push({ + 'name': modifierName, + 'element': modifierCard.cloneNode(true), + 'originElement': modifierCard, + 'previews': modifierPreviews + }) + toggleCardState(trimmedName, true) + } + + refreshTagsList() + document.dispatchEvent(new Event('refreshImageModifiers')) + }) + } + }) + + let brk = document.createElement('br') + brk.style.clear = 'both' + modifiersEl.appendChild(brk) + + let e = document.createElement('div') + e.className = 'modifier-category' + e.appendChild(titleEl) + e.appendChild(modifiersEl) + + editorModifierEntries.insertBefore(e, customModifierEntriesToolbar.nextSibling) + + return e +} + +function trimModifiers(tag) { + return tag.replace(/^\(+|\)+$/g, '').replace(/^\[+|\]+$/g, '') +} + +async function loadModifiers() { + try { + let res = await fetch('/get/modifiers') + if (res.status === 200) { + res = await res.json() + + modifiers = res; // update global variable + + res.reverse() + + res.forEach((modifierGroup, idx) => { + createModifierGroup(modifierGroup, idx === res.length - 1, modifierGroup === 'Artist' ? true : false) // only remove "By " for artists + }) + + createCollapsibles(editorModifierEntries) + } + } catch (e) { + console.error('error fetching modifiers', e) + } + + loadCustomModifiers() + resizeModifierCards(modifierCardSizeSlider.value) + document.dispatchEvent(new Event('loadImageModifiers')) +} + +function refreshModifiersState(newTags) { + // clear existing modifiers + document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => { + const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName // pick the full modifier name + if (activeTags.map(x => x.name).includes(modifierName)) { + modifierCard.classList.remove(activeCardClass) + modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+' + } + }) + activeTags = [] + + // set new modifiers + newTags.forEach(tag => { + let found = false + document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => { + const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName + const shortModifierName = modifierCard.querySelector('.modifier-card-label p').innerText + if (trimModifiers(tag) == trimModifiers(modifierName)) { + // add modifier to active array + if (!activeTags.map(x => x.name).includes(tag)) { // only add each tag once even if several custom modifier cards share the same tag + const imageModifierCard = modifierCard.cloneNode(true) + imageModifierCard.querySelector('.modifier-card-label p').innerText = tag.replace(modifierName, shortModifierName) + activeTags.push({ + 'name': tag, + 'element': imageModifierCard, + 'originElement': modifierCard + }) + } + modifierCard.classList.add(activeCardClass) + modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-' + found = true + } + }) + if (found == false) { // custom tag went missing, create one here + let modifierCard = createModifierCard(tag, undefined, false) // create a modifier card for the missing tag, no image + + modifierCard.addEventListener('click', () => { + if (activeTags.map(x => x.name).includes(tag)) { + // remove modifier from active array + activeTags = activeTags.filter(x => x.name != tag) + modifierCard.classList.remove(activeCardClass) + + modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+' + } + refreshTagsList() + }) + + activeTags.push({ + 'name': tag, + 'element': modifierCard, + 'originElement': undefined // no origin element for missing tags + }) + } + }) + refreshTagsList() +} + +function refreshInactiveTags(inactiveTags) { + // update inactive tags + if (inactiveTags !== undefined && inactiveTags.length > 0) { + activeTags.forEach (tag => { + if (inactiveTags.find(element => element === tag.name) !== undefined) { + tag.inactive = true + } + }) + } + + // update cards + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText + if (inactiveTags.find(element => element === modifierName) !== undefined) { + i.parentElement.classList.add('modifier-toggle-inactive') + } + }) +} + +function refreshTagsList() { + editorModifierTagsList.innerHTML = '' + + if (activeTags.length == 0) { + editorTagsContainer.style.display = 'none' + return + } else { + editorTagsContainer.style.display = 'block' + } + + activeTags.forEach((tag, index) => { + tag.element.querySelector('.modifier-card-image-overlay').innerText = '-' + tag.element.classList.add('modifier-card-tiny') + + editorModifierTagsList.appendChild(tag.element) + + tag.element.addEventListener('click', () => { + let idx = activeTags.findIndex(o => { return o.name === tag.name }) + + if (idx !== -1) { + toggleCardState(activeTags[idx].name, false) + + activeTags.splice(idx, 1) + refreshTagsList() + } + document.dispatchEvent(new Event('refreshImageModifiers')) + }) + }) + + let brk = document.createElement('br') + brk.style.clear = 'both' + editorModifierTagsList.appendChild(brk) + document.dispatchEvent(new Event('refreshImageModifiers')) // notify plugins that the image tags have been refreshed +} + +function toggleCardState(modifierName, makeActive) { + document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(card => { + const name = card.querySelector('.modifier-card-label').innerText + if ( trimModifiers(modifierName) == trimModifiers(name) + || trimModifiers(modifierName) == 'by ' + trimModifiers(name)) { + if(makeActive) { + card.classList.add(activeCardClass) + card.querySelector('.modifier-card-image-overlay').innerText = '-' + } + else{ + card.classList.remove(activeCardClass) + card.querySelector('.modifier-card-image-overlay').innerText = '+' + } + } + }) +} + +function changePreviewImages(val) { + const previewImages = document.querySelectorAll('.modifier-card-image-container img') + + let previewArr = [] + + modifiers.map(x => x.modifiers).forEach(x => previewArr.push(...x.map(m => m.previews))) + + previewArr = previewArr.map(x => { + let obj = {} + + x.forEach(preview => { + obj[preview.name] = preview.path + }) + + return obj + }) + + previewImages.forEach(previewImage => { + const currentPreviewType = previewImage.getAttribute('preview-type') + const relativePreviewPath = previewImage.src.split(modifierThumbnailPath + '/').pop() + + const previews = previewArr.find(preview => relativePreviewPath == preview[currentPreviewType]) + + if(typeof previews == 'object') { + let preview = null + + if (val == 'portrait') { + preview = previews.portrait + } + else if (val == 'landscape') { + preview = previews.landscape + } + + if(preview != null) { + previewImage.src = `${modifierThumbnailPath}/${preview}` + previewImage.setAttribute('preview-type', val) + } + } + }) +} + +function resizeModifierCards(val) { + const cardSizePrefix = 'modifier-card-size_' + const modifierCardClass = 'modifier-card' + + const modifierCards = document.querySelectorAll(`.${modifierCardClass}`) + const cardSize = n => `${cardSizePrefix}${n}` + + modifierCards.forEach(card => { + // remove existing size classes + const classes = card.className.split(' ').filter(c => !c.startsWith(cardSizePrefix)) + card.className = classes.join(' ').trim() + + if(val != 0) { + card.classList.add(cardSize(val)) + } + }) +} + +modifierCardSizeSlider.onchange = () => resizeModifierCards(modifierCardSizeSlider.value) +previewImageField.onchange = () => changePreviewImages(previewImageField.value) + +modifierSettingsBtn.addEventListener('click', function(e) { + modifierSettingsOverlay.classList.add("active") + customModifiersTextBox.setSelectionRange(0, 0) + customModifiersTextBox.focus() + customModifiersInitialContent = customModifiersTextBox.value // preserve the initial content + e.stopPropagation() +}) + +modifierSettingsOverlay.addEventListener('keydown', function(e) { + switch (e.key) { + case "Escape": // Escape to cancel + customModifiersTextBox.value = customModifiersInitialContent // undo the changes + modifierSettingsOverlay.classList.remove("active") + e.stopPropagation() + break + case "Enter": + if (e.ctrlKey) { // Ctrl+Enter to confirm + modifierSettingsOverlay.classList.remove("active") + e.stopPropagation() + break + } + } +}) + +function saveCustomModifiers() { + localStorage.setItem(CUSTOM_MODIFIERS_KEY, customModifiersTextBox.value.trim()) + + loadCustomModifiers() +} + +function loadCustomModifiers() { + PLUGINS['MODIFIERS_LOAD'].forEach(fn=>fn.loader.call()) +} + +customModifiersTextBox.addEventListener('change', saveCustomModifiers) diff --git a/ui/media/js/jquery-3.6.1.min.js b/ui/media/js/jquery-3.6.1.min.js new file mode 100644 index 0000000000000000000000000000000000000000..2c69bc908b10d854c2c3fe6e3268dcffe20e1b5a --- /dev/null +++ b/ui/media/js/jquery-3.6.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0').html(that.buttons[key].text).addClass(that.buttons[key].btnClass).prop("disabled",that.buttons[key].isDisabled).css("display",that.buttons[key].isHidden?"none":"").click(function(e){e.preventDefault();var res=that.buttons[key].action.apply(that,[that.buttons[key]]);that.onAction.apply(that,[key,that.buttons[key]]);that._stopCountDown();if(typeof res==="undefined"||res){that.close();}});that.buttons[key].el=button_element;that.buttons[key].setText=function(text){button_element.html(text);};that.buttons[key].addClass=function(className){button_element.addClass(className);};that.buttons[key].removeClass=function(className){button_element.removeClass(className);};that.buttons[key].disable=function(){that.buttons[key].isDisabled=true;button_element.prop("disabled",true);};that.buttons[key].enable=function(){that.buttons[key].isDisabled=false;button_element.prop("disabled",false);};that.buttons[key].show=function(){that.buttons[key].isHidden=false;button_element.css("display","");};that.buttons[key].hide=function(){that.buttons[key].isHidden=true;button_element.css("display","none");};that["$_"+key]=that["$$"+key]=button_element;that.$btnc.append(button_element);});if(total_buttons===0){this.$btnc.hide();}if(this.closeIcon===null&&total_buttons===0){this.closeIcon=true;}if(this.closeIcon){if(this.closeIconClass){var closeHtml='';this.$closeIcon.html(closeHtml);}this.$closeIcon.click(function(e){e.preventDefault();var buttonName=false;var shouldClose=false;var str;if(typeof that.closeIcon=="function"){str=that.closeIcon();}else{str=that.closeIcon;}if(typeof str=="string"&&typeof that.buttons[str]!="undefined"){buttonName=str;shouldClose=false;}else{if(typeof str=="undefined"||!!(str)==true){shouldClose=true;}else{shouldClose=false;}}if(buttonName){var btnResponse=that.buttons[buttonName].action.apply(that);shouldClose=(typeof btnResponse=="undefined")||!!(btnResponse);}if(shouldClose){that.close();}});this.$closeIcon.show();}else{this.$closeIcon.hide();}},setTitle:function(string,force){force=force||false;if(typeof string!=="undefined"){if(typeof string=="string"){this.title=string;}else{if(typeof string=="function"){if(typeof string.promise=="function"){console.error("Promise was returned from title function, this is not supported.");}var response=string();if(typeof response=="string"){this.title=response;}else{this.title=false;}}else{this.title=false;}}}if(this.isAjaxLoading&&!force){return;}this.$title.html(this.title||"");this.updateTitleContainer();},setIcon:function(iconClass,force){force=force||false;if(typeof iconClass!=="undefined"){if(typeof iconClass=="string"){this.icon=iconClass;}else{if(typeof iconClass==="function"){var response=iconClass();if(typeof response=="string"){this.icon=response;}else{this.icon=false;}}else{this.icon=false;}}}if(this.isAjaxLoading&&!force){return;}this.$icon.html(this.icon?'':"");this.updateTitleContainer();},updateTitleContainer:function(){if(!this.title&&!this.icon){this.$titleContainer.hide();}else{this.$titleContainer.show();}},setContentPrepend:function(content,force){if(!content){return;}this.contentParsed.prepend(content);},setContentAppend:function(content){if(!content){return;}this.contentParsed.append(content);},setContent:function(content,force){force=!!force;var that=this;if(content){this.contentParsed.html("").append(content);}if(this.isAjaxLoading&&!force){return;}this.$content.html("");this.$content.append(this.contentParsed);setTimeout(function(){that.$body.find("input[autofocus]:visible:first").focus();},100);},loadingSpinner:false,showLoading:function(disableButtons){this.loadingSpinner=true;this.$jconfirmBox.addClass("loading");if(disableButtons){this.$btnc.find("button").prop("disabled",true);}},hideLoading:function(enableButtons){this.loadingSpinner=false;this.$jconfirmBox.removeClass("loading");if(enableButtons){this.$btnc.find("button").prop("disabled",false);}},ajaxResponse:false,contentParsed:"",isAjax:false,isAjaxLoading:false,_parseContent:function(){var that=this;var e=" ";if(typeof this.content=="function"){var res=this.content.apply(this);if(typeof res=="string"){this.content=res;}else{if(typeof res=="object"&&typeof res.always=="function"){this.isAjax=true;this.isAjaxLoading=true;res.always(function(data,status,xhr){that.ajaxResponse={data:data,status:status,xhr:xhr};that._contentReady.resolve(data,status,xhr);if(typeof that.contentLoaded=="function"){that.contentLoaded(data,status,xhr);}});this.content=e;}else{this.content=e;}}}if(typeof this.content=="string"&&this.content.substr(0,4).toLowerCase()==="url:"){this.isAjax=true;this.isAjaxLoading=true;var u=this.content.substring(4,this.content.length);$.get(u).done(function(html){that.contentParsed.html(html);}).always(function(data,status,xhr){that.ajaxResponse={data:data,status:status,xhr:xhr};that._contentReady.resolve(data,status,xhr);if(typeof that.contentLoaded=="function"){that.contentLoaded(data,status,xhr);}});}if(!this.content){this.content=e;}if(!this.isAjax){this.contentParsed.html(this.content);this.setContent();that._contentReady.resolve();}},_stopCountDown:function(){clearInterval(this.autoCloseInterval);if(this.$cd){this.$cd.remove();}},_startCountDown:function(){var that=this;var opt=this.autoClose.split("|");if(opt.length!==2){console.error("Invalid option for autoClose. example 'close|10000'");return false;}var button_key=opt[0];var time=parseInt(opt[1]);if(typeof this.buttons[button_key]==="undefined"){console.error("Invalid button key '"+button_key+"' for autoClose");return false;}var seconds=Math.ceil(time/1000);this.$cd=$(' ('+seconds+")").appendTo(this["$_"+button_key]);this.autoCloseInterval=setInterval(function(){that.$cd.html(" ("+(seconds-=1)+") ");if(seconds<=0){that["$$"+button_key].trigger("click");that._stopCountDown();}},1000);},_getKey:function(key){switch(key){case 192:return"tilde";case 13:return"enter";case 16:return"shift";case 9:return"tab";case 20:return"capslock";case 17:return"ctrl";case 91:return"win";case 18:return"alt";case 27:return"esc";case 32:return"space";}var initial=String.fromCharCode(key);if(/^[A-z0-9]+$/.test(initial)){return initial.toLowerCase();}else{return false;}},reactOnKey:function(e){var that=this;var a=$(".jconfirm");if(a.eq(a.length-1)[0]!==this.$el[0]){return false;}var key=e.which;if(this.$content.find(":input").is(":focus")&&/13|32/.test(key)){return false;}var keyChar=this._getKey(key);if(keyChar==="esc"&&this.escapeKey){if(this.escapeKey===true){this.$scrollPane.trigger("click");}else{if(typeof this.escapeKey==="string"||typeof this.escapeKey==="function"){var buttonKey;if(typeof this.escapeKey==="function"){buttonKey=this.escapeKey();}else{buttonKey=this.escapeKey;}if(buttonKey){if(typeof this.buttons[buttonKey]==="undefined"){console.warn("Invalid escapeKey, no buttons found with key "+buttonKey);}else{this["$_"+buttonKey].trigger("click");}}}}}$.each(this.buttons,function(key,button){if(button.keys.indexOf(keyChar)!=-1){that["$_"+key].trigger("click");}});},setDialogCenter:function(){console.info("setDialogCenter is deprecated, dialogs are centered with CSS3 tables");},_unwatchContent:function(){clearInterval(this._timer);},close:function(onClosePayload){var that=this;if(typeof this.onClose==="function"){this.onClose(onClosePayload);}this._unwatchContent();$(window).unbind("resize."+this._id);$(window).unbind("keyup."+this._id);$(window).unbind("jcKeyDown."+this._id);if(this.draggable){$(window).unbind("mousemove."+this._id);$(window).unbind("mouseup."+this._id);this.$titleContainer.unbind("mousedown");}that.$el.removeClass(that.loadedClass);$("body").removeClass("jconfirm-no-scroll-"+that._id);that.$jconfirmBoxContainer.removeClass("jconfirm-no-transition");setTimeout(function(){that.$body.addClass(that.closeAnimationParsed);that.$jconfirmBg.addClass("jconfirm-bg-h");var closeTimer=(that.closeAnimation==="none")?1:that.animationSpeed;setTimeout(function(){that.$el.remove();var l=jconfirm.instances;var i=jconfirm.instances.length-1;for(i;i>=0;i--){if(jconfirm.instances[i]._id===that._id){jconfirm.instances.splice(i,1);}}if(!jconfirm.instances.length){if(that.scrollToPreviousElement&&jconfirm.lastFocused&&jconfirm.lastFocused.length&&$.contains(document,jconfirm.lastFocused[0])){var $lf=jconfirm.lastFocused;if(that.scrollToPreviousElementAnimate){var st=$(window).scrollTop();var ot=jconfirm.lastFocused.offset().top;var wh=$(window).height();if(!(ot>st&&ot<(st+wh))){var scrollTo=(ot-Math.round((wh/3)));$("html, body").animate({scrollTop:scrollTo},that.animationSpeed,"swing",function(){$lf.focus();});}else{$lf.focus();}}else{$lf.focus();}jconfirm.lastFocused=false;}}if(typeof that.onDestroy==="function"){that.onDestroy();}},closeTimer*0.4);},50);return true;},open:function(){if(this.isOpen()){return false;}this._buildHTML();this._bindEvents();this._open();return true;},setStartingPoint:function(){var el=false;if(this.animateFromElement!==true&&this.animateFromElement){el=this.animateFromElement;jconfirm.lastClicked=false;}else{if(jconfirm.lastClicked&&this.animateFromElement===true){el=jconfirm.lastClicked;jconfirm.lastClicked=false;}else{return false;}}if(!el){return false;}var offset=el.offset();var iTop=el.outerHeight()/2;var iLeft=el.outerWidth()/2;iTop-=this.$jconfirmBox.outerHeight()/2;iLeft-=this.$jconfirmBox.outerWidth()/2;var sourceTop=offset.top+iTop;sourceTop=sourceTop-this._scrollTop();var sourceLeft=offset.left+iLeft;var wh=$(window).height()/2;var ww=$(window).width()/2;var targetH=wh-this.$jconfirmBox.outerHeight()/2;var targetW=ww-this.$jconfirmBox.outerWidth()/2;sourceTop-=targetH;sourceLeft-=targetW;if(Math.abs(sourceTop)>wh||Math.abs(sourceLeft)>ww){return false;}this.$jconfirmBoxContainer.css("transform","translate("+sourceLeft+"px, "+sourceTop+"px)");},_open:function(){var that=this;if(typeof that.onOpenBefore==="function"){that.onOpenBefore();}this.$body.removeClass(this.animationParsed);this.$jconfirmBg.removeClass("jconfirm-bg-h");this.$body.focus();that.$jconfirmBoxContainer.css("transform","translate("+0+"px, "+0+"px)");setTimeout(function(){that.$body.css(that._getCSS(that.animationSpeed,1));that.$body.css({"transition-property":that.$body.css("transition-property")+", margin"});that.$jconfirmBoxContainer.addClass("jconfirm-no-transition");that._modalReady.resolve();if(typeof that.onOpen==="function"){that.onOpen();}that.$el.addClass(that.loadedClass);},this.animationSpeed);},loadedClass:"jconfirm-open",isClosed:function(){return !this.$el||this.$el.css("display")==="";},isOpen:function(){return !this.isClosed();},toggle:function(){if(!this.isOpen()){this.open();}else{this.close();}}};jconfirm.instances=[];jconfirm.lastFocused=false;jconfirm.pluginDefaults={template:'
',title:"Hello",titleClass:"",type:"default",typeAnimated:true,draggable:true,dragWindowGap:15,dragWindowBorder:true,animateFromElement:true,alignMiddle:true,smoothContent:true,content:"Are you sure to continue?",buttons:{},defaultButtons:{ok:{action:function(){}},close:{action:function(){}}},contentLoaded:function(){},icon:"",lazyOpen:false,bgOpacity:null,theme:"light",animation:"scale",closeAnimation:"scale",animationSpeed:400,animationBounce:1,escapeKey:true,rtl:false,container:"body",containerFluid:false,backgroundDismiss:false,backgroundDismissAnimation:"shake",autoClose:false,closeIcon:null,closeIconClass:false,watchInterval:100,columnClass:"col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-10 col-xs-offset-1",boxWidth:"50%",scrollToPreviousElement:true,scrollToPreviousElementAnimate:true,useBootstrap:true,offsetTop:40,offsetBottom:40,bootstrapClasses:{container:"container",containerFluid:"container-fluid",row:"row"},onContentReady:function(){},onOpenBefore:function(){},onOpen:function(){},onClose:function(){},onDestroy:function(){},onAction:function(){}};var keyDown=false;$(window).on("keydown",function(e){if(!keyDown){var $target=$(e.target);var pass=false;if($target.closest(".jconfirm-box").length){pass=true;}if(pass){$(window).trigger("jcKeyDown");}keyDown=true;}});$(window).on("keyup",function(){keyDown=false;});jconfirm.lastClicked=false;$(document).on("mousedown","button, a",function(){jconfirm.lastClicked=$(this);});})(jQuery,window); \ No newline at end of file diff --git a/ui/media/js/jszip.min.js b/ui/media/js/jszip.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ff4cfd5e8fdc49176c2d1d409afa897f40be01f4 --- /dev/null +++ b/ui/media/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;rTip: To skip this dialog, use shift-click or disable the "Confirm dangerous actions" setting in the Settings tab.', + buttons: { + yes: () => { fn(e) }, + cancel: () => {} + } + }); + } +} + +function logMsg(msg, level, outputMsg) { + if (outputMsg.hasChildNodes()) { + outputMsg.appendChild(document.createElement('br')) + } + if (level === 'error') { + outputMsg.innerHTML += 'Error: ' + msg + '' + } else if (level === 'warn') { + outputMsg.innerHTML += 'Warning: ' + msg + '' + } else { + outputMsg.innerText += msg + } + console.log(level, msg) +} + +function logError(msg, res, outputMsg) { + logMsg(msg, 'error', outputMsg) + + console.log('request error', res) + setStatus('request', 'error', 'error') +} + +function playSound() { + const audio = new Audio('/media/ding.mp3') + audio.volume = 0.2 + var promise = audio.play() + if (promise !== undefined) { + promise.then(_ => {}).catch(error => { + console.warn("browser blocked autoplay") + }) + } +} + +function showImages(reqBody, res, outputContainer, livePreview) { + let imageItemElements = outputContainer.querySelectorAll('.imgItem') + if(typeof res != 'object') return + res.output.reverse() + res.output.forEach((result, index) => { + const imageData = result?.data || result?.path + '?t=' + Date.now(), + imageSeed = result?.seed, + imagePrompt = reqBody.prompt, + imageInferenceSteps = reqBody.num_inference_steps, + imageGuidanceScale = reqBody.guidance_scale, + imageWidth = reqBody.width, + imageHeight = reqBody.height; + + if (!imageData.includes('/')) { + // res contained no data for the image, stop execution + setStatus('request', 'invalid image', 'error') + return + } + + let imageItemElem = (index < imageItemElements.length ? imageItemElements[index] : null) + if(!imageItemElem) { + imageItemElem = document.createElement('div') + imageItemElem.className = 'imgItem' + imageItemElem.innerHTML = ` +
+ +
+
+ +
+
+ + +
+ ` + outputContainer.appendChild(imageItemElem) + const imageRemoveBtn = imageItemElem.querySelector('.imgPreviewItemClearBtn') + let parentTaskContainer = imageRemoveBtn.closest('.imageTaskContainer') + imageRemoveBtn.addEventListener('click', (e) => { + shiftOrConfirm(e, "Remove the image from the results?", () => { + imageItemElem.style.display = 'none' + let allHidden = true; + let children = parentTaskContainer.querySelectorAll('.imgItem'); + for(let x = 0; x < children.length; x++) { + let child = children[x]; + if(child.style.display != "none") { + allHidden = false; + } + } + if(allHidden === true) { + const req = htmlTaskMap.get(parentTaskContainer) + if(!req.isProcessing || req.batchesDone == req.batchCount) {parentTaskContainer.parentNode.removeChild(parentTaskContainer)} + } + }) + }) + } + const imageElem = imageItemElem.querySelector('img') + imageElem.src = imageData + imageElem.width = parseInt(imageWidth) + imageElem.height = parseInt(imageHeight) + imageElem.setAttribute('data-prompt', imagePrompt) + imageElem.setAttribute('data-steps', imageInferenceSteps) + imageElem.setAttribute('data-guidance', imageGuidanceScale) + + imageElem.addEventListener('load', function() { + imageItemElem.querySelector('.img_bottom_label').innerText = `${this.naturalWidth} x ${this.naturalHeight}` + }) + + const imageInfo = imageItemElem.querySelector('.imgItemInfo') + imageInfo.style.visibility = (livePreview ? 'hidden' : 'visible') + + if ('seed' in result && !imageElem.hasAttribute('data-seed')) { + const imageExpandBtn = imageItemElem.querySelector('.imgExpandBtn') + imageExpandBtn.addEventListener('click', function() { + imageModal(imageElem.src) + }) + + const req = Object.assign({}, reqBody, { + seed: result?.seed || reqBody.seed + }) + imageElem.setAttribute('data-seed', req.seed) + imageElem.setAttribute('data-imagecounter', ++imageCounter) + imageRequest[imageCounter] = req + const imageSeedLabel = imageItemElem.querySelector('.imgSeedLabel') + imageSeedLabel.innerText = 'Seed: ' + req.seed + + let buttons = [ + { text: 'Use as Input', on_click: onUseAsInputClick }, + [ + { html: ' Download Image', on_click: onDownloadImageClick, class: "download-img" }, + { html: ' JSON', on_click: onDownloadJSONClick, class: "download-json" } + ], + { text: 'Make Similar Images', on_click: onMakeSimilarClick }, + { text: 'Draw another 25 steps', on_click: onContinueDrawingClick }, + [ + { text: 'Upscale', on_click: onUpscaleClick, filter: (req, img) => !req.use_upscale }, + { text: 'Fix Faces', on_click: onFixFacesClick, filter: (req, img) => !req.use_face_correction } + ] + ] + + // include the plugins + buttons = buttons.concat(PLUGINS['IMAGE_INFO_BUTTONS']) + + const imgItemInfo = imageItemElem.querySelector('.imgItemInfo') + const img = imageItemElem.querySelector('img') + const createButton = function(btnInfo) { + if (Array.isArray(btnInfo)) { + const wrapper = document.createElement('div'); + btnInfo + .map(createButton) + .forEach(buttonElement => wrapper.appendChild(buttonElement)) + return wrapper + } + + const isLabel = btnInfo.type === 'label' + + const newButton = document.createElement(isLabel ? 'span' : 'button') + newButton.classList.add('tasksBtns') + + if (btnInfo.html) { + const html = typeof btnInfo.html === 'function' ? btnInfo.html() : btnInfo.html + if (html instanceof HTMLElement) { + newButton.appendChild(html) + } else { + newButton.innerHTML = html + } + } else { + newButton.innerText = typeof btnInfo.text === 'function' ? btnInfo.text() : btnInfo.text + } + + if (btnInfo.on_click || !isLabel) { + newButton.addEventListener('click', function(event) { + btnInfo.on_click(req, img, event) + }) + } + + if (btnInfo.class !== undefined) { + if (Array.isArray(btnInfo.class)) { + newButton.classList.add(...btnInfo.class) + } else { + newButton.classList.add(btnInfo.class) + } + } + return newButton + } + buttons.forEach(btn => { + if (Array.isArray(btn)) { + btn = btn.filter(btnInfo => !btnInfo.filter || btnInfo.filter(req, img) === true) + if (btn.length === 0) { + return + } + } else if (btn.filter && btn.filter(req, img) === false) { + return + } + + try { + imgItemInfo.appendChild(createButton(btn)) + } catch (err) { + console.error('Error creating image info button from plugin: ', btn, err) + } + }) + } + }) +} + +function onUseAsInputClick(req, img) { + const imgData = img.src + + initImageSelector.value = null + initImagePreview.src = imgData + + maskSetting.checked = false +} + +function getDownloadFilename(img, suffix) { + const imageSeed = img.getAttribute('data-seed') + const imagePrompt = img.getAttribute('data-prompt') + const imageInferenceSteps = img.getAttribute('data-steps') + const imageGuidanceScale = img.getAttribute('data-guidance') + + return createFileName(imagePrompt, imageSeed, imageInferenceSteps, imageGuidanceScale, suffix) +} + +function onDownloadJSONClick(req, img) { + const name = getDownloadFilename(img, 'json') + const blob = new Blob([JSON.stringify(req, null, 2)], { type: 'text/plain' }) + saveAs(blob, name) +} + +function onDownloadImageClick(req, img) { + const name = getDownloadFilename(img, req['output_format']) + const blob = dataURItoBlob(img.src) + saveAs(blob, name) +} + +function modifyCurrentRequest(...reqDiff) { + const newTaskRequest = getCurrentUserRequest() + + newTaskRequest.reqBody = Object.assign(newTaskRequest.reqBody, ...reqDiff, { + use_cpu: useCPUField.checked + }) + newTaskRequest.seed = newTaskRequest.reqBody.seed + + return newTaskRequest +} + +function onMakeSimilarClick(req, img) { + const newTaskRequest = modifyCurrentRequest(req, { + num_outputs: 1, + num_inference_steps: 50, + guidance_scale: 7.5, + prompt_strength: 0.7, + init_image: img.src, + seed: Math.floor(Math.random() * 10000000) + }) + + newTaskRequest.numOutputsTotal = 5 + newTaskRequest.batchCount = 5 + + delete newTaskRequest.reqBody.mask + + createTask(newTaskRequest) +} + +function enqueueImageVariationTask(req, img, reqDiff) { + const imageSeed = img.getAttribute('data-seed') + + const newRequestBody = { + num_outputs: 1, // this can be user-configurable in the future + seed: imageSeed + } + + // If the user is editing pictures, stop modifyCurrentRequest from importing + // new values by setting the missing properties to undefined + if (!('init_image' in req) && !('init_image' in reqDiff)) { + newRequestBody.init_image = undefined + newRequestBody.mask = undefined + } else if (!('mask' in req) && !('mask' in reqDiff)) { + newRequestBody.mask = undefined + } + + const newTaskRequest = modifyCurrentRequest(req, reqDiff, newRequestBody) + newTaskRequest.numOutputsTotal = 1 // this can be user-configurable in the future + newTaskRequest.batchCount = 1 + + createTask(newTaskRequest) +} + +function onUpscaleClick(req, img) { + enqueueImageVariationTask(req, img, { + use_upscale: upscaleModelField.value + }) +} + +function onFixFacesClick(req, img) { + enqueueImageVariationTask(req, img, { + use_face_correction: gfpganModelField.value + }) +} + +function onContinueDrawingClick(req, img) { + enqueueImageVariationTask(req, img, { + num_inference_steps: parseInt(req.num_inference_steps) + 25 + }) +} + +function getUncompletedTaskEntries() { + const taskEntries = Array.from( + document.querySelectorAll('#preview .imageTaskContainer .taskStatusLabel') + ).filter((taskLabel) => taskLabel.style.display !== 'none' + ).map(function(taskLabel) { + let imageTaskContainer = taskLabel.parentNode + while(!imageTaskContainer.classList.contains('imageTaskContainer') && imageTaskContainer.parentNode) { + imageTaskContainer = imageTaskContainer.parentNode + } + return imageTaskContainer + }) + if (!processOrder.checked) { + taskEntries.reverse() + } + return taskEntries +} + +function makeImage() { + if (typeof performance == "object" && performance.mark) { + performance.mark('click-makeImage') + } + + if (!SD.isServerAvailable()) { + alert('The server is not available.') + return + } + if (!randomSeedField.checked && seedField.value == '') { + alert('The "Seed" field must not be empty.') + return + } + if (numInferenceStepsField.value == '') { + alert('The "Inference Steps" field must not be empty.') + return + } + if (numOutputsTotalField.value == '' || numOutputsTotalField.value == 0) { + numOutputsTotalField.value = 1 + } + if (numOutputsParallelField.value == '' || numOutputsParallelField.value == 0) { + numOutputsParallelField.value = 1 + } + if (guidanceScaleField.value == '') { + guidanceScaleField.value = guidanceScaleSlider.value / 10 + } + const taskTemplate = getCurrentUserRequest() + const newTaskRequests = getPrompts().map((prompt) => Object.assign({}, taskTemplate, { + reqBody: Object.assign({ prompt: prompt }, taskTemplate.reqBody) + })) + newTaskRequests.forEach(createTask) + + initialText.style.display = 'none' +} + +async function onIdle() { + const serverCapacity = SD.serverCapacity + if (pauseClient===true) { + await resumeClient() + } + + for (const taskEntry of getUncompletedTaskEntries()) { + if (SD.activeTasks.size >= serverCapacity) { + break + } + const task = htmlTaskMap.get(taskEntry) + if (!task) { + const taskStatusLabel = taskEntry.querySelector('.taskStatusLabel') + taskStatusLabel.style.display = 'none' + continue + } + await onTaskStart(task) + } +} + +function getTaskUpdater(task, reqBody, outputContainer) { + const outputMsg = task['outputMsg'] + const progressBar = task['progressBar'] + const progressBarInner = progressBar.querySelector("div") + + const batchCount = task.batchCount + let lastStatus = undefined + return async function(event) { + if (this.status !== lastStatus) { + lastStatus = this.status + switch(this.status) { + case SD.TaskStatus.pending: + task['taskStatusLabel'].innerText = "Pending" + task['taskStatusLabel'].classList.add('waitingTaskLabel') + break + case SD.TaskStatus.waiting: + task['taskStatusLabel'].innerText = "Waiting" + task['taskStatusLabel'].classList.add('waitingTaskLabel') + task['taskStatusLabel'].classList.remove('activeTaskLabel') + break + case SD.TaskStatus.processing: + case SD.TaskStatus.completed: + task['taskStatusLabel'].innerText = "Processing" + task['taskStatusLabel'].classList.add('activeTaskLabel') + task['taskStatusLabel'].classList.remove('waitingTaskLabel') + break + case SD.TaskStatus.stopped: + break + case SD.TaskStatus.failed: + if (!SD.isServerAvailable()) { + logError("Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.", event, outputMsg) + } else if (typeof event?.response === 'object') { + let msg = 'Stable Diffusion had an error reading the response:
'
+                        if (this.exception) {
+                            msg += `Error: ${this.exception.message}
` + } + try { // 'Response': body stream already read + msg += 'Read: ' + await event.response.text() + } catch(e) { + msg += 'Unexpected end of stream. ' + } + const bufferString = event.reader.bufferedString + if (bufferString) { + msg += 'Buffered data: ' + bufferString + } + msg += '
' + logError(msg, event, outputMsg) + } else { + let msg = `Unexpected Read Error:
Error:${this.exception}
EventInfo: ${JSON.stringify(event, undefined, 4)}
` + logError(msg, event, outputMsg) + } + break + } + } + if ('update' in event) { + const stepUpdate = event.update + if (!('step' in stepUpdate)) { + return + } + // task.instances can be a mix of different tasks with uneven number of steps (Render Vs Filter Tasks) + const overallStepCount = task.instances.reduce( + (sum, instance) => sum + (instance.isPending ? Math.max(0, instance.step || stepUpdate.step) / (instance.total_steps || stepUpdate.total_steps) : 1), + 0 // Initial value + ) * stepUpdate.total_steps // Scale to current number of steps. + const totalSteps = task.instances.reduce( + (sum, instance) => sum + (instance.total_steps || stepUpdate.total_steps), + stepUpdate.total_steps * (batchCount - task.batchesDone) // Initial value at (unstarted task count * Nbr of steps) + ) + const percent = Math.min(100, 100 * (overallStepCount / totalSteps)).toFixed(0) + + const timeTaken = stepUpdate.step_time // sec + const stepsRemaining = Math.max(0, totalSteps - overallStepCount) + const timeRemaining = (timeTaken < 0 ? '' : millisecondsToStr(stepsRemaining * timeTaken * 1000)) + outputMsg.innerHTML = `Batch ${task.batchesDone} of ${batchCount}. Generating image(s): ${percent}%. Time remaining (approx): ${timeRemaining}` + outputMsg.style.display = 'block' + progressBarInner.style.width = `${percent}%` + + if (stepUpdate.output) { + showImages(reqBody, stepUpdate, outputContainer, true) + } + } + } +} + +function abortTask(task) { + if (!task.isProcessing) { + return false + } + task.isProcessing = false + task.progressBar.classList.remove("active") + task['taskStatusLabel'].style.display = 'none' + task['stopTask'].innerHTML = ' Remove' + if (!task.instances?.some((r) => r.isPending)) { + return + } + task.instances.forEach((instance) => { + try { + instance.abort() + } catch (e) { + console.error(e) + } + }) +} + +function onTaskErrorHandler(task, reqBody, instance, reason) { + if (!task.isProcessing) { + return + } + console.log('Render request %o, Instance: %o, Error: %s', reqBody, instance, reason) + abortTask(task) + const outputMsg = task['outputMsg'] + logError('Stable Diffusion had an error. Please check the logs in the command-line window.

' + reason + '
' + reason.stack + '
', task, outputMsg) + setStatus('request', 'error', 'error') +} + +function onTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) { + if (typeof stepUpdate === 'object') { + if (stepUpdate.status === 'succeeded') { + showImages(reqBody, stepUpdate, outputContainer, false) + } else { + task.isProcessing = false + const outputMsg = task['outputMsg'] + let msg = '' + if ('detail' in stepUpdate && typeof stepUpdate.detail === 'string' && stepUpdate.detail.length > 0) { + msg = stepUpdate.detail + if (msg.toLowerCase().includes('out of memory')) { + msg += `

+ Suggestions: +
+ 1. If you have set an initial image, please try reducing its dimension to ${MAX_INIT_IMAGE_DIMENSION}x${MAX_INIT_IMAGE_DIMENSION} or smaller.
+ 2. Try picking a lower level in the 'GPU Memory Usage' setting (in the 'Settings' tab).
+ 3. Try generating a smaller image.
` + } + } else { + msg = `Unexpected Read Error:
StepUpdate: ${JSON.stringify(stepUpdate, undefined, 4)}
` + } + logError(msg, stepUpdate, outputMsg) + } + } + if (task.isProcessing && task.batchesDone < task.batchCount) { + task['taskStatusLabel'].innerText = "Pending" + task['taskStatusLabel'].classList.add('waitingTaskLabel') + task['taskStatusLabel'].classList.remove('activeTaskLabel') + return + } + if ('instances' in task && task.instances.some((ins) => ins != instance && ins.isPending)) { + return + } + + task.isProcessing = false + task['stopTask'].innerHTML = ' Remove' + task['taskStatusLabel'].style.display = 'none' + + let time = millisecondsToStr( Date.now() - task.startTime ) + + if (task.batchesDone == task.batchCount) { + if (!task.outputMsg.innerText.toLowerCase().includes('error')) { + task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time}` + } + task.progressBar.style.height = "0px" + task.progressBar.style.border = "0px solid var(--background-color3)" + task.progressBar.classList.remove("active") + setStatus('request', 'done', 'success') + } else { + task.outputMsg.innerText += `. Task ended after ${time}` + } + + if (randomSeedField.checked) { + seedField.value = task.seed + } + + if (SD.activeTasks.size > 0) { + return + } + const uncompletedTasks = getUncompletedTaskEntries() + if (uncompletedTasks && uncompletedTasks.length > 0) { + return + } + + if (pauseClient) { + resumeBtn.click() + } + renderButtons.style.display = 'none' + renameMakeImageButton() + + if (isSoundEnabled()) { + playSound() + } +} + + +async function onTaskStart(task) { + if (!task.isProcessing || task.batchesDone >= task.batchCount) { + return + } + + if (typeof task.startTime !== 'number') { + task.startTime = Date.now() + } + if (!('instances' in task)) { + task['instances'] = [] + } + + task['stopTask'].innerHTML = ' Stop' + task['taskStatusLabel'].innerText = "Starting" + task['taskStatusLabel'].classList.add('waitingTaskLabel') + + let newTaskReqBody = task.reqBody + if (task.batchCount > 1) { + // Each output render batch needs it's own task reqBody instance to avoid altering the other runs after they are completed. + newTaskReqBody = Object.assign({}, task.reqBody) + if (task.batchesDone == task.batchCount-1) { + // Last batch of the task + // If the number of parallel jobs is no factor of the total number of images, the last batch must create less than "parallel jobs count" images + // E.g. with numOutputsTotal = 6 and num_outputs = 5, the last batch shall only generate 1 image. + newTaskReqBody.num_outputs = task.numOutputsTotal - task.reqBody.num_outputs * (task.batchCount-1) + } + } + + const startSeed = task.seed || newTaskReqBody.seed + const genSeeds = Boolean(typeof newTaskReqBody.seed !== 'number' || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1)) + if (genSeeds) { + newTaskReqBody.seed = parseInt(startSeed) + (task.batchesDone * task.reqBody.num_outputs) + } + + // Update the seed *before* starting the processing so it's retained if user stops the task + if (randomSeedField.checked) { + seedField.value = task.seed + } + + const outputContainer = document.createElement('div') + outputContainer.className = 'img-batch' + task.outputContainer.insertBefore(outputContainer, task.outputContainer.firstChild) + + const eventInfo = {reqBody:newTaskReqBody} + const callbacksPromises = PLUGINS['TASK_CREATE'].map((hook) => { + if (typeof hook !== 'function') { + console.error('The provided TASK_CREATE hook is not a function. Hook: %o', hook) + return Promise.reject(new Error('hook is not a function.')) + } + try { + return Promise.resolve(hook.call(task, eventInfo)) + } catch (err) { + console.error(err) + return Promise.reject(err) + } + }) + await Promise.allSettled(callbacksPromises) + let instance = eventInfo.instance + if (!instance) { + const factory = PLUGINS.OUTPUTS_FORMATS.get(eventInfo.reqBody?.output_format || newTaskReqBody.output_format) + if (factory) { + instance = await Promise.resolve(factory(eventInfo.reqBody || newTaskReqBody)) + } + if (!instance) { + console.error(`${factory ? "Factory " + String(factory) : 'No factory defined'} for output format ${eventInfo.reqBody?.output_format || newTaskReqBody.output_format}. Instance is ${instance || 'undefined'}. Using default renderer.`) + instance = new SD.RenderTask(eventInfo.reqBody || newTaskReqBody) + } + } + + task['instances'].push(instance) + task.batchesDone++ + + instance.enqueue(getTaskUpdater(task, newTaskReqBody, outputContainer)).then( + (renderResult) => { + onTaskCompleted(task, newTaskReqBody, instance, outputContainer, renderResult) + }, + (reason) => { + onTaskErrorHandler(task, newTaskReqBody, instance, reason) + } + ) + + setStatus('request', 'fetching..') + renderButtons.style.display = 'flex' + renameMakeImageButton() + previewTools.style.display = 'block' +} + +/* Hover effect for the init image in the task list */ +function createInitImageHover(taskEntry) { + var $tooltip = $( taskEntry.querySelector('.task-fs-initimage') ) + var img = document.createElement('img') + img.src = taskEntry.querySelector('div.task-initimg > img').src + $tooltip.append(img) + $tooltip.append(`
`) + $tooltip.find('button').on('click', (e) => { + e.stopPropagation() + onUseAsInputClick(null,img) + }) +} + +let startX, startY; +function onTaskEntryDragOver(event) { + imagePreview.querySelectorAll(".imageTaskContainer").forEach(itc => { + if(itc != event.target.closest(".imageTaskContainer")){ + itc.classList.remove('dropTargetBefore','dropTargetAfter'); + } + }); + if(event.target.closest(".imageTaskContainer")){ + if(startX && startY){ + if(event.target.closest(".imageTaskContainer").offsetTop > startY){ + event.target.closest(".imageTaskContainer").classList.add('dropTargetAfter'); + }else if(event.target.closest(".imageTaskContainer").offsetTop < startY){ + event.target.closest(".imageTaskContainer").classList.add('dropTargetBefore'); + }else if (event.target.closest(".imageTaskContainer").offsetLeft > startX){ + event.target.closest(".imageTaskContainer").classList.add('dropTargetAfter'); + }else if (event.target.closest(".imageTaskContainer").offsetLeft < startX){ + event.target.closest(".imageTaskContainer").classList.add('dropTargetBefore'); + } + } + } +} + +function createTask(task) { + let taskConfig = '' + + if (task.reqBody.init_image !== undefined) { + let h = 80 + let w = task.reqBody.width * h / task.reqBody.height >>0 + taskConfig += `
` + } + taskConfig += `Seed: ${task.seed}, Sampler: ${task.reqBody.sampler_name}, Inference Steps: ${task.reqBody.num_inference_steps}, Guidance Scale: ${task.reqBody.guidance_scale}, Model: ${task.reqBody.use_stable_diffusion_model}` + + if (task.reqBody.use_vae_model.trim() !== '') { + taskConfig += `, VAE: ${task.reqBody.use_vae_model}` + } + if (task.reqBody.negative_prompt.trim() !== '') { + taskConfig += `, Negative Prompt: ${task.reqBody.negative_prompt}` + } + if (task.reqBody.init_image !== undefined) { + taskConfig += `, Prompt Strength: ${task.reqBody.prompt_strength}` + } + if (task.reqBody.use_face_correction) { + taskConfig += `, Fix Faces: ${task.reqBody.use_face_correction}` + } + if (task.reqBody.use_upscale) { + taskConfig += `, Upscale: ${task.reqBody.use_upscale} (${task.reqBody.upscale_amount || 4}x)` + } + if (task.reqBody.use_hypernetwork_model) { + taskConfig += `, Hypernetwork: ${task.reqBody.use_hypernetwork_model}` + taskConfig += `, Hypernetwork Strength: ${task.reqBody.hypernetwork_strength}` + } + if (task.reqBody.preserve_init_image_color_profile) { + taskConfig += `, Preserve Color Profile: true` + } + + let taskEntry = document.createElement('div') + taskEntry.id = `imageTaskContainer-${Date.now()}` + taskEntry.className = 'imageTaskContainer' + taskEntry.innerHTML = `
+ +
Enqueued
+ + +
+
${taskConfig}
+
+
+
+
+
+
` + + createCollapsibles(taskEntry) + + let draghandle = taskEntry.querySelector('.drag-handle') + draghandle.addEventListener('mousedown', (e) => { + taskEntry.setAttribute('draggable', true) + }) + // Add a debounce delay to allow mobile to bouble tap. + draghandle.addEventListener('mouseup', debounce((e) => { + taskEntry.setAttribute('draggable', false) + }, 2000)) + draghandle.addEventListener('click', (e) => { + e.preventDefault() // Don't allow the results to be collapsed... + }) + taskEntry.addEventListener('dragend', (e) => { + taskEntry.setAttribute('draggable', false); + imagePreview.querySelectorAll(".imageTaskContainer").forEach(itc => { + itc.classList.remove('dropTargetBefore','dropTargetAfter'); + }); + imagePreview.removeEventListener("dragover", onTaskEntryDragOver ); + }) + taskEntry.addEventListener('dragstart', function(e) { + imagePreview.addEventListener("dragover", onTaskEntryDragOver ); + e.dataTransfer.setData("text/plain", taskEntry.id); + startX = e.target.closest(".imageTaskContainer").offsetLeft; + startY = e.target.closest(".imageTaskContainer").offsetTop; + }) + + if (task.reqBody.init_image !== undefined) { + createInitImageHover(taskEntry) + } + + task['taskStatusLabel'] = taskEntry.querySelector('.taskStatusLabel') + task['outputContainer'] = taskEntry.querySelector('.img-preview') + task['outputMsg'] = taskEntry.querySelector('.outputMsg') + task['previewPrompt'] = taskEntry.querySelector('.preview-prompt') + task['progressBar'] = taskEntry.querySelector('.progress-bar') + task['stopTask'] = taskEntry.querySelector('.stopTask') + + task['stopTask'].addEventListener('click', (e) => { + e.stopPropagation() + + let question = (task['isProcessing'] ? "Stop this task?" : "Remove this task?") + shiftOrConfirm(e, question, async function(e) { + if (task.batchesDone <= 0 || !task.isProcessing) { + removeTask(taskEntry) + } + abortTask(task) + }) + }) + + task['useSettings'] = taskEntry.querySelector('.useSettings') + task['useSettings'].addEventListener('click', function(e) { + e.stopPropagation() + restoreTaskToUI(task, TASK_REQ_NO_EXPORT) + }) + + task.isProcessing = true + taskEntry = imagePreviewContent.insertBefore(taskEntry, previewTools.nextSibling) + htmlTaskMap.set(taskEntry, task) + + task.previewPrompt.innerText = task.reqBody.prompt + if (task.previewPrompt.innerText.trim() === '') { + task.previewPrompt.innerHTML = ' ' // allows the results to be collapsed + } + return taskEntry.id +} + +function getCurrentUserRequest() { + const numOutputsTotal = parseInt(numOutputsTotalField.value) + const numOutputsParallel = parseInt(numOutputsParallelField.value) + const seed = (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value)) + + const newTask = { + batchesDone: 0, + numOutputsTotal: numOutputsTotal, + batchCount: Math.ceil(numOutputsTotal / numOutputsParallel), + seed, + + reqBody: { + seed, + used_random_seed: randomSeedField.checked, + negative_prompt: negativePromptField.value.trim(), + num_outputs: numOutputsParallel, + num_inference_steps: parseInt(numInferenceStepsField.value), + guidance_scale: parseFloat(guidanceScaleField.value), + width: parseInt(widthField.value), + height: parseInt(heightField.value), + // allow_nsfw: allowNSFWField.checked, + vram_usage_level: vramUsageLevelField.value, + //render_device: undefined, // Set device affinity. Prefer this device, but wont activate. + use_stable_diffusion_model: stableDiffusionModelField.value, + use_vae_model: vaeModelField.value, + stream_progress_updates: true, + stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked), + show_only_filtered_image: showOnlyFilteredImageField.checked, + block_nsfw: blockNSFWField.checked, + output_format: outputFormatField.value, + output_quality: parseInt(outputQualityField.value), + metadata_output_format: metadataOutputFormatField.value, + original_prompt: promptField.value, + active_tags: (activeTags.map(x => x.name)), + inactive_tags: (activeTags.filter(tag => tag.inactive === true).map(x => x.name)) + } + } + if (IMAGE_REGEX.test(initImagePreview.src)) { + newTask.reqBody.init_image = initImagePreview.src + newTask.reqBody.prompt_strength = parseFloat(promptStrengthField.value) + + // if (IMAGE_REGEX.test(maskImagePreview.src)) { + // newTask.reqBody.mask = maskImagePreview.src + // } + if (maskSetting.checked) { + newTask.reqBody.mask = imageInpainter.getImg() + } + newTask.reqBody.preserve_init_image_color_profile = applyColorCorrectionField.checked + newTask.reqBody.sampler_name = 'ddim' + } else { + newTask.reqBody.sampler_name = samplerField.value + } + if (saveToDiskField.checked && diskPathField.value.trim() !== '') { + newTask.reqBody.save_to_disk_path = diskPathField.value.trim() + } + if (useFaceCorrectionField.checked) { + newTask.reqBody.use_face_correction = gfpganModelField.value + } + if (useUpscalingField.checked) { + newTask.reqBody.use_upscale = upscaleModelField.value + newTask.reqBody.upscale_amount = upscaleAmountField.value + } + if (hypernetworkModelField.value) { + newTask.reqBody.use_hypernetwork_model = hypernetworkModelField.value + newTask.reqBody.hypernetwork_strength = parseFloat(hypernetworkStrengthField.value) + } + return newTask +} + +function getPrompts(prompts) { + if (typeof prompts === 'undefined') { + prompts = promptField.value + } + if (prompts.trim() === '' && activeTags.length === 0) { + return [''] + } + + let promptsToMake = [] + if (prompts.trim() !== '') { + prompts = prompts.split('\n') + prompts = prompts.map(prompt => prompt.trim()) + prompts = prompts.filter(prompt => prompt !== '') + + promptsToMake = applyPermuteOperator(prompts) + promptsToMake = applySetOperator(promptsToMake) + } + const newTags = activeTags.filter(tag => tag.inactive === undefined || tag.inactive === false) + if (newTags.length > 0) { + const promptTags = newTags.map(x => x.name).join(", ") + if (promptsToMake.length > 0) { + promptsToMake = promptsToMake.map((prompt) => `${prompt}, ${promptTags}`) + } + else + { + promptsToMake.push(promptTags) + } + } + + promptsToMake = applyPermuteOperator(promptsToMake) + promptsToMake = applySetOperator(promptsToMake) + + PLUGINS['GET_PROMPTS_HOOK'].forEach(fn => { promptsToMake = fn(promptsToMake) }) + + return promptsToMake +} + +function applySetOperator(prompts) { + let promptsToMake = [] + let braceExpander = new BraceExpander() + prompts.forEach(prompt => { + let expandedPrompts = braceExpander.expand(prompt) + promptsToMake = promptsToMake.concat(expandedPrompts) + }) + + return promptsToMake +} + +function applyPermuteOperator(prompts) { + let promptsToMake = [] + prompts.forEach(prompt => { + let promptMatrix = prompt.split('|') + prompt = promptMatrix.shift().trim() + promptsToMake.push(prompt) + + promptMatrix = promptMatrix.map(p => p.trim()) + promptMatrix = promptMatrix.filter(p => p !== '') + + if (promptMatrix.length > 0) { + let promptPermutations = permutePrompts(prompt, promptMatrix) + promptsToMake = promptsToMake.concat(promptPermutations) + } + }) + + return promptsToMake +} + +function permutePrompts(promptBase, promptMatrix) { + let prompts = [] + let permutations = permute(promptMatrix) + permutations.forEach(perm => { + let prompt = promptBase + + if (perm.length > 0) { + let promptAddition = perm.join(', ') + if (promptAddition.trim() === '') { + return + } + + prompt += ', ' + promptAddition + } + + prompts.push(prompt) + }) + + return prompts +} + +// create a file name with embedded prompt and metadata +// for easier cateloging and comparison +function createFileName(prompt, seed, steps, guidance, outputFormat) { + + // Most important information is the prompt + let underscoreName = prompt.replace(/[^a-zA-Z0-9]/g, '_') + underscoreName = underscoreName.substring(0, 100) + //const steps = numInferenceStepsField.value + //const guidance = guidanceScaleField.value + + // name and the top level metadata + let fileName = `${underscoreName}_Seed-${seed}_Steps-${steps}_Guidance-${guidance}` + + // add the tags + // let tags = [] + // let tagString = '' + // document.querySelectorAll(modifyTagsSelector).forEach(function(tag) { + // tags.push(tag.innerHTML) + // }) + + // join the tags with a pipe + // if (activeTags.length > 0) { + // tagString = '_Tags-' + // tagString += tags.join('|') + // } + + // // append empty or populated tags + // fileName += `${tagString}` + + // add the file extension + fileName += '.' + outputFormat + + return fileName +} + +async function stopAllTasks() { + getUncompletedTaskEntries().forEach((taskEntry) => { + const taskStatusLabel = taskEntry.querySelector('.taskStatusLabel') + if (taskStatusLabel) { + taskStatusLabel.style.display = 'none' + } + const task = htmlTaskMap.get(taskEntry) + if (!task) { + return + } + abortTask(task) + }) +} + +function removeTask(taskToRemove) { + taskToRemove.remove() + + if (document.querySelector('.imageTaskContainer') === null) { + previewTools.style.display = 'none' + initialText.style.display = 'block' + } +} + +clearAllPreviewsBtn.addEventListener('click', (e) => { shiftOrConfirm(e, "Clear all the results and tasks in this window?", async function() { + await stopAllTasks() + + let taskEntries = document.querySelectorAll('.imageTaskContainer') + taskEntries.forEach(removeTask) +})}) + +/* Download images popup */ +showDownloadPopupBtn.addEventListener("click", (e) => { saveAllImagesPopup.classList.add("active") }) + +saveAllZipToggle.addEventListener('change', (e) => { + if (saveAllZipToggle.checked) { + saveAllFoldersOption.classList.remove('displayNone') + } else { + saveAllFoldersOption.classList.add('displayNone') + } +}) + +// convert base64 to raw binary data held in a string +function dataURItoBlob(dataURI) { + var byteString = atob(dataURI.split(',')[1]) + + // separate out the mime component + var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0] + + // write the bytes of the string to an ArrayBuffer + var ab = new ArrayBuffer(byteString.length) + + // create a view into the buffer + var ia = new Uint8Array(ab) + + // set the bytes of the buffer to the correct values + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i) + } + + // write the ArrayBuffer to a blob, and you're done + return new Blob([ab], {type: mimeString}) +} + +function downloadAllImages() { + let i = 0 + + let optZIP = saveAllZipToggle.checked + let optTree = optZIP && saveAllTreeToggle.checked + let optJSON = saveAllJSONToggle.checked + + let zip = new JSZip() + let folder = zip + + document.querySelectorAll(".imageTaskContainer").forEach(container => { + if (optTree) { + let name = ++i + '-' + container.querySelector('.preview-prompt').textContent.replace(/[^a-zA-Z0-9]/g, '_') + folder = zip.folder(name) + } + container.querySelectorAll(".imgContainer img").forEach(img => { + let imgItem = img.closest('.imgItem') + + if (imgItem.style.display === 'none') { + return + } + + let req = imageRequest[img.dataset['imagecounter']] + if (optZIP) { + let suffix = img.dataset['imagecounter'] + '.' + req['output_format'] + folder.file(getDownloadFilename(img, suffix), dataURItoBlob(img.src)) + if (optJSON) { + suffix = img.dataset['imagecounter'] + '.json' + folder.file(getDownloadFilename(img, suffix), JSON.stringify(req, null, 2)) + } + } else { + setTimeout(() => {imgItem.querySelector('.download-img').click()}, i*200) + i = i+1 + if (optJSON) { + setTimeout(() => {imgItem.querySelector('.download-json').click()}, i*200) + i = i+1 + } + } + }) + }) + if (optZIP) { + let now = new Date() + zip.generateAsync({type:"blob"}).then(function (blob) { + saveAs(blob, `EasyDiffusion-Images-${now.toISOString()}.zip`); + }) + } + +} + +saveAllImagesBtn.addEventListener('click', (e) => { downloadAllImages() }) + +stopImageBtn.addEventListener('click', (e) => { shiftOrConfirm(e, "Stop all the tasks?", async function(e) { + await stopAllTasks() +})}) + +widthField.addEventListener('change', onDimensionChange) +heightField.addEventListener('change', onDimensionChange) + +function renameMakeImageButton() { + let totalImages = Math.max(parseInt(numOutputsTotalField.value), parseInt(numOutputsParallelField.value)) * getPrompts().length + let imageLabel = 'Image' + if (totalImages > 1) { + imageLabel = totalImages + ' Images' + } + if (SD.activeTasks.size == 0) { + makeImageBtn.innerText = 'Make ' + imageLabel + } else { + makeImageBtn.innerText = 'Enqueue Next ' + imageLabel + } +} +numOutputsTotalField.addEventListener('change', renameMakeImageButton) +numOutputsTotalField.addEventListener('keyup', debounce(renameMakeImageButton, 300)) +numOutputsParallelField.addEventListener('change', renameMakeImageButton) +numOutputsParallelField.addEventListener('keyup', debounce(renameMakeImageButton, 300)) + +function onDimensionChange() { + let widthValue = parseInt(widthField.value) + let heightValue = parseInt(heightField.value) + if (!initImagePreviewContainer.classList.contains("has-image")) { + imageEditor.setImage(null, widthValue, heightValue) + } + else { + imageInpainter.setImage(initImagePreview.src, widthValue, heightValue) + } + if ( widthValue < 512 && heightValue < 512 ) { + smallImageWarning.classList.remove('displayNone') + } else { + smallImageWarning.classList.add('displayNone') + } +} + +diskPathField.disabled = !saveToDiskField.checked +metadataOutputFormatField.disabled = !saveToDiskField.checked + +gfpganModelField.disabled = !useFaceCorrectionField.checked +useFaceCorrectionField.addEventListener('change', function(e) { + gfpganModelField.disabled = !this.checked +}) + +upscaleModelField.disabled = !useUpscalingField.checked +upscaleAmountField.disabled = !useUpscalingField.checked +useUpscalingField.addEventListener('change', function(e) { + upscaleModelField.disabled = !this.checked + upscaleAmountField.disabled = !this.checked +}) + +makeImageBtn.addEventListener('click', makeImage) + +document.onkeydown = function(e) { + if (e.ctrlKey && e.code === 'Enter') { + makeImage() + e.preventDefault() + } +} + +/********************* Guidance **************************/ +function updateGuidanceScale() { + guidanceScaleField.value = guidanceScaleSlider.value / 10 + guidanceScaleField.dispatchEvent(new Event("change")) +} + +function updateGuidanceScaleSlider() { + if (guidanceScaleField.value < 0) { + guidanceScaleField.value = 0 + } else if (guidanceScaleField.value > 50) { + guidanceScaleField.value = 50 + } + + guidanceScaleSlider.value = guidanceScaleField.value * 10 + guidanceScaleSlider.dispatchEvent(new Event("change")) +} + +guidanceScaleSlider.addEventListener('input', updateGuidanceScale) +guidanceScaleField.addEventListener('input', updateGuidanceScaleSlider) +updateGuidanceScale() + +/********************* Prompt Strength *******************/ +function updatePromptStrength() { + promptStrengthField.value = promptStrengthSlider.value / 100 + promptStrengthField.dispatchEvent(new Event("change")) +} + +function updatePromptStrengthSlider() { + if (promptStrengthField.value < 0) { + promptStrengthField.value = 0 + } else if (promptStrengthField.value > 0.99) { + promptStrengthField.value = 0.99 + } + + promptStrengthSlider.value = promptStrengthField.value * 100 + promptStrengthSlider.dispatchEvent(new Event("change")) +} + +promptStrengthSlider.addEventListener('input', updatePromptStrength) +promptStrengthField.addEventListener('input', updatePromptStrengthSlider) +updatePromptStrength() + +/********************* Hypernetwork Strength **********************/ +function updateHypernetworkStrength() { + hypernetworkStrengthField.value = hypernetworkStrengthSlider.value / 100 + hypernetworkStrengthField.dispatchEvent(new Event("change")) +} + +function updateHypernetworkStrengthSlider() { + if (hypernetworkStrengthField.value < 0) { + hypernetworkStrengthField.value = 0 + } else if (hypernetworkStrengthField.value > 0.99) { + hypernetworkStrengthField.value = 0.99 + } + + hypernetworkStrengthSlider.value = hypernetworkStrengthField.value * 100 + hypernetworkStrengthSlider.dispatchEvent(new Event("change")) +} + +hypernetworkStrengthSlider.addEventListener('input', updateHypernetworkStrength) +hypernetworkStrengthField.addEventListener('input', updateHypernetworkStrengthSlider) +updateHypernetworkStrength() + +function updateHypernetworkStrengthContainer() { + document.querySelector("#hypernetwork_strength_container").style.display = (hypernetworkModelField.value === "" ? 'none' : '') +} +hypernetworkModelField.addEventListener('change', updateHypernetworkStrengthContainer) +updateHypernetworkStrengthContainer() + +/********************* JPEG/WEBP Quality **********************/ +function updateOutputQuality() { + outputQualityField.value = 0 | outputQualitySlider.value + outputQualityField.dispatchEvent(new Event("change")) +} + +function updateOutputQualitySlider() { + if (outputQualityField.value < 10) { + outputQualityField.value = 10 + } else if (outputQualityField.value > 95) { + outputQualityField.value = 95 + } + + outputQualitySlider.value = 0 | outputQualityField.value + outputQualitySlider.dispatchEvent(new Event("change")) +} + +outputQualitySlider.addEventListener('input', updateOutputQuality) +outputQualityField.addEventListener('input', debounce(updateOutputQualitySlider, 1500)) +updateOutputQuality() + +outputFormatField.addEventListener('change', e => { + if (outputFormatField.value === 'png') { + outputQualityRow.style.display='none' + } else { + outputQualityRow.style.display='table-row' + } +}) +/********************* Zoom Slider **********************/ +thumbnailSizeField.addEventListener('change', () => { + (function (s) { + for (var j =0; j < document.styleSheets.length; j++) { + let cssSheet = document.styleSheets[j] + for (var i = 0; i < cssSheet.cssRules.length; i++) { + var rule = cssSheet.cssRules[i]; + if (rule.selectorText == "div.img-preview img") { + rule.style['max-height'] = s+'vh'; + rule.style['max-width'] = s+'vw'; + return; + } + } + } + })(thumbnailSizeField.value) +}) + +function onAutoScrollUpdate() { + if (autoScroll.checked) { + autoscrollBtn.classList.add('pressed') + } else { + autoscrollBtn.classList.remove('pressed') + } + autoscrollBtn.querySelector(".state").innerHTML = (autoScroll.checked ? "ON" : "OFF") +} +autoscrollBtn.addEventListener('click', function() { + autoScroll.checked = !autoScroll.checked + autoScroll.dispatchEvent(new Event("change")) + onAutoScrollUpdate() +}) +autoScroll.addEventListener('change', onAutoScrollUpdate) + +function checkRandomSeed() { + if (randomSeedField.checked) { + seedField.disabled = true + //seedField.value = "0" // This causes the seed to be lost if the user changes their mind after toggling the checkbox + } else { + seedField.disabled = false + } +} +randomSeedField.addEventListener('input', checkRandomSeed) +checkRandomSeed() + +function loadImg2ImgFromFile() { + if (initImageSelector.files.length === 0) { + return + } + + let reader = new FileReader() + let file = initImageSelector.files[0] + + reader.addEventListener('load', function(event) { + initImagePreview.src = reader.result + }) + + if (file) { + reader.readAsDataURL(file) + } +} +initImageSelector.addEventListener('change', loadImg2ImgFromFile) +loadImg2ImgFromFile() + +function img2imgLoad() { + promptStrengthContainer.style.display = 'table-row' + samplerSelectionContainer.style.display = "none" + initImagePreviewContainer.classList.add("has-image") + colorCorrectionSetting.style.display = '' + + initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight + imageEditor.setImage(this.src, initImagePreview.naturalWidth, initImagePreview.naturalHeight) + imageInpainter.setImage(this.src, parseInt(widthField.value), parseInt(heightField.value)) +} + +function img2imgUnload() { + initImageSelector.value = null + initImagePreview.src = '' + maskSetting.checked = false + + promptStrengthContainer.style.display = "none" + samplerSelectionContainer.style.display = "" + initImagePreviewContainer.classList.remove("has-image") + colorCorrectionSetting.style.display = 'none' + imageEditor.setImage(null, parseInt(widthField.value), parseInt(heightField.value)) + +} +initImagePreview.addEventListener('load', img2imgLoad) +initImageClearBtn.addEventListener('click', img2imgUnload) + +maskSetting.addEventListener('click', function() { + onDimensionChange() +}) + +promptsFromFileBtn.addEventListener('click', function() { + promptsFromFileSelector.click() +}) + +promptsFromFileSelector.addEventListener('change', async function() { + if (promptsFromFileSelector.files.length === 0) { + return + } + + let reader = new FileReader() + let file = promptsFromFileSelector.files[0] + + reader.addEventListener('load', async function() { + await parseContent(reader.result) + }) + + if (file) { + reader.readAsText(file) + } +}) + +/* setup popup handlers */ +document.querySelectorAll('.popup').forEach(popup => { + popup.addEventListener('click', event => { + if (event.target == popup) { + popup.classList.remove("active") + } + }) + var closeButton = popup.querySelector(".close-button") + if (closeButton) { + closeButton.addEventListener('click', () => { + popup.classList.remove("active") + }) + } +}) + +var tabElements = [] +function selectTab(tab_id) { + let tabInfo = tabElements.find(t => t.tab.id == tab_id) + if (!tabInfo.tab.classList.contains("active")) { + tabElements.forEach(info => { + if (info.tab.classList.contains("active") && info.tab.parentNode === tabInfo.tab.parentNode) { + info.tab.classList.toggle("active") + info.content.classList.toggle("active") + } + }) + tabInfo.tab.classList.toggle("active") + tabInfo.content.classList.toggle("active") + } + document.dispatchEvent(new CustomEvent('tabClick', { detail: tabInfo })) +} +function linkTabContents(tab) { + var name = tab.id.replace("tab-", "") + var content = document.getElementById(`tab-content-${name}`) + tabElements.push({ + name: name, + tab: tab, + content: content + }) + + tab.addEventListener("click", event => selectTab(tab.id)) +} +function isTabActive(tab) { + return tab.classList.contains("active") +} + +let pauseClient = false + +function resumeClient() { + if (pauseClient) { + document.body.classList.remove('wait-pause') + document.body.classList.add('pause') + } + return new Promise(resolve => { + let playbuttonclick = function () { + resumeBtn.removeEventListener("click", playbuttonclick); + resolve("resolved"); + } + resumeBtn.addEventListener("click", playbuttonclick) + }) +} + +promptField.addEventListener("input", debounce( renameMakeImageButton, 1000) ) + + +pauseBtn.addEventListener("click", function () { + pauseClient = true + pauseBtn.style.display="none" + resumeBtn.style.display = "inline" + document.body.classList.add('wait-pause') +}) + +resumeBtn.addEventListener("click", function () { + pauseClient = false + resumeBtn.style.display = "none" + pauseBtn.style.display = "inline" + document.body.classList.remove('pause') + document.body.classList.remove('wait-pause') +}) + +/* Pause function */ +document.querySelectorAll(".tab").forEach(linkTabContents) + +window.addEventListener("beforeunload", function(e) { + const msg = "Unsaved pictures will be lost!"; + + let elementList = document.getElementsByClassName("imageTaskContainer"); + if (elementList.length != 0) { + e.preventDefault(); + (e || window.event).returnValue = msg; + return msg; + } else { + return true; + } +}); + +createCollapsibles() +prettifyInputs(document); + +// set the textbox as focused on start +promptField.focus() +promptField.selectionStart = promptField.value.length diff --git a/ui/media/js/marked.min.js b/ui/media/js/marked.min.js new file mode 100644 index 0000000000000000000000000000000000000000..e94e70d49600dfe6cfc16d9089d865571af70071 --- /dev/null +++ b/ui/media/js/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2022, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,function(r){"use strict";function i(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[u++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function e(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}r.defaults=e();function u(e){return t[e]}var n=/[&<>"']/,l=/[&<>"']/g,a=/[<>"']|&(?!#?\w+;)/,o=/[<>"']|&(?!#?\w+;)/g,t={"&":"&","<":"<",">":">",'"':""","'":"'"};function D(e,t){if(t){if(n.test(e))return e.replace(l,u)}else if(a.test(e))return e.replace(o,u);return e}var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function x(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var h=/(^|[^\[])\^/g;function p(u,e){u="string"==typeof u?u:u.source,e=e||"";var n={replace:function(e,t){return t=(t=t.source||t).replace(h,"$1"),u=u.replace(e,t),n},getRegex:function(){return new RegExp(u,e)}};return n}var f=/[^\w:]/g,Z=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function g(e,t,u){if(e){try{n=decodeURIComponent(x(u)).replace(f,"").toLowerCase()}catch(e){return null}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return null}var n;t&&!Z.test(u)&&(e=u,F[" "+(n=t)]||(O.test(n)?F[" "+n]=n+"/":F[" "+n]=k(n,"/",!0)),t=-1===(n=F[" "+n]).indexOf(":"),u="//"===e.substring(0,2)?t?e:n.replace(q,"$1")+e:"/"===e.charAt(0)?t?e:n.replace(L,"$1")+e:n+e);try{u=encodeURI(u).replace(/%25/g,"%")}catch(e){return null}return u}var F={},O=/^[^:]+:\/*[^/]*$/,q=/^([^:]+:)[\s\S]*$/,L=/^([^:]+:\/*[^/]*)[\s\S]*$/;var A={exec:function(){}};function d(e){for(var t,u,n=1;nt)u.splice(t);else for(;u.length>=1,e+=e;return u+e}function b(e,t,u,n){var r=t.href,t=t.title?D(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?(n.state.inLink=!0,e={type:"link",raw:u,href:r,title:t,text:i,tokens:n.inlineTokens(i)},n.state.inLink=!1,e):{type:"image",raw:u,href:r,title:t,text:D(i)}}var w=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e&&0=r.length?e.slice(r.length):e}).join("\n")),{type:"code",raw:t,lang:e[2]&&e[2].trim().replace(this.rules.inline._escapes,"$1"),text:u}},t.heading=function(e){var t,u,e=this.rules.block.heading.exec(e);if(e)return t=e[2].trim(),/#$/.test(t)&&(u=k(t,"#"),!this.options.pedantic&&u&&!/ $/.test(u)||(t=u.trim())),{type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:this.lexer.inline(t)}},t.hr=function(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}},t.blockquote=function(e){var t,e=this.rules.block.blockquote.exec(e);if(e)return t=e[0].replace(/^ *>[ \t]?/gm,""),{type:"blockquote",raw:e[0],tokens:this.lexer.blockTokens(t,[]),text:t}},t.list=function(e){var t=this.rules.block.list.exec(e);if(t){var u,n,r,i,s,l,a,o,D,c,h,p=1<(g=t[1].trim()).length,f={type:"list",raw:"",ordered:p,start:p?+g.slice(0,-1):"",loose:!1,items:[]},g=p?"\\d{1,9}\\"+g.slice(-1):"\\"+g;this.options.pedantic&&(g=p?g:"[*+-]");for(var F=new RegExp("^( {0,3}"+g+")((?:[\t ][^\\n]*)?(?:\\n|$))");e&&(h=!1,t=F.exec(e))&&!this.rules.block.hr.test(e);){if(u=t[0],e=e.substring(u.length),a=t[2].split("\n",1)[0],o=e.split("\n",1)[0],this.options.pedantic?(i=2,c=a.trimLeft()):(i=t[2].search(/[^ ]/),c=a.slice(i=4=i||!a.trim())c+="\n"+a.slice(i);else{if(s)break;c+="\n"+a}s||a.trim()||(s=!0),u+=D+"\n",e=e.substring(D.length+1)}f.loose||(l?f.loose=!0:/\n *\n *$/.test(u)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(c))&&(r="[ ] "!==n[0],c=c.replace(/^\[[ xX]\] +/,"")),f.items.push({type:"list_item",raw:u,task:!!n,checked:r,loose:!1,text:c}),f.raw+=u}f.items[f.items.length-1].raw=u.trimRight(),f.items[f.items.length-1].text=c.trimRight(),f.raw=f.raw.trimRight();for(var E=f.items.length,x=0;x/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]}},t.link=function(e){e=this.rules.inline.link.exec(e);if(e){var t=e[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;var u=k(t.slice(0,-1),"\\");if((t.length-u.length)%2==0)return}else{u=function(e,t){if(-1!==e.indexOf(t[1]))for(var u=e.length,n=0,r=0;r$/.test(t)?u.slice(1):u.slice(1,-1):u)&&u.replace(this.rules.inline._escapes,"$1"),title:r&&r.replace(this.rules.inline._escapes,"$1")},e[0],this.lexer)}},t.reflink=function(e,t){var u;if(u=(u=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e))return(e=t[(e=(u[2]||u[1]).replace(/\s+/g," ")).toLowerCase()])&&e.href?b(u,e,u[0],this.lexer):{type:"text",raw:t=u[0].charAt(0),text:t}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||""===u||this.rules.inline.punctuation.exec(u)){var i=n[0].length-1,s=i,l=0,a="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+i);null!=(n=a.exec(t));){var o,D=n[1]||n[2]||n[3]||n[4]||n[5]||n[6];if(D)if(o=D.length,n[3]||n[4])s+=o;else if((n[5]||n[6])&&i%3&&!((i+o)%3))l+=o;else if(!(0<(s-=o)))return o=Math.min(o,o+s+l),D=e.slice(0,i+n.index+(n[0].length-D.length)+o),Math.min(i,o)%2?(o=D.slice(1,-1),{type:"em",raw:D,text:o,tokens:this.lexer.inlineTokens(o)}):(o=D.slice(2,-2),{type:"strong",raw:D,text:o,tokens:this.lexer.inlineTokens(o)})}}}},t.codespan=function(e){var t,u,n,e=this.rules.inline.code.exec(e);if(e)return n=e[2].replace(/\n/g," "),t=/[^ ]/.test(n),u=/^ /.test(n)&&/ $/.test(n),n=D(n=t&&u?n.substring(1,n.length-1):n,!0),{type:"codespan",raw:e[0],text:n}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}},t.autolink=function(e,t){var u,e=this.rules.inline.autolink.exec(e);if(e)return t="@"===e[2]?"mailto:"+(u=D(this.options.mangle?t(e[1]):e[1])):u=D(e[1]),{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=D(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=D(u[0]),r="www."===u[1]?"http://"+n:n}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t){e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]:D(this.options.smartypants?t(e[0]):e[0]),{type:"text",raw:e[0],text:t}},e}(),y={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?]+)>?(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:A,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/},v=(y.def=p(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}[.)])/,y.listItemStart=p(/^( *)(bull) */).replace("bull",y.bullet).getRegex(),y.list=p(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=/|$)/,y.html=p(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=p(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.blockquote=p(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),y.gfm.table=p(y.gfm.table).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.gfm.paragraph=p(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",y.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.pedantic=d({},y.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:A,paragraph:p(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),{escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:A,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:A,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",v.punctuation=p(v.punctuation).replace(/punctuation/g,v._punctuation).getRegex(),v.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,v.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,v._comment=p(y._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),v.emStrong.lDelim=p(v.emStrong.lDelim).replace(/punct/g,v._punctuation).getRegex(),v.emStrong.rDelimAst=p(v.emStrong.rDelimAst,"g").replace(/punct/g,v._punctuation).getRegex(),v.emStrong.rDelimUnd=p(v.emStrong.rDelimUnd,"g").replace(/punct/g,v._punctuation).getRegex(),v._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,v._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,v._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,v.autolink=p(v.autolink).replace("scheme",v._scheme).replace("email",v._email).getRegex(),v._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,v.tag=p(v.tag).replace("comment",v._comment).replace("attribute",v._attribute).getRegex(),v._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,v._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,v._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,v.link=p(v.link).replace("label",v._label).replace("href",v._href).replace("title",v._title).getRegex(),v.reflink=p(v.reflink).replace("label",v._label).replace("ref",y._label).getRegex(),v.nolink=p(v.nolink).replace("ref",y._label).getRegex(),v.reflinkSearch=p(v.reflinkSearch,"g").replace("reflink",v.reflink).replace("nolink",v.nolink).getRegex(),v.normal=d({},v),v.pedantic=d({},v.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",v._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",v._label).getRegex()}),v.gfm=d({},v.normal,{escape:p(v.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:D(e,!0))+"\n":"
"+(u?e:D(e,!0))+"
\n"},t.blockquote=function(e){return"
\n"+e+"
\n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},t.hr=function(){return this.options.xhtml?"
\n":"
\n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"\n"},t.listitem=function(e){return"
  • "+e+"
  • \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return"

    "+e+"

    \n"},t.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},t.tablerow=function(e){return"\n"+e+"\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"\n"},t.strong=function(e){return""+e+""},t.em=function(e){return""+e+""},t.codespan=function(e){return""+e+""},t.br=function(){return this.options.xhtml?"
    ":"
    "},t.del=function(e){return""+e+""},t.link=function(e,t,u){return null===(e=g(this.options.sanitize,this.options.baseUrl,e))?u:(e='"+u+"")},t.image=function(e,t,u){return null===(e=g(this.options.sanitize,this.options.baseUrl,e))?u:(e=''+u+'":">"))},t.text=function(e){return e},e}(),S=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),T=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});e=this.serialize(e);return this.getNextSafeSlug(e,t.dryrun)},e}(),R=function(){function u(e){this.options=e||r.defaults,this.options.renderer=this.options.renderer||new $,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new S,this.slugger=new T}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,a,o,D,c,h,p,f,g,F,A,d="",C=e.length,k=0;kAn error occurred:

    "+D(e.message+"",!0)+"
    ";throw e}try{var a=z.lex(e,u);if(u.walkTokens){if(u.async)return Promise.all(I.walkTokens(a,u.walkTokens)).then(function(){return R.parse(a,u)}).catch(t);I.walkTokens(a,u.walkTokens)}return R.parse(a,u)}catch(e){t(e)}}I.options=I.setOptions=function(e){return d(I.defaults,e),e=I.defaults,r.defaults=e,I},I.getDefaults=e,I.defaults=r.defaults,I.use=function(){for(var e=arguments.length,t=new Array(e),u=0;uAn error occurred:

    "+D(e.message+"",!0)+"
    ";throw e}},I.Parser=R,I.parser=R.parse,I.Renderer=$,I.TextRenderer=S,I.Lexer=z,I.lexer=z.lex,I.Tokenizer=w,I.Slugger=T;var A=(I.parse=I).options,P=I.setOptions,Q=I.use,U=I.walkTokens,M=I.parseInline,N=I,X=R.parse,G=z.lex;r.Lexer=z,r.Parser=R,r.Renderer=$,r.Slugger=T,r.TextRenderer=S,r.Tokenizer=w,r.getDefaults=e,r.lexer=G,r.marked=I,r.options=A,r.parse=N,r.parseInline=M,r.parser=X,r.setOptions=P,r.use=Q,r.walkTokens=U,Object.defineProperty(r,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/ui/media/js/parameters.js b/ui/media/js/parameters.js new file mode 100644 index 0000000000000000000000000000000000000000..0e4b9b224f264c160f8c03da1952428430a25ad9 --- /dev/null +++ b/ui/media/js/parameters.js @@ -0,0 +1,479 @@ +/** + * Enum of parameter types + * @readonly + * @enum {string} + */ + var ParameterType = { + checkbox: "checkbox", + select: "select", + select_multiple: "select_multiple", + slider: "slider", + custom: "custom", +}; + +/** + * JSDoc style + * @typedef {object} Parameter + * @property {string} id + * @property {ParameterType} type + * @property {string} label + * @property {?string} note + * @property {number|boolean|string} default + */ + + +/** @type {Array.} */ +var PARAMETERS = [ + { + id: "theme", + type: ParameterType.select, + label: "Theme", + default: "theme-default", + note: "customize the look and feel of the ui", + options: [ // Note: options expanded dynamically + { + value: "theme-default", + label: "Default" + } + ], + icon: "fa-palette" + }, + { + id: "save_to_disk", + type: ParameterType.checkbox, + label: "Auto-Save Images", + note: "automatically saves images to the specified location", + icon: "fa-download", + default: false, + }, + { + id: "diskPath", + type: ParameterType.custom, + label: "Save Location", + render: (parameter) => { + return `` + } + }, + { + id: "metadata_output_format", + type: ParameterType.select, + label: "Metadata format", + note: "will be saved to disk in this format", + default: "txt", + options: [ + { + value: "none", + label: "none" + }, + { + value: "txt", + label: "txt" + }, + { + value: "json", + label: "json" + }, + { + value: "embed", + label: "embed" + } + ], + }, + { + id: "block_nsfw", + type: ParameterType.checkbox, + label: "Block NSFW images", + note: "blurs out NSFW images", + icon: "fa-land-mine-on", + default: false, + }, + { + id: "sound_toggle", + type: ParameterType.checkbox, + label: "Enable Sound", + note: "plays a sound on task completion", + icon: "fa-volume-low", + default: true, + }, + { + id: "process_order_toggle", + type: ParameterType.checkbox, + label: "Process newest jobs first", + note: "reverse the normal processing order", + icon: "fa-arrow-down-short-wide", + default: false, + }, + { + id: "ui_open_browser_on_start", + type: ParameterType.checkbox, + label: "Open browser on startup", + note: "starts the default browser on startup", + icon: "fa-window-restore", + default: true, + }, + { + id: "vram_usage_level", + type: ParameterType.select, + label: "GPU Memory Usage", + note: "Faster performance requires more GPU memory (VRAM)

    " + + "Balanced: nearly as fast as High, much lower VRAM usage
    " + + "High: fastest, maximum GPU memory usage
    " + + "Low: slowest, recommended for GPUs with 3 to 4 GB memory", + icon: "fa-forward", + default: "balanced", + options: [ + {value: "balanced", label: "Balanced"}, + {value: "high", label: "High"}, + {value: "low", label: "Low"} + ], + }, + { + id: "use_cpu", + type: ParameterType.checkbox, + label: "Use CPU (not GPU)", + note: "warning: this will be *very* slow", + icon: "fa-microchip", + default: false, + }, + { + id: "auto_pick_gpus", + type: ParameterType.checkbox, + label: "Automatically pick the GPUs (experimental)", + default: false, + }, + { + id: "use_gpus", + type: ParameterType.select_multiple, + label: "GPUs to use (experimental)", + note: "to process in parallel", + default: false, + }, + { + id: "auto_save_settings", + type: ParameterType.checkbox, + label: "Auto-Save Settings", + note: "restores settings on browser load", + icon: "fa-gear", + default: true, + }, + { + id: "confirm_dangerous_actions", + type: ParameterType.checkbox, + label: "Confirm dangerous actions", + note: "Actions that might lead to data loss must either be clicked with the shift key pressed, or confirmed in an 'Are you sure?' dialog", + icon: "fa-check-double", + default: true, + }, + { + id: "listen_to_network", + type: ParameterType.checkbox, + label: "Make Stable Diffusion available on your network", + note: "Other devices on your network can access this web page", + icon: "fa-network-wired", + default: true, + }, + { + id: "listen_port", + type: ParameterType.custom, + label: "Network port", + note: "Port that this server listens to. The '9000' part in 'http://localhost:9000'", + icon: "fa-anchor", + render: (parameter) => { + return `` + } + }, + { + id: "use_beta_channel", + type: ParameterType.checkbox, + label: "Beta channel", + note: "Get the latest features immediately (but could be less stable). Please restart the program after changing this.", + icon: "fa-fire", + default: false, + }, +]; + +function getParameterSettingsEntry(id) { + let parameter = PARAMETERS.filter(p => p.id === id) + if (parameter.length === 0) { + return + } + return parameter[0].settingsEntry +} + +function sliderUpdate(event) { + if (event.srcElement.id.endsWith('-input')) { + let slider = document.getElementById(event.srcElement.id.slice(0,-6)) + slider.value = event.srcElement.value + slider.dispatchEvent(new Event("change")) + } else { + let field = document.getElementById(event.srcElement.id+'-input') + field.value = event.srcElement.value + field.dispatchEvent(new Event("change")) + } +} + +function getParameterElement(parameter) { + switch (parameter.type) { + case ParameterType.checkbox: + var is_checked = parameter.default ? " checked" : ""; + return `` + case ParameterType.select: + case ParameterType.select_multiple: + var options = (parameter.options || []).map(option => ``).join("") + var multiple = (parameter.type == ParameterType.select_multiple ? 'multiple' : '') + return `` + case ParameterType.slider: + return `  ${parameter.slider_unit}` + case ParameterType.custom: + return parameter.render(parameter) + default: + console.error(`Invalid type for parameter ${parameter.id}`); + return "ERROR: Invalid Type" + } +} + +let parametersTable = document.querySelector("#system-settings .parameters-table") +/* fill in the system settings popup table */ +function initParameters() { + PARAMETERS.forEach(parameter => { + var element = getParameterElement(parameter) + var note = parameter.note ? `${parameter.note}` : ""; + var icon = parameter.icon ? `` : ""; + var newrow = document.createElement('div') + newrow.innerHTML = ` +
    ${icon}
    +
    ${note}
    +
    ${element}
    ` + parametersTable.appendChild(newrow) + parameter.settingsEntry = newrow + }) +} + +initParameters() + +let vramUsageLevelField = document.querySelector('#vram_usage_level') +let useCPUField = document.querySelector('#use_cpu') +let autoPickGPUsField = document.querySelector('#auto_pick_gpus') +let useGPUsField = document.querySelector('#use_gpus') +let saveToDiskField = document.querySelector('#save_to_disk') +let diskPathField = document.querySelector('#diskPath') +let metadataOutputFormatField = document.querySelector('#metadata_output_format') +let listenToNetworkField = document.querySelector("#listen_to_network") +let listenPortField = document.querySelector("#listen_port") +let useBetaChannelField = document.querySelector("#use_beta_channel") +let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start") +let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions") + +let saveSettingsBtn = document.querySelector('#save-system-settings-btn') + + +async function changeAppConfig(configDelta) { + try { + let res = await fetch('/app_config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(configDelta) + }) + res = await res.json() + + console.log('set config status response', res) + } catch (e) { + console.log('set config status error', e) + } +} + +async function getAppConfig() { + try { + let res = await fetch('/get/app_config') + const config = await res.json() + + if (config.update_branch === 'beta') { + useBetaChannelField.checked = true + document.querySelector("#updateBranchLabel").innerText = "(beta)" + } + if (config.ui && config.ui.open_browser_on_start === false) { + uiOpenBrowserOnStartField.checked = false + } + if (config.net && config.net.listen_to_network === false) { + listenToNetworkField.checked = false + } + if (config.net && config.net.listen_port !== undefined) { + listenPortField.value = config.net.listen_port + } + + console.log('get config status response', config) + } catch (e) { + console.log('get config status error', e) + } +} + +saveToDiskField.addEventListener('change', function(e) { + diskPathField.disabled = !this.checked + metadataOutputFormatField.disabled = !this.checked +}) + +function getCurrentRenderDeviceSelection() { + let selectedGPUs = $('#use_gpus').val() + + if (useCPUField.checked && !autoPickGPUsField.checked) { + return 'cpu' + } + if (autoPickGPUsField.checked || selectedGPUs.length == 0) { + return 'auto' + } + + return selectedGPUs.join(',') +} + +useCPUField.addEventListener('click', function() { + let gpuSettingEntry = getParameterSettingsEntry('use_gpus') + let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus') + if (this.checked) { + gpuSettingEntry.style.display = 'none' + autoPickGPUSettingEntry.style.display = 'none' + autoPickGPUsField.setAttribute('data-old-value', autoPickGPUsField.checked) + autoPickGPUsField.checked = false + } else if (useGPUsField.options.length >= MIN_GPUS_TO_SHOW_SELECTION) { + gpuSettingEntry.style.display = '' + autoPickGPUSettingEntry.style.display = '' + let oldVal = autoPickGPUsField.getAttribute('data-old-value') + if (oldVal === null || oldVal === undefined) { // the UI started with CPU selected by default + autoPickGPUsField.checked = true + } else { + autoPickGPUsField.checked = (oldVal === 'true') + } + gpuSettingEntry.style.display = (autoPickGPUsField.checked ? 'none' : '') + } +}) + +useGPUsField.addEventListener('click', function() { + let selectedGPUs = $('#use_gpus').val() + autoPickGPUsField.checked = (selectedGPUs.length === 0) +}) + +autoPickGPUsField.addEventListener('click', function() { + if (this.checked) { + $('#use_gpus').val([]) + } + + let gpuSettingEntry = getParameterSettingsEntry('use_gpus') + gpuSettingEntry.style.display = (this.checked ? 'none' : '') +}) + +async function setDiskPath(defaultDiskPath, force=false) { + var diskPath = getSetting("diskPath") + if (force || diskPath == '' || diskPath == undefined || diskPath == "undefined") { + setSetting("diskPath", defaultDiskPath) + } +} + +function setDeviceInfo(devices) { + let cpu = devices.all.cpu.name + let allGPUs = Object.keys(devices.all).filter(d => d != 'cpu') + let activeGPUs = Object.keys(devices.active) + + function ID_TO_TEXT(d) { + let info = devices.all[d] + if ("mem_free" in info && "mem_total" in info) { + return `${info.name} (${d}) (${info.mem_free.toFixed(1)}Gb free / ${info.mem_total.toFixed(1)} Gb total)` + } else { + return `${info.name} (${d}) (no memory info)` + } + } + + allGPUs = allGPUs.map(ID_TO_TEXT) + activeGPUs = activeGPUs.map(ID_TO_TEXT) + + let systemInfoEl = document.querySelector('#system-info') + systemInfoEl.querySelector('#system-info-cpu').innerText = cpu + systemInfoEl.querySelector('#system-info-gpus-all').innerHTML = allGPUs.join('
    ') + systemInfoEl.querySelector('#system-info-rendering-devices').innerHTML = activeGPUs.join('
    ') +} + +function setHostInfo(hosts) { + let port = listenPortField.value + hosts = hosts.map(addr => `http://${addr}:${port}/`).map(url => ``) + document.querySelector('#system-info-server-hosts').innerHTML = hosts.join('') +} + +async function getSystemInfo() { + try { + const res = await SD.getSystemInfo() + let devices = res['devices'] + + let allDeviceIds = Object.keys(devices['all']).filter(d => d !== 'cpu') + let activeDeviceIds = Object.keys(devices['active']).filter(d => d !== 'cpu') + + if (activeDeviceIds.length === 0) { + useCPUField.checked = true + } + + if (allDeviceIds.length < MIN_GPUS_TO_SHOW_SELECTION || useCPUField.checked) { + let gpuSettingEntry = getParameterSettingsEntry('use_gpus') + gpuSettingEntry.style.display = 'none' + let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus') + autoPickGPUSettingEntry.style.display = 'none' + } + + if (allDeviceIds.length === 0) { + useCPUField.checked = true + useCPUField.disabled = true // no compatible GPUs, so make the CPU mandatory + } + + autoPickGPUsField.checked = (devices['config'] === 'auto') + + useGPUsField.innerHTML = '' + allDeviceIds.forEach(device => { + let deviceName = devices['all'][device]['name'] + let deviceOption = `` + useGPUsField.insertAdjacentHTML('beforeend', deviceOption) + }) + + if (autoPickGPUsField.checked) { + let gpuSettingEntry = getParameterSettingsEntry('use_gpus') + gpuSettingEntry.style.display = 'none' + } else { + $('#use_gpus').val(activeDeviceIds) + } + + setDeviceInfo(devices) + setHostInfo(res['hosts']) + let force = false + if (res['enforce_output_dir'] !== undefined) { + force = res['enforce_output_dir'] + if (force == true) { + saveToDiskField.checked = true + metadataOutputFormatField.disabled = false + } + saveToDiskField.disabled = force + diskPathField.disabled = force + } + setDiskPath(res['default_output_dir'], force) + } catch (e) { + console.log('error fetching devices', e) + } +} + +saveSettingsBtn.addEventListener('click', function() { + if (listenPortField.value == '') { + alert('The network port field must not be empty.') + return + } + if (listenPortField.value < 1 || listenPortField.value > 65535) { + alert('The network port must be a number from 1 to 65535') + return + } + let updateBranch = (useBetaChannelField.checked ? 'beta' : 'main') + changeAppConfig({ + 'render_devices': getCurrentRenderDeviceSelection(), + 'update_branch': updateBranch, + 'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked, + 'listen_to_network': listenToNetworkField.checked, + 'listen_port': listenPortField.value + }) + saveSettingsBtn.classList.add('active') + asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active')) +}) + diff --git a/ui/media/js/plugins.js b/ui/media/js/plugins.js new file mode 100644 index 0000000000000000000000000000000000000000..0e5ba261186f8fdb388c19533f9c71c8f1393d5c --- /dev/null +++ b/ui/media/js/plugins.js @@ -0,0 +1,74 @@ +const PLUGIN_API_VERSION = "1.0" + +const PLUGINS = { + /** + * Register new buttons to show on each output image. + * + * Example: + * PLUGINS['IMAGE_INFO_BUTTONS'].push({ + * text: 'Make a Similar Image', + * on_click: function(origRequest, image) { + * let newTaskRequest = getCurrentUserRequest() + * newTaskRequest.reqBody = Object.assign({}, origRequest, { + * init_image: image.src, + * prompt_strength: 0.7, + * seed: Math.floor(Math.random() * 10000000) + * }) + * newTaskRequest.seed = newTaskRequest.reqBody.seed + * createTask(newTaskRequest) + * }, + * filter: function(origRequest, image) { + * // this is an optional function. return true/false to show/hide the button + * // if this function isn't set, the button will always be visible + * return true + * } + * }) + */ + IMAGE_INFO_BUTTONS: [], + GET_PROMPTS_HOOK: [], + MODIFIERS_LOAD: [], + TASK_CREATE: [], + OUTPUTS_FORMATS: new ServiceContainer( + function png() { return (reqBody) => new SD.RenderTask(reqBody) } + , function jpeg() { return (reqBody) => new SD.RenderTask(reqBody) } + , function webp() { return (reqBody) => new SD.RenderTask(reqBody) } + ), +} +PLUGINS.OUTPUTS_FORMATS.register = function(...args) { + const service = ServiceContainer.prototype.register.apply(this, args) + if (typeof outputFormatField !== 'undefined') { + const newOption = document.createElement("option") + newOption.setAttribute("value", service.name) + newOption.innerText = service.name + outputFormatField.appendChild(newOption) + } + return service +} + +function loadScript(url) { + const script = document.createElement('script') + const promiseSrc = new PromiseSource() + script.addEventListener('error', () => promiseSrc.reject(new Error(`Script "${url}" couldn't be loaded.`))) + script.addEventListener('load', () => promiseSrc.resolve(url)) + script.src = url + '?t=' + Date.now() + + console.log('loading script', url) + document.head.appendChild(script) + + return promiseSrc.promise +} + +async function loadUIPlugins() { + try { + const res = await fetch('/get/ui_plugins') + if (!res.ok) { + console.error(`Error HTTP${res.status} while loading plugins list. - ${res.statusText}`) + return + } + const plugins = await res.json() + const loadingPromises = plugins.map(loadScript) + return await Promise.allSettled(loadingPromises) + } catch (e) { + console.log('error fetching plugin paths', e) + } +} diff --git a/ui/media/js/searchable-models.js b/ui/media/js/searchable-models.js new file mode 100644 index 0000000000000000000000000000000000000000..4f311a763768223c3b2df8079929c84456ef3554 --- /dev/null +++ b/ui/media/js/searchable-models.js @@ -0,0 +1,677 @@ +"use strict" + +let modelsCache +let modelsOptions + +/* +*** SEARCHABLE MODELS *** +Creates searchable dropdowns for SD, VAE, or HN models. +Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models). +More reload buttons may be added at strategic UI locations as needed. +Merely calling getModels() makes all the magic happen behind the scene to refresh the dropdowns. + +HOW TO CREATE A MODEL DROPDOWN: +1) Create an input element. Make sure to add a data-path property, as this is how model dropdowns are identified in auto-save.js. + + +2) Just declare one of these for your own dropdown (remember to change the element id, e.g. #stable_diffusion_models to your own input's id). +let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion') +let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None') +let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None') + +3) Model dropdowns will be refreshed automatically when the reload models button is invoked. +*/ +class ModelDropdown +{ + modelFilter //= document.querySelector("#model-filter") + modelFilterArrow //= document.querySelector("#model-filter-arrow") + modelList //= document.querySelector("#model-list") + modelResult //= document.querySelector("#model-result") + modelNoResult //= document.querySelector("#model-no-result") + + currentSelection //= { elem: undefined, value: '', path: ''} + highlightedModelEntry //= undefined + activeModel //= undefined + + inputModels //= undefined + modelKey //= undefined + flatModelList //= [] + noneEntry //= '' + modelFilterInitialized //= undefined + + /* MIMIC A REGULAR INPUT FIELD */ + get parentElement() { + return this.modelFilter.parentElement + } + get parentNode() { + return this.modelFilter.parentNode + } + get value() { + return this.modelFilter.dataset.path + } + set value(path) { + this.modelFilter.dataset.path = path + this.selectEntry(path) + } + get disabled() { + return this.modelFilter.disabled + } + set disabled(state) { + this.modelFilter.disabled = state + if (this.modelFilterArrow) { + this.modelFilterArrow.style.color = state ? 'dimgray' : '' + } + } + get modelElements() { + return this.modelList.querySelectorAll('.model-file') + } + addEventListener(type, listener, options) { + return this.modelFilter.addEventListener(type, listener, options) + } + dispatchEvent(event) { + return this.modelFilter.dispatchEvent(event) + } + appendChild(option) { + // do nothing + } + + // remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html + bind(f, obj) { + return function() { + return f.apply(obj, arguments) + } + } + + /* SEARCHABLE INPUT */ + constructor (input, modelKey, noneEntry = '') { + this.modelFilter = input + this.noneEntry = noneEntry + this.modelKey = modelKey + + if (modelsOptions !== undefined) { // reuse models from cache (only useful for plugins, which are loaded after models) + this.inputModels = modelsOptions[this.modelKey] + this.populateModels() + } + document.addEventListener("refreshModels", this.bind(function(e) { + // reload the models + this.inputModels = modelsOptions[this.modelKey] + this.populateModels() + }, this)) + } + + saveCurrentSelection(elem, value, path) { + this.currentSelection.elem = elem + this.currentSelection.value = value + this.currentSelection.path = path + this.modelFilter.dataset.path = path + this.modelFilter.value = value + this.modelFilter.dispatchEvent(new Event('change')) + } + + processClick(e) { + e.preventDefault() + if (e.srcElement.classList.contains('model-file') || e.srcElement.classList.contains('fa-file')) { + const elem = e.srcElement.classList.contains('model-file') ? e.srcElement : e.srcElement.parentElement + this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path) + this.hideModelList() + this.modelFilter.focus() + this.modelFilter.select() + } + } + + getPreviousVisibleSibling(elem) { + const modelElements = Array.from(this.modelElements) + const index = modelElements.indexOf(elem) + if (index <= 0) { + return undefined + } + + return modelElements.slice(0, index).reverse().find(e => e.style.display === 'list-item') + } + + getLastVisibleChild(elem) { + let lastElementChild = elem.lastElementChild + if (lastElementChild.style.display == 'list-item') return lastElementChild + return this.getPreviousVisibleSibling(lastElementChild) + } + + getNextVisibleSibling(elem) { + const modelElements = Array.from(this.modelElements) + const index = modelElements.indexOf(elem) + return modelElements.slice(index + 1).find(e => e.style.display === 'list-item') + } + + getFirstVisibleChild(elem) { + let firstElementChild = elem.firstElementChild + if (firstElementChild.style.display == 'list-item') return firstElementChild + return this.getNextVisibleSibling(firstElementChild) + } + + selectModelEntry(elem) { + if (elem) { + if (this.highlightedModelEntry !== undefined) { + this.highlightedModelEntry.classList.remove('selected') + } + this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path) + elem.classList.add('selected') + elem.scrollIntoView({block: 'nearest'}) + this.highlightedModelEntry = elem + } + } + + selectPreviousFile() { + const elem = this.getPreviousVisibleSibling(this.highlightedModelEntry) + if (elem) { + this.selectModelEntry(elem) + } + else + { + //this.highlightedModelEntry.parentElement.parentElement.scrollIntoView({block: 'nearest'}) + this.highlightedModelEntry.closest('.model-list').scrollTop = 0 + } + this.modelFilter.select() + } + + selectNextFile() { + this.selectModelEntry(this.getNextVisibleSibling(this.highlightedModelEntry)) + this.modelFilter.select() + } + + selectFirstFile() { + this.selectModelEntry(this.modelList.querySelector('.model-file')) + this.highlightedModelEntry.scrollIntoView({block: 'nearest'}) + this.modelFilter.select() + } + + selectLastFile() { + const elems = this.modelList.querySelectorAll('.model-file:last-child') + this.selectModelEntry(elems[elems.length -1]) + this.modelFilter.select() + } + + resetSelection() { + this.hideModelList() + this.showAllEntries() + this.modelFilter.value = this.currentSelection.value + this.modelFilter.focus() + this.modelFilter.select() + } + + validEntrySelected() { + return (this.modelNoResult.style.display === 'none') + } + + processKey(e) { + switch (e.key) { + case 'Escape': + e.preventDefault() + this.resetSelection() + break + case 'Enter': + e.preventDefault() + if (this.validEntrySelected()) { + if (this.modelList.style.display != 'block') { + this.showModelList() + } + else + { + this.saveCurrentSelection(this.highlightedModelEntry, this.highlightedModelEntry.innerText, this.highlightedModelEntry.dataset.path) + this.hideModelList() + this.showAllEntries() + } + this.modelFilter.focus() + } + else + { + this.resetSelection() + } + break + case 'ArrowUp': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectPreviousFile() + } + break + case 'ArrowDown': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectNextFile() + } + break + case 'ArrowLeft': + if (this.modelList.style.display != 'block') { + e.preventDefault() + } + break + case 'ArrowRight': + if (this.modelList.style.display != 'block') { + e.preventDefault() + } + break + case 'PageUp': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + this.selectPreviousFile() + } + break + case 'PageDown': + e.preventDefault() + if (this.validEntrySelected()) { + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + this.selectNextFile() + } + break + case 'Home': + //if (this.modelList.style.display != 'block') { + e.preventDefault() + if (this.validEntrySelected()) { + this.selectFirstFile() + } + //} + break + case 'End': + //if (this.modelList.style.display != 'block') { + e.preventDefault() + if (this.validEntrySelected()) { + this.selectLastFile() + } + //} + break + default: + //console.log(e.key) + } + } + + modelListFocus() { + this.selectEntry() + this.showAllEntries() + } + + showModelList() { + this.modelList.style.display = 'block' + this.selectEntry() + this.showAllEntries() + //this.modelFilter.value = '' + this.modelFilter.select() // preselect the entire string so user can just start typing. + this.modelFilter.focus() + this.modelFilter.style.cursor = 'auto' + } + + hideModelList() { + this.modelList.style.display = 'none' + this.modelFilter.value = this.currentSelection.value + this.modelFilter.style.cursor = '' + } + + toggleModelList(e) { + e.preventDefault() + if (!this.modelFilter.disabled) { + if (this.modelList.style.display != 'block') { + this.showModelList() + } + else + { + this.hideModelList() + this.modelFilter.select() + } + } + } + + selectEntry(path) { + if (path !== undefined) { + const entries = this.modelElements; + + for (const elem of entries) { + if (elem.dataset.path == path) { + this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path) + this.highlightedModelEntry = elem + elem.scrollIntoView({block: 'nearest'}) + break + } + } + } + + if (this.currentSelection.elem !== undefined) { + // select the previous element + if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != this.currentSelection.elem) { + this.highlightedModelEntry.classList.remove('selected') + } + this.currentSelection.elem.classList.add('selected') + this.highlightedModelEntry = this.currentSelection.elem + this.currentSelection.elem.scrollIntoView({block: 'nearest'}) + } + else + { + this.selectFirstFile() + } + } + + highlightModelAtPosition(e) { + let elem = document.elementFromPoint(e.clientX, e.clientY) + + if (elem.classList.contains('model-file')) { + this.highlightModel(elem) + } + } + + highlightModel(elem) { + if (elem.classList.contains('model-file')) { + if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != elem) { + this.highlightedModelEntry.classList.remove('selected') + } + elem.classList.add('selected') + this.highlightedModelEntry = elem + } + } + + showAllEntries() { + this.modelList.querySelectorAll('li').forEach(function(li) { + if (li.id !== 'model-no-result') { + li.style.display = 'list-item' + } + }) + this.modelNoResult.style.display = 'none' + } + + filterList(e) { + const filter = this.modelFilter.value.toLowerCase() + let found = false + let showAllChildren = false + + this.modelList.querySelectorAll('li').forEach(function(li) { + if (li.classList.contains('model-folder')) { + showAllChildren = false + } + if (filter == '') { + li.style.display = 'list-item' + found = true + } else if (showAllChildren || li.textContent.toLowerCase().match(filter)) { + li.style.display = 'list-item' + if (li.classList.contains('model-folder') && li.firstChild.textContent.toLowerCase().match(filter)) { + showAllChildren = true + } + found = true + } else { + li.style.display = 'none' + } + }) + + if (found) { + this.modelResult.style.display = 'list-item' + this.modelNoResult.style.display = 'none' + const elem = this.getNextVisibleSibling(this.modelList.querySelector('.model-file')) + this.highlightModel(elem) + elem.scrollIntoView({block: 'nearest'}) + } + else + { + this.modelResult.style.display = 'none' + this.modelNoResult.style.display = 'list-item' + } + this.modelList.style.display = 'block' + } + + /* MODEL LOADER */ + getElementDimensions(element) { + // Clone the element + const clone = element.cloneNode(true) + + // Copy the styles of the original element to the cloned element + const originalStyles = window.getComputedStyle(element) + for (let i = 0; i < originalStyles.length; i++) { + const property = originalStyles[i] + clone.style[property] = originalStyles.getPropertyValue(property) + } + + // Set its visibility to hidden and display to inline-block + clone.style.visibility = "hidden" + clone.style.display = "inline-block" + + // Put the cloned element next to the original element + element.parentNode.insertBefore(clone, element.nextSibling) + + // Get its width and height + const width = clone.offsetWidth + const height = clone.offsetHeight + + // Remove it from the DOM + clone.remove() + + // Return its width and height + return { width, height } + } + + /** + * @param {Array} models + */ + sortStringArray(models) { + models.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + } + + populateModels() { + this.activeModel = this.modelFilter.dataset.path + + this.currentSelection = { elem: undefined, value: '', path: ''} + this.highlightedModelEntry = undefined + this.flatModelList = [] + + if(this.modelList !== undefined) { + this.modelList.remove() + this.modelFilterArrow.remove() + } + this.createDropdown() + } + + createDropdown() { + // create dropdown entries + let rootModelList = this.createRootModelList(this.inputModels) + this.modelFilter.insertAdjacentElement('afterend', rootModelList) + this.modelFilter.insertAdjacentElement( + 'afterend', + createElement( + 'i', + { id: `${this.modelFilter.id}-model-filter-arrow` }, + ['model-selector-arrow', 'fa-solid', 'fa-angle-down'], + ), + ) + this.modelFilter.classList.add('model-selector') + this.modelFilterArrow = document.querySelector(`#${this.modelFilter.id}-model-filter-arrow`) + if (this.modelFilterArrow) { + this.modelFilterArrow.style.color = this.modelFilter.disabled ? 'dimgray' : '' + } + this.modelList = document.querySelector(`#${this.modelFilter.id}-model-list`) + this.modelResult = document.querySelector(`#${this.modelFilter.id}-model-result`) + this.modelNoResult = document.querySelector(`#${this.modelFilter.id}-model-no-result`) + + if (this.modelFilterInitialized !== true) { + this.modelFilter.addEventListener('input', this.bind(this.filterList, this)) + this.modelFilter.addEventListener('focus', this.bind(this.modelListFocus, this)) + this.modelFilter.addEventListener('blur', this.bind(this.hideModelList, this)) + this.modelFilter.addEventListener('click', this.bind(this.showModelList, this)) + this.modelFilter.addEventListener('keydown', this.bind(this.processKey, this)) + + this.modelFilterInitialized = true + } + this.modelFilterArrow.addEventListener('mousedown', this.bind(this.toggleModelList, this)) + this.modelList.addEventListener('mousemove', this.bind(this.highlightModelAtPosition, this)) + this.modelList.addEventListener('mousedown', this.bind(this.processClick, this)) + + let mf = this.modelFilter + this.modelFilter.addEventListener('focus', function() { + let modelFilterStyle = window.getComputedStyle(mf) + rootModelList.style.minWidth = modelFilterStyle.width + }) + + this.selectEntry(this.activeModel) + } + + /** + * @param {Array { + if (Array.isArray(model)) { + const [childFolderName, childModels] = model + foldersMap.set( + childFolderName, + this.createModelNodeList( + `${folderName || ''}/${childFolderName}`, + childModels, + false, + ), + ) + } else { + const classes = ['model-file'] + if (isRootFolder) { + classes.push('in-root-folder') + } + // Remove the leading slash from the model path + const fullPath = folderName ? `${folderName.substring(1)}/${model}` : model + modelsMap.set( + model, + createElement( + 'li', + { 'data-path': fullPath }, + classes, + [ + createElement('i', undefined, ['fa-regular', 'fa-file', 'icon']), + model, + ], + ), + ) + } + }) + + const childFolderNames = Array.from(foldersMap.keys()) + this.sortStringArray(childFolderNames) + const folderElements = childFolderNames.map(name => foldersMap.get(name)) + + const modelNames = Array.from(modelsMap.keys()) + this.sortStringArray(modelNames) + const modelElements = modelNames.map(name => modelsMap.get(name)) + + if (modelElements.length && folderName) { + listElement.appendChild( + createElement( + 'li', + undefined, + ['model-folder'], + [ + createElement('i', undefined, ['fa-regular', 'fa-folder-open', 'icon']), + folderName.substring(1), + ], + ) + ) + } + + // const allModelElements = isRootFolder ? [...folderElements, ...modelElements] : [...modelElements, ...folderElements] + const allModelElements = [...modelElements, ...folderElements] + allModelElements.forEach(e => listElement.appendChild(e)) + return listElement + } + + /** + * @param {object} modelTree + * @returns {HTMLElement} + */ + createRootModelList(modelTree) { + const rootList = createElement( + 'ul', + { id: `${this.modelFilter.id}-model-list` }, + ['model-list'], + ) + rootList.appendChild( + createElement( + 'li', + { id: `${this.modelFilter.id}-model-no-result` }, + ['model-no-result'], + 'No result' + ), + ) + + if (this.noneEntry) { + rootList.appendChild( + createElement( + 'li', + { 'data-path': '' }, + ['model-file', 'in-root-folder'], + this.noneEntry, + ), + ) + } + + if (modelTree.length > 0) { + const containerListItem = createElement( + 'li', + { id: `${this.modelFilter.id}-model-result` }, + ['model-result'], + ) + //console.log(containerListItem) + containerListItem.appendChild(this.createModelNodeList(undefined, modelTree, true)) + rootList.appendChild(containerListItem) + } + + return rootList + } +} + +/* (RE)LOAD THE MODELS */ +async function getModels() { + try { + modelsCache = await SD.getModels() + modelsOptions = modelsCache['options'] + if ("scan-error" in modelsCache) { + // let previewPane = document.getElementById('tab-content-wrapper') + let previewPane = document.getElementById('preview') + previewPane.style.background="red" + previewPane.style.textAlign="center" + previewPane.innerHTML = '

    🔥Malware alert!🔥

    The file ' + modelsCache['scan-error'] + ' in your models/stable-diffusion folder is probably malware infected.

    Please delete this file from the folder before proceeding!

    After deleting the file, reload this page.

    ' + makeImageBtn.disabled = true + } + + /* This code should no longer be needed. Commenting out for now, will cleanup later. + const sd_model_setting_key = "stable_diffusion_model" + const vae_model_setting_key = "vae_model" + const hypernetwork_model_key = "hypernetwork_model" + + const stableDiffusionOptions = modelsOptions['stable-diffusion'] + const vaeOptions = modelsOptions['vae'] + const hypernetworkOptions = modelsOptions['hypernetwork'] + + // TODO: set default for model here too + SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0] + if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') { + setSetting(sd_model_setting_key, stableDiffusionOptions[0]) + } + */ + + // notify ModelDropdown objects to refresh + document.dispatchEvent(new Event('refreshModels')) + } catch (e) { + console.log('get models error', e) + } +} + +// reload models button +document.querySelector('#reload-models').addEventListener('click', getModels) diff --git a/ui/media/js/themes.js b/ui/media/js/themes.js new file mode 100644 index 0000000000000000000000000000000000000000..d8577d6844a43298d8d096fbb328f908e4a02961 --- /dev/null +++ b/ui/media/js/themes.js @@ -0,0 +1,82 @@ +const themeField = document.getElementById("theme"); +var DEFAULT_THEME = {}; +var THEMES = []; // initialized in initTheme from data in css + +function getThemeName(theme) { + theme = theme.replace("theme-", ""); + theme = theme.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); + return theme; +} +// init themefield +function initTheme() { + Array.from(document.styleSheets) + .filter(sheet => sheet.href?.startsWith(window.location.origin)) + .flatMap(sheet => Array.from(sheet.cssRules)) + .forEach(rule => { + var selector = rule.selectorText; + if (selector && selector.startsWith(".theme-") && !selector.includes(" ")) { + if (DEFAULT_THEME) { // re-add props that dont change (css needs this so they update correctly) + Array.from(DEFAULT_THEME.rule.style) + .filter(cssVariable => !Array.from(rule.style).includes(cssVariable)) + .forEach(cssVariable => { + rule.style.setProperty(cssVariable, DEFAULT_THEME.rule.style.getPropertyValue(cssVariable)); + }); + } + var theme_key = selector.substring(1); + THEMES.push({ + key: theme_key, + name: getThemeName(theme_key), + rule: rule + }) + } + if (selector && selector == ":root") { + DEFAULT_THEME = { + key: "theme-default", + name: "Default", + rule: rule + }; + } + }); + + THEMES.forEach(theme => { + var new_option = document.createElement("option"); + new_option.setAttribute("value", theme.key); + new_option.innerText = theme.name; + themeField.appendChild(new_option); + }); + + + // setup the style transitions a second after app initializes, so initial style is instant + setTimeout(() => { + var body = document.querySelector("body"); + var style = document.createElement('style'); + style.innerHTML = "* { transition: background 0.5s, color 0.5s, background-color 0.5s; }"; + body.appendChild(style); + }, 1000); +} +initTheme(); + +function themeFieldChanged() { + var theme_key = themeField.value; + + var body = document.querySelector("body"); + body.classList.remove(...THEMES.map(theme => theme.key)); + body.classList.add(theme_key); + + // + + body.style = ""; + var theme = THEMES.find(t => t.key == theme_key); + let borderColor = undefined + if (theme) { + borderColor = theme.rule.style.getPropertyValue('--input-border-color').trim() + if (!borderColor.startsWith('#')) { + borderColor = theme.rule.style.getPropertyValue('--theme-color-fallback') + } + } else { + borderColor = DEFAULT_THEME.rule.style.getPropertyValue('--theme-color-fallback') + } + document.querySelector('meta[name="theme-color"]').setAttribute("content", borderColor) +} + +themeField.addEventListener('change', themeFieldChanged); diff --git a/ui/media/js/utils.js b/ui/media/js/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..6eb0d643902e288ee105ab39bb8ebca1912330fe --- /dev/null +++ b/ui/media/js/utils.js @@ -0,0 +1,710 @@ +"use strict"; + +// https://gomakethings.com/finding-the-next-and-previous-sibling-elements-that-match-a-selector-with-vanilla-js/ +function getNextSibling(elem, selector) { + // Get the next sibling element + let sibling = elem.nextElementSibling + + // If there's no selector, return the first sibling + if (!selector) { + return sibling + } + + // If the sibling matches our selector, use it + // If not, jump to the next sibling and continue the loop + while (sibling) { + if (sibling.matches(selector)) { + return sibling + } + sibling = sibling.nextElementSibling + } +} + + +/* Panel Stuff */ + +// true = open +let COLLAPSIBLES_INITIALIZED = false; +const COLLAPSIBLES_KEY = "collapsibles"; +const COLLAPSIBLE_PANELS = []; // filled in by createCollapsibles with all the elements matching .collapsible + +// on-init call this for any panels that are marked open +function toggleCollapsible(element) { + const collapsibleHeader = element.querySelector(".collapsible"); + const handle = element.querySelector(".collapsible-handle"); + collapsibleHeader.classList.toggle("active") + let content = getNextSibling(collapsibleHeader, '.collapsible-content') + if (!collapsibleHeader.classList.contains("active")) { + content.style.display = "none" + if (handle != null) { // render results don't have a handle + handle.innerHTML = '➕' // plus + } + } else { + content.style.display = "block" + if (handle != null) { // render results don't have a handle + handle.innerHTML = '➖' // minus + } + } + document.dispatchEvent(new CustomEvent('collapsibleClick', { detail: collapsibleHeader })) + + if (COLLAPSIBLES_INITIALIZED && COLLAPSIBLE_PANELS.includes(element)) { + saveCollapsibles() + } +} + +function saveCollapsibles() { + let values = {} + COLLAPSIBLE_PANELS.forEach(element => { + let value = element.querySelector(".collapsible").className.indexOf("active") !== -1 + values[element.id] = value + }) + localStorage.setItem(COLLAPSIBLES_KEY, JSON.stringify(values)) +} + +function createCollapsibles(node) { + let save = false + if (!node) { + node = document + save = true + } + let collapsibles = node.querySelectorAll(".collapsible") + collapsibles.forEach(function(c) { + if (save && c.parentElement.id) { + COLLAPSIBLE_PANELS.push(c.parentElement) + } + let handle = document.createElement('span') + handle.className = 'collapsible-handle' + + if (c.classList.contains("active")) { + handle.innerHTML = '➖' // minus + } else { + handle.innerHTML = '➕' // plus + } + c.insertBefore(handle, c.firstChild) + + c.addEventListener('click', function() { + toggleCollapsible(c.parentElement) + }) + }) + if (save) { + let saved = localStorage.getItem(COLLAPSIBLES_KEY) + if (!saved) { + saved = tryLoadOldCollapsibles(); + } + if (!saved) { + saveCollapsibles() + saved = localStorage.getItem(COLLAPSIBLES_KEY) + } + let values = JSON.parse(saved) + COLLAPSIBLE_PANELS.forEach(element => { + let value = element.querySelector(".collapsible").className.indexOf("active") !== -1 + if (values[element.id] != value) { + toggleCollapsible(element) + } + }) + COLLAPSIBLES_INITIALIZED = true + } +} + +function tryLoadOldCollapsibles() { + const old_map = { + "advancedPanelOpen": "editor-settings", + "modifiersPanelOpen": "editor-modifiers", + "negativePromptPanelOpen": "editor-inputs-prompt" + }; + if (localStorage.getItem(Object.keys(old_map)[0])) { + let result = {}; + Object.keys(old_map).forEach(key => { + const value = localStorage.getItem(key); + if (value !== null) { + result[old_map[key]] = (value == true || value == "true") + localStorage.removeItem(key) + } + }); + result = JSON.stringify(result) + localStorage.setItem(COLLAPSIBLES_KEY, result) + return result + } + return null; +} + +function permute(arr) { + let permutations = [] + let n = arr.length + let n_permutations = Math.pow(2, n) + for (let i = 0; i < n_permutations; i++) { + let perm = [] + let mask = Number(i).toString(2).padStart(n, '0') + + for (let idx = 0; idx < mask.length; idx++) { + if (mask[idx] === '1' && arr[idx].trim() !== '') { + perm.push(arr[idx]) + } + } + + if (perm.length > 0) { + permutations.push(perm) + } + } + + return permutations +} + +// https://stackoverflow.com/a/8212878 +function millisecondsToStr(milliseconds) { + function numberEnding (number) { + return (number > 1) ? 's' : '' + } + + let temp = Math.floor(milliseconds / 1000) + let hours = Math.floor((temp %= 86400) / 3600) + let s = '' + if (hours) { + s += hours + ' hour' + numberEnding(hours) + ' ' + } + let minutes = Math.floor((temp %= 3600) / 60) + if (minutes) { + s += minutes + ' minute' + numberEnding(minutes) + ' ' + } + let seconds = temp % 60 + if (!hours && minutes < 4 && seconds) { + s += seconds + ' second' + numberEnding(seconds) + } + + return s +} + +// https://rosettacode.org/wiki/Brace_expansion#JavaScript +function BraceExpander() { + 'use strict' + + // Index of any closing brace matching the opening + // brace at iPosn, + // with the indices of any immediately-enclosed commas. + function bracePair(tkns, iPosn, iNest, lstCommas) { + if (iPosn >= tkns.length || iPosn < 0) return null; + + let t = tkns[iPosn], + n = (t === '{') ? ( + iNest + 1 + ) : (t === '}' ? ( + iNest - 1 + ) : iNest), + lst = (t === ',' && iNest === 1) ? ( + lstCommas.concat(iPosn) + ) : lstCommas; + + return n ? bracePair(tkns, iPosn + 1, n, lst) : { + close: iPosn, + commas: lst + }; + } + + // Parse of a SYNTAGM subtree + function andTree(dctSofar, tkns) { + if (!tkns.length) return [dctSofar, []]; + + let dctParse = dctSofar ? dctSofar : { + fn: and, + args: [] + }, + + head = tkns[0], + tail = head ? tkns.slice(1) : [], + + dctBrace = head === '{' ? bracePair( + tkns, 0, 0, [] + ) : null, + + lstOR = dctBrace && ( + dctBrace.close + ) && dctBrace.commas.length ? ( + splitAt(dctBrace.close + 1, tkns) + ) : null; + + return andTree({ + fn: and, + args: dctParse.args.concat( + lstOR ? ( + orTree(dctParse, lstOR[0], dctBrace.commas) + ) : head + ) + }, lstOR ? ( + lstOR[1] + ) : tail); + } + + // Parse of a PARADIGM subtree + function orTree(dctSofar, tkns, lstCommas) { + if (!tkns.length) return [dctSofar, []]; + let iLast = lstCommas.length; + + return { + fn: or, + args: splitsAt( + lstCommas, tkns + ).map(function (x, i) { + let ts = x.slice( + 1, i === iLast ? ( + -1 + ) : void 0 + ); + + return ts.length ? ts : ['']; + }).map(function (ts) { + return ts.length > 1 ? ( + andTree(null, ts)[0] + ) : ts[0]; + }) + }; + } + + // List of unescaped braces and commas, and remaining strings + function tokens(str) { + // Filter function excludes empty splitting artefacts + let toS = function (x) { + return x.toString(); + }; + + return str.split(/(\\\\)/).filter(toS).reduce(function (a, s) { + return a.concat(s.charAt(0) === '\\' ? s : s.split( + /(\\*[{,}])/ + ).filter(toS)); + }, []); + } + + // PARSE TREE OPERATOR (1 of 2) + // Each possible head * each possible tail + function and(args) { + let lng = args.length, + head = lng ? args[0] : null, + lstHead = "string" === typeof head ? ( + [head] + ) : head; + + return lng ? ( + 1 < lng ? lstHead.reduce(function (a, h) { + return a.concat( + and(args.slice(1)).map(function (t) { + return h + t; + }) + ); + }, []) : lstHead + ) : []; + } + + // PARSE TREE OPERATOR (2 of 2) + // Each option flattened + function or(args) { + return args.reduce(function (a, b) { + return a.concat(b); + }, []); + } + + // One list split into two (first sublist length n) + function splitAt(n, lst) { + return n < lst.length + 1 ? [ + lst.slice(0, n), lst.slice(n) + ] : [lst, []]; + } + + // One list split into several (sublist lengths [n]) + function splitsAt(lstN, lst) { + return lstN.reduceRight(function (a, x) { + return splitAt(x, a[0]).concat(a.slice(1)); + }, [lst]); + } + + // Value of the parse tree + function evaluated(e) { + return typeof e === 'string' ? e : + e.fn(e.args.map(evaluated)); + } + + // JSON prettyprint (for parse tree, token list etc) + function pp(e) { + return JSON.stringify(e, function (k, v) { + return typeof v === 'function' ? ( + '[function ' + v.name + ']' + ) : v; + }, 2) + } + + + // ----------------------- MAIN ------------------------ + + // s -> [s] + this.expand = function(s) { + // BRACE EXPRESSION PARSED + let dctParse = andTree(null, tokens(s))[0]; + + // ABSTRACT SYNTAX TREE LOGGED + // console.log(pp(dctParse)); + + // AST EVALUATED TO LIST OF STRINGS + return evaluated(dctParse); + } + +} + + +/** Pause the execution of an async function until timer elapse. + * @Returns a promise that will resolve after the specified timeout. + */ +function asyncDelay(timeout) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, timeout, true) + }) +} + +function PromiseSource() { + const srcPromise = new Promise((resolve, reject) => { + Object.defineProperties(this, { + resolve: { value: resolve, writable: false } + , reject: { value: reject, writable: false } + }) + }) + Object.defineProperties(this, { + promise: {value: makeQuerablePromise(srcPromise), writable: false} + }) +} + +/** A debounce is a higher-order function, which is a function that returns another function + * that, as long as it continues to be invoked, will not be triggered. + * The function will be called after it stops being called for N milliseconds. + * If `immediate` is passed, trigger the function on the leading edge, instead of the trailing. + * @Returns a promise that will resolve to func return value. + */ +function debounce (func, wait, immediate) { + if (typeof wait === "undefined") { + wait = 40 + } + if (typeof wait !== "number") { + throw new Error("wait is not an number.") + } + let timeout = null + let lastPromiseSrc = new PromiseSource() + const applyFn = function(context, args) { + let result = undefined + try { + result = func.apply(context, args) + } catch (err) { + lastPromiseSrc.reject(err) + } + if (result instanceof Promise) { + result.then(lastPromiseSrc.resolve, lastPromiseSrc.reject) + } else { + lastPromiseSrc.resolve(result) + } + } + return function(...args) { + const callNow = Boolean(immediate && !timeout) + const context = this; + if (timeout) { + clearTimeout(timeout) + } + timeout = setTimeout(function () { + if (!immediate) { + applyFn(context, args) + } + lastPromiseSrc = new PromiseSource() + timeout = null + }, wait) + if (callNow) { + applyFn(context, args) + } + return lastPromiseSrc.promise + } +} + +function preventNonNumericalInput(e) { + e = e || window.event; + let charCode = (typeof e.which == "undefined") ? e.keyCode : e.which; + let charStr = String.fromCharCode(charCode); + let re = e.target.getAttribute('pattern') || '^[0-9]+$' + re = new RegExp(re) + + if (!charStr.match(re)) { + e.preventDefault(); + } +} + +/** Returns the global object for the current execution environement. + * @Returns window in a browser, global in node and self in a ServiceWorker. + * @Notes Allows unit testing and use of the engine outside of a browser. + */ +function getGlobal() { + if (typeof globalThis === 'object') { + return globalThis + } else if (typeof global === 'object') { + return global + } else if (typeof self === 'object') { + return self + } + try { + return Function('return this')() + } catch { + // If the Function constructor fails, we're in a browser with eval disabled by CSP headers. + return window + } // Returns undefined if global can't be found. +} + +/** Check if x is an Array or a TypedArray. + * @Returns true if x is an Array or a TypedArray, false otherwise. + */ +function isArrayOrTypedArray(x) { + return Boolean(typeof x === 'object' && (Array.isArray(x) || (ArrayBuffer.isView(x) && !(x instanceof DataView)))) +} + +function makeQuerablePromise(promise) { + if (typeof promise !== 'object') { + throw new Error('promise is not an object.') + } + if (!(promise instanceof Promise)) { + throw new Error('Argument is not a promise.') + } + // Don't modify a promise that's been already modified. + if ('isResolved' in promise || 'isRejected' in promise || 'isPending' in promise) { + return promise + } + let isPending = true + let isRejected = false + let rejectReason = undefined + let isResolved = false + let resolvedValue = undefined + const qurPro = promise.then( + function(val){ + isResolved = true + isPending = false + resolvedValue = val + return val + } + , function(reason) { + rejectReason = reason + isRejected = true + isPending = false + throw reason + } + ) + Object.defineProperties(qurPro, { + 'isResolved': { + get: () => isResolved + } + , 'resolvedValue': { + get: () => resolvedValue + } + , 'isPending': { + get: () => isPending + } + , 'isRejected': { + get: () => isRejected + } + , 'rejectReason': { + get: () => rejectReason + } + }) + return qurPro +} + +/* inserts custom html to allow prettifying of inputs */ +function prettifyInputs(root_element) { + root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => { + if (element.style.display === "none") { + return + } + var parent = element.parentNode; + if (!parent.classList.contains("input-toggle")) { + var wrapper = document.createElement("div"); + wrapper.classList.add("input-toggle"); + parent.replaceChild(wrapper, element); + wrapper.appendChild(element); + var label = document.createElement("label"); + label.htmlFor = element.id; + wrapper.appendChild(label); + } + }) +} + +class GenericEventSource { + #events = {}; + #types = [] + constructor(...eventsTypes) { + if (Array.isArray(eventsTypes) && eventsTypes.length === 1 && Array.isArray(eventsTypes[0])) { + eventsTypes = eventsTypes[0] + } + this.#types.push(...eventsTypes) + } + get eventTypes() { + return this.#types + } + /** Add a new event listener + */ + addEventListener(name, handler) { + if (!this.#types.includes(name)) { + throw new Error('Invalid event name.') + } + if (this.#events.hasOwnProperty(name)) { + this.#events[name].push(handler) + } else { + this.#events[name] = [handler] + } + } + /** Remove the event listener + */ + removeEventListener(name, handler) { + if (!this.#events.hasOwnProperty(name)) { + return + } + const index = this.#events[name].indexOf(handler) + if (index != -1) { + this.#events[name].splice(index, 1) + } + } + fireEvent(name, ...args) { + if (!this.#types.includes(name)) { + throw new Error(`Event ${String(name)} missing from Events.types`) + } + if (!this.#events.hasOwnProperty(name)) { + return Promise.resolve() + } + if (!args || !args.length) { + args = [] + } + const evs = this.#events[name] + if (evs.length <= 0) { + return Promise.resolve() + } + return Promise.allSettled(evs.map((callback) => { + try { + return Promise.resolve(callback.apply(SD, args)) + } catch (ex) { + return Promise.reject(ex) + } + })) + } +} + +class ServiceContainer { + #services = new Map() + #singletons = new Map() + constructor(...servicesParams) { + servicesParams.forEach(this.register.bind(this)) + } + get services () { + return this.#services + } + get singletons() { + return this.#singletons + } + register(params) { + if (ServiceContainer.isConstructor(params)) { + if (typeof params.name !== 'string') { + throw new Error('params.name is not a string.') + } + params = {name:params.name, definition:params} + } + if (typeof params !== 'object') { + throw new Error('params is not an object.') + } + [ 'name', + 'definition', + ].forEach((key) => { + if (!(key in params)) { + console.error('Invalid service %o registration.', params) + throw new Error(`params.${key} is not defined.`) + } + }) + const opts = {definition: params.definition} + if ('dependencies' in params) { + if (Array.isArray(params.dependencies)) { + params.dependencies.forEach((dep) => { + if (typeof dep !== 'string') { + throw new Error('dependency name is not a string.') + } + }) + opts.dependencies = params.dependencies + } else { + throw new Error('params.dependencies is not an array.') + } + } + if (params.singleton) { + opts.singleton = true + } + this.#services.set(params.name, opts) + return Object.assign({name: params.name}, opts) + } + get(name) { + const ctorInfos = this.#services.get(name) + if (!ctorInfos) { + return + } + if(!ServiceContainer.isConstructor(ctorInfos.definition)) { + return ctorInfos.definition + } + if(!ctorInfos.singleton) { + return this._createInstance(ctorInfos) + } + const singletonInstance = this.#singletons.get(name) + if(singletonInstance) { + return singletonInstance + } + const newSingletonInstance = this._createInstance(ctorInfos) + this.#singletons.set(name, newSingletonInstance) + return newSingletonInstance + } + + _getResolvedDependencies(service) { + let classDependencies = [] + if(service.dependencies) { + classDependencies = service.dependencies.map(this.get.bind(this)) + } + return classDependencies + } + + _createInstance(service) { + if (!ServiceContainer.isClass(service.definition)) { + // Call as normal function. + return service.definition(...this._getResolvedDependencies(service)) + } + // Use new + return new service.definition(...this._getResolvedDependencies(service)) + } + + static isClass(definition) { + return typeof definition === 'function' && Boolean(definition.prototype) && definition.prototype.constructor === definition + } + static isConstructor(definition) { + return typeof definition === 'function' + } +} + +/** + * + * @param {string} tag + * @param {object} attributes + * @param {string | Array} classes + * @param {string | HTMLElement | Array} + * @returns {HTMLElement} + */ +function createElement(tagName, attributes, classes, textOrElements) { + const element = document.createElement(tagName) + if (attributes) { + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value) + }); + } + if (classes) { + (Array.isArray(classes) ? classes : [classes]).forEach(className => element.classList.add(className)) + } + if (textOrElements) { + const children = Array.isArray(textOrElements) ? textOrElements : [textOrElements] + children.forEach(textOrElem => { + if (textOrElem instanceof HTMLElement) { + element.appendChild(textOrElem) + } else { + element.appendChild(document.createTextNode(textOrElem)) + } + }) + } + return element +} diff --git a/ui/media/manifest.webmanifest b/ui/media/manifest.webmanifest new file mode 100644 index 0000000000000000000000000000000000000000..bdb665e96213f7f3ac1e8072930ed7a6547cb595 --- /dev/null +++ b/ui/media/manifest.webmanifest @@ -0,0 +1,8 @@ +{ + "name": "Stable Diffusion UI", + "display": "standalone", + "display_override": [ + "window-controls-overlay" + ], + "theme_color": "#000000" +} diff --git a/ui/media/modifier-thumbnails/artist/artstation/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/artstation/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66bb6606d6085a46ff7ba65792a0a42ea021d1bd Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/artstation/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/artstation/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/artstation/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ab66239fb5720b21e6af688fd862e3248b0583e3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/artstation/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..06ba6e97466c313e9d9c03bbcc2ffe194c5b2843 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39a757af0a7c918fcbcda8e4ce3b47d3d7bdc913 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_agnes_lawrence_pelton/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ef89ca57253d453fbf39e1b460500b0c42f1c995 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..008cba8ff6a0dae53c5df7db5aa7ae70df53d09a Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_akihito_yoshida/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alex_grey/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_alex_grey/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..50a7b7fad299ab046b0f9ef330f9bfbb3be11517 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alex_grey/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alex_grey/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_alex_grey/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..465d892acb59e829669e8f83a0797f138cd5cbb6 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alex_grey/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alexander_jansson/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_alexander_jansson/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b99ed16ea595f149ed031e8cb937d1721276896d Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alexander_jansson/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alexander_jansson/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_alexander_jansson/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c27a25ed7e74a725d2bdc0ece3ab90a6b079d0ce Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alexander_jansson/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d170e1b2ff7cf1ac43c923edb844ab9a7750ebf Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa07839b5ed02172ccb52fb17865d6509f23e290 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_alphonse_mucha/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_andy_warhol/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_andy_warhol/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..480aa3cc48f52d965853394f1e1960c0984992ab Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_andy_warhol/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_andy_warhol/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_andy_warhol/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8c1ee6c7cb58b2ca9d86145cd00d8c58b24b725 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_andy_warhol/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_artgerm/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_artgerm/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a96a0e69355cbdb925af7651ec4c5fc5fb596f95 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_artgerm/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_artgerm/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_artgerm/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7f2e2b37f5745c4d2a5e203d1c0ec076b0027f7 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_artgerm/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a844326871b45ab2105316c537bef6b2e7a8836d Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3775a2fa0d7239edd8890fb77dcead8b8f32c9d0 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_asaf_hanuka/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae388a252600408fcf25e0f61d03d483a2837b0a Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da3fe32dd6eeaf56120c9fd2c0376a44c9257c95 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_aubrey_beardsley/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_banksy/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_banksy/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a3b6d562ce1785f816d62e43e4b6dabf989e2a01 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_banksy/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_banksy/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_banksy/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45b5f80891b9e69477063bede5b5cd52c56d5afb Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_banksy/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_beeple/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_beeple/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..984ee5e0139ba595513d7c0a941a22ff818095d1 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_beeple/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_beeple/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_beeple/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91546ba64fd2a8d276592bdd78a3102a72ea0e93 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_beeple/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d183d4e4fbecc0608fc6207d159c18c85803353 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66f45838f426110714327fc45802304676e55253 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ben_enwonwu/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_bob_eggleton/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_bob_eggleton/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ee7b32fb94740b58bd7c96371d25a78e7e91a2b9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_bob_eggleton/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_bob_eggleton/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_bob_eggleton/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6df2a4afad5b76a54a448327f8d13e2b09409f31 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_bob_eggleton/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c71e98f57b0cd705d057ca8fb53203e2fa0326d6 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..50ec70c6b768bbb4f4126a6f2af2b71ed71085d9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_caravaggio_michelangelo_merisi/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e141286e5a7c7d90be1a8d95a76c8978c106d5f3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ae777c56602d1b4bb96f2e873473fd040b40dad Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_caspar_david_friedrich/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_chris_foss/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_chris_foss/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e4faa6150e14dd52537c1167e671729392ac1bd Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_chris_foss/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_chris_foss/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_chris_foss/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6bc604e2cd310fa8260d50d9841419fe36faad6 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_chris_foss/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_claude_monet/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_claude_monet/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a1ddc05d3f646ef1f4e175f410d0e9b67a31673 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_claude_monet/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_claude_monet/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_claude_monet/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a18a04296b232bf1ed9b1c922c3bd0460210c2b Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_claude_monet/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_dan_mumford/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_dan_mumford/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ac12bc5f596b7502a0863662364f4f45c7247a2 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_dan_mumford/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_dan_mumford/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_dan_mumford/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32f1ce244892b2537c51aeb7f5cc83d4102d0011 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_dan_mumford/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_david_mann/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_david_mann/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eed7c8ad221b10b3938fe6b66340ec35fa864402 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_david_mann/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_david_mann/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_david_mann/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f3810f8105d6697a3af3b79d4f09009009fa7d5c Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_david_mann/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a9574690c5a59dbbc56b9838e8cc513efd2f926 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43306a99a699e1b786ea18d1dce24c476cfc209a Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_diego_vela_zquez/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ecaa33e8f7f17489a06d3ff6e4cd0710604e60f9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f2086403b8cfec69d13ee4149af9681cd21ebf Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_disney_animation_studios/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_e_douard_manet/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_e_douard_manet/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e02199b0af1bb45587faff777c4c3733df9155a Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_e_douard_manet/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_e_douard_manet/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_e_douard_manet/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e2161865e858d89d71f59599a366eccaeddd1197 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_e_douard_manet/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_esao_andrews/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_esao_andrews/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2c58b22f6cbffc34c7d900ba5d181ca684d4fa3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_esao_andrews/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_esao_andrews/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_esao_andrews/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3628d559e5cbfc0bf175eb13f99cf7e19cee219 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_esao_andrews/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_frida_kahlo/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_frida_kahlo/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6d66b4bc92067e2cad439fcb4d6aad271c88a3c Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_frida_kahlo/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_frida_kahlo/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_frida_kahlo/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc8d20043ce3acce49125b5450ce5f4802b84cbc Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_frida_kahlo/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cde1099c12f987db11e362d2b432c3fdac2ed3d0 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..89f9fce2dc9aad7da8fcd40d2dff9073c975f565 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gediminas_pranckevicius/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f374b239b2d5d8d9454f057cd8a26aa8c2281c01 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2667524453aa302a368242f6fe092d770f66f8bf Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_georgia_o_keeffe/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..65a6bc39ec28748d8c7620c41da0c670a94dba18 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5b7db7f6890081a6d81714a6e6c9409e3f43368 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_greg_rutkowski/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gustave_dore_/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_gustave_dore_/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70f9acf059f2e1ec259686052352c2e597b670e4 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gustave_dore_/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gustave_dore_/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_gustave_dore_/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..541fbad68cb64fa816acbdc04ea3466698bd3b44 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gustave_dore_/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gustave_klimt/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_gustave_klimt/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..00b13cd09ccc27c8fd10756e9e657524290d990e Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gustave_klimt/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_gustave_klimt/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_gustave_klimt/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8460065fb62b8b6d5640ad033ec3c83b40ded933 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_gustave_klimt/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_h_r_giger/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_h_r_giger/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c7255ed7e5008e93da5323053e8e4058bf8aa314 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_h_r_giger/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_h_r_giger/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_h_r_giger/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eca5d6e5986bd23666ed1ddba6960ab81db923d4 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_h_r_giger/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a975ce016015e3286ce0f648d17cf63c8d252483 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec758dd6c034fcceda98a444972dd99b93dd9237 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_hayao_miyazaki/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_henri_matisse/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_henri_matisse/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8361f88d4178d1481aa9a52085c92fdd3132725b Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_henri_matisse/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_henri_matisse/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_henri_matisse/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31c0ae891f512a53459dc121eeed8472a65a6089 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_henri_matisse/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bf630c7e25e93b787df9bb7e0e6261d6c2ee9b25 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..820233bcc9c1623057337d72be044a61dc0ee9dd Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_hp_lovecraft/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5847701ceea656e4988bb6ffbc4e29f3834b038c Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc7d850d3d547ea6b55e9af346bcc7f6f4d200c1 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ivan_shishkin/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jack_kirby/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_jack_kirby/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2c079f29b0d1dce863ed22e6906e2ce344f5af8 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jack_kirby/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jack_kirby/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_jack_kirby/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc6bed88190be98f33ba2db4dbf03710427bc69f Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jack_kirby/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jackson_pollock/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_jackson_pollock/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a83d9b17b112fd6442315c50bbe8870f3eb6e5cc Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jackson_pollock/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jackson_pollock/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_jackson_pollock/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1be354da65ec86611893ef3d5a5ef1854d9405e Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jackson_pollock/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_james_jean/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_james_jean/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e6cb00860c79214b1d8bc36b80c3236a7e1d334 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_james_jean/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_james_jean/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_james_jean/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9fe5fd1adc09a83b06d41996cc303ce9ee3167b6 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_james_jean/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jim_burns/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_jim_burns/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4158915b82f485e317ae5c159cf6c6b322abd6de Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jim_burns/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_jim_burns/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_jim_burns/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fbaf00b5c3dbbfebbb8086f31c654024b164b8d2 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_jim_burns/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be39bf4485b220babef55ad550a2ed98a9a6835d Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8aeef0a9b7015f4c73e1f38ceb1cc111abf908ce Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_johannes_vermeer/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..97fe5e615963e476aab1fb24170e2f1368e0eafe Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e735dec04d9f531f032f25284b78eedd17ea89b3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_john_william_waterhouse/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23a9f4ec6959c6e321189c7fe7ab4b89dd680da8 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a479148fea1f30ca75e1dc5482a19c33c90dc47 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_katsushika_hokusai/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..281eab8f1b095ec341f4b50ea6315ad3f32c277b Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e914a0d12b3bf8c4d985d93a27cd3966e816015 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_kim_tschang_yeul/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34bcb6acdf5377cec95bbb8165c4dc7c76f28ccc Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed22e9cf2f204eace08b08f020ba152ad1ad2807 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ko_young_hoon/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d040800793f4b008b11909f31b8c3d4cfbf9a448 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..01c8881dcc9ef19a141632c7386a43d1711e7312 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_leonardo_da_vinci/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_lisa_frank/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_lisa_frank/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f21f0e2f77d47b3e54b1b50727105fa3063afe62 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_lisa_frank/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_lisa_frank/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_lisa_frank/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ff81a8901da35def30a87ece2689a44dad98012 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_lisa_frank/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_m_c_escher/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_m_c_escher/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90dda89d5541ef48e4bf23ca0112753d74d9daa1 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_m_c_escher/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_m_c_escher/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_m_c_escher/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfe78a3f8c2a18a771834147b09c812cd7b2d3aa Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_m_c_escher/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4955bb28ca9f15aba2f204fe642ed981f0b49506 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f0ddbd041f79e38790944829ce144066b205ec08 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_mahmoud_sai_d/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85634b1001286318fd01ebe58a7b09c3d3b83d7f Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3551f2823ab414971e50c0f27540794722dfe1a7 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_makoto_shinkai/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_marc_simonetti/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_marc_simonetti/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..57d3e30825e3c25e589431002bb63c372b654dd5 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_marc_simonetti/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_marc_simonetti/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_marc_simonetti/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7722a8137364692248d6503116362af1d6246acb Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_marc_simonetti/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_mark_brooks/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_mark_brooks/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5cdb1681c06b64a9526041c09c9338454c3e65b4 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_mark_brooks/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_mark_brooks/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_mark_brooks/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76200b793fd05b071ab1da731c925063cfea2591 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_mark_brooks/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_michelangelo/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_michelangelo/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e64d6c87daeacda536dc8f15ca8d40eb42a8c952 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_michelangelo/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_michelangelo/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_michelangelo/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0b99fe850223e5b30938ac973d0cffcf9d85d4a5 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_michelangelo/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pablo_picasso/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_pablo_picasso/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c00b82780a71ba610fae1133779e6491cf5e5c77 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pablo_picasso/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pablo_picasso/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_pablo_picasso/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5932b9d18bbdbd97002a29efe535034a5b0b0eaf Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pablo_picasso/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_paul_klee/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_paul_klee/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b753e9e0344bce9ead2364c64a57115d6fff73f9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_paul_klee/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_paul_klee/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_paul_klee/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6d9803bf326618b56a7db9a2df6eedf1097e181 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_paul_klee/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d1400bf3c6460b2e97122bf8b02cdbd55c261d31 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85b6c887a351f5f3ef8f2e902dfbfd2c2034ba95 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_peter_mohrbacher/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7219bf46b5162043804ef6f7288e18a8cbafc39 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c4d6e715f0e07cb0cd99d45dc8c54e90280276a7 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pierre-auguste_renoir/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..767aa50243c0aaa8cd09ffa8dfae0fe7eb5085ca Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..078893a181f8b33955791c22b8118450066a6669 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_pixar_animation_studios/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_rembrandt/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_rembrandt/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bbdad0b9ff61f237bde04dd1a6c93a7aa755975 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_rembrandt/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_rembrandt/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_rembrandt/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1207645b939ced0530adc2dd0c5eb6463918cf2 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_rembrandt/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_richard_dadd/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_richard_dadd/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f25919c03ef8f963491fafb8dddd7b0470cc99a2 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_richard_dadd/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_richard_dadd/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_richard_dadd/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6530cc13d635f7c460d9c92ce401f1367c0c68c9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_richard_dadd/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_rossdraws/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_rossdraws/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4eb47e5c1329c42e343ce8743e1b17e997fa4373 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_rossdraws/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_rossdraws/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_rossdraws/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa93315413e5a2016b5a909df4964fd7b7b6a982 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_rossdraws/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_salvador_dali_/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_salvador_dali_/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa645ada5c03926bf2a6952af5e682d711c3f6fe Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_salvador_dali_/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_salvador_dali_/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_salvador_dali_/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64ea516057c80cc36c4319532b11090a2f932590 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_salvador_dali_/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_sam_does_arts/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_sam_does_arts/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..87a30e81f982d3d96ec16d6165137ceae70451d3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_sam_does_arts/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_sam_does_arts/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_sam_does_arts/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2fccacd02fb6414c6b63932eff0dc08b840ea9a2 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_sam_does_arts/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84ac59ec7e554a2c19db2f7a2e9134109aab5ac3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..89ebfad7b7b2126268f1be45a573df01834eba15 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_sandro_botticelli/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ted_nasmith/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_ted_nasmith/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..73188f832c9db576b4992b7599dc1f2274b78ba8 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ted_nasmith/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ted_nasmith/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_ted_nasmith/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6034d3547dd84c24a17ea9163b8635f10e5ac515 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ted_nasmith/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ten_hundred/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_ten_hundred/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..558c2528084abaf3bfd9c05a083578ab12092869 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ten_hundred/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_ten_hundred/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_ten_hundred/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7227be3d7c71b433ebb7f5f1a17d2089709de100 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_ten_hundred/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f3c4ab25e0b8fdcf6ec017329cb07ee13338f65 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..49f5f59b17f1d7cc194c053bac3234268c1fddfd Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_thomas_kinkade/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4839ba1a276700435aea509c5a40b297acc9cdf9 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5229d550e5b447fc47ff3029f79279ce44e69b1b Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_tivadar_csontva_ry_kosztka/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_victo_ngai/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_victo_ngai/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..632091c622c82b1d12fb3946c94f12f42b1e74f0 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_victo_ngai/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_victo_ngai/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_victo_ngai/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4bbb3ed903581ae01f888418cc69fc7bc55738ad Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_victo_ngai/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe4becf9e91050bb7da400522a8cb3a18b6d6806 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8087d73b00d92b959879814bd2d4d99990877354 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_vincent_di_fate/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..15ced986f11f64ba6713ea28fcfcf7818b6bc542 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f60ae9464ce0a436f0ee655df33fa3f834501742 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_vincent_van_gogh/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_wes_anderson/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_wes_anderson/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f3b05ed5d81caff6016e213beec4b44ddd84e5b0 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_wes_anderson/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_wes_anderson/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_wes_anderson/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f40313c5088ce00f837db2306f47e53fcdf8a00 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_wes_anderson/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_wlop/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_wlop/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7378e735b10d47d9ce2967439fafe86e5dc6bcf4 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_wlop/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_wlop/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_wlop/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..100280b1c4658e1527a515a02f65f2dcb08c86e3 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_wlop/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/landscape-0.jpg b/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ad61929d15f64bbc0e521392d86af8d9fa1c4d5 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/portrait-0.jpg b/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3989acaa115d76ca9351833a276819ae22db5f59 Binary files /dev/null and b/ui/media/modifier-thumbnails/artist/by_yoshitaka_amano/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/aerial_view/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/aerial_view/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a62102fc86e571d7115f8a761cdca6db121b1d1 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/aerial_view/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/aerial_view/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/aerial_view/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ceaffb1936248fd8243f2d40e7db5bf519a90a3 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/aerial_view/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/canon50/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/canon50/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e3ade7b77087c39a5e0c59289b7718240bc4483 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/canon50/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/canon50/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/canon50/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c2de7729a52ac65b939f3a3197f9fd059969d25 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/canon50/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/cinematic/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/cinematic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e582e1fc8b035e55dad694a2974b62594a29d88 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/cinematic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/cinematic/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/cinematic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..58513f7654a54601f9526f8a0a3ce28272f236a3 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/cinematic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/close-up/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/close-up/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9080e385af5c70e5a05c5ef43aecc9a658d85d78 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/close-up/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/close-up/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/close-up/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f91e25f06e2f6777f2c5e0d766a6e75b3bbc1683 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/close-up/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/color_grading/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/color_grading/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed3b7d13e54c6bce0377b8f598927c44ac8ae6fa Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/color_grading/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/color_grading/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/color_grading/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..91a0fec291f4710a578157d7a990428644a4996d Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/color_grading/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/dramatic/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/dramatic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db9cd87060722eade82629b26cbe1f8c38db1826 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/dramatic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/dramatic/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/dramatic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..54c6c95b5030c5cfef9f1c2d262b304b809d4206 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/dramatic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/film_grain/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/film_grain/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b045299e01451bc45616403113d1f19f3b0514f3 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/film_grain/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/film_grain/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/film_grain/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..063c29230656f9ba14fc01eb29b42d1db677c5b1 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/film_grain/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/fisheye_lens/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/fisheye_lens/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c4a0e65841eb530dd3a86d014334af38e101fdd0 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/fisheye_lens/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/fisheye_lens/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/fisheye_lens/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74ce9fce606582a468ba0595a66a90d650dce949 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/fisheye_lens/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/glamor_shot/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/glamor_shot/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd7878b07d697a52eb9af271acfa518c6f24a5bb Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/glamor_shot/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/glamor_shot/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/glamor_shot/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a9e71512dd0ada12cfd843d613523aef6d8fc1f Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/glamor_shot/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/golden_hour/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/golden_hour/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d4814bba6df20fded9a77ef67905fff0ffcc07f7 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/golden_hour/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/golden_hour/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/golden_hour/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86dc055ab12fe574fca3900c3ac4c69156d1a342 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/golden_hour/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/hd/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/hd/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd98b0f7fef516da7fae02c9487789db2cef211a Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/hd/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/hd/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/hd/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bf7a90021d6374c65481caa52445c95a8fe94d26 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/hd/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/landscape/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/landscape/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..58faf8f15dd339ef17ab65e8c3e799d2193b9647 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/landscape/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/landscape/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/landscape/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6acf3a07a43538174d9466592db870dff95abbb2 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/landscape/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/lens_flare/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/lens_flare/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2bbe565e33060ae051da9068f30eb123b08119d8 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/lens_flare/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/lens_flare/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/lens_flare/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..099e0da886455c3577c9736bf3031764755e3541 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/lens_flare/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/macro/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/macro/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a1efe3f6ff782b8d122efb3981f844d2e9690bf Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/macro/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/macro/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/macro/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74842b1374cebd379d05ce306e6f5231a25763dd Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/macro/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/photoshoot/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/photoshoot/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f271289dabff82bed887f0ee9c247f8c9e49f39d Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/photoshoot/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/photoshoot/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/photoshoot/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..01010840750a7a4113085adfa303d227203e5f98 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/photoshoot/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/polaroid/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/polaroid/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..970bd6016fddab6e56050a33778f88ee0b4274d7 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/polaroid/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/polaroid/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/polaroid/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6aa809f5219bac77fa937be02a4a1f701005c1dc Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/polaroid/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/portrait/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/portrait/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..26bfee5b1b7c4e089caa1491dd1a0f23d5548b00 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/portrait/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/portrait/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/portrait/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81ebdf5d4e119cb8ef8f2ac0452e8796bb44c791 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/portrait/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/studio_lighting/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/studio_lighting/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..adaeca662b892942ced872e470743721edd1c651 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/studio_lighting/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/studio_lighting/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/studio_lighting/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5b34c4589c6c920bcb217e7764d72105f9cb9d5 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/studio_lighting/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/vintage/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/vintage/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b63e895318b894746d5149177ce099a83facf6d Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/vintage/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/vintage/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/vintage/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1b83031b539d2b4e4f47f8d031ad9d51a2d7fc27 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/vintage/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/war_photography/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/war_photography/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24c47d9edc7def99d079972dc693f540f82ce1b4 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/war_photography/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/war_photography/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/war_photography/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d925a5df060f006ee89f3b01253a2375a6f645f9 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/war_photography/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/white_balance/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/white_balance/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4dc3810544d0fa1f8fdb0841e801ddb150505713 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/white_balance/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/white_balance/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/white_balance/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..27a6d9ebac9d88775f9da05b417b3e767575b587 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/white_balance/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/wildlife_photography/landscape-0.jpg b/ui/media/modifier-thumbnails/camera/wildlife_photography/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..13e10b6c7e08997e948ba4214453b307b0553992 Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/wildlife_photography/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/camera/wildlife_photography/portrait-0.jpg b/ui/media/modifier-thumbnails/camera/wildlife_photography/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..57870f3ea9c34ff828696f7cfb8afbf862a8a04b Binary files /dev/null and b/ui/media/modifier-thumbnails/camera/wildlife_photography/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/etching/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/etching/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..909c2c5398ee2b1685ae5c7aef79d240f67c92d1 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/etching/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/etching/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/etching/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4cecabb3eb6031062baf9c648f27a01c9a2a19fa Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/etching/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/linocut/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/linocut/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..edf51779132917eaac8bf083c7667a86188c57d6 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/linocut/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/linocut/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/linocut/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f3bb2b633e09009bfd41a34b38210dd89466ea2 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/linocut/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d75c8ebea7b0774bc030981ee55f139bf2bf5623 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..30c54d06b380ce8e72b8ab6a86d77f8433387782 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/paper-mache/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/paper_model/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/paper_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6325126c65248c369010cab6c8cf0c0fd7afc836 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/paper_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/paper_model/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/paper_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e5d11b7dfb0c19f5659b0b6d0e2682014199b13 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/paper_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/papercutting/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/papercutting/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f69f4762e94f4444defeb7be600f21b375c4a2b6 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/papercutting/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/papercutting/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/papercutting/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44ae9422b9e7d7420ac7f96e6ae6e2c8312de9b0 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/papercutting/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/pyrography/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/pyrography/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a6525277fa8c4e67a1e61df3e7f38a4847c4463 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/pyrography/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/pyrography/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/pyrography/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c65e3cca6552773afab76f5f78d0ec64e2fe1826 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/pyrography/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/landscape-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d47075606de0b7707ce9521ea26a6b98d64187ac Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/portrait-0.jpg b/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..715ae3f1c0f117985434917eb89afececacb4db7 Binary files /dev/null and b/ui/media/modifier-thumbnails/carving_and_etching/wood-carving/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/3d_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/3d_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..738715033f35b244f8c22452f7ed301508d0b4cc Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/3d_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/3d_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/3d_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9137bdad1dea666b69702e1679dfbfd39594679 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/3d_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/corona_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/corona_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e625a78ef6d4adc79099d858b382ac40b0b7145d Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/corona_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/corona_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/corona_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e650d2e240e0f343bbf7150ba57b99bc48a3c131 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/corona_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/creature_design/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/creature_design/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b0bb80938773dc57aab1a88f5e35a921c37524e3 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/creature_design/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/creature_design/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/creature_design/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ed08e5a1f881ae0516268ddfb275275de1e8ad2a Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/creature_design/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1073277c422bd26fdcca8a0304213e6dcad80b07 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c62a0ef47a0e5f576237099cdc3656fcbfa544b Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/cycles_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5880e0d8dce707d89e2bdd4f5e8addf5ee5d35a5 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..71b378f09a1963085c95e04c83d9038c548ef8eb Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/detailed_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/environment_design/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/environment_design/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a48c2b4be76d82e3455ca4da649976dab992944 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/environment_design/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/environment_design/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/environment_design/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..96a1dfbc21a2ba5849820109bab1fe031ff9eaa2 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/environment_design/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88590bdccebb6887b72b79f3410a1ae8dc644865 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88590bdccebb6887b72b79f3410a1ae8dc644865 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/glass_caustics/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0deff28335152593672b4a0a0bd1c76fad8b50d Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5dabf50f7426cfae05be99ec95ee8072581e2fde Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/global_illumination/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6ceaf8d30f190837f725e7556bcdad4e9b840b52 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9df0f6f32eb222b4f9db92bd6117e08a6649258 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/intricate_environment/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11f3a0fafb423976772a3991469f15fe0d866237 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f2dc13d4ef937ef3331c612deb6ca82ed79f35e Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/lsd_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/octane_render/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/octane_render/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c73b479029193944214265c71884c69d9aaba27 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/octane_render/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/octane_render/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/octane_render/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..883f483ec543f438ee9d4fb0e4a521d05b9cc8ac Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/octane_render/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/pbr/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/pbr/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d8a117386b499b32c6f4fa4efd1cfc97112a2871 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/pbr/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/pbr/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/pbr/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e33515560c2a08ee20c86cc3b70c7967441f45c Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/pbr/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af16505e047f1bca82aa02f81bbc9e331262db87 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d047f4d82115fb4dce969c1c5a1444724e15ee74 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_rendering/subsurface_scattering/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3d_model/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3d_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e107ee558feccf6790885a892f768f105b78a60 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3d_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3d_model/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3d_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd23e093c2ed100a1db8da5fb87ed9bbef6f38fe Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3d_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a3d437b306f81054c32d41094ea0669210b87f4f Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40bfcba44674569c46343947329ba9883661a5bc Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3d_sculpt/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52156b479ff3b221d929563869c8311a1e1c3c34 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f85538cd292aa9e11f2414cf63dcb55826ac159f Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/3ds_max_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/blender_model/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/blender_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bdcfd1f87f39bfec00f1b6c7089e9690a430a8e2 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/blender_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/blender_model/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/blender_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..136baa81574ab63ef0770670be8eb74a11f65872 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/blender_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a3d0ee862c1134ec801f530c85b8e1d9b1e7d1d Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d5a55841656fdebdb8a024d1a3ccfb3e85489771 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/cinema4d_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/maya_model/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/maya_model/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..38be657a1e8b6567f965bab90887fe5cafdfba29 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/maya_model/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/maya_model/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/maya_model/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d3e4fda8e8acb8ec89d26706f6fbf5bb6c7772a Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/maya_model/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/unreal_engine/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/unreal_engine/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d4532e3a7a2773a5e439b69fe761beb68018723d Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/unreal_engine/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/unreal_engine/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/unreal_engine/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32d0b9fcc2332c2111b2b9e6edb0a4df1335e8db Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/unreal_engine/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/landscape-0.jpg b/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..49270c61a6ed9d5165244098681c3d64661d75b4 Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/portrait-0.jpg b/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6bd4bbfa9c62dbea161230f30a45fcab6f7bedfe Binary files /dev/null and b/ui/media/modifier-thumbnails/cgi_software/zbrush_sculpt/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/beautiful_lighting/landscape-0.jpg b/ui/media/modifier-thumbnails/color/beautiful_lighting/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d183a6008b203464055b78b45795438adf07d083 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/beautiful_lighting/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/beautiful_lighting/portrait-0.jpg b/ui/media/modifier-thumbnails/color/beautiful_lighting/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8ecacfd91d3f7f31784bb615d20cd9c395f0c75 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/beautiful_lighting/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/cold_color_palette/landscape-0.jpg b/ui/media/modifier-thumbnails/color/cold_color_palette/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7039a9ce7f99d785fbe4b5116584eaa17f15aeee Binary files /dev/null and b/ui/media/modifier-thumbnails/color/cold_color_palette/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/cold_color_palette/portrait-0.jpg b/ui/media/modifier-thumbnails/color/cold_color_palette/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..abf02c82dd21eb338ababe6daabe93178d304d1a Binary files /dev/null and b/ui/media/modifier-thumbnails/color/cold_color_palette/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/colorful/landscape-0.jpg b/ui/media/modifier-thumbnails/color/colorful/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe708bf6f19da6aa5a17ddc5598685ae1ee19cc1 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/colorful/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/colorful/portrait-0.jpg b/ui/media/modifier-thumbnails/color/colorful/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c327c1f363aaaeb4b3196e723781788560ca69a1 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/colorful/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/dynamic_lighting/landscape-0.jpg b/ui/media/modifier-thumbnails/color/dynamic_lighting/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..632e8694cf3508401f521e759ab7a09e0efc2a47 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/dynamic_lighting/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/dynamic_lighting/portrait-0.jpg b/ui/media/modifier-thumbnails/color/dynamic_lighting/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f85330b18bf0ef2c9a4a9233b5b2af72034f720 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/dynamic_lighting/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/electric_colors/landscape-0.jpg b/ui/media/modifier-thumbnails/color/electric_colors/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..27bfa1022aeed4ea5539bc0a6793a37d4adb0920 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/electric_colors/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/electric_colors/portrait-0.jpg b/ui/media/modifier-thumbnails/color/electric_colors/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1427089553511e784ce5f6eea0c25a53892217af Binary files /dev/null and b/ui/media/modifier-thumbnails/color/electric_colors/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/infrared/landscape-0.jpg b/ui/media/modifier-thumbnails/color/infrared/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16a6e133263f5bc2d9ffe7ca745715451b0b02e3 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/infrared/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/infrared/portrait-0.jpg b/ui/media/modifier-thumbnails/color/infrared/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3b82b22ee64207b39e9c7591c7e24191475f9b2 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/infrared/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/neon/landscape-0.jpg b/ui/media/modifier-thumbnails/color/neon/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3dee39a5aa017c3b7c3813babd8342b4a5ef001a Binary files /dev/null and b/ui/media/modifier-thumbnails/color/neon/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/neon/portrait-0.jpg b/ui/media/modifier-thumbnails/color/neon/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a00ae357c8759e97ccb857c30229accdd0344b14 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/neon/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/pastel/landscape-0.jpg b/ui/media/modifier-thumbnails/color/pastel/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1ec1d9656cbd4ad6877d4767e196ebc90edf2d9 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/pastel/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/pastel/portrait-0.jpg b/ui/media/modifier-thumbnails/color/pastel/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8de9e72f007c9d5e18629aa37d6107208c839a64 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/pastel/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/synthwave/landscape-0.jpg b/ui/media/modifier-thumbnails/color/synthwave/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f9c0e6114d7a55cd49100e29b719718e6469c63 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/synthwave/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/synthwave/portrait-0.jpg b/ui/media/modifier-thumbnails/color/synthwave/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1c385eae11485f1c7822b2576de0cfc1ab4de2e1 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/synthwave/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/warm_color_palette/landscape-0.jpg b/ui/media/modifier-thumbnails/color/warm_color_palette/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..87c8501fdc3b82ae1cd6c6446bfc852d4ff33bf8 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/warm_color_palette/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/color/warm_color_palette/portrait-0.jpg b/ui/media/modifier-thumbnails/color/warm_color_palette/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4fb9758e0fb1a9f86c31167db093e363f9a45416 Binary files /dev/null and b/ui/media/modifier-thumbnails/color/warm_color_palette/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/cel_shading/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/cel_shading/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e00cd16399e59412a7c255e16f8094d70e34fff0 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/cel_shading/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/cel_shading/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/cel_shading/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..715f3e0e6fe8ddf8df2b3ece16cb53ffe07ed037 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/cel_shading/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..79d70a721c8a8bb76749c160e2fea839d6d04877 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc29298ff5e04f267436a6c27b2e75c617e3eb8b Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/children_s_drawing/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/crosshatch/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/crosshatch/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6144a67dea183027df057884bb8abbb2b9c9b4c Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/crosshatch/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/crosshatch/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/crosshatch/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2bb7acfb1b9a21104762185d9e4cdcd8f3183e7 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/crosshatch/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5048c432460a3ff3751cbd0928d8a19eae0b5069 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..699f92d9a2a54a1a8124f2c0cca47353a5a5ed8a Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/detailed_and_intricate/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/doodle/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/doodle/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92cec1c497640e1bf5ec719f6d3d4b2dd1aa9abe Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/doodle/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/doodle/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/doodle/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7524efa50e208416beb16c668e2fdc4a097f5a2d Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/doodle/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/dot_art/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/dot_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9fcb939f3fbac643b32b912fad0584eb6e85f844 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/dot_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/dot_art/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/dot_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..815628a2db9bc72b6cb9bbda775dd630179f786c Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/dot_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/line_art/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/line_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d3e1330411085a31a1f71828a002710e59a09c31 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/line_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/line_art/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/line_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..469b3004411470f7e95f116e69facea42672563e Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/line_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/sketch/landscape-0.jpg b/ui/media/modifier-thumbnails/drawing_style/sketch/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4211b2292166ec36a1bb53bf70dad1fb4e9e0671 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/sketch/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/drawing_style/sketch/portrait-0.jpg b/ui/media/modifier-thumbnails/drawing_style/sketch/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4c222d077c1848f1548b29a5f11ffa00f3174585 Binary files /dev/null and b/ui/media/modifier-thumbnails/drawing_style/sketch/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/angry/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/angry/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..221392553b04d4f890af16f7f6058fa773c782db Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/angry/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/angry/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/angry/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8065e42cb874b72450a2c510203eece17912179 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/angry/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/bitter/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/bitter/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..63810553e547886416f514b5f07729f2f4d6a125 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/bitter/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/bitter/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/bitter/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9071251015c39860b2d21959190ee61a6585e5d9 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/bitter/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/disgusted/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/disgusted/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6fdf176a7687c50f2a1b6841a9e7f83069c892a Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/disgusted/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/disgusted/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/disgusted/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9bb8497b954d3e02b100284326762641b9d1c0e8 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/disgusted/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/embarrassed/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/embarrassed/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bd3e1d65a2236b7de90b2c2a93bf5b9df207f217 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/embarrassed/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/embarrassed/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/embarrassed/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ed2ae9632f432977d579e4e97b9d0edfae0b04a Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/embarrassed/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/evil/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/evil/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7aac3ea350215fee54d2c95475441bbcc7f7cc16 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/evil/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/evil/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/evil/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9adb9196041e19b754ac06fe8f43f1ed15bbc04 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/evil/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/excited/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/excited/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfbd3a20189b47f41fc04d828323347751b71ad3 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/excited/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/excited/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/excited/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82b8033a6d40ac3c945c757e0ed514b8a86aafc5 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/excited/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/fear/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/fear/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d55b0d5d9b01bbf2b2e7e3426ad3858dfc619b65 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/fear/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/fear/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/fear/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7fb810f3c29ca3ec03bd01eee4f0e2396d8c038 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/fear/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/funny/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/funny/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5b05507ac9ec45f4df6062f97cdc1c87274ab9d1 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/funny/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/funny/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/funny/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1590e8a5c1086733a124e1735514fa606621c07e Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/funny/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/happy/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/happy/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..30429d0424d4d0d86569196354ad8041cdd9e72d Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/happy/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/happy/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/happy/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a7a4bf5662cbbfc4670720e065a6ecf4cc99fea Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/happy/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/horrifying/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/horrifying/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31b5cc4c88a9c01c1e620a5a06c904fc6ff8fa87 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/horrifying/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/horrifying/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/horrifying/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aef8da4065869bd301fd00f1242bd3f48da9a1b5 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/horrifying/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/lonely/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/lonely/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cce5400b62fd55af7657afc11c5edd95d9c87bf0 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/lonely/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/lonely/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/lonely/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6f451950ed0541312b54f345f11e87082fe1736d Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/lonely/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/melancholic/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/melancholic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..346a626e468b5555b207985cac4b010711cad59b Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/melancholic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/melancholic/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/melancholic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a503597f48e13c9dd3dbdd65bab58ddd7867dc4a Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/melancholic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/sad/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/sad/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a086851d7e4370655a1c76157d14b83c8572c2e2 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/sad/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/sad/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/sad/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e3f4fc6b747c8be3433d6891c460782354cda7f8 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/sad/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/serene/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/serene/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e508f95ef41eeddeb2018b62e8e58450529bf5e6 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/serene/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/serene/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/serene/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca5a0a311986ed3356ae5886a462e29da477370f Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/serene/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/surprised/landscape-0.jpg b/ui/media/modifier-thumbnails/emotions/surprised/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e0aaaf176f33712d150893f2a81fcb071cc101a Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/surprised/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/emotions/surprised/portrait-0.jpg b/ui/media/modifier-thumbnails/emotions/surprised/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d3bb4672a073c08be899ddbdcbe94ebf5efd0b7 Binary files /dev/null and b/ui/media/modifier-thumbnails/emotions/surprised/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/chalk/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/chalk/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7f8de12ff52f370d6395f6f90758600ebbc8df9 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/chalk/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/chalk/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/chalk/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0cbe5f9194f52f1c873ecc7dbdfd5ea72d9b7311 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/chalk/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/colored_pencil/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/colored_pencil/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a9f93581e78749a3f046e6caf2426405bd64665f Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/colored_pencil/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/colored_pencil/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/colored_pencil/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b07e5f36d0c72f967a2a4d1cb18b166d8cf5b65 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/colored_pencil/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/graphite/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/graphite/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b764c32577898b041d9e13ce401865a07d58e703 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/graphite/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/graphite/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/graphite/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8d9ce682a08ef338c4d173d765cdd7cacf908b87 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/graphite/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/ink/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/ink/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04e9bb8df68b80f721e1e313e47fe6d060afdfe5 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/ink/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/ink/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/ink/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..58912a5c347ed558f71ed8bc41a9c3e88bffe5db Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/ink/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/oil_paint/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/oil_paint/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8487eb3e2cf5b6c54f94fea6b50c6c85d845d87 Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/oil_paint/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/oil_paint/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/oil_paint/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39f49db7b26b356f328287c4fe69f8c48468e7ae Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/oil_paint/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/pastel_art/landscape-0.jpg b/ui/media/modifier-thumbnails/pen/pastel_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d47f061f344fe3631102ce7be934ff433be7fc6d Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/pastel_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/pen/pastel_art/portrait-0.jpg b/ui/media/modifier-thumbnails/pen/pastel_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8389468a83fdb5e0d378a9a3f16520318246f2f Binary files /dev/null and b/ui/media/modifier-thumbnails/pen/pastel_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/16-bit/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/16-bit/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cab6dabaeb48210c11ab3b7d457d463a1671c294 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/16-bit/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/16-bit/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/16-bit/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec0d327c62c81278135cb136e55a9b09b8b810ff Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/16-bit/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/2d/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/2d/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c171c474a7bb4a2864f6aa361e6fd0ba321256dd Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/2d/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/2d/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/2d/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5da42777816e5f6bdffe124fe59e5288f43afa27 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/2d/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/8-bit/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/8-bit/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de645e6d09475dad6e9856a999ef45e8329c2cce Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/8-bit/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/8-bit/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/8-bit/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..922cda9126c7a080afa6676117347c6d66e4c48c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/8-bit/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/anaglyph/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/anaglyph/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..acf16435bd41fee3be7d7aeae4ac07aea55cba68 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/anaglyph/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/anaglyph/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/anaglyph/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48246fa5b3b2d7b058fcd8893420c6779c742d2f Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/anaglyph/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/anime/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/anime/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84b8deb44be54578ff1182acb9552e6851cc9273 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/anime/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/anime/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/anime/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa34b2a22045b3643a8af089bea1293fc466ddd1 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/anime/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/art_nouveau/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/art_nouveau/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aa8239f8800482af8c185e4802e83164897d830d Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/art_nouveau/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/art_nouveau/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/art_nouveau/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff30bc9c3dbdc9b3ef4fe4cfb4d330913e6000fa Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/art_nouveau/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/baroque/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/baroque/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfc78d2f9e88513eddb8fbf683f95ed87e6ea317 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/baroque/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/baroque/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/baroque/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32e901978ae392737a1b5ec55bb28d28eb507d31 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/baroque/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/bauhaus/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/bauhaus/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2b4337801740f09e32e9ca6ded8edf756167807 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/bauhaus/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/bauhaus/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/bauhaus/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7ee00252285b4acd5f29219bc3d0b2ece26c576 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/bauhaus/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cartoon/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/cartoon/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e0690e17076ed29e8f512d1fe3fd4defbb5ef6f Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cartoon/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cartoon/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/cartoon/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..95997d3c9c259f95af2fa1550e17d04594acd880 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cartoon/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cgi/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/cgi/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e987dcefe3f1e7c447ceef94b9c2041072f8cf03 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cgi/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cgi/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/cgi/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6184ba711536ef31d111b64bac8c6f42848afa63 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cgi/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/comic_book/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/comic_book/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c0b91a35895ed1ce0944e2bad39a713b0a078d4 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/comic_book/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/comic_book/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/comic_book/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d435d523d1813577d1cf1140e5bda06cec14ca2 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/comic_book/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/concept_art/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/concept_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..26817477e489263c8914a3b1a3ac53f6930db726 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/concept_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/concept_art/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/concept_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e7c0b07d35c30cb1db7dc0504249d9bf80acdae Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/concept_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/constructivist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/constructivist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e82b2d5a3f8e84fe8ba875ef7809da709df55c20 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/constructivist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/constructivist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/constructivist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d015f6660dc648961b7fffb6dd00940b8f7f9155 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/constructivist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cubist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/cubist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b4f22661ec496af67757baef596098c217aa639 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cubist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/cubist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/cubist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..679b23b0d9bbdf9705ee509f0c4e1a6d8f94e29c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/cubist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/dadaist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/dadaist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31ec96eaf1baa85799ee3a9a078976154e3ec69a Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/dadaist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/dadaist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/dadaist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04f4443b6f61bd77340940513820487d00fc5596 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/dadaist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/digital_art/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/digital_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6977ef1a2f97ab7e4e4381ecd1edbd862d782948 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/digital_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/digital_art/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/digital_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9dde746ad1aab06915a0372b88312b439c0e4843 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/digital_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/expressionist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/expressionist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c317ff449b694115dd108b9e5736e5c4a65eb7e2 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/expressionist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/expressionist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/expressionist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0d4f0581f8ab1fde709cbfb40640935b83f4a8f Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/expressionist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/fantasy/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/fantasy/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df3890e554e9a99c951d07db8f43be2ea71edcfe Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/fantasy/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/fantasy/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/fantasy/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5e7ed735b6cccd20f11947bb7cce658f67f4788d Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/fantasy/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/fauvist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/fauvist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..57128ce1a9ff63920d4c8b54c725bef4bfb63aa1 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/fauvist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/fauvist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/fauvist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1d75bdba437cb0c0e25395995901a74058b434cf Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/fauvist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/figurative/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/figurative/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..323ca7b2ee1f80dc4d36475304d702dd8cc4ac90 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/figurative/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/figurative/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/figurative/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..547ca26542827bbfd874b01943d2f2efbef87461 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/figurative/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/geometric/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/geometric/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2cb79e10fbd46a52827167c763a82743ef5c6625 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/geometric/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/geometric/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/geometric/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a07a8453cb9a89268fee85806bcd045cec88dd68 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/geometric/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/graphic_novel/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/graphic_novel/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c4b4d130535b7a5f0425074550489b1e6dbf193 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/graphic_novel/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/graphic_novel/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/graphic_novel/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae186e55e098a63ece1818eab6158228b5f1b5d2 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/graphic_novel/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6f5a1a6b00ae18738f452c43ba4760c4342441e2 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4651a8e8030e486f5d40eb6be06bcce5231eae9c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/hard_edge_painting/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/hydrodipped/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/hydrodipped/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..01181f167ac2fe1d8b26011461810de01a0bc18c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/hydrodipped/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/hydrodipped/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/hydrodipped/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b8e721fe28279cf048acb110bd6e2eb741c5532 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/hydrodipped/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/impressionistic/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/impressionistic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64d765dab265a695078d51c050266a7f01d26a2f Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/impressionistic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/impressionistic/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/impressionistic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..60fe0cf0134a0e9f7ce2215662e0d1d1910f79be Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/impressionistic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/lithography/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/lithography/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d7ef50cdcbd678b4603814e5ce0e41d821918405 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/lithography/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/lithography/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/lithography/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a9e81d807bfc6d10ec97e9b2f2f598006d83a96 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/lithography/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/manga/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/manga/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..54faf7f19f91ab89c4a1dd2e05064af3d5ee35c3 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/manga/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/manga/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/manga/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fab81d2d482ffada8e319b9d99a1c05a37aa3870 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/manga/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/minimalist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/minimalist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f36b5b1fdadeab86a9b88ddb8565ce2e45454ab Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/minimalist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/minimalist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/minimalist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2812a6dbdfe18242c4bced4bc2ec835ec9ab0c9e Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/minimalist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/modern_art/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/modern_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec4323084cc9666650fefd010b1c66154c4cf76c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/modern_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/modern_art/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/modern_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1eb036508d03f73705949d2d8ca0aa37f3903bf9 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/modern_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/mosaic/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/mosaic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32284748edfc228cbb97a5f77c2ce03416ac20da Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/mosaic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/mosaic/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/mosaic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43c07d870a3baaf641950007a7701387201a7d5b Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/mosaic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/mural/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/mural/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2455f4d507aad0aff71348d7e7ffed798a1149eb Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/mural/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/mural/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/mural/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a31bd7d2e166f03a1d82356521a06480200fd44e Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/mural/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/naive/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/naive/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aec27ab0832d3c0c894091f7f6dc3dc392629e46 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/naive/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/naive/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/naive/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..37dab0d4a535b3b655a3b9f293f10e3e726ff92c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/naive/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/neoclassical/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/neoclassical/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc7d151fa556a94314f9609656abcce158bbee70 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/neoclassical/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/neoclassical/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/neoclassical/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..517d2970338edf2bd0a73d72419b562f59b0ef95 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/neoclassical/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/photo/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/photo/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e888b747c9d9cac4db06eab50299eacfac60809 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/photo/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/photo/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/photo/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28b15e1fa91c713ab6b848d892fa6465aa7504c6 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/photo/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/realistic/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/realistic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..001896d16bec01f96eb3872dc60da27ce2b02a7b Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/realistic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/realistic/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/realistic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e3b19a4b5b6995595abcc0cf30046331db1dc549 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/realistic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/rococo/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/rococo/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cff9271a5c19f3e5caa4cc57af188cf6de5beeb2 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/rococo/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/rococo/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/rococo/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7c0aa58dd4e77055770e68635d1aa9140ff9c31e Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/rococo/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/romantic/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/romantic/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a5e03db8996f11e5f8c567f5ed6807ad2c5d767 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/romantic/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/romantic/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/romantic/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..efb2ace0bfb61b0ddcd26ff95f8734fbf2f3833c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/romantic/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/street_art/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/street_art/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd9340dca69f83c362a7437a1c747b7be1a75035 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/street_art/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/street_art/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/street_art/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..191209a804d555eaf7aa383a4d0b724890e75414 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/street_art/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/stuckist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/stuckist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..53d2ba03f9c74075e18454d97450482ed6c82ddc Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/stuckist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/stuckist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/stuckist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a69865e8c6e6638a6c8a93fff006f454614e889c Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/stuckist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/surrealist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/surrealist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fa18e3b1e5b121e32f6954c3dde7c40a2e4ecf76 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/surrealist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/surrealist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/surrealist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8f2230241e12b5d4e3ab54847b07477818a25be Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/surrealist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/symbolist/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/symbolist/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b5b7f762cc1a9806d29e73732fa7a87423d5b88 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/symbolist/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/symbolist/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/symbolist/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..59dfe64bb77425809e0b9e1dd55c150fe9c42a26 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/symbolist/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/visual_novel/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/visual_novel/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd7534a1a46c5f72af36fb1134f11fc4648a27a9 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/visual_novel/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/visual_novel/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/visual_novel/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1cb5bce04b33c51b39d4d6765ee529facbe1584a Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/visual_novel/portrait-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/watercolor/landscape-0.jpg b/ui/media/modifier-thumbnails/visual_style/watercolor/landscape-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08e6faa0b1b49f880ace7c1c185c9b9b23eb2cd5 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/watercolor/landscape-0.jpg differ diff --git a/ui/media/modifier-thumbnails/visual_style/watercolor/portrait-0.jpg b/ui/media/modifier-thumbnails/visual_style/watercolor/portrait-0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b21e4f26d5dda0ae1b4fd9454fd029292d03691 Binary files /dev/null and b/ui/media/modifier-thumbnails/visual_style/watercolor/portrait-0.jpg differ diff --git a/ui/modifiers.json b/ui/modifiers.json new file mode 100644 index 0000000000000000000000000000000000000000..325e724edd57c4f69a941d3142e2499b37ac0f95 --- /dev/null +++ b/ui/modifiers.json @@ -0,0 +1,2717 @@ +[ + { + "category": "Drawing Style", + "modifiers": [ + { + "modifier": "Cel Shading", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/cel_shading/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/cel_shading/landscape-0.jpg" + } + ] + }, + { + "modifier": "Children's Drawing", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/children_s_drawing/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/children_s_drawing/landscape-0.jpg" + } + ] + }, + { + "modifier": "Crosshatch", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/crosshatch/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/crosshatch/landscape-0.jpg" + } + ] + }, + { + "modifier": "Detailed and Intricate", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/detailed_and_intricate/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/detailed_and_intricate/landscape-0.jpg" + } + ] + }, + { + "modifier": "Doodle", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/doodle/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/doodle/landscape-0.jpg" + } + ] + }, + { + "modifier": "Dot Art", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/dot_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/dot_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Line Art", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/line_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/line_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Sketch", + "previews": [ + { + "name": "portrait", + "path": "drawing_style/sketch/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "drawing_style/sketch/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Visual Style", + "modifiers": [ + { + "modifier": "2D", + "previews": [ + { + "name": "portrait", + "path": "visual_style/2d/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/2d/landscape-0.jpg" + } + ] + }, + { + "modifier": "8-Bit", + "previews": [ + { + "name": "portrait", + "path": "visual_style/8-bit/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/8-bit/landscape-0.jpg" + } + ] + }, + { + "modifier": "16-Bit", + "previews": [ + { + "name": "portrait", + "path": "visual_style/16-bit/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/16-bit/landscape-0.jpg" + } + ] + }, + { + "modifier": "Anaglyph", + "previews": [ + { + "name": "portrait", + "path": "visual_style/anaglyph/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/anaglyph/landscape-0.jpg" + } + ] + }, + { + "modifier": "Anime", + "previews": [ + { + "name": "portrait", + "path": "visual_style/anime/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/anime/landscape-0.jpg" + } + ] + }, + { + "modifier": "Art Nouveau", + "previews": [ + { + "name": "portrait", + "path": "visual_style/art_nouveau/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/art_nouveau/landscape-0.jpg" + } + ] + }, + { + "modifier": "Bauhaus", + "previews": [ + { + "name": "portrait", + "path": "visual_style/bauhaus/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/bauhaus/landscape-0.jpg" + } + ] + }, + { + "modifier": "Baroque", + "previews": [ + { + "name": "portrait", + "path": "visual_style/baroque/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/baroque/landscape-0.jpg" + } + ] + }, + { + "modifier": "CGI", + "previews": [ + { + "name": "portrait", + "path": "visual_style/cgi/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/cgi/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cartoon", + "previews": [ + { + "name": "portrait", + "path": "visual_style/cartoon/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/cartoon/landscape-0.jpg" + } + ] + }, + { + "modifier": "Comic Book", + "previews": [ + { + "name": "portrait", + "path": "visual_style/comic_book/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/comic_book/landscape-0.jpg" + } + ] + }, + { + "modifier": "Concept Art", + "previews": [ + { + "name": "portrait", + "path": "visual_style/concept_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/concept_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Constructivist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/constructivist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/constructivist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cubist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/cubist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/cubist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Digital Art", + "previews": [ + { + "name": "portrait", + "path": "visual_style/digital_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/digital_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Dadaist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/dadaist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/dadaist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Expressionist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/expressionist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/expressionist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Fantasy", + "previews": [ + { + "name": "portrait", + "path": "visual_style/fantasy/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/fantasy/landscape-0.jpg" + } + ] + }, + { + "modifier": "Fauvist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/fauvist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/fauvist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Figurative", + "previews": [ + { + "name": "portrait", + "path": "visual_style/figurative/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/figurative/landscape-0.jpg" + } + ] + }, + { + "modifier": "Graphic Novel", + "previews": [ + { + "name": "portrait", + "path": "visual_style/graphic_novel/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/graphic_novel/landscape-0.jpg" + } + ] + }, + { + "modifier": "Geometric", + "previews": [ + { + "name": "portrait", + "path": "visual_style/geometric/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/geometric/landscape-0.jpg" + } + ] + }, + { + "modifier": "Hard Edge Painting", + "previews": [ + { + "name": "portrait", + "path": "visual_style/hard_edge_painting/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/hard_edge_painting/landscape-0.jpg" + } + ] + }, + { + "modifier": "Hydrodipped", + "previews": [ + { + "name": "portrait", + "path": "visual_style/hydrodipped/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/hydrodipped/landscape-0.jpg" + } + ] + }, + { + "modifier": "Impressionistic", + "previews": [ + { + "name": "portrait", + "path": "visual_style/impressionistic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/impressionistic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Lithography", + "previews": [ + { + "name": "portrait", + "path": "visual_style/lithography/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/lithography/landscape-0.jpg" + } + ] + }, + { + "modifier": "Manga", + "previews": [ + { + "name": "portrait", + "path": "visual_style/manga/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/manga/landscape-0.jpg" + } + ] + }, + { + "modifier": "Minimalist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/minimalist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/minimalist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Modern Art", + "previews": [ + { + "name": "portrait", + "path": "visual_style/modern_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/modern_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Mosaic", + "previews": [ + { + "name": "portrait", + "path": "visual_style/mosaic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/mosaic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Mural", + "previews": [ + { + "name": "portrait", + "path": "visual_style/mural/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/mural/landscape-0.jpg" + } + ] + }, + { + "modifier": "Naive", + "previews": [ + { + "name": "portrait", + "path": "visual_style/naive/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/naive/landscape-0.jpg" + } + ] + }, + { + "modifier": "Neoclassical", + "previews": [ + { + "name": "portrait", + "path": "visual_style/neoclassical/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/neoclassical/landscape-0.jpg" + } + ] + }, + { + "modifier": "Photo", + "previews": [ + { + "name": "portrait", + "path": "visual_style/photo/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/photo/landscape-0.jpg" + } + ] + }, + { + "modifier": "Realistic", + "previews": [ + { + "name": "portrait", + "path": "visual_style/realistic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/realistic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Rococo", + "previews": [ + { + "name": "portrait", + "path": "visual_style/rococo/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/rococo/landscape-0.jpg" + } + ] + }, + { + "modifier": "Romantic", + "previews": [ + { + "name": "portrait", + "path": "visual_style/romantic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/romantic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Street Art", + "previews": [ + { + "name": "portrait", + "path": "visual_style/street_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/street_art/landscape-0.jpg" + } + ] + }, + { + "modifier": "Symbolist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/symbolist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/symbolist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Stuckist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/stuckist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/stuckist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Surrealist", + "previews": [ + { + "name": "portrait", + "path": "visual_style/surrealist/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/surrealist/landscape-0.jpg" + } + ] + }, + { + "modifier": "Visual Novel", + "previews": [ + { + "name": "portrait", + "path": "visual_style/visual_novel/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/visual_novel/landscape-0.jpg" + } + ] + }, + { + "modifier": "Watercolor", + "previews": [ + { + "name": "portrait", + "path": "visual_style/watercolor/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "visual_style/watercolor/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Pen", + "modifiers": [ + { + "modifier": "Chalk", + "previews": [ + { + "name": "portrait", + "path": "pen/chalk/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/chalk/landscape-0.jpg" + } + ] + }, + { + "modifier": "Colored Pencil", + "previews": [ + { + "name": "portrait", + "path": "pen/colored_pencil/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/colored_pencil/landscape-0.jpg" + } + ] + }, + { + "modifier": "Graphite", + "previews": [ + { + "name": "portrait", + "path": "pen/graphite/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/graphite/landscape-0.jpg" + } + ] + }, + { + "modifier": "Ink", + "previews": [ + { + "name": "portrait", + "path": "pen/ink/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/ink/landscape-0.jpg" + } + ] + }, + { + "modifier": "Oil Paint", + "previews": [ + { + "name": "portrait", + "path": "pen/oil_paint/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/oil_paint/landscape-0.jpg" + } + ] + }, + { + "modifier": "Pastel Art", + "previews": [ + { + "name": "portrait", + "path": "pen/pastel_art/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "pen/pastel_art/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Carving and Etching", + "modifiers": [ + { + "modifier": "etching", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/etching/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/etching/landscape-0.jpg" + } + ] + }, + { + "modifier": "Linocut", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/linocut/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/linocut/landscape-0.jpg" + } + ] + }, + { + "modifier": "Paper Model", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/paper_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/paper_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "Paper-Mache", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/paper-mache/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/paper-mache/landscape-0.jpg" + } + ] + }, + { + "modifier": "Papercutting", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/papercutting/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/papercutting/landscape-0.jpg" + } + ] + }, + { + "modifier": "Pyrography", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/pyrography/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/pyrography/landscape-0.jpg" + } + ] + }, + { + "modifier": "Wood-Carving", + "previews": [ + { + "name": "portrait", + "path": "carving_and_etching/wood-carving/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "carving_and_etching/wood-carving/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Camera", + "modifiers": [ + { + "modifier": "Aerial View", + "previews": [ + { + "name": "portrait", + "path": "camera/aerial_view/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/aerial_view/landscape-0.jpg" + } + ] + }, + { + "modifier": "Canon50", + "previews": [ + { + "name": "portrait", + "path": "camera/canon50/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/canon50/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cinematic", + "previews": [ + { + "name": "portrait", + "path": "camera/cinematic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/cinematic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Close-up", + "previews": [ + { + "name": "portrait", + "path": "camera/close-up/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/close-up/landscape-0.jpg" + } + ] + }, + { + "modifier": "Color Grading", + "previews": [ + { + "name": "portrait", + "path": "camera/color_grading/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/color_grading/landscape-0.jpg" + } + ] + }, + { + "modifier": "Dramatic", + "previews": [ + { + "name": "portrait", + "path": "camera/dramatic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/dramatic/landscape-0.jpg" + } + ] + }, + { + "modifier": "Film Grain", + "previews": [ + { + "name": "portrait", + "path": "camera/film_grain/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/film_grain/landscape-0.jpg" + } + ] + }, + { + "modifier": "Fisheye Lens", + "previews": [ + { + "name": "portrait", + "path": "camera/fisheye_lens/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/fisheye_lens/landscape-0.jpg" + } + ] + }, + { + "modifier": "Glamor Shot", + "previews": [ + { + "name": "portrait", + "path": "camera/glamor_shot/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/glamor_shot/landscape-0.jpg" + } + ] + }, + { + "modifier": "Golden Hour", + "previews": [ + { + "name": "portrait", + "path": "camera/golden_hour/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/golden_hour/landscape-0.jpg" + } + ] + }, + { + "modifier": "HD", + "previews": [ + { + "name": "portrait", + "path": "camera/hd/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/hd/landscape-0.jpg" + } + ] + }, + { + "modifier": "Landscape", + "previews": [ + { + "name": "portrait", + "path": "camera/landscape/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/landscape/landscape-0.jpg" + } + ] + }, + { + "modifier": "Lens Flare", + "previews": [ + { + "name": "portrait", + "path": "camera/lens_flare/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/lens_flare/landscape-0.jpg" + } + ] + }, + { + "modifier": "Macro", + "previews": [ + { + "name": "portrait", + "path": "camera/macro/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/macro/landscape-0.jpg" + } + ] + }, + { + "modifier": "Polaroid", + "previews": [ + { + "name": "portrait", + "path": "camera/polaroid/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/polaroid/landscape-0.jpg" + } + ] + }, + { + "modifier": "Photoshoot", + "previews": [ + { + "name": "portrait", + "path": "camera/photoshoot/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/photoshoot/landscape-0.jpg" + } + ] + }, + { + "modifier": "Portrait", + "previews": [ + { + "name": "portrait", + "path": "camera/portrait/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/portrait/landscape-0.jpg" + } + ] + }, + { + "modifier": "Studio Lighting", + "previews": [ + { + "name": "portrait", + "path": "camera/studio_lighting/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/studio_lighting/landscape-0.jpg" + } + ] + }, + { + "modifier": "Vintage", + "previews": [ + { + "name": "portrait", + "path": "camera/vintage/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/vintage/landscape-0.jpg" + } + ] + }, + { + "modifier": "War Photography", + "previews": [ + { + "name": "portrait", + "path": "camera/war_photography/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/war_photography/landscape-0.jpg" + } + ] + }, + { + "modifier": "White Balance", + "previews": [ + { + "name": "portrait", + "path": "camera/white_balance/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/white_balance/landscape-0.jpg" + } + ] + }, + { + "modifier": "Wildlife Photography", + "previews": [ + { + "name": "portrait", + "path": "camera/wildlife_photography/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "camera/wildlife_photography/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Color", + "modifiers": [ + { + "modifier": "Beautiful Lighting", + "previews": [ + { + "name": "portrait", + "path": "color/beautiful_lighting/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/beautiful_lighting/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cold Color Palette", + "previews": [ + { + "name": "portrait", + "path": "color/cold_color_palette/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/cold_color_palette/landscape-0.jpg" + } + ] + }, + { + "modifier": "Colorful", + "previews": [ + { + "name": "portrait", + "path": "color/colorful/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/colorful/landscape-0.jpg" + } + ] + }, + { + "modifier": "Dynamic Lighting", + "previews": [ + { + "name": "portrait", + "path": "color/dynamic_lighting/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/dynamic_lighting/landscape-0.jpg" + } + ] + }, + { + "modifier": "Electric Colors", + "previews": [ + { + "name": "portrait", + "path": "color/electric_colors/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/electric_colors/landscape-0.jpg" + } + ] + }, + { + "modifier": "Infrared", + "previews": [ + { + "name": "portrait", + "path": "color/infrared/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/infrared/landscape-0.jpg" + } + ] + }, + { + "modifier": "Pastel", + "previews": [ + { + "name": "portrait", + "path": "color/pastel/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/pastel/landscape-0.jpg" + } + ] + }, + { + "modifier": "Neon", + "previews": [ + { + "name": "portrait", + "path": "color/neon/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/neon/landscape-0.jpg" + } + ] + }, + { + "modifier": "Synthwave", + "previews": [ + { + "name": "portrait", + "path": "color/synthwave/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/synthwave/landscape-0.jpg" + } + ] + }, + { + "modifier": "Warm Color Palette", + "previews": [ + { + "name": "portrait", + "path": "color/warm_color_palette/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "color/warm_color_palette/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Emotions", + "modifiers": [ + { + "modifier": "Angry", + "previews": [ + { + "name": "portrait", + "path": "emotions/angry/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/angry/landscape-0.jpg" + } + ] + }, + { + "modifier": "Bitter", + "previews": [ + { + "name": "portrait", + "path": "emotions/bitter/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/bitter/landscape-0.jpg" + } + ] + }, + { + "modifier": "Disgusted", + "previews": [ + { + "name": "portrait", + "path": "emotions/disgusted/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/disgusted/landscape-0.jpg" + } + ] + }, + { + "modifier": "Embarrassed", + "previews": [ + { + "name": "portrait", + "path": "emotions/embarrassed/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/embarrassed/landscape-0.jpg" + } + ] + }, + { + "modifier": "Evil", + "previews": [ + { + "name": "portrait", + "path": "emotions/evil/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/evil/landscape-0.jpg" + } + ] + }, + { + "modifier": "Excited", + "previews": [ + { + "name": "portrait", + "path": "emotions/excited/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/excited/landscape-0.jpg" + } + ] + }, + { + "modifier": "Fear", + "previews": [ + { + "name": "portrait", + "path": "emotions/fear/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/fear/landscape-0.jpg" + } + ] + }, + { + "modifier": "Funny", + "previews": [ + { + "name": "portrait", + "path": "emotions/funny/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/funny/landscape-0.jpg" + } + ] + }, + { + "modifier": "Happy", + "previews": [ + { + "name": "portrait", + "path": "emotions/happy/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/happy/landscape-0.jpg" + } + ] + }, + { + "modifier": "Horrifying", + "previews": [ + { + "name": "portrait", + "path": "emotions/horrifying/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/horrifying/landscape-0.jpg" + } + ] + }, + { + "modifier": "Lonely", + "previews": [ + { + "name": "portrait", + "path": "emotions/lonely/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/lonely/landscape-0.jpg" + } + ] + }, + { + "modifier": "Sad", + "previews": [ + { + "name": "portrait", + "path": "emotions/sad/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/sad/landscape-0.jpg" + } + ] + }, + { + "modifier": "Serene", + "previews": [ + { + "name": "portrait", + "path": "emotions/serene/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/serene/landscape-0.jpg" + } + ] + }, + { + "modifier": "Surprised", + "previews": [ + { + "name": "portrait", + "path": "emotions/surprised/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/surprised/landscape-0.jpg" + } + ] + }, + { + "modifier": "Melancholic", + "previews": [ + { + "name": "portrait", + "path": "emotions/melancholic/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "emotions/melancholic/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "Artist", + "modifiers": [ + { + "modifier": "Artstation", + "previews": [ + { + "name": "portrait", + "path": "artist/artstation/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/artstation/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Agnes Lawrence Pelton", + "previews": [ + { + "name": "portrait", + "path": "artist/by_agnes_lawrence_pelton/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_agnes_lawrence_pelton/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Akihito Yoshida", + "previews": [ + { + "name": "portrait", + "path": "artist/by_akihito_yoshida/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_akihito_yoshida/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Alex Grey", + "previews": [ + { + "name": "portrait", + "path": "artist/by_alex_grey/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_alex_grey/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Alexander Jansson", + "previews": [ + { + "name": "portrait", + "path": "artist/by_alexander_jansson/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_alexander_jansson/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Alphonse Mucha", + "previews": [ + { + "name": "portrait", + "path": "artist/by_alphonse_mucha/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_alphonse_mucha/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Andy Warhol", + "previews": [ + { + "name": "portrait", + "path": "artist/by_andy_warhol/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_andy_warhol/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Artgerm", + "previews": [ + { + "name": "portrait", + "path": "artist/by_artgerm/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_artgerm/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Asaf Hanuka", + "previews": [ + { + "name": "portrait", + "path": "artist/by_asaf_hanuka/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_asaf_hanuka/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Aubrey Beardsley", + "previews": [ + { + "name": "portrait", + "path": "artist/by_aubrey_beardsley/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_aubrey_beardsley/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Banksy", + "previews": [ + { + "name": "portrait", + "path": "artist/by_banksy/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_banksy/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Beeple", + "previews": [ + { + "name": "portrait", + "path": "artist/by_beeple/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_beeple/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Ben Enwonwu", + "previews": [ + { + "name": "portrait", + "path": "artist/by_ben_enwonwu/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_ben_enwonwu/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Bob Eggleton", + "previews": [ + { + "name": "portrait", + "path": "artist/by_bob_eggleton/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_bob_eggleton/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Caravaggio Michelangelo Merisi", + "previews": [ + { + "name": "portrait", + "path": "artist/by_caravaggio_michelangelo_merisi/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_caravaggio_michelangelo_merisi/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Caspar David Friedrich", + "previews": [ + { + "name": "portrait", + "path": "artist/by_caspar_david_friedrich/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_caspar_david_friedrich/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Chris Foss", + "previews": [ + { + "name": "portrait", + "path": "artist/by_chris_foss/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_chris_foss/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Claude Monet", + "previews": [ + { + "name": "portrait", + "path": "artist/by_claude_monet/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_claude_monet/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Dan Mumford", + "previews": [ + { + "name": "portrait", + "path": "artist/by_dan_mumford/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_dan_mumford/landscape-0.jpg" + } + ] + }, + { + "modifier": "by David Mann", + "previews": [ + { + "name": "portrait", + "path": "artist/by_david_mann/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_david_mann/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Diego Velázquez", + "previews": [ + { + "name": "portrait", + "path": "artist/by_diego_vela_zquez/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_diego_vela_zquez/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Disney Animation Studios", + "previews": [ + { + "name": "portrait", + "path": "artist/by_disney_animation_studios/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_disney_animation_studios/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Édouard Manet", + "previews": [ + { + "name": "portrait", + "path": "artist/by_e_douard_manet/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_e_douard_manet/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Esao Andrews", + "previews": [ + { + "name": "portrait", + "path": "artist/by_esao_andrews/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_esao_andrews/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Frida Kahlo", + "previews": [ + { + "name": "portrait", + "path": "artist/by_frida_kahlo/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_frida_kahlo/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Gediminas Pranckevicius", + "previews": [ + { + "name": "portrait", + "path": "artist/by_gediminas_pranckevicius/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_gediminas_pranckevicius/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Georgia O'Keeffe", + "previews": [ + { + "name": "portrait", + "path": "artist/by_georgia_o_keeffe/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_georgia_o_keeffe/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Greg Rutkowski", + "previews": [ + { + "name": "portrait", + "path": "artist/by_greg_rutkowski/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_greg_rutkowski/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Gustave Doré", + "previews": [ + { + "name": "portrait", + "path": "artist/by_gustave_dore_/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_gustave_dore_/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Gustave Klimt", + "previews": [ + { + "name": "portrait", + "path": "artist/by_gustave_klimt/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_gustave_klimt/landscape-0.jpg" + } + ] + }, + { + "modifier": "by H.R. Giger", + "previews": [ + { + "name": "portrait", + "path": "artist/by_h_r_giger/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_h_r_giger/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Hayao Miyazaki", + "previews": [ + { + "name": "portrait", + "path": "artist/by_hayao_miyazaki/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_hayao_miyazaki/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Henri Matisse", + "previews": [ + { + "name": "portrait", + "path": "artist/by_henri_matisse/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_henri_matisse/landscape-0.jpg" + } + ] + }, + { + "modifier": "by HP Lovecraft", + "previews": [ + { + "name": "portrait", + "path": "artist/by_hp_lovecraft/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_hp_lovecraft/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Ivan Shishkin", + "previews": [ + { + "name": "portrait", + "path": "artist/by_ivan_shishkin/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_ivan_shishkin/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Jack Kirby", + "previews": [ + { + "name": "portrait", + "path": "artist/by_jack_kirby/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_jack_kirby/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Jackson Pollock", + "previews": [ + { + "name": "portrait", + "path": "artist/by_jackson_pollock/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_jackson_pollock/landscape-0.jpg" + } + ] + }, + { + "modifier": "by James Jean", + "previews": [ + { + "name": "portrait", + "path": "artist/by_james_jean/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_james_jean/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Jim Burns", + "previews": [ + { + "name": "portrait", + "path": "artist/by_jim_burns/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_jim_burns/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Johannes Vermeer", + "previews": [ + { + "name": "portrait", + "path": "artist/by_johannes_vermeer/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_johannes_vermeer/landscape-0.jpg" + } + ] + }, + { + "modifier": "by John William Waterhouse", + "previews": [ + { + "name": "portrait", + "path": "artist/by_john_william_waterhouse/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_john_william_waterhouse/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Katsushika Hokusai", + "previews": [ + { + "name": "portrait", + "path": "artist/by_katsushika_hokusai/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_katsushika_hokusai/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Kim Tschang Yeul", + "previews": [ + { + "name": "portrait", + "path": "artist/by_kim_tschang_yeul/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_kim_tschang_yeul/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Ko Young Hoon", + "previews": [ + { + "name": "portrait", + "path": "artist/by_ko_young_hoon/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_ko_young_hoon/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Leonardo da Vinci", + "previews": [ + { + "name": "portrait", + "path": "artist/by_leonardo_da_vinci/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_leonardo_da_vinci/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Lisa Frank", + "previews": [ + { + "name": "portrait", + "path": "artist/by_lisa_frank/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_lisa_frank/landscape-0.jpg" + } + ] + }, + { + "modifier": "by M.C Escher", + "previews": [ + { + "name": "portrait", + "path": "artist/by_m_c_escher/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_m_c_escher/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Mahmoud Saïd", + "previews": [ + { + "name": "portrait", + "path": "artist/by_mahmoud_sai_d/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_mahmoud_sai_d/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Makoto Shinkai", + "previews": [ + { + "name": "portrait", + "path": "artist/by_makoto_shinkai/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_makoto_shinkai/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Marc Simonetti", + "previews": [ + { + "name": "portrait", + "path": "artist/by_marc_simonetti/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_marc_simonetti/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Mark Brooks", + "previews": [ + { + "name": "portrait", + "path": "artist/by_mark_brooks/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_mark_brooks/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Michelangelo", + "previews": [ + { + "name": "portrait", + "path": "artist/by_michelangelo/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_michelangelo/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Pablo Picasso", + "previews": [ + { + "name": "portrait", + "path": "artist/by_pablo_picasso/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_pablo_picasso/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Paul Klee", + "previews": [ + { + "name": "portrait", + "path": "artist/by_paul_klee/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_paul_klee/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Peter Mohrbacher", + "previews": [ + { + "name": "portrait", + "path": "artist/by_peter_mohrbacher/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_peter_mohrbacher/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Pierre-Auguste Renoir", + "previews": [ + { + "name": "portrait", + "path": "artist/by_pierre-auguste_renoir/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_pierre-auguste_renoir/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Pixar Animation Studios", + "previews": [ + { + "name": "portrait", + "path": "artist/by_pixar_animation_studios/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_pixar_animation_studios/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Rembrandt", + "previews": [ + { + "name": "portrait", + "path": "artist/by_rembrandt/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_rembrandt/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Richard Dadd", + "previews": [ + { + "name": "portrait", + "path": "artist/by_richard_dadd/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_richard_dadd/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Rossdraws", + "previews": [ + { + "name": "portrait", + "path": "artist/by_rossdraws/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_rossdraws/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Salvador Dalí", + "previews": [ + { + "name": "portrait", + "path": "artist/by_salvador_dali_/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_salvador_dali_/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Sam does Arts", + "previews": [ + { + "name": "portrait", + "path": "artist/by_sam_does_arts/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_sam_does_arts/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Sandro Botticelli", + "previews": [ + { + "name": "portrait", + "path": "artist/by_sandro_botticelli/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_sandro_botticelli/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Ted Nasmith", + "previews": [ + { + "name": "portrait", + "path": "artist/by_ted_nasmith/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_ted_nasmith/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Ten Hundred", + "previews": [ + { + "name": "portrait", + "path": "artist/by_ten_hundred/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_ten_hundred/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Thomas Kinkade", + "previews": [ + { + "name": "portrait", + "path": "artist/by_thomas_kinkade/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_thomas_kinkade/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Tivadar Csontváry Kosztka", + "previews": [ + { + "name": "portrait", + "path": "artist/by_tivadar_csontva_ry_kosztka/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_tivadar_csontva_ry_kosztka/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Victo Ngai", + "previews": [ + { + "name": "portrait", + "path": "artist/by_victo_ngai/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_victo_ngai/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Vincent di Fate", + "previews": [ + { + "name": "portrait", + "path": "artist/by_vincent_di_fate/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_vincent_di_fate/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Vincent van Gogh", + "previews": [ + { + "name": "portrait", + "path": "artist/by_vincent_van_gogh/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_vincent_van_gogh/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Wes Anderson", + "previews": [ + { + "name": "portrait", + "path": "artist/by_wes_anderson/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_wes_anderson/landscape-0.jpg" + } + ] + }, + { + "modifier": "by wlop", + "previews": [ + { + "name": "portrait", + "path": "artist/by_wlop/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_wlop/landscape-0.jpg" + } + ] + }, + { + "modifier": "by Yoshitaka Amano", + "previews": [ + { + "name": "portrait", + "path": "artist/by_yoshitaka_amano/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "artist/by_yoshitaka_amano/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "CGI Software", + "modifiers": [ + { + "modifier": "3D Model", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/3d_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/3d_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "3D Sculpt", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/3d_sculpt/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/3d_sculpt/landscape-0.jpg" + } + ] + }, + { + "modifier": "3Ds Max Model", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/3ds_max_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/3ds_max_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "Blender Model", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/blender_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/blender_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cinema4d Model", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/cinema4d_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/cinema4d_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "Maya Model", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/maya_model/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/maya_model/landscape-0.jpg" + } + ] + }, + { + "modifier": "Unreal Engine", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/unreal_engine/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/unreal_engine/landscape-0.jpg" + } + ] + }, + { + "modifier": "Zbrush Sculpt", + "previews": [ + { + "name": "portrait", + "path": "cgi_software/zbrush_sculpt/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_software/zbrush_sculpt/landscape-0.jpg" + } + ] + } + ] + }, + { + "category": "CGI Rendering", + "modifiers": [ + { + "modifier": "3D Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/3d_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/3d_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "Corona Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/corona_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/corona_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "Creature Design", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/creature_design/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/creature_design/landscape-0.jpg" + } + ] + }, + { + "modifier": "Cycles Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/cycles_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/cycles_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "Detailed Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/detailed_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/detailed_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "Environment Design", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/environment_design/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/environment_design/landscape-0.jpg" + } + ] + }, + { + "modifier": "Intricate Environment", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/intricate_environment/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/intricate_environment/landscape-0.jpg" + } + ] + }, + { + "modifier": "LSD Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/lsd_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/lsd_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "Octane Render", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/octane_render/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/octane_render/landscape-0.jpg" + } + ] + }, + { + "modifier": "PBR", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/pbr/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/pbr/landscape-0.jpg" + } + ] + }, + { + "modifier": "Glass Caustics", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/glass_caustics/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/glass_caustics/landscape-0.jpg" + } + ] + }, + { + "modifier": "Global Illumination", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/global_illumination/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/global_illumination/landscape-0.jpg" + } + ] + }, + { + "modifier": "Subsurface Scattering", + "previews": [ + { + "name": "portrait", + "path": "cgi_rendering/subsurface_scattering/portrait-0.jpg" + }, + { + "name": "landscape", + "path": "cgi_rendering/subsurface_scattering/landscape-0.jpg" + } + ] + } + ] + } +] diff --git a/ui/plugins/ui/Autoscroll.plugin.js b/ui/plugins/ui/Autoscroll.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..088b1457428dc8c7e209ceacdf29c2d3ae8f5ead --- /dev/null +++ b/ui/plugins/ui/Autoscroll.plugin.js @@ -0,0 +1,28 @@ +(function () { + "use strict" + + let autoScroll = document.querySelector("#auto_scroll") + + // observe for changes in the preview pane + var observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.target.className == 'img-batch') { + Autoscroll(mutation.target) + } + }) + }) + + observer.observe(document.getElementById('preview'), { + childList: true, + subtree: true + }) + + function Autoscroll(target) { + if (autoScroll.checked && target !== null) { + const img = target.querySelector('img') + img.addEventListener('load', function() { + img.closest('.imageTaskContainer').scrollIntoView() + }, { once: true }) + } + } +})() diff --git a/ui/plugins/ui/DO NOT PLACE USER PLUGINS HERE, unless you are a developer.txt b/ui/plugins/ui/DO NOT PLACE USER PLUGINS HERE, unless you are a developer.txt new file mode 100644 index 0000000000000000000000000000000000000000..028157f5e64381e25e52e6d56974e2f317f5c0ec --- /dev/null +++ b/ui/plugins/ui/DO NOT PLACE USER PLUGINS HERE, unless you are a developer.txt @@ -0,0 +1,3 @@ +Custom plugins in this folder will be shipped to all the users by default. + +This allows UI features to be built as plugins (testing our Plugins API, and keeping our core lean and modular). \ No newline at end of file diff --git a/ui/plugins/ui/Modifiers-dnd.plugin.js b/ui/plugins/ui/Modifiers-dnd.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..b0aaaa952a4faff0d61364adab1b0267333f4142 --- /dev/null +++ b/ui/plugins/ui/Modifiers-dnd.plugin.js @@ -0,0 +1,95 @@ +(function () { "use strict" + if (typeof editorModifierTagsList !== 'object') { + console.error('editorModifierTagsList missing...') + return + } + + const styleSheet = document.createElement("style"); + styleSheet.textContent = ` + .modifier-card-tiny.drag-sort-active { + background: transparent; + border: 2px dashed white; + opacity:0.2; + } + `; + document.head.appendChild(styleSheet); + + // observe for changes in tag list + const observer = new MutationObserver(function (mutations) { + // mutations.forEach(function (mutation) { + if (editorModifierTagsList.childNodes.length > 0) { + ModifierDragAndDrop(editorModifierTagsList) + } + // }) + }) + + observer.observe(editorModifierTagsList, { + childList: true + }) + + let current + function ModifierDragAndDrop(target) { + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + i.parentElement.draggable = true; + + i.parentElement.ondragstart = (e) => { + current = i + i.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].innerText = '' + i.parentElement.draggable = true + i.parentElement.classList.add('drag-sort-active') + for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) { + if (item.parentElement.parentElement.getElementsByClassName('modifier-card-overlay')[0] != current) { + item.parentElement.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].style.opacity = 0 + if(item.parentElement.getElementsByClassName('modifier-card-image').length > 0) { + item.parentElement.getElementsByClassName('modifier-card-image')[0].style.filter = 'none' + } + item.parentElement.parentElement.style.transform = 'none' + item.parentElement.parentElement.style.boxShadow = 'none' + } + item.innerText = '' + } + } + + i.ondragenter = (e) => { + e.preventDefault() + if (i != current) { + let currentPos = 0, droppedPos = 0; + for (let it = 0; it < overlays.length; it++) { + if (current == overlays[it]) { currentPos = it; } + if (i == overlays[it]) { droppedPos = it; } + } + + if (i.parentElement != current.parentElement) { + let currentPos = 0, droppedPos = 0 + for (let it = 0; it < overlays.length; it++) { + if (current == overlays[it]) { currentPos = it } + if (i == overlays[it]) { droppedPos = it } + } + if (currentPos < droppedPos) { + current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement.nextSibling).getElementsByClassName('modifier-card-overlay')[0] + } else { + current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement).getElementsByClassName('modifier-card-overlay')[0] + } + // update activeTags + const tag = activeTags.splice(currentPos, 1) + activeTags.splice(droppedPos, 0, tag[0]) + document.dispatchEvent(new Event('refreshImageModifiers')) + } + } + }; + + i.ondragover = (e) => { + e.preventDefault() + } + + i.parentElement.ondragend = (e) => { + i.parentElement.classList.remove('drag-sort-active') + for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) { + item.style.opacity = '' + item.innerText = '-' + } + } + }) + } +})() diff --git a/ui/plugins/ui/Modifiers-wheel.plugin.js b/ui/plugins/ui/Modifiers-wheel.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..322cca8f2229ab8f928970099cd2ae8f3e5fc4cc --- /dev/null +++ b/ui/plugins/ui/Modifiers-wheel.plugin.js @@ -0,0 +1,78 @@ +(function () { "use strict" + if (typeof editorModifierTagsList !== 'object') { + console.error('editorModifierTagsList missing...') + return + } + + // observe for changes in tag list + const observer = new MutationObserver(function (mutations) { + // mutations.forEach(function (mutation) { + if (editorModifierTagsList.childNodes.length > 0) { + ModifierMouseWheel(editorModifierTagsList) + } + // }) + }) + + observer.observe(editorModifierTagsList, { + childList: true + }) + + function ModifierMouseWheel(target) { + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + i.onwheel = (e) => { + if (e.ctrlKey == true) { + e.preventDefault() + + const delta = Math.sign(event.deltaY) + let s = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText + let t + // find the corresponding tag + for (let it = 0; it < overlays.length; it++) { + if (i == overlays[it]) { + t = activeTags[it].name + break + } + } + if (delta < 0) { + // wheel scrolling up + if (s.substring(0, 1) == '[' && s.substring(s.length-1) == ']') { + s = s.substring(1, s.length - 1) + t = t.substring(1, t.length - 1) + } + else + { + if (s.substring(0, 10) !== '('.repeat(10) && s.substring(s.length-10) !== ')'.repeat(10)) { + s = '(' + s + ')' + t = '(' + t + ')' + } + } + } + else{ + // wheel scrolling down + if (s.substring(0, 1) == '(' && s.substring(s.length-1) == ')') { + s = s.substring(1, s.length - 1) + t = t.substring(1, t.length - 1) + } + else + { + if (s.substring(0, 10) !== '['.repeat(10) && s.substring(s.length-10) !== ']'.repeat(10)) { + s = '[' + s + ']' + t = '[' + t + ']' + } + } + } + i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText = s + // update activeTags + for (let it = 0; it < overlays.length; it++) { + if (i == overlays[it]) { + activeTags[it].name = t + break + } + } + document.dispatchEvent(new Event('refreshImageModifiers')) + } + } + }) + } +})() diff --git a/ui/plugins/ui/SpecRunner.html b/ui/plugins/ui/SpecRunner.html new file mode 100644 index 0000000000000000000000000000000000000000..9a20e6d628217e43c43122eda51fead5ab6cd6cc --- /dev/null +++ b/ui/plugins/ui/SpecRunner.html @@ -0,0 +1,29 @@ + + + + + Jasmine Spec Runner v4.5.0 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/plugins/ui/custom-modifiers.plugin.js b/ui/plugins/ui/custom-modifiers.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..68599c63a204b33cfbcc771443102dca41be107c --- /dev/null +++ b/ui/plugins/ui/custom-modifiers.plugin.js @@ -0,0 +1,31 @@ +(function() { + PLUGINS['MODIFIERS_LOAD'].push({ + loader: function() { + let customModifiers = localStorage.getItem(CUSTOM_MODIFIERS_KEY, '') + customModifiersTextBox.value = customModifiers + + if (customModifiersGroupElement !== undefined) { + customModifiersGroupElement.remove() + } + + if (customModifiers && customModifiers.trim() !== '') { + customModifiers = customModifiers.split('\n') + customModifiers = customModifiers.filter(m => m.trim() !== '') + customModifiers = customModifiers.map(function(m) { + return { + "modifier": m + } + }) + + let customGroup = { + 'category': 'Custom Modifiers', + 'modifiers': customModifiers + } + + customModifiersGroupElement = createModifierGroup(customGroup, true) + + createCollapsibles(customModifiersGroupElement) + } + } + }) +})() diff --git a/ui/plugins/ui/jasmine/boot0.js b/ui/plugins/ui/jasmine/boot0.js new file mode 100644 index 0000000000000000000000000000000000000000..c773ba8eb7fd5f5ad36924084e7bc18b8e25fc30 --- /dev/null +++ b/ui/plugins/ui/jasmine/boot0.js @@ -0,0 +1,64 @@ +/* +Copyright (c) 2008-2022 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/** + This file starts the process of "booting" Jasmine. It initializes Jasmine, + makes its globals available, and creates the env. This file should be loaded + after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project + source files or spec files are loaded. + */ +(function() { + const jasmineRequire = window.jasmineRequire || require('./jasmine.js'); + + /** + * ## Require & Instantiate + * + * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. + */ + const jasmine = jasmineRequire.core(jasmineRequire), + global = jasmine.getGlobal(); + global.jasmine = jasmine; + + /** + * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. + */ + jasmineRequire.html(jasmine); + + /** + * Create the Jasmine environment. This is used to run all specs in a project. + */ + const env = jasmine.getEnv(); + + /** + * ## The Global Interface + * + * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. + */ + const jasmineInterface = jasmineRequire.interface(jasmine, env); + + /** + * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. + */ + for (const property in jasmineInterface) { + global[property] = jasmineInterface[property]; + } +})(); diff --git a/ui/plugins/ui/jasmine/boot1.js b/ui/plugins/ui/jasmine/boot1.js new file mode 100644 index 0000000000000000000000000000000000000000..5fe49e4189f5dd14a240ff4f1afdf35051f1eced --- /dev/null +++ b/ui/plugins/ui/jasmine/boot1.js @@ -0,0 +1,132 @@ +/* +Copyright (c) 2008-2022 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/** + This file finishes 'booting' Jasmine, performing all of the necessary + initialization before executing the loaded environment and all of a project's + specs. This file should be loaded after `boot0.js` but before any project + source files or spec files are loaded. Thus this file can also be used to + customize Jasmine for a project. + + If a project is using Jasmine via the standalone distribution, this file can + be customized directly. If you only wish to configure the Jasmine env, you + can load another file that calls `jasmine.getEnv().configure({...})` + after `boot0.js` is loaded and before this file is loaded. + */ + +(function() { + const env = jasmine.getEnv(); + + /** + * ## Runner Parameters + * + * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. + */ + + const queryString = new jasmine.QueryString({ + getWindowLocation: function() { + return window.location; + } + }); + + const filterSpecs = !!queryString.getParam('spec'); + + const config = { + stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'), + stopSpecOnExpectationFailure: queryString.getParam( + 'stopSpecOnExpectationFailure' + ), + hideDisabled: queryString.getParam('hideDisabled') + }; + + const random = queryString.getParam('random'); + + if (random !== undefined && random !== '') { + config.random = random; + } + + const seed = queryString.getParam('seed'); + if (seed) { + config.seed = seed; + } + + /** + * ## Reporters + * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). + */ + const htmlReporter = new jasmine.HtmlReporter({ + env: env, + navigateWithNewParam: function(key, value) { + return queryString.navigateWithNewParam(key, value); + }, + addToExistingQueryString: function(key, value) { + return queryString.fullStringWithNewParam(key, value); + }, + getContainer: function() { + return document.body; + }, + createElement: function() { + return document.createElement.apply(document, arguments); + }, + createTextNode: function() { + return document.createTextNode.apply(document, arguments); + }, + timer: new jasmine.Timer(), + filterSpecs: filterSpecs + }); + + /** + * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. + */ + env.addReporter(jsApiReporter); + env.addReporter(htmlReporter); + + /** + * Filter which specs will be run by matching the start of the full name against the `spec` query param. + */ + const specFilter = new jasmine.HtmlSpecFilter({ + filterString: function() { + return queryString.getParam('spec'); + } + }); + + config.specFilter = function(spec) { + return specFilter.matches(spec.getFullName()); + }; + + env.configure(config); + + /** + * ## Execution + * + * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. + */ + const currentWindowOnload = window.onload; + + window.onload = function() { + if (currentWindowOnload) { + currentWindowOnload(); + } + htmlReporter.initialize(); + env.execute(); + }; +})(); diff --git a/ui/plugins/ui/jasmine/jasmine-html.js b/ui/plugins/ui/jasmine/jasmine-html.js new file mode 100644 index 0000000000000000000000000000000000000000..2ebc6d0e5de6a50a4107f14e2b7faa5b62950792 --- /dev/null +++ b/ui/plugins/ui/jasmine/jasmine-html.js @@ -0,0 +1,964 @@ +/* +Copyright (c) 2008-2022 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +// eslint-disable-next-line no-var +var jasmineRequire = window.jasmineRequire || require('./jasmine.js'); + +jasmineRequire.html = function(j$) { + j$.ResultsNode = jasmineRequire.ResultsNode(); + j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); + j$.QueryString = jasmineRequire.QueryString(); + j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); +}; + +jasmineRequire.HtmlReporter = function(j$) { + function ResultsStateBuilder() { + this.topResults = new j$.ResultsNode({}, '', null); + this.currentParent = this.topResults; + this.specsExecuted = 0; + this.failureCount = 0; + this.pendingSpecCount = 0; + } + + ResultsStateBuilder.prototype.suiteStarted = function(result) { + this.currentParent.addChild(result, 'suite'); + this.currentParent = this.currentParent.last(); + }; + + ResultsStateBuilder.prototype.suiteDone = function(result) { + this.currentParent.updateResult(result); + if (this.currentParent !== this.topResults) { + this.currentParent = this.currentParent.parent; + } + + if (result.status === 'failed') { + this.failureCount++; + } + }; + + ResultsStateBuilder.prototype.specStarted = function(result) {}; + + ResultsStateBuilder.prototype.specDone = function(result) { + this.currentParent.addChild(result, 'spec'); + + if (result.status !== 'excluded') { + this.specsExecuted++; + } + + if (result.status === 'failed') { + this.failureCount++; + } + + if (result.status == 'pending') { + this.pendingSpecCount++; + } + }; + + ResultsStateBuilder.prototype.jasmineDone = function(result) { + if (result.failedExpectations) { + this.failureCount += result.failedExpectations.length; + } + }; + + function HtmlReporter(options) { + function config() { + return (options.env && options.env.configuration()) || {}; + } + + const getContainer = options.getContainer; + const createElement = options.createElement; + const createTextNode = options.createTextNode; + const navigateWithNewParam = options.navigateWithNewParam || function() {}; + const addToExistingQueryString = + options.addToExistingQueryString || defaultQueryString; + const filterSpecs = options.filterSpecs; + let htmlReporterMain; + let symbols; + const deprecationWarnings = []; + const failures = []; + + this.initialize = function() { + clearPrior(); + htmlReporterMain = createDom( + 'div', + { className: 'jasmine_html-reporter' }, + createDom( + 'div', + { className: 'jasmine-banner' }, + createDom('a', { + className: 'jasmine-title', + href: 'http://jasmine.github.io/', + target: '_blank' + }), + createDom('span', { className: 'jasmine-version' }, j$.version) + ), + createDom('ul', { className: 'jasmine-symbol-summary' }), + createDom('div', { className: 'jasmine-alert' }), + createDom( + 'div', + { className: 'jasmine-results' }, + createDom('div', { className: 'jasmine-failures' }) + ) + ); + getContainer().appendChild(htmlReporterMain); + }; + + let totalSpecsDefined; + this.jasmineStarted = function(options) { + totalSpecsDefined = options.totalSpecsDefined || 0; + }; + + const summary = createDom('div', { className: 'jasmine-summary' }); + + const stateBuilder = new ResultsStateBuilder(); + + this.suiteStarted = function(result) { + stateBuilder.suiteStarted(result); + }; + + this.suiteDone = function(result) { + stateBuilder.suiteDone(result); + + if (result.status === 'failed') { + failures.push(failureDom(result)); + } + addDeprecationWarnings(result, 'suite'); + }; + + this.specStarted = function(result) { + stateBuilder.specStarted(result); + }; + + this.specDone = function(result) { + stateBuilder.specDone(result); + + if (noExpectations(result)) { + const noSpecMsg = "Spec '" + result.fullName + "' has no expectations."; + if (result.status === 'failed') { + console.error(noSpecMsg); + } else { + console.warn(noSpecMsg); + } + } + + if (!symbols) { + symbols = find('.jasmine-symbol-summary'); + } + + symbols.appendChild( + createDom('li', { + className: this.displaySpecInCorrectFormat(result), + id: 'spec_' + result.id, + title: result.fullName + }) + ); + + if (result.status === 'failed') { + failures.push(failureDom(result)); + } + + addDeprecationWarnings(result, 'spec'); + }; + + this.displaySpecInCorrectFormat = function(result) { + return noExpectations(result) && result.status === 'passed' + ? 'jasmine-empty' + : this.resultStatus(result.status); + }; + + this.resultStatus = function(status) { + if (status === 'excluded') { + return config().hideDisabled + ? 'jasmine-excluded-no-display' + : 'jasmine-excluded'; + } + return 'jasmine-' + status; + }; + + this.jasmineDone = function(doneResult) { + stateBuilder.jasmineDone(doneResult); + const banner = find('.jasmine-banner'); + const alert = find('.jasmine-alert'); + const order = doneResult && doneResult.order; + + alert.appendChild( + createDom( + 'span', + { className: 'jasmine-duration' }, + 'finished in ' + doneResult.totalTime / 1000 + 's' + ) + ); + + banner.appendChild(optionsMenu(config())); + + if (stateBuilder.specsExecuted < totalSpecsDefined) { + const skippedMessage = + 'Ran ' + + stateBuilder.specsExecuted + + ' of ' + + totalSpecsDefined + + ' specs - run all'; + // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 + const skippedLink = + (window.location.pathname || '') + + addToExistingQueryString('spec', ''); + alert.appendChild( + createDom( + 'span', + { className: 'jasmine-bar jasmine-skipped' }, + createDom( + 'a', + { href: skippedLink, title: 'Run all specs' }, + skippedMessage + ) + ) + ); + } + let statusBarMessage = ''; + let statusBarClassName = 'jasmine-overall-result jasmine-bar '; + const globalFailures = + (doneResult && doneResult.failedExpectations) || []; + const failed = stateBuilder.failureCount + globalFailures.length > 0; + + if (totalSpecsDefined > 0 || failed) { + statusBarMessage += + pluralize('spec', stateBuilder.specsExecuted) + + ', ' + + pluralize('failure', stateBuilder.failureCount); + if (stateBuilder.pendingSpecCount) { + statusBarMessage += + ', ' + pluralize('pending spec', stateBuilder.pendingSpecCount); + } + } + + if (doneResult.overallStatus === 'passed') { + statusBarClassName += ' jasmine-passed '; + } else if (doneResult.overallStatus === 'incomplete') { + statusBarClassName += ' jasmine-incomplete '; + statusBarMessage = + 'Incomplete: ' + + doneResult.incompleteReason + + ', ' + + statusBarMessage; + } else { + statusBarClassName += ' jasmine-failed '; + } + + let seedBar; + if (order && order.random) { + seedBar = createDom( + 'span', + { className: 'jasmine-seed-bar' }, + ', randomized with seed ', + createDom( + 'a', + { + title: 'randomized with seed ' + order.seed, + href: seedHref(order.seed) + }, + order.seed + ) + ); + } + + alert.appendChild( + createDom( + 'span', + { className: statusBarClassName }, + statusBarMessage, + seedBar + ) + ); + + const errorBarClassName = 'jasmine-bar jasmine-errored'; + const afterAllMessagePrefix = 'AfterAll '; + + for (let i = 0; i < globalFailures.length; i++) { + alert.appendChild( + createDom( + 'span', + { className: errorBarClassName }, + globalFailureMessage(globalFailures[i]) + ) + ); + } + + function globalFailureMessage(failure) { + if (failure.globalErrorType === 'load') { + const prefix = 'Error during loading: ' + failure.message; + + if (failure.filename) { + return ( + prefix + ' in ' + failure.filename + ' line ' + failure.lineno + ); + } else { + return prefix; + } + } else if (failure.globalErrorType === 'afterAll') { + return afterAllMessagePrefix + failure.message; + } else { + return failure.message; + } + } + + addDeprecationWarnings(doneResult); + + for (let i = 0; i < deprecationWarnings.length; i++) { + const children = []; + let context; + + switch (deprecationWarnings[i].runnableType) { + case 'spec': + context = '(in spec: ' + deprecationWarnings[i].runnableName + ')'; + break; + case 'suite': + context = '(in suite: ' + deprecationWarnings[i].runnableName + ')'; + break; + default: + context = ''; + } + + deprecationWarnings[i].message.split('\n').forEach(function(line) { + children.push(line); + children.push(createDom('br')); + }); + + children[0] = 'DEPRECATION: ' + children[0]; + children.push(context); + + if (deprecationWarnings[i].stack) { + children.push(createExpander(deprecationWarnings[i].stack)); + } + + alert.appendChild( + createDom( + 'span', + { className: 'jasmine-bar jasmine-warning' }, + children + ) + ); + } + + const results = find('.jasmine-results'); + results.appendChild(summary); + + summaryList(stateBuilder.topResults, summary); + + if (failures.length) { + alert.appendChild( + createDom( + 'span', + { className: 'jasmine-menu jasmine-bar jasmine-spec-list' }, + createDom('span', {}, 'Spec List | '), + createDom( + 'a', + { className: 'jasmine-failures-menu', href: '#' }, + 'Failures' + ) + ) + ); + alert.appendChild( + createDom( + 'span', + { className: 'jasmine-menu jasmine-bar jasmine-failure-list' }, + createDom( + 'a', + { className: 'jasmine-spec-list-menu', href: '#' }, + 'Spec List' + ), + createDom('span', {}, ' | Failures ') + ) + ); + + find('.jasmine-failures-menu').onclick = function() { + setMenuModeTo('jasmine-failure-list'); + return false; + }; + find('.jasmine-spec-list-menu').onclick = function() { + setMenuModeTo('jasmine-spec-list'); + return false; + }; + + setMenuModeTo('jasmine-failure-list'); + + const failureNode = find('.jasmine-failures'); + for (let i = 0; i < failures.length; i++) { + failureNode.appendChild(failures[i]); + } + } + }; + + return this; + + function failureDom(result) { + const failure = createDom( + 'div', + { className: 'jasmine-spec-detail jasmine-failed' }, + failureDescription(result, stateBuilder.currentParent), + createDom('div', { className: 'jasmine-messages' }) + ); + const messages = failure.childNodes[1]; + + for (let i = 0; i < result.failedExpectations.length; i++) { + const expectation = result.failedExpectations[i]; + messages.appendChild( + createDom( + 'div', + { className: 'jasmine-result-message' }, + expectation.message + ) + ); + messages.appendChild( + createDom( + 'div', + { className: 'jasmine-stack-trace' }, + expectation.stack + ) + ); + } + + if (result.failedExpectations.length === 0) { + messages.appendChild( + createDom( + 'div', + { className: 'jasmine-result-message' }, + 'Spec has no expectations' + ) + ); + } + + if (result.debugLogs) { + messages.appendChild(debugLogTable(result.debugLogs)); + } + + return failure; + } + + function debugLogTable(debugLogs) { + const tbody = createDom('tbody'); + + debugLogs.forEach(function(entry) { + tbody.appendChild( + createDom( + 'tr', + {}, + createDom('td', {}, entry.timestamp.toString()), + createDom('td', {}, entry.message) + ) + ); + }); + + return createDom( + 'div', + { className: 'jasmine-debug-log' }, + createDom( + 'div', + { className: 'jasmine-debug-log-header' }, + 'Debug logs' + ), + createDom( + 'table', + {}, + createDom( + 'thead', + {}, + createDom( + 'tr', + {}, + createDom('th', {}, 'Time (ms)'), + createDom('th', {}, 'Message') + ) + ), + tbody + ) + ); + } + + function summaryList(resultsTree, domParent) { + let specListNode; + for (let i = 0; i < resultsTree.children.length; i++) { + const resultNode = resultsTree.children[i]; + if (filterSpecs && !hasActiveSpec(resultNode)) { + continue; + } + if (resultNode.type === 'suite') { + const suiteListNode = createDom( + 'ul', + { className: 'jasmine-suite', id: 'suite-' + resultNode.result.id }, + createDom( + 'li', + { + className: + 'jasmine-suite-detail jasmine-' + resultNode.result.status + }, + createDom( + 'a', + { href: specHref(resultNode.result) }, + resultNode.result.description + ) + ) + ); + + summaryList(resultNode, suiteListNode); + domParent.appendChild(suiteListNode); + } + if (resultNode.type === 'spec') { + if (domParent.getAttribute('class') !== 'jasmine-specs') { + specListNode = createDom('ul', { className: 'jasmine-specs' }); + domParent.appendChild(specListNode); + } + let specDescription = resultNode.result.description; + if (noExpectations(resultNode.result)) { + specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; + } + if ( + resultNode.result.status === 'pending' && + resultNode.result.pendingReason !== '' + ) { + specDescription = + specDescription + + ' PENDING WITH MESSAGE: ' + + resultNode.result.pendingReason; + } + specListNode.appendChild( + createDom( + 'li', + { + className: 'jasmine-' + resultNode.result.status, + id: 'spec-' + resultNode.result.id + }, + createDom( + 'a', + { href: specHref(resultNode.result) }, + specDescription + ) + ) + ); + } + } + } + + function optionsMenu(config) { + const optionsMenuDom = createDom( + 'div', + { className: 'jasmine-run-options' }, + createDom('span', { className: 'jasmine-trigger' }, 'Options'), + createDom( + 'div', + { className: 'jasmine-payload' }, + createDom( + 'div', + { className: 'jasmine-stop-on-failure' }, + createDom('input', { + className: 'jasmine-fail-fast', + id: 'jasmine-fail-fast', + type: 'checkbox' + }), + createDom( + 'label', + { className: 'jasmine-label', for: 'jasmine-fail-fast' }, + 'stop execution on spec failure' + ) + ), + createDom( + 'div', + { className: 'jasmine-throw-failures' }, + createDom('input', { + className: 'jasmine-throw', + id: 'jasmine-throw-failures', + type: 'checkbox' + }), + createDom( + 'label', + { className: 'jasmine-label', for: 'jasmine-throw-failures' }, + 'stop spec on expectation failure' + ) + ), + createDom( + 'div', + { className: 'jasmine-random-order' }, + createDom('input', { + className: 'jasmine-random', + id: 'jasmine-random-order', + type: 'checkbox' + }), + createDom( + 'label', + { className: 'jasmine-label', for: 'jasmine-random-order' }, + 'run tests in random order' + ) + ), + createDom( + 'div', + { className: 'jasmine-hide-disabled' }, + createDom('input', { + className: 'jasmine-disabled', + id: 'jasmine-hide-disabled', + type: 'checkbox' + }), + createDom( + 'label', + { className: 'jasmine-label', for: 'jasmine-hide-disabled' }, + 'hide disabled tests' + ) + ) + ) + ); + + const failFastCheckbox = optionsMenuDom.querySelector( + '#jasmine-fail-fast' + ); + failFastCheckbox.checked = config.stopOnSpecFailure; + failFastCheckbox.onclick = function() { + navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure); + }; + + const throwCheckbox = optionsMenuDom.querySelector( + '#jasmine-throw-failures' + ); + throwCheckbox.checked = config.stopSpecOnExpectationFailure; + throwCheckbox.onclick = function() { + navigateWithNewParam( + 'stopSpecOnExpectationFailure', + !config.stopSpecOnExpectationFailure + ); + }; + + const randomCheckbox = optionsMenuDom.querySelector( + '#jasmine-random-order' + ); + randomCheckbox.checked = config.random; + randomCheckbox.onclick = function() { + navigateWithNewParam('random', !config.random); + }; + + const hideDisabled = optionsMenuDom.querySelector( + '#jasmine-hide-disabled' + ); + hideDisabled.checked = config.hideDisabled; + hideDisabled.onclick = function() { + navigateWithNewParam('hideDisabled', !config.hideDisabled); + }; + + const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'), + optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'), + isOpen = /\bjasmine-open\b/; + + optionsTrigger.onclick = function() { + if (isOpen.test(optionsPayload.className)) { + optionsPayload.className = optionsPayload.className.replace( + isOpen, + '' + ); + } else { + optionsPayload.className += ' jasmine-open'; + } + }; + + return optionsMenuDom; + } + + function failureDescription(result, suite) { + const wrapper = createDom( + 'div', + { className: 'jasmine-description' }, + createDom( + 'a', + { title: result.description, href: specHref(result) }, + result.description + ) + ); + let suiteLink; + + while (suite && suite.parent) { + wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild); + suiteLink = createDom( + 'a', + { href: suiteHref(suite) }, + suite.result.description + ); + wrapper.insertBefore(suiteLink, wrapper.firstChild); + + suite = suite.parent; + } + + return wrapper; + } + + function suiteHref(suite) { + const els = []; + + while (suite && suite.parent) { + els.unshift(suite.result.description); + suite = suite.parent; + } + + // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 + return ( + (window.location.pathname || '') + + addToExistingQueryString('spec', els.join(' ')) + ); + } + + function addDeprecationWarnings(result, runnableType) { + if (result && result.deprecationWarnings) { + for (let i = 0; i < result.deprecationWarnings.length; i++) { + const warning = result.deprecationWarnings[i].message; + deprecationWarnings.push({ + message: warning, + stack: result.deprecationWarnings[i].stack, + runnableName: result.fullName, + runnableType: runnableType + }); + } + } + } + + function createExpander(stackTrace) { + const expandLink = createDom('a', { href: '#' }, 'Show stack trace'); + const root = createDom( + 'div', + { className: 'jasmine-expander' }, + expandLink, + createDom( + 'div', + { className: 'jasmine-expander-contents jasmine-stack-trace' }, + stackTrace + ) + ); + + expandLink.addEventListener('click', function(e) { + e.preventDefault(); + + if (root.classList.contains('jasmine-expanded')) { + root.classList.remove('jasmine-expanded'); + expandLink.textContent = 'Show stack trace'; + } else { + root.classList.add('jasmine-expanded'); + expandLink.textContent = 'Hide stack trace'; + } + }); + + return root; + } + + function find(selector) { + return getContainer().querySelector('.jasmine_html-reporter ' + selector); + } + + function clearPrior() { + const oldReporter = find(''); + + if (oldReporter) { + getContainer().removeChild(oldReporter); + } + } + + function createDom(type, attrs, childrenArrayOrVarArgs) { + const el = createElement(type); + let children; + + if (j$.isArray_(childrenArrayOrVarArgs)) { + children = childrenArrayOrVarArgs; + } else { + children = []; + + for (let i = 2; i < arguments.length; i++) { + children.push(arguments[i]); + } + } + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + if (typeof child === 'string') { + el.appendChild(createTextNode(child)); + } else { + if (child) { + el.appendChild(child); + } + } + } + + for (const attr in attrs) { + if (attr == 'className') { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; + } + + function pluralize(singular, count) { + const word = count == 1 ? singular : singular + 's'; + + return '' + count + ' ' + word; + } + + function specHref(result) { + // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 + return ( + (window.location.pathname || '') + + addToExistingQueryString('spec', result.fullName) + ); + } + + function seedHref(seed) { + // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 + return ( + (window.location.pathname || '') + + addToExistingQueryString('seed', seed) + ); + } + + function defaultQueryString(key, value) { + return '?' + key + '=' + value; + } + + function setMenuModeTo(mode) { + htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); + } + + function noExpectations(result) { + const allExpectations = + result.failedExpectations.length + result.passedExpectations.length; + + return ( + allExpectations === 0 && + (result.status === 'passed' || result.status === 'failed') + ); + } + + function hasActiveSpec(resultNode) { + if (resultNode.type == 'spec' && resultNode.result.status != 'excluded') { + return true; + } + + if (resultNode.type == 'suite') { + for (let i = 0, j = resultNode.children.length; i < j; i++) { + if (hasActiveSpec(resultNode.children[i])) { + return true; + } + } + } + } + } + + return HtmlReporter; +}; + +jasmineRequire.HtmlSpecFilter = function() { + function HtmlSpecFilter(options) { + const filterString = + options && + options.filterString() && + options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + const filterPattern = new RegExp(filterString); + + this.matches = function(specName) { + return filterPattern.test(specName); + }; + } + + return HtmlSpecFilter; +}; + +jasmineRequire.ResultsNode = function() { + function ResultsNode(result, type, parent) { + this.result = result; + this.type = type; + this.parent = parent; + + this.children = []; + + this.addChild = function(result, type) { + this.children.push(new ResultsNode(result, type, this)); + }; + + this.last = function() { + return this.children[this.children.length - 1]; + }; + + this.updateResult = function(result) { + this.result = result; + }; + } + + return ResultsNode; +}; + +jasmineRequire.QueryString = function() { + function QueryString(options) { + this.navigateWithNewParam = function(key, value) { + options.getWindowLocation().search = this.fullStringWithNewParam( + key, + value + ); + }; + + this.fullStringWithNewParam = function(key, value) { + const paramMap = queryStringToParamMap(); + paramMap[key] = value; + return toQueryString(paramMap); + }; + + this.getParam = function(key) { + return queryStringToParamMap()[key]; + }; + + return this; + + function toQueryString(paramMap) { + const qStrPairs = []; + for (const prop in paramMap) { + qStrPairs.push( + encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop]) + ); + } + return '?' + qStrPairs.join('&'); + } + + function queryStringToParamMap() { + const paramStr = options.getWindowLocation().search.substring(1); + let params = []; + const paramMap = {}; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + for (let i = 0; i < params.length; i++) { + const p = params[i].split('='); + let value = decodeURIComponent(p[1]); + if (value === 'true' || value === 'false') { + value = JSON.parse(value); + } + paramMap[decodeURIComponent(p[0])] = value; + } + } + + return paramMap; + } + } + + return QueryString; +}; diff --git a/ui/plugins/ui/jasmine/jasmine.css b/ui/plugins/ui/jasmine/jasmine.css new file mode 100644 index 0000000000000000000000000000000000000000..f9c5a8a4b4613a5b797a5a276511e059da631012 --- /dev/null +++ b/ui/plugins/ui/jasmine/jasmine.css @@ -0,0 +1,301 @@ +@charset "UTF-8"; +body { + overflow-y: scroll; + background: black; +} + +.jasmine_html-reporter { + width: 100%; + background-color: rgb(32, 33, 36); + padding: 5px; + margin: -8px; + font-size: 11px; + font-family: Monaco, "Lucida Console", monospace; + line-height: 14px; + color: #eee; +} +.jasmine_html-reporter a { + text-decoration: none; +} +.jasmine_html-reporter a:hover { + text-decoration: underline; +} +.jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { + margin: 0; + line-height: 14px; +} +.jasmine_html-reporter .jasmine-banner, +.jasmine_html-reporter .jasmine-symbol-summary, +.jasmine_html-reporter .jasmine-summary, +.jasmine_html-reporter .jasmine-result-message, +.jasmine_html-reporter .jasmine-spec .jasmine-description, +.jasmine_html-reporter .jasmine-spec-detail .jasmine-description, +.jasmine_html-reporter .jasmine-alert .jasmine-bar, +.jasmine_html-reporter .jasmine-stack-trace { + padding-left: 9px; + padding-right: 9px; +} +.jasmine_html-reporter .jasmine-banner { + position: relative; +} +.jasmine_html-reporter .jasmine-banner .jasmine-title { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==") no-repeat; + background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=") left no-repeat, url("/media/images/favicon-32x32.png") right no-repeat, none; + background-size: contain; + display: block; + float: left; + width: 120px; + height: 25px; +} +.jasmine_html-reporter .jasmine-banner .jasmine-version { + margin-left: 14px; + position: relative; + top: 6px; +} +.jasmine_html-reporter #jasmine_content { + position: fixed; + right: 100%; +} +.jasmine_html-reporter .jasmine-version { + color: #aaa; +} +.jasmine_html-reporter .jasmine-banner { + margin-top: 14px; +} +.jasmine_html-reporter .jasmine-duration { + color: #fff; + float: right; + line-height: 28px; + padding-right: 9px; +} +.jasmine_html-reporter .jasmine-symbol-summary { + overflow: hidden; + margin: 14px 0; +} +.jasmine_html-reporter .jasmine-symbol-summary li { + display: inline-block; + height: 10px; + width: 14px; + font-size: 16px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed { + font-size: 14px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { + color: #009e08; + content: "•"; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed { + line-height: 9px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { + color: #5000b8; + content: "×"; + font-weight: bold; + margin-left: -1px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded { + font-size: 14px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded:before { + color: #340077; + content: "•"; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded-no-display { + font-size: 14px; + display: none; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending { + line-height: 17px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before { + color: #ba9d37; + content: "*"; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty { + font-size: 14px; +} +.jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { + color: #ba9d37; + content: "•"; +} +.jasmine_html-reporter .jasmine-run-options { + float: right; + margin-right: 5px; + border: 1px solid #8a4182; + color: #8a4182; + position: relative; + line-height: 20px; +} +.jasmine_html-reporter .jasmine-run-options .jasmine-trigger { + cursor: pointer; + padding: 8px 16px; +} +.jasmine_html-reporter .jasmine-run-options .jasmine-payload { + position: absolute; + display: none; + right: -1px; + border: 1px solid #8a4182; + background-color: #eee; + white-space: nowrap; + padding: 4px 8px; +} +.jasmine_html-reporter .jasmine-run-options .jasmine-payload.jasmine-open { + display: block; +} +.jasmine_html-reporter .jasmine-bar { + line-height: 28px; + font-size: 14px; + display: block; + color: #eee; +} +.jasmine_html-reporter .jasmine-bar.jasmine-failed, .jasmine_html-reporter .jasmine-bar.jasmine-errored { + background-color: #5000b8; + border-bottom: 1px solid #eee; +} +.jasmine_html-reporter .jasmine-bar.jasmine-passed { + background-color: #009e08; +} +.jasmine_html-reporter .jasmine-bar.jasmine-incomplete { + background-color: #340077; +} +.jasmine_html-reporter .jasmine-bar.jasmine-skipped { + background-color: #340077; +} +.jasmine_html-reporter .jasmine-bar.jasmine-warning { + margin-top: 14px; + margin-bottom: 14px; + background-color: #ba9d37; + color: #eee; +} +.jasmine_html-reporter .jasmine-bar.jasmine-menu { + background-color: #2b0064; + color: #aaa; +} +.jasmine_html-reporter .jasmine-bar.jasmine-menu a { + color: #eee; +} +.jasmine_html-reporter .jasmine-bar a { + color: white; +} +.jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list, +.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures { + display: none; +} +.jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list, +.jasmine_html-reporter.jasmine-failure-list .jasmine-summary { + display: none; +} +.jasmine_html-reporter .jasmine-results { + margin-top: 14px; +} +.jasmine_html-reporter .jasmine-summary { + margin-top: 14px; +} +.jasmine_html-reporter .jasmine-summary ul { + list-style-type: none; + margin-left: 14px; + padding-top: 0; + padding-left: 0; +} +.jasmine_html-reporter .jasmine-summary ul.jasmine-suite { + margin-top: 7px; + margin-bottom: 7px; +} +.jasmine_html-reporter .jasmine-summary li.jasmine-passed a { + color: #009e08; +} +.jasmine_html-reporter .jasmine-summary li.jasmine-failed a { + color: #5000b8; +} +.jasmine_html-reporter .jasmine-summary li.jasmine-empty a { + color: #ba9d37; +} +.jasmine_html-reporter .jasmine-summary li.jasmine-pending a { + color: #ba9d37; +} +.jasmine_html-reporter .jasmine-summary li.jasmine-excluded a { + color: #5600c7; +} +.jasmine_html-reporter .jasmine-specs li.jasmine-passed a:before { + content: "• "; +} +.jasmine_html-reporter .jasmine-specs li.jasmine-failed a:before { + content: "× "; +} +.jasmine_html-reporter .jasmine-specs li.jasmine-empty a:before { + content: "* "; +} +.jasmine_html-reporter .jasmine-specs li.jasmine-pending a:before { + content: "• "; +} +.jasmine_html-reporter .jasmine-specs li.jasmine-excluded a:before { + content: "• "; +} +.jasmine_html-reporter .jasmine-description + .jasmine-suite { + margin-top: 0; +} +.jasmine_html-reporter .jasmine-suite { + margin-top: 14px; +} +.jasmine_html-reporter .jasmine-suite a { + color: #eee; +} +.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail { + margin-bottom: 28px; +} +.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description { + background-color: #5000b8; + color: white; +} +.jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description a { + color: white; +} +.jasmine_html-reporter .jasmine-result-message { + padding-top: 14px; + color: #eee; + white-space: pre-wrap; +} +.jasmine_html-reporter .jasmine-result-message span.jasmine-result { + display: block; +} +.jasmine_html-reporter .jasmine-stack-trace { + margin: 5px 0 0 0; + max-height: 224px; + overflow: auto; + line-height: 18px; + color: #1f0047; + border: 1px solid #ddd; + background: white; + white-space: pre; +} +.jasmine_html-reporter .jasmine-expander a { + display: block; + margin-left: 14px; + color: blue; + text-decoration: underline; +} +.jasmine_html-reporter .jasmine-expander-contents { + display: none; +} +.jasmine_html-reporter .jasmine-expanded { + padding-bottom: 10px; +} +.jasmine_html-reporter .jasmine-expanded .jasmine-expander-contents { + display: block; + margin-left: 14px; + padding: 5px; +} +.jasmine_html-reporter .jasmine-debug-log { + margin: 5px 0 0 0; + padding: 5px; + color: #1f0047; + border: 1px solid #ddd; + background: white; +} +.jasmine_html-reporter .jasmine-debug-log table { + border-spacing: 0; +} +.jasmine_html-reporter .jasmine-debug-log table, .jasmine_html-reporter .jasmine-debug-log th, .jasmine_html-reporter .jasmine-debug-log td { + border: 1px solid #ddd; +} \ No newline at end of file diff --git a/ui/plugins/ui/jasmine/jasmine.js b/ui/plugins/ui/jasmine/jasmine.js new file mode 100644 index 0000000000000000000000000000000000000000..9160c7532323a034e4add30638a9ba807a50b2d6 --- /dev/null +++ b/ui/plugins/ui/jasmine/jasmine.js @@ -0,0 +1,10468 @@ +/* +Copyright (c) 2008-2022 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +// eslint-disable-next-line no-unused-vars,no-var +var getJasmineRequireObj = (function(jasmineGlobal) { + let jasmineRequire; + + if ( + typeof module !== 'undefined' && + module.exports && + typeof exports !== 'undefined' + ) { + if (typeof global !== 'undefined') { + jasmineGlobal = global; + } else { + jasmineGlobal = {}; + } + jasmineRequire = exports; + } else { + if ( + typeof window !== 'undefined' && + typeof window.toString === 'function' && + window.toString() === '[object GjsGlobal]' + ) { + jasmineGlobal = window; + } + jasmineRequire = jasmineGlobal.jasmineRequire = {}; + } + + function getJasmineRequire() { + return jasmineRequire; + } + + getJasmineRequire().core = function(jRequire) { + const j$ = {}; + + jRequire.base(j$, jasmineGlobal); + j$.util = jRequire.util(j$); + j$.errors = jRequire.errors(); + j$.formatErrorMsg = jRequire.formatErrorMsg(); + j$.Any = jRequire.Any(j$); + j$.Anything = jRequire.Anything(j$); + j$.CallTracker = jRequire.CallTracker(j$); + j$.MockDate = jRequire.MockDate(j$); + j$.getClearStack = jRequire.clearStack(j$); + j$.Clock = jRequire.Clock(); + j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); + j$.Deprecator = jRequire.Deprecator(j$); + j$.Env = jRequire.Env(j$); + j$.StackTrace = jRequire.StackTrace(j$); + j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); + j$.ExpectationFilterChain = jRequire.ExpectationFilterChain(); + j$.Expector = jRequire.Expector(j$); + j$.Expectation = jRequire.Expectation(j$); + j$.buildExpectationResult = jRequire.buildExpectationResult(j$); + j$.JsApiReporter = jRequire.JsApiReporter(j$); + j$.makePrettyPrinter = jRequire.makePrettyPrinter(j$); + j$.basicPrettyPrinter_ = j$.makePrettyPrinter(); + j$.MatchersUtil = jRequire.MatchersUtil(j$); + j$.ObjectContaining = jRequire.ObjectContaining(j$); + j$.ArrayContaining = jRequire.ArrayContaining(j$); + j$.ArrayWithExactContents = jRequire.ArrayWithExactContents(j$); + j$.MapContaining = jRequire.MapContaining(j$); + j$.SetContaining = jRequire.SetContaining(j$); + j$.QueueRunner = jRequire.QueueRunner(j$); + j$.NeverSkipPolicy = jRequire.NeverSkipPolicy(j$); + j$.SkipAfterBeforeAllErrorPolicy = jRequire.SkipAfterBeforeAllErrorPolicy( + j$ + ); + j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy( + j$ + ); + j$.ReportDispatcher = jRequire.ReportDispatcher(j$); + j$.RunableResources = jRequire.RunableResources(j$); + j$.Runner = jRequire.Runner(j$); + j$.Spec = jRequire.Spec(j$); + j$.Spy = jRequire.Spy(j$); + j$.SpyFactory = jRequire.SpyFactory(j$); + j$.SpyRegistry = jRequire.SpyRegistry(j$); + j$.SpyStrategy = jRequire.SpyStrategy(j$); + j$.StringMatching = jRequire.StringMatching(j$); + j$.StringContaining = jRequire.StringContaining(j$); + j$.UserContext = jRequire.UserContext(j$); + j$.Suite = jRequire.Suite(j$); + j$.SuiteBuilder = jRequire.SuiteBuilder(j$); + j$.Timer = jRequire.Timer(); + j$.TreeProcessor = jRequire.TreeProcessor(); + j$.version = jRequire.version(); + j$.Order = jRequire.Order(); + j$.DiffBuilder = jRequire.DiffBuilder(j$); + j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$); + j$.ObjectPath = jRequire.ObjectPath(j$); + j$.MismatchTree = jRequire.MismatchTree(j$); + j$.GlobalErrors = jRequire.GlobalErrors(j$); + + j$.Truthy = jRequire.Truthy(j$); + j$.Falsy = jRequire.Falsy(j$); + j$.Empty = jRequire.Empty(j$); + j$.NotEmpty = jRequire.NotEmpty(j$); + j$.Is = jRequire.Is(j$); + + j$.matchers = jRequire.requireMatchers(jRequire, j$); + j$.asyncMatchers = jRequire.requireAsyncMatchers(jRequire, j$); + + return j$; + }; + + return getJasmineRequire; +})(this); + +getJasmineRequireObj().requireMatchers = function(jRequire, j$) { + const availableMatchers = [ + 'nothing', + 'toBe', + 'toBeCloseTo', + 'toBeDefined', + 'toBeInstanceOf', + 'toBeFalse', + 'toBeFalsy', + 'toBeGreaterThan', + 'toBeGreaterThanOrEqual', + 'toBeLessThan', + 'toBeLessThanOrEqual', + 'toBeNaN', + 'toBeNegativeInfinity', + 'toBeNull', + 'toBePositiveInfinity', + 'toBeTrue', + 'toBeTruthy', + 'toBeUndefined', + 'toContain', + 'toEqual', + 'toHaveSize', + 'toHaveBeenCalled', + 'toHaveBeenCalledBefore', + 'toHaveBeenCalledOnceWith', + 'toHaveBeenCalledTimes', + 'toHaveBeenCalledWith', + 'toHaveClass', + 'toHaveSpyInteractions', + 'toMatch', + 'toThrow', + 'toThrowError', + 'toThrowMatching' + ], + matchers = {}; + + for (const name of availableMatchers) { + matchers[name] = jRequire[name](j$); + } + + return matchers; +}; + +getJasmineRequireObj().base = function(j$, jasmineGlobal) { + /** + * Maximum object depth the pretty printer will print to. + * Set this to a lower value to speed up pretty printing if you have large objects. + * @name jasmine.MAX_PRETTY_PRINT_DEPTH + * @default 8 + * @since 1.3.0 + */ + j$.MAX_PRETTY_PRINT_DEPTH = 8; + /** + * Maximum number of array elements to display when pretty printing objects. + * This will also limit the number of keys and values displayed for an object. + * Elements past this number will be ellipised. + * @name jasmine.MAX_PRETTY_PRINT_ARRAY_LENGTH + * @default 50 + * @since 2.7.0 + */ + j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 50; + /** + * Maximum number of characters to display when pretty printing objects. + * Characters past this number will be ellipised. + * @name jasmine.MAX_PRETTY_PRINT_CHARS + * @default 100 + * @since 2.9.0 + */ + j$.MAX_PRETTY_PRINT_CHARS = 1000; + /** + * Default number of milliseconds Jasmine will wait for an asynchronous spec, + * before, or after function to complete. This can be overridden on a case by + * case basis by passing a time limit as the third argument to {@link it}, + * {@link beforeEach}, {@link afterEach}, {@link beforeAll}, or + * {@link afterAll}. The value must be no greater than the largest number of + * milliseconds supported by setTimeout, which is usually 2147483647. + * + * While debugging tests, you may want to set this to a large number (or pass + * a large number to one of the functions mentioned above) so that Jasmine + * does not move on to after functions or the next spec while you're debugging. + * @name jasmine.DEFAULT_TIMEOUT_INTERVAL + * @default 5000 + * @since 1.3.0 + */ + let DEFAULT_TIMEOUT_INTERVAL = 5000; + Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', { + get: function() { + return DEFAULT_TIMEOUT_INTERVAL; + }, + set: function(newValue) { + j$.util.validateTimeout(newValue, 'jasmine.DEFAULT_TIMEOUT_INTERVAL'); + DEFAULT_TIMEOUT_INTERVAL = newValue; + } + }); + + j$.getGlobal = function() { + return jasmineGlobal; + }; + + /** + * Get the currently booted Jasmine Environment. + * + * @name jasmine.getEnv + * @since 1.3.0 + * @function + * @return {Env} + */ + j$.getEnv = function(options) { + const env = (j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options)); + //jasmine. singletons in here (setTimeout blah blah). + return env; + }; + + j$.isArray_ = function(value) { + return j$.isA_('Array', value); + }; + + j$.isObject_ = function(value) { + return ( + !j$.util.isUndefined(value) && value !== null && j$.isA_('Object', value) + ); + }; + + j$.isString_ = function(value) { + return j$.isA_('String', value); + }; + + j$.isNumber_ = function(value) { + return j$.isA_('Number', value); + }; + + j$.isFunction_ = function(value) { + return j$.isA_('Function', value); + }; + + j$.isAsyncFunction_ = function(value) { + return j$.isA_('AsyncFunction', value); + }; + + j$.isGeneratorFunction_ = function(value) { + return j$.isA_('GeneratorFunction', value); + }; + + j$.isTypedArray_ = function(value) { + return ( + j$.isA_('Float32Array', value) || + j$.isA_('Float64Array', value) || + j$.isA_('Int16Array', value) || + j$.isA_('Int32Array', value) || + j$.isA_('Int8Array', value) || + j$.isA_('Uint16Array', value) || + j$.isA_('Uint32Array', value) || + j$.isA_('Uint8Array', value) || + j$.isA_('Uint8ClampedArray', value) + ); + }; + + j$.isA_ = function(typeName, value) { + return j$.getType_(value) === '[object ' + typeName + ']'; + }; + + j$.isError_ = function(value) { + if (!value) { + return false; + } + + if (value instanceof Error) { + return true; + } + + return typeof value.stack === 'string' && typeof value.message === 'string'; + }; + + j$.isAsymmetricEqualityTester_ = function(obj) { + return obj ? j$.isA_('Function', obj.asymmetricMatch) : false; + }; + + j$.getType_ = function(value) { + return Object.prototype.toString.apply(value); + }; + + j$.isDomNode = function(obj) { + // Node is a function, because constructors + return typeof jasmineGlobal.Node !== 'undefined' + ? obj instanceof jasmineGlobal.Node + : obj !== null && + typeof obj === 'object' && + typeof obj.nodeType === 'number' && + typeof obj.nodeName === 'string'; + // return obj.nodeType > 0; + }; + + j$.isMap = function(obj) { + return ( + obj !== null && + typeof obj !== 'undefined' && + obj.constructor === jasmineGlobal.Map + ); + }; + + j$.isSet = function(obj) { + return ( + obj !== null && + typeof obj !== 'undefined' && + obj.constructor === jasmineGlobal.Set + ); + }; + + j$.isWeakMap = function(obj) { + return ( + obj !== null && + typeof obj !== 'undefined' && + obj.constructor === jasmineGlobal.WeakMap + ); + }; + + j$.isURL = function(obj) { + return ( + obj !== null && + typeof obj !== 'undefined' && + obj.constructor === jasmineGlobal.URL + ); + }; + + j$.isIterable_ = function(value) { + return value && !!value[Symbol.iterator]; + }; + + j$.isDataView = function(obj) { + return ( + obj !== null && + typeof obj !== 'undefined' && + obj.constructor === jasmineGlobal.DataView + ); + }; + + j$.isPromise = function(obj) { + return !!obj && obj.constructor === jasmineGlobal.Promise; + }; + + j$.isPromiseLike = function(obj) { + return !!obj && j$.isFunction_(obj.then); + }; + + j$.fnNameFor = function(func) { + if (func.name) { + return func.name; + } + + const matches = + func.toString().match(/^\s*function\s*(\w+)\s*\(/) || + func.toString().match(/^\s*\[object\s*(\w+)Constructor\]/); + + return matches ? matches[1] : ''; + }; + + j$.isPending_ = function(promise) { + const sentinel = {}; + return Promise.race([promise, Promise.resolve(sentinel)]).then( + function(result) { + return result === sentinel; + }, + function() { + return false; + } + ); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is an instance of the specified class/constructor. + * @name jasmine.any + * @since 1.3.0 + * @function + * @param {Constructor} clazz - The constructor to check against. + */ + j$.any = function(clazz) { + return new j$.Any(clazz); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is not `null` and not `undefined`. + * @name jasmine.anything + * @since 2.2.0 + * @function + */ + j$.anything = function() { + return new j$.Anything(); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is `true` or anything truthy. + * @name jasmine.truthy + * @since 3.1.0 + * @function + */ + j$.truthy = function() { + return new j$.Truthy(); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is `null`, `undefined`, `0`, `false` or anything falsey. + * @name jasmine.falsy + * @since 3.1.0 + * @function + */ + j$.falsy = function() { + return new j$.Falsy(); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is empty. + * @name jasmine.empty + * @since 3.1.0 + * @function + */ + j$.empty = function() { + return new j$.Empty(); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} + * that passes if the actual value is the same as the sample as determined + * by the `===` operator. + * @name jasmine.is + * @function + * @param {Object} sample - The value to compare the actual to. + */ + j$.is = function(sample) { + return new j$.Is(sample); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared is not empty. + * @name jasmine.notEmpty + * @since 3.1.0 + * @function + */ + j$.notEmpty = function() { + return new j$.NotEmpty(); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value being compared contains at least the keys and values. + * @name jasmine.objectContaining + * @since 1.3.0 + * @function + * @param {Object} sample - The subset of properties that _must_ be in the actual. + */ + j$.objectContaining = function(sample) { + return new j$.ObjectContaining(sample); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value is a `String` that matches the `RegExp` or `String`. + * @name jasmine.stringMatching + * @since 2.2.0 + * @function + * @param {RegExp|String} expected + */ + j$.stringMatching = function(expected) { + return new j$.StringMatching(expected); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value is a `String` that contains the specified `String`. + * @name jasmine.stringContaining + * @since 3.10.0 + * @function + * @param {String} expected + */ + j$.stringContaining = function(expected) { + return new j$.StringContaining(expected); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value is an `Array` that contains at least the elements in the sample. + * @name jasmine.arrayContaining + * @since 2.2.0 + * @function + * @param {Array} sample + */ + j$.arrayContaining = function(sample) { + return new j$.ArrayContaining(sample); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if the actual value is an `Array` that contains all of the elements in the sample in any order. + * @name jasmine.arrayWithExactContents + * @since 2.8.0 + * @function + * @param {Array} sample + */ + j$.arrayWithExactContents = function(sample) { + return new j$.ArrayWithExactContents(sample); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if every key/value pair in the sample passes the deep equality comparison + * with at least one key/value pair in the actual value being compared + * @name jasmine.mapContaining + * @since 3.5.0 + * @function + * @param {Map} sample - The subset of items that _must_ be in the actual. + */ + j$.mapContaining = function(sample) { + return new j$.MapContaining(sample); + }; + + /** + * Get an {@link AsymmetricEqualityTester}, usable in any {@link matchers|matcher} that uses Jasmine's equality (e.g. {@link matchers#toEqual|toEqual}, {@link matchers#toContain|toContain}, or {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}), + * that will succeed if every item in the sample passes the deep equality comparison + * with at least one item in the actual value being compared + * @name jasmine.setContaining + * @since 3.5.0 + * @function + * @param {Set} sample - The subset of items that _must_ be in the actual. + */ + j$.setContaining = function(sample) { + return new j$.SetContaining(sample); + }; + + /** + * Determines whether the provided function is a Jasmine spy. + * @name jasmine.isSpy + * @since 2.0.0 + * @function + * @param {Function} putativeSpy - The function to check. + * @return {Boolean} + */ + j$.isSpy = function(putativeSpy) { + if (!putativeSpy) { + return false; + } + return ( + putativeSpy.and instanceof j$.SpyStrategy && + putativeSpy.calls instanceof j$.CallTracker + ); + }; + + /** + * Logs a message for use in debugging. If the spec fails, trace messages + * will be included in the {@link SpecResult|result} passed to the + * reporter's specDone method. + * + * This method should be called only when a spec (including any associated + * beforeEach or afterEach functions) is running. + * @function + * @name jasmine.debugLog + * @since 4.0.0 + * @param {String} msg - The message to log + */ + j$.debugLog = function(msg) { + j$.getEnv().debugLog(msg); + }; + + /** + * Replaces Jasmine's global error handling with a spy. This prevents Jasmine + * from treating uncaught exceptions and unhandled promise rejections + * as spec failures and allows them to be inspected using the spy's + * {@link Spy#calls|calls property} and related matchers such as + * {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}. + * + * After installing the spy, spyOnGlobalErrorsAsync immediately calls its + * argument, which must be an async or promise-returning function. The spy + * will be passed as the first argument to that callback. Normal error + * handling will be restored when the promise returned from the callback is + * settled. + * + * Note: The JavaScript runtime may deliver uncaught error events and unhandled + * rejection events asynchronously, especially in browsers. If the event + * occurs after the promise returned from the callback is settled, it won't + * be routed to the spy even if the underlying error occurred previously. + * It's up to you to ensure that the returned promise isn't resolved until + * all of the error/rejection events that you want to handle have occurred. + * + * You must await the return value of spyOnGlobalErrorsAsync. + * @name jasmine.spyOnGlobalErrorsAsync + * @function + * @async + * @param {AsyncFunction} fn - A function to run, during which the global error spy will be effective + * @example + * it('demonstrates global error spies', async function() { + * await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) { + * setTimeout(function() { + * throw new Error('the expected error'); + * }); + * await new Promise(function(resolve) { + * setTimeout(resolve); + * }); + * const expected = new Error('the expected error'); + * expect(globalErrorSpy).toHaveBeenCalledWith(expected); + * }); + * }); + */ + j$.spyOnGlobalErrorsAsync = async function(fn) { + await jasmine.getEnv().spyOnGlobalErrorsAsync(fn); + }; +}; + +getJasmineRequireObj().util = function(j$) { + const util = {}; + + util.isUndefined = function(obj) { + return obj === void 0; + }; + + util.clone = function(obj) { + if (Object.prototype.toString.apply(obj) === '[object Array]') { + return obj.slice(); + } + + const cloned = {}; + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + cloned[prop] = obj[prop]; + } + } + + return cloned; + }; + + util.cloneArgs = function(args) { + return Array.from(args).map(function(arg) { + const str = Object.prototype.toString.apply(arg), + primitives = /^\[object (Boolean|String|RegExp|Number)/; + + // All falsey values are either primitives, `null`, or `undefined. + if (!arg || str.match(primitives)) { + return arg; + } else if (str === '[object Date]') { + return new Date(arg.valueOf()); + } else { + return j$.util.clone(arg); + } + }); + }; + + util.getPropertyDescriptor = function(obj, methodName) { + let descriptor, + proto = obj; + + do { + descriptor = Object.getOwnPropertyDescriptor(proto, methodName); + proto = Object.getPrototypeOf(proto); + } while (!descriptor && proto); + + return descriptor; + }; + + util.has = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + }; + + util.errorWithStack = function errorWithStack() { + // Don't throw and catch. That makes it harder for users to debug their + // code with exception breakpoints, and it's unnecessary since all + // supported environments populate new Error().stack + return new Error(); + }; + + function callerFile() { + const trace = new j$.StackTrace(util.errorWithStack()); + return trace.frames[2].file; + } + + util.jasmineFile = (function() { + let result; + + return function() { + if (!result) { + result = callerFile(); + } + + return result; + }; + })(); + + util.validateTimeout = function(timeout, msgPrefix) { + // Timeouts are implemented with setTimeout, which only supports a limited + // range of values. The limit is unspecified, as is the behavior when it's + // exceeded. But on all currently supported JS runtimes, setTimeout calls + // the callback immediately when the timeout is greater than 2147483647 + // (the maximum value of a signed 32 bit integer). + const max = 2147483647; + + if (timeout > max) { + throw new Error( + (msgPrefix || 'Timeout value') + ' cannot be greater than ' + max + ); + } + }; + + return util; +}; + +getJasmineRequireObj().Spec = function(j$) { + function Spec(attrs) { + this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; + this.resultCallback = attrs.resultCallback || function() {}; + this.id = attrs.id; + this.description = attrs.description || ''; + this.queueableFn = attrs.queueableFn; + this.beforeAndAfterFns = + attrs.beforeAndAfterFns || + function() { + return { befores: [], afters: [] }; + }; + this.userContext = + attrs.userContext || + function() { + return {}; + }; + this.onStart = attrs.onStart || function() {}; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; + this.getSpecName = + attrs.getSpecName || + function() { + return ''; + }; + this.onLateError = attrs.onLateError || function() {}; + this.catchingExceptions = + attrs.catchingExceptions || + function() { + return true; + }; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + this.timer = attrs.timer || new j$.Timer(); + + if (!this.queueableFn.fn) { + this.exclude(); + } + + /** + * @typedef SpecResult + * @property {String} id - The unique id of this spec. + * @property {String} description - The description passed to the {@link it} that created this spec. + * @property {String} fullName - The full description including all ancestors of this spec. + * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. + * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. + * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. + * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. + * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec. + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: '', + duration: null, + properties: null, + debugLogs: null + }; + + this.reportedDone = false; + } + + Spec.prototype.addExpectationResult = function(passed, data, isError) { + const expectationResult = j$.buildExpectationResult(data); + + if (passed) { + this.result.passedExpectations.push(expectationResult); + } else { + if (this.reportedDone) { + this.onLateError(expectationResult); + } else { + this.result.failedExpectations.push(expectationResult); + + // TODO: refactor so that we don't need to override cached status + if (this.result.status) { + this.result.status = 'failed'; + } + } + + if (this.throwOnExpectationFailure && !isError) { + throw new j$.errors.ExpectationFailed(); + } + } + }; + + Spec.prototype.setSpecProperty = function(key, value) { + this.result.properties = this.result.properties || {}; + this.result.properties[key] = value; + }; + + Spec.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Spec.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + + Spec.prototype.execute = function( + queueRunnerFactory, + onComplete, + excluded, + failSpecWithNoExp + ) { + const onStart = { + fn: done => { + this.timer.start(); + this.onStart(this, done); + } + }; + + const complete = { + fn: done => { + if (this.autoCleanClosures) { + this.queueableFn.fn = null; + } + this.result.status = this.status(excluded, failSpecWithNoExp); + this.result.duration = this.timer.elapsed(); + + if (this.result.status !== 'failed') { + this.result.debugLogs = null; + } + + this.resultCallback(this.result, done); + }, + type: 'specCleanup' + }; + + const fns = this.beforeAndAfterFns(); + + const runnerConfig = { + isLeaf: true, + queueableFns: [...fns.befores, this.queueableFn, ...fns.afters], + onException: e => this.handleException(e), + onMultipleDone: () => { + // Issue a deprecation. Include the context ourselves and pass + // ignoreRunnable: true, since getting here always means that we've already + // moved on and the current runnable isn't the one that caused the problem. + this.onLateError( + new Error( + 'An asynchronous spec, beforeEach, or afterEach function called its ' + + "'done' callback more than once.\n(in spec: " + + this.getFullName() + + ')' + ) + ); + }, + onComplete: () => { + if (this.result.status === 'failed') { + onComplete(new j$.StopExecutionError('spec failed')); + } else { + onComplete(); + } + }, + userContext: this.userContext(), + runnableName: this.getFullName.bind(this) + }; + + if (this.markedPending || excluded === true) { + runnerConfig.queueableFns = []; + } + + runnerConfig.queueableFns.unshift(onStart); + runnerConfig.queueableFns.push(complete); + + queueRunnerFactory(runnerConfig); + }; + + Spec.prototype.reset = function() { + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: this.excludeMessage, + duration: null, + properties: null, + debugLogs: null + }; + this.markedPending = this.markedExcluding; + this.reportedDone = false; + }; + + Spec.prototype.handleException = function handleException(e) { + if (Spec.isPendingSpecException(e)) { + this.pend(extractCustomPendingMessage(e)); + return; + } + + if (e instanceof j$.errors.ExpectationFailed) { + return; + } + + this.addExpectationResult( + false, + { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: e + }, + true + ); + }; + + /* + * Marks state as pending + * @param {string} [message] An optional reason message + */ + Spec.prototype.pend = function(message) { + this.markedPending = true; + if (message) { + this.result.pendingReason = message; + } + }; + + /* + * Like {@link Spec#pend}, but pending state will survive {@link Spec#reset} + * Useful for fit, xit, where pending state remains. + * @param {string} [message] An optional reason message + */ + Spec.prototype.exclude = function(message) { + this.markedExcluding = true; + if (this.message) { + this.excludeMessage = message; + } + this.pend(message); + }; + + Spec.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; + }; + + Spec.prototype.status = function(excluded, failSpecWithNoExpectations) { + if (excluded === true) { + return 'excluded'; + } + + if (this.markedPending) { + return 'pending'; + } + + if ( + this.result.failedExpectations.length > 0 || + (failSpecWithNoExpectations && + this.result.failedExpectations.length + + this.result.passedExpectations.length === + 0) + ) { + return 'failed'; + } + + return 'passed'; + }; + + Spec.prototype.getFullName = function() { + return this.getSpecName(this); + }; + + Spec.prototype.addDeprecationWarning = function(deprecation) { + if (typeof deprecation === 'string') { + deprecation = { message: deprecation }; + } + this.result.deprecationWarnings.push( + j$.buildExpectationResult(deprecation) + ); + }; + + Spec.prototype.debugLog = function(msg) { + if (!this.result.debugLogs) { + this.result.debugLogs = []; + } + + /** + * @typedef DebugLogEntry + * @property {String} message - The message that was passed to {@link jasmine.debugLog}. + * @property {number} timestamp - The time when the entry was added, in + * milliseconds from the spec's start time + */ + this.result.debugLogs.push({ + message: msg, + timestamp: this.timer.elapsed() + }); + }; + + const extractCustomPendingMessage = function(e) { + const fullMessage = e.toString(), + boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage), + boilerplateEnd = + boilerplateStart + Spec.pendingSpecExceptionMessage.length; + + return fullMessage.slice(boilerplateEnd); + }; + + Spec.pendingSpecExceptionMessage = '=> marked Pending'; + + Spec.isPendingSpecException = function(e) { + return !!( + e && + e.toString && + e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1 + ); + }; + + /** + * @interface Spec + * @see Configuration#specFilter + * @since 2.0.0 + */ + Object.defineProperty(Spec.prototype, 'metadata', { + get: function() { + if (!this.metadata_) { + this.metadata_ = { + /** + * The unique ID of this spec. + * @name Spec#id + * @readonly + * @type {string} + * @since 2.0.0 + */ + id: this.id, + + /** + * The description passed to the {@link it} that created this spec. + * @name Spec#description + * @readonly + * @type {string} + * @since 2.0.0 + */ + description: this.description, + + /** + * The full description including all ancestors of this spec. + * @name Spec#getFullName + * @function + * @returns {string} + * @since 2.0.0 + */ + getFullName: this.getFullName.bind(this) + }; + } + + return this.metadata_; + } + }); + + return Spec; +}; + +getJasmineRequireObj().Order = function() { + function Order(options) { + this.random = 'random' in options ? options.random : true; + const seed = (this.seed = options.seed || generateSeed()); + this.sort = this.random ? randomOrder : naturalOrder; + + function naturalOrder(items) { + return items; + } + + function randomOrder(items) { + const copy = items.slice(); + copy.sort(function(a, b) { + return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id); + }); + return copy; + } + + function generateSeed() { + return String(Math.random()).slice(-5); + } + + // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function + // used to get a different output when the key changes slightly. + // We use your return to sort the children randomly in a consistent way when + // used in conjunction with a seed + + function jenkinsHash(key) { + let hash, i; + for (hash = i = 0; i < key.length; ++i) { + hash += key.charCodeAt(i); + hash += hash << 10; + hash ^= hash >> 6; + } + hash += hash << 3; + hash ^= hash >> 11; + hash += hash << 15; + return hash; + } + } + + return Order; +}; + +getJasmineRequireObj().Env = function(j$) { + /** + * @class Env + * @since 2.0.0 + * @classdesc The Jasmine environment.
    + * _Note:_ Do not construct this directly. You can obtain the Env instance by + * calling {@link jasmine.getEnv}. + * @hideconstructor + */ + function Env(options) { + options = options || {}; + + const self = this; + const global = options.global || j$.getGlobal(); + + const realSetTimeout = global.setTimeout; + const realClearTimeout = global.clearTimeout; + const clearStack = j$.getClearStack(global); + this.clock = new j$.Clock( + global, + function() { + return new j$.DelayedFunctionScheduler(); + }, + new j$.MockDate(global) + ); + + const globalErrors = new j$.GlobalErrors(); + const installGlobalErrors = (function() { + let installed = false; + return function() { + if (!installed) { + globalErrors.install(); + installed = true; + } + }; + })(); + + const runableResources = new j$.RunableResources({ + getCurrentRunableId: function() { + const r = runner.currentRunable(); + return r ? r.id : null; + }, + globalErrors + }); + + let reporter; + let topSuite; + let runner; + + /** + * This represents the available options to configure Jasmine. + * Options that are not provided will use their default values. + * @see Env#configure + * @interface Configuration + * @since 3.3.0 + */ + const config = { + /** + * Whether to randomize spec execution order + * @name Configuration#random + * @since 3.3.0 + * @type Boolean + * @default true + */ + random: true, + /** + * Seed to use as the basis of randomization. + * Null causes the seed to be determined randomly at the start of execution. + * @name Configuration#seed + * @since 3.3.0 + * @type (number|string) + * @default null + */ + seed: null, + /** + * Whether to stop execution of the suite after the first spec failure + * @name Configuration#stopOnSpecFailure + * @since 3.9.0 + * @type Boolean + * @default false + */ + stopOnSpecFailure: false, + /** + * Whether to fail the spec if it ran no expectations. By default + * a spec that ran no expectations is reported as passed. Setting this + * to true will report such spec as a failure. + * @name Configuration#failSpecWithNoExpectations + * @since 3.5.0 + * @type Boolean + * @default false + */ + failSpecWithNoExpectations: false, + /** + * Whether to cause specs to only have one expectation failure. + * @name Configuration#stopSpecOnExpectationFailure + * @since 3.3.0 + * @type Boolean + * @default false + */ + stopSpecOnExpectationFailure: false, + /** + * A function that takes a spec and returns true if it should be executed + * or false if it should be skipped. + * @callback SpecFilter + * @param {Spec} spec - The spec that the filter is being applied to. + * @return boolean + */ + /** + * Function to use to filter specs + * @name Configuration#specFilter + * @since 3.3.0 + * @type SpecFilter + * @default A function that always returns true. + */ + specFilter: function() { + return true; + }, + /** + * Whether or not reporters should hide disabled specs from their output. + * Currently only supported by Jasmine's HTMLReporter + * @name Configuration#hideDisabled + * @since 3.3.0 + * @type Boolean + * @default false + */ + hideDisabled: false, + /** + * Clean closures when a suite is done running (done by clearing the stored function reference). + * This prevents memory leaks, but you won't be able to run jasmine multiple times. + * @name Configuration#autoCleanClosures + * @since 3.10.0 + * @type boolean + * @default true + */ + autoCleanClosures: true, + /** + * Whether or not to issue warnings for certain deprecated functionality + * every time it's used. If not set or set to false, deprecation warnings + * for methods that tend to be called frequently will be issued only once + * or otherwise throttled to to prevent the suite output from being flooded + * with warnings. + * @name Configuration#verboseDeprecations + * @since 3.6.0 + * @type Boolean + * @default false + */ + verboseDeprecations: false + }; + + if (!options.suppressLoadErrors) { + installGlobalErrors(); + globalErrors.pushListener(function loadtimeErrorHandler( + message, + filename, + lineno, + colNo, + err + ) { + topSuite.result.failedExpectations.push({ + passed: false, + globalErrorType: 'load', + message: message, + stack: err && err.stack, + filename: filename, + lineno: lineno + }); + }); + } + + /** + * Configure your jasmine environment + * @name Env#configure + * @since 3.3.0 + * @argument {Configuration} configuration + * @function + */ + this.configure = function(configuration) { + const booleanProps = [ + 'random', + 'failSpecWithNoExpectations', + 'hideDisabled', + 'stopOnSpecFailure', + 'stopSpecOnExpectationFailure', + 'autoCleanClosures' + ]; + + booleanProps.forEach(function(prop) { + if (typeof configuration[prop] !== 'undefined') { + config[prop] = !!configuration[prop]; + } + }); + + if (configuration.specFilter) { + config.specFilter = configuration.specFilter; + } + + if (typeof configuration.seed !== 'undefined') { + config.seed = configuration.seed; + } + + if (configuration.hasOwnProperty('verboseDeprecations')) { + config.verboseDeprecations = configuration.verboseDeprecations; + deprecator.verboseDeprecations(config.verboseDeprecations); + } + }; + + /** + * Get the current configuration for your jasmine environment + * @name Env#configuration + * @since 3.3.0 + * @function + * @returns {Configuration} + */ + this.configuration = function() { + const result = {}; + for (const property in config) { + result[property] = config[property]; + } + return result; + }; + + this.setDefaultSpyStrategy = function(defaultStrategyFn) { + runableResources.setDefaultSpyStrategy(defaultStrategyFn); + }; + + this.addSpyStrategy = function(name, fn) { + runableResources.customSpyStrategies()[name] = fn; + }; + + this.addCustomEqualityTester = function(tester) { + runableResources.customEqualityTesters().push(tester); + }; + + this.addMatchers = function(matchersToAdd) { + runableResources.addCustomMatchers(matchersToAdd); + }; + + this.addAsyncMatchers = function(matchersToAdd) { + runableResources.addCustomAsyncMatchers(matchersToAdd); + }; + + this.addCustomObjectFormatter = function(formatter) { + runableResources.customObjectFormatters().push(formatter); + }; + + j$.Expectation.addCoreMatchers(j$.matchers); + j$.Expectation.addAsyncCoreMatchers(j$.asyncMatchers); + + const expectationFactory = function(actual, spec) { + return j$.Expectation.factory({ + matchersUtil: runableResources.makeMatchersUtil(), + customMatchers: runableResources.customMatchers(), + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + return spec.addExpectationResult(passed, result); + } + }; + + // TODO: Unify recordLateError with recordLateExpectation? The extra + // diagnostic info added by the latter is probably useful in most cases. + function recordLateError(error) { + const isExpectationResult = + error.matcherName !== undefined && error.passed !== undefined; + const result = isExpectationResult + ? error + : j$.buildExpectationResult({ + error, + passed: false, + matcherName: '', + expected: '', + actual: '' + }); + routeLateFailure(result); + } + + function recordLateExpectation(runable, runableType, result) { + const delayedExpectationResult = {}; + Object.keys(result).forEach(function(k) { + delayedExpectationResult[k] = result[k]; + }); + delayedExpectationResult.passed = false; + delayedExpectationResult.globalErrorType = 'lateExpectation'; + delayedExpectationResult.message = + runableType + + ' "' + + runable.getFullName() + + '" ran a "' + + result.matcherName + + '" expectation after it finished.\n'; + + if (result.message) { + delayedExpectationResult.message += + 'Message: "' + result.message + '"\n'; + } + + delayedExpectationResult.message += + '1. Did you forget to return or await the result of expectAsync?\n' + + '2. Was done() invoked before an async operation completed?\n' + + '3. Did an expectation follow a call to done()?'; + + topSuite.result.failedExpectations.push(delayedExpectationResult); + } + + function routeLateFailure(expectationResult) { + // Report the result on the nearest ancestor suite that hasn't already + // been reported done. + for (let r = runner.currentRunable(); r; r = r.parentSuite) { + if (!r.reportedDone) { + if (r === topSuite) { + expectationResult.globalErrorType = 'lateError'; + } + + r.result.failedExpectations.push(expectationResult); + return; + } + } + + // If we get here, all results have been reported and there's nothing we + // can do except log the result and hope the user sees it. + console.error('Jasmine received a result after the suite finished:'); + console.error(expectationResult); + } + + const asyncExpectationFactory = function(actual, spec, runableType) { + return j$.Expectation.asyncFactory({ + matchersUtil: runableResources.makeMatchersUtil(), + customAsyncMatchers: runableResources.customAsyncMatchers(), + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + if (runner.currentRunable() !== spec) { + recordLateExpectation(spec, runableType, result); + } + return spec.addExpectationResult(passed, result); + } + }; + + /** + * Causes a deprecation warning to be logged to the console and reported to + * reporters. + * + * The optional second parameter is an object that can have either of the + * following properties: + * + * omitStackTrace: Whether to omit the stack trace. Optional. Defaults to + * false. This option is ignored if the deprecation is an Error. Set this + * when the stack trace will not contain anything that helps the user find + * the source of the deprecation. + * + * ignoreRunnable: Whether to log the deprecation on the root suite, ignoring + * the spec or suite that's running when it happens. Optional. Defaults to + * false. + * + * @name Env#deprecated + * @since 2.99 + * @function + * @param {String|Error} deprecation The deprecation message + * @param {Object} [options] Optional extra options, as described above + */ + this.deprecated = function(deprecation, options) { + const runable = runner.currentRunable() || topSuite; + deprecator.addDeprecationWarning(runable, deprecation, options); + }; + + function queueRunnerFactory(options) { + options.clearStack = options.clearStack || clearStack; + options.timeout = { + setTimeout: realSetTimeout, + clearTimeout: realClearTimeout + }; + options.fail = self.fail; + options.globalErrors = globalErrors; + options.onException = + options.onException || + function(e) { + (runner.currentRunable() || topSuite).handleException(e); + }; + options.deprecated = self.deprecated; + + new j$.QueueRunner(options).execute(); + } + + const suiteBuilder = new j$.SuiteBuilder({ + env: this, + expectationFactory, + asyncExpectationFactory, + onLateError: recordLateError, + specResultCallback, + specStarted, + queueRunnerFactory + }); + topSuite = suiteBuilder.topSuite; + const deprecator = new j$.Deprecator(topSuite); + + /** + * Provides the root suite, through which all suites and specs can be + * accessed. + * @function + * @name Env#topSuite + * @return {Suite} the root suite + * @since 2.0.0 + */ + this.topSuite = function() { + return topSuite.metadata; + }; + + /** + * This represents the available reporter callback for an object passed to {@link Env#addReporter}. + * @interface Reporter + * @see custom_reporter + */ + reporter = new j$.ReportDispatcher( + [ + /** + * `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts. + * @function + * @name Reporter#jasmineStarted + * @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'jasmineStarted', + /** + * When the entire suite has finished execution `jasmineDone` is called + * @function + * @name Reporter#jasmineDone + * @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running. + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'jasmineDone', + /** + * `suiteStarted` is invoked when a `describe` starts to run + * @function + * @name Reporter#suiteStarted + * @param {SuiteResult} result Information about the individual {@link describe} being run + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'suiteStarted', + /** + * `suiteDone` is invoked when all of the child specs and suites for a given suite have been run + * + * While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`. + * @function + * @name Reporter#suiteDone + * @param {SuiteResult} result + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'suiteDone', + /** + * `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions) + * @function + * @name Reporter#specStarted + * @param {SpecResult} result Information about the individual {@link it} being run + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'specStarted', + /** + * `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run. + * + * While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed. + * @function + * @name Reporter#specDone + * @param {SpecResult} result + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + * @see async + */ + 'specDone' + ], + function(options) { + options.SkipPolicy = j$.NeverSkipPolicy; + return queueRunnerFactory(options); + }, + recordLateError + ); + + runner = new j$.Runner({ + topSuite, + totalSpecsDefined: () => suiteBuilder.totalSpecsDefined, + focusedRunables: () => suiteBuilder.focusedRunables, + runableResources, + reporter, + queueRunnerFactory, + getConfig: () => config, + reportSpecDone + }); + + /** + * Executes the specs. + * + * If called with no parameters or with a falsy value as the first parameter, + * all specs will be executed except those that are excluded by a + * [spec filter]{@link Configuration#specFilter} or other mechanism. If the + * first parameter is a list of spec/suite IDs, only those specs/suites will + * be run. + * + * Both parameters are optional, but a completion callback is only valid as + * the second parameter. To specify a completion callback but not a list of + * specs/suites to run, pass null or undefined as the first parameter. The + * completion callback is supported for backward compatibility. In most + * cases it will be more convenient to use the returned promise instead. + * + * execute should not be called more than once unless the env has been + * configured with `{autoCleanClosures: false}`. + * + * execute returns a promise. The promise will be resolved to the same + * {@link JasmineDoneInfo|overall result} that's passed to a reporter's + * `jasmineDone` method, even if the suite did not pass. To determine + * whether the suite passed, check the value that the promise resolves to + * or use a {@link Reporter}. + * + * @name Env#execute + * @since 2.0.0 + * @function + * @param {(string[])=} runablesToRun IDs of suites and/or specs to run + * @param {Function=} onComplete Function that will be called after all specs have run + * @return {Promise} + */ + this.execute = function(runablesToRun, onComplete) { + installGlobalErrors(); + + return runner.execute(runablesToRun).then(function(jasmineDoneInfo) { + if (onComplete) { + onComplete(); + } + + return jasmineDoneInfo; + }); + }; + + /** + * Add a custom reporter to the Jasmine environment. + * @name Env#addReporter + * @since 2.0.0 + * @function + * @param {Reporter} reporterToAdd The reporter to be added. + * @see custom_reporter + */ + this.addReporter = function(reporterToAdd) { + reporter.addReporter(reporterToAdd); + }; + + /** + * Provide a fallback reporter if no other reporters have been specified. + * @name Env#provideFallbackReporter + * @since 2.5.0 + * @function + * @param {Reporter} reporterToAdd The reporter + * @see custom_reporter + */ + this.provideFallbackReporter = function(reporterToAdd) { + reporter.provideFallbackReporter(reporterToAdd); + }; + + /** + * Clear all registered reporters + * @name Env#clearReporters + * @since 2.5.2 + * @function + */ + this.clearReporters = function() { + reporter.clearReporters(); + }; + + /** + * Configures whether Jasmine should allow the same function to be spied on + * more than once during the execution of a spec. By default, spying on + * a function that is already a spy will cause an error. + * @name Env#allowRespy + * @function + * @since 2.5.0 + * @param {boolean} allow Whether to allow respying + */ + this.allowRespy = function(allow) { + runableResources.spyRegistry.allowRespy(allow); + }; + + this.spyOn = function() { + return runableResources.spyRegistry.spyOn.apply( + runableResources.spyRegistry, + arguments + ); + }; + + this.spyOnProperty = function() { + return runableResources.spyRegistry.spyOnProperty.apply( + runableResources.spyRegistry, + arguments + ); + }; + + this.spyOnAllFunctions = function() { + return runableResources.spyRegistry.spyOnAllFunctions.apply( + runableResources.spyRegistry, + arguments + ); + }; + + this.createSpy = function(name, originalFn) { + return runableResources.spyFactory.createSpy(name, originalFn); + }; + + this.createSpyObj = function(baseName, methodNames, propertyNames) { + return runableResources.spyFactory.createSpyObj( + baseName, + methodNames, + propertyNames + ); + }; + + this.spyOnGlobalErrorsAsync = async function(fn) { + const spy = this.createSpy('global error handler'); + const associatedRunable = runner.currentRunable(); + let cleanedUp = false; + + globalErrors.setOverrideListener(spy, () => { + if (!cleanedUp) { + const message = + 'Global error spy was not uninstalled. (Did you ' + + 'forget to await the return value of spyOnGlobalErrorsAsync?)'; + associatedRunable.addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + message, + error: null + }); + } + + cleanedUp = true; + }); + + try { + const maybePromise = fn(spy); + + if (!j$.isPromiseLike(maybePromise)) { + throw new Error( + 'The callback to spyOnGlobalErrorsAsync must be an async or promise-returning function' + ); + } + + await maybePromise; + } finally { + if (!cleanedUp) { + cleanedUp = true; + globalErrors.removeOverrideListener(); + } + } + }; + + function ensureIsNotNested(method) { + const runable = runner.currentRunable(); + if (runable !== null && runable !== undefined) { + throw new Error( + "'" + method + "' should only be used in 'describe' function" + ); + } + } + + this.describe = function(description, definitionFn) { + ensureIsNotNested('describe'); + return suiteBuilder.describe(description, definitionFn).metadata; + }; + + this.xdescribe = function(description, definitionFn) { + ensureIsNotNested('xdescribe'); + return suiteBuilder.xdescribe(description, definitionFn).metadata; + }; + + this.fdescribe = function(description, definitionFn) { + ensureIsNotNested('fdescribe'); + return suiteBuilder.fdescribe(description, definitionFn).metadata; + }; + + function specResultCallback(spec, result, next) { + runableResources.clearForRunable(spec.id); + runner.currentSpec = null; + + if (result.status === 'failed') { + runner.hasFailures = true; + } + + reportSpecDone(spec, result, next); + } + + function specStarted(spec, suite, next) { + runner.currentSpec = spec; + runableResources.initForRunable(spec.id, suite.id); + reporter.specStarted(spec.result).then(next); + } + + function reportSpecDone(spec, result, next) { + spec.reportedDone = true; + reporter.specDone(result).then(next); + } + + this.it = function(description, fn, timeout) { + ensureIsNotNested('it'); + return suiteBuilder.it(description, fn, timeout).metadata; + }; + + this.xit = function(description, fn, timeout) { + ensureIsNotNested('xit'); + return suiteBuilder.xit(description, fn, timeout).metadata; + }; + + this.fit = function(description, fn, timeout) { + ensureIsNotNested('fit'); + return suiteBuilder.fit(description, fn, timeout).metadata; + }; + + /** + * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult} + * @name Env#setSpecProperty + * @since 3.6.0 + * @function + * @param {String} key The name of the property + * @param {*} value The value of the property + */ + this.setSpecProperty = function(key, value) { + if ( + !runner.currentRunable() || + runner.currentRunable() == runner.currentSuite() + ) { + throw new Error( + "'setSpecProperty' was used when there was no current spec" + ); + } + runner.currentRunable().setSpecProperty(key, value); + }; + + /** + * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SuiteResult} + * @name Env#setSuiteProperty + * @since 3.6.0 + * @function + * @param {String} key The name of the property + * @param {*} value The value of the property + */ + this.setSuiteProperty = function(key, value) { + if (!runner.currentSuite()) { + throw new Error( + "'setSuiteProperty' was used when there was no current suite" + ); + } + runner.currentSuite().setSuiteProperty(key, value); + }; + + this.debugLog = function(msg) { + const maybeSpec = runner.currentRunable(); + + if (!maybeSpec || !maybeSpec.debugLog) { + throw new Error("'debugLog' was called when there was no current spec"); + } + + maybeSpec.debugLog(msg); + }; + + this.expect = function(actual) { + if (!runner.currentRunable()) { + throw new Error( + "'expect' was used when there was no current spec, this could be because an asynchronous test timed out" + ); + } + + return runner.currentRunable().expect(actual); + }; + + this.expectAsync = function(actual) { + if (!runner.currentRunable()) { + throw new Error( + "'expectAsync' was used when there was no current spec, this could be because an asynchronous test timed out" + ); + } + + return runner.currentRunable().expectAsync(actual); + }; + + this.beforeEach = function(beforeEachFunction, timeout) { + ensureIsNotNested('beforeEach'); + suiteBuilder.beforeEach(beforeEachFunction, timeout); + }; + + this.beforeAll = function(beforeAllFunction, timeout) { + ensureIsNotNested('beforeAll'); + suiteBuilder.beforeAll(beforeAllFunction, timeout); + }; + + this.afterEach = function(afterEachFunction, timeout) { + ensureIsNotNested('afterEach'); + suiteBuilder.afterEach(afterEachFunction, timeout); + }; + + this.afterAll = function(afterAllFunction, timeout) { + ensureIsNotNested('afterAll'); + suiteBuilder.afterAll(afterAllFunction, timeout); + }; + + this.pending = function(message) { + let fullMessage = j$.Spec.pendingSpecExceptionMessage; + if (message) { + fullMessage += message; + } + throw fullMessage; + }; + + this.fail = function(error) { + if (!runner.currentRunable()) { + throw new Error( + "'fail' was used when there was no current spec, this could be because an asynchronous test timed out" + ); + } + + let message = 'Failed'; + if (error) { + message += ': '; + if (error.message) { + message += error.message; + } else if (j$.isString_(error)) { + message += error; + } else { + // pretty print all kind of objects. This includes arrays. + const pp = runableResources.makePrettyPrinter(); + message += pp(error); + } + } + + runner.currentRunable().addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + message: message, + error: error && error.message ? error : null + }); + + if (config.stopSpecOnExpectationFailure) { + throw new Error(message); + } + }; + + this.cleanup_ = function() { + if (globalErrors) { + globalErrors.uninstall(); + } + }; + } + + return Env; +}; + +getJasmineRequireObj().JsApiReporter = function(j$) { + /** + * @name jsApiReporter + * @classdesc {@link Reporter} added by default in `boot.js` to record results for retrieval in javascript code. An instance is made available as `jsApiReporter` on the global object. + * @class + * @hideconstructor + */ + function JsApiReporter(options) { + const timer = options.timer || new j$.Timer(); + let status = 'loaded'; + + this.started = false; + this.finished = false; + this.runDetails = {}; + + this.jasmineStarted = function() { + this.started = true; + status = 'started'; + timer.start(); + }; + + let executionTime; + + this.jasmineDone = function(runDetails) { + this.finished = true; + this.runDetails = runDetails; + executionTime = timer.elapsed(); + status = 'done'; + }; + + /** + * Get the current status for the Jasmine environment. + * @name jsApiReporter#status + * @since 2.0.0 + * @function + * @return {String} - One of `loaded`, `started`, or `done` + */ + this.status = function() { + return status; + }; + + const suites = [], + suites_hash = {}; + + this.suiteStarted = function(result) { + suites_hash[result.id] = result; + }; + + this.suiteDone = function(result) { + storeSuite(result); + }; + + /** + * Get the results for a set of suites. + * + * Retrievable in slices for easier serialization. + * @name jsApiReporter#suiteResults + * @since 2.1.0 + * @function + * @param {Number} index - The position in the suites list to start from. + * @param {Number} length - Maximum number of suite results to return. + * @return {SuiteResult[]} + */ + this.suiteResults = function(index, length) { + return suites.slice(index, index + length); + }; + + function storeSuite(result) { + suites.push(result); + suites_hash[result.id] = result; + } + + /** + * Get all of the suites in a single object, with their `id` as the key. + * @name jsApiReporter#suites + * @since 2.0.0 + * @function + * @return {Object} - Map of suite id to {@link SuiteResult} + */ + this.suites = function() { + return suites_hash; + }; + + const specs = []; + + this.specDone = function(result) { + specs.push(result); + }; + + /** + * Get the results for a set of specs. + * + * Retrievable in slices for easier serialization. + * @name jsApiReporter#specResults + * @since 2.0.0 + * @function + * @param {Number} index - The position in the specs list to start from. + * @param {Number} length - Maximum number of specs results to return. + * @return {SpecResult[]} + */ + this.specResults = function(index, length) { + return specs.slice(index, index + length); + }; + + /** + * Get all spec results. + * @name jsApiReporter#specs + * @since 2.0.0 + * @function + * @return {SpecResult[]} + */ + this.specs = function() { + return specs; + }; + + /** + * Get the number of milliseconds it took for the full Jasmine suite to run. + * @name jsApiReporter#executionTime + * @since 2.0.0 + * @function + * @return {Number} + */ + this.executionTime = function() { + return executionTime; + }; + } + + return JsApiReporter; +}; + +getJasmineRequireObj().Any = function(j$) { + function Any(expectedObject) { + if (typeof expectedObject === 'undefined') { + throw new TypeError( + 'jasmine.any() expects to be passed a constructor function. ' + + 'Please pass one or use jasmine.anything() to match any object.' + ); + } + this.expectedObject = expectedObject; + } + + Any.prototype.asymmetricMatch = function(other) { + if (this.expectedObject == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedObject == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedObject == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedObject == Object) { + return other !== null && typeof other == 'object'; + } + + if (this.expectedObject == Boolean) { + return typeof other == 'boolean'; + } + + if (typeof Symbol != 'undefined' && this.expectedObject == Symbol) { + return typeof other == 'symbol'; + } + + return other instanceof this.expectedObject; + }; + + Any.prototype.jasmineToString = function() { + return ''; + }; + + return Any; +}; + +getJasmineRequireObj().Anything = function(j$) { + function Anything() {} + + Anything.prototype.asymmetricMatch = function(other) { + return !j$.util.isUndefined(other) && other !== null; + }; + + Anything.prototype.jasmineToString = function() { + return ''; + }; + + return Anything; +}; + +getJasmineRequireObj().ArrayContaining = function(j$) { + function ArrayContaining(sample) { + this.sample = sample; + } + + ArrayContaining.prototype.asymmetricMatch = function(other, matchersUtil) { + if (!j$.isArray_(this.sample)) { + throw new Error( + 'You must provide an array to arrayContaining, not ' + + j$.basicPrettyPrinter_(this.sample) + + '.' + ); + } + + // If the actual parameter is not an array, we can fail immediately, since it couldn't + // possibly be an "array containing" anything. However, we also want an empty sample + // array to match anything, so we need to double-check we aren't in that case + if (!j$.isArray_(other) && this.sample.length > 0) { + return false; + } + + for (const item of this.sample) { + if (!matchersUtil.contains(other, item)) { + return false; + } + } + + return true; + }; + + ArrayContaining.prototype.jasmineToString = function(pp) { + return ''; + }; + + return ArrayContaining; +}; + +getJasmineRequireObj().ArrayWithExactContents = function(j$) { + function ArrayWithExactContents(sample) { + this.sample = sample; + } + + ArrayWithExactContents.prototype.asymmetricMatch = function( + other, + matchersUtil + ) { + if (!j$.isArray_(this.sample)) { + throw new Error( + 'You must provide an array to arrayWithExactContents, not ' + + j$.basicPrettyPrinter_(this.sample) + + '.' + ); + } + + if (this.sample.length !== other.length) { + return false; + } + + for (const item of this.sample) { + if (!matchersUtil.contains(other, item)) { + return false; + } + } + + return true; + }; + + ArrayWithExactContents.prototype.jasmineToString = function(pp) { + return ''; + }; + + return ArrayWithExactContents; +}; + +getJasmineRequireObj().Empty = function(j$) { + function Empty() {} + + Empty.prototype.asymmetricMatch = function(other) { + if (j$.isString_(other) || j$.isArray_(other) || j$.isTypedArray_(other)) { + return other.length === 0; + } + + if (j$.isMap(other) || j$.isSet(other)) { + return other.size === 0; + } + + if (j$.isObject_(other)) { + return Object.keys(other).length === 0; + } + return false; + }; + + Empty.prototype.jasmineToString = function() { + return ''; + }; + + return Empty; +}; + +getJasmineRequireObj().Falsy = function(j$) { + function Falsy() {} + + Falsy.prototype.asymmetricMatch = function(other) { + return !other; + }; + + Falsy.prototype.jasmineToString = function() { + return ''; + }; + + return Falsy; +}; + +getJasmineRequireObj().Is = function(j$) { + class Is { + constructor(expected) { + this.expected_ = expected; + } + + asymmetricMatch(actual) { + return actual === this.expected_; + } + + jasmineToString(pp) { + return ``; + } + } + + return Is; +}; + +getJasmineRequireObj().MapContaining = function(j$) { + function MapContaining(sample) { + if (!j$.isMap(sample)) { + throw new Error( + 'You must provide a map to `mapContaining`, not ' + + j$.basicPrettyPrinter_(sample) + ); + } + + this.sample = sample; + } + + MapContaining.prototype.asymmetricMatch = function(other, matchersUtil) { + if (!j$.isMap(other)) return false; + + for (const [key, value] of this.sample) { + // for each key/value pair in `sample` + // there should be at least one pair in `other` whose key and value both match + let hasMatch = false; + for (const [oKey, oValue] of other) { + if ( + matchersUtil.equals(oKey, key) && + matchersUtil.equals(oValue, value) + ) { + hasMatch = true; + break; + } + } + + if (!hasMatch) { + return false; + } + } + + return true; + }; + + MapContaining.prototype.jasmineToString = function(pp) { + return ''; + }; + + return MapContaining; +}; + +getJasmineRequireObj().NotEmpty = function(j$) { + function NotEmpty() {} + + NotEmpty.prototype.asymmetricMatch = function(other) { + if (j$.isString_(other) || j$.isArray_(other) || j$.isTypedArray_(other)) { + return other.length !== 0; + } + + if (j$.isMap(other) || j$.isSet(other)) { + return other.size !== 0; + } + + if (j$.isObject_(other)) { + return Object.keys(other).length !== 0; + } + + return false; + }; + + NotEmpty.prototype.jasmineToString = function() { + return ''; + }; + + return NotEmpty; +}; + +getJasmineRequireObj().ObjectContaining = function(j$) { + function ObjectContaining(sample) { + this.sample = sample; + } + + function hasProperty(obj, property) { + if (!obj || typeof obj !== 'object') { + return false; + } + + if (Object.prototype.hasOwnProperty.call(obj, property)) { + return true; + } + + return hasProperty(Object.getPrototypeOf(obj), property); + } + + ObjectContaining.prototype.asymmetricMatch = function(other, matchersUtil) { + if (typeof this.sample !== 'object') { + throw new Error( + "You must provide an object to objectContaining, not '" + + this.sample + + "'." + ); + } + if (typeof other !== 'object') { + return false; + } + + for (const property in this.sample) { + if ( + !hasProperty(other, property) || + !matchersUtil.equals(this.sample[property], other[property]) + ) { + return false; + } + } + + return true; + }; + + ObjectContaining.prototype.valuesForDiff_ = function(other, pp) { + if (!j$.isObject_(other)) { + return { + self: this.jasmineToString(pp), + other: other + }; + } + + const filteredOther = {}; + Object.keys(this.sample).forEach(function(k) { + // eq short-circuits comparison of objects that have different key sets, + // so include all keys even if undefined. + filteredOther[k] = other[k]; + }); + + return { + self: this.sample, + other: filteredOther + }; + }; + + ObjectContaining.prototype.jasmineToString = function(pp) { + return ''; + }; + + return ObjectContaining; +}; + +getJasmineRequireObj().SetContaining = function(j$) { + function SetContaining(sample) { + if (!j$.isSet(sample)) { + throw new Error( + 'You must provide a set to `setContaining`, not ' + + j$.basicPrettyPrinter_(sample) + ); + } + + this.sample = sample; + } + + SetContaining.prototype.asymmetricMatch = function(other, matchersUtil) { + if (!j$.isSet(other)) return false; + + for (const item of this.sample) { + // for each item in `sample` there should be at least one matching item in `other` + // (not using `matchersUtil.contains` because it compares set members by reference, + // not by deep value equality) + let hasMatch = false; + for (const oItem of other) { + if (matchersUtil.equals(oItem, item)) { + hasMatch = true; + break; + } + } + + if (!hasMatch) { + return false; + } + } + + return true; + }; + + SetContaining.prototype.jasmineToString = function(pp) { + return ''; + }; + + return SetContaining; +}; + +getJasmineRequireObj().StringContaining = function(j$) { + function StringContaining(expected) { + if (!j$.isString_(expected)) { + throw new Error('Expected is not a String'); + } + + this.expected = expected; + } + + StringContaining.prototype.asymmetricMatch = function(other) { + if (!j$.isString_(other)) { + // Arrays, etc. don't match no matter what their indexOf returns. + return false; + } + + return other.indexOf(this.expected) !== -1; + }; + + StringContaining.prototype.jasmineToString = function() { + return ''; + }; + + return StringContaining; +}; + +getJasmineRequireObj().StringMatching = function(j$) { + function StringMatching(expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error('Expected is not a String or a RegExp'); + } + + this.regexp = new RegExp(expected); + } + + StringMatching.prototype.asymmetricMatch = function(other) { + return this.regexp.test(other); + }; + + StringMatching.prototype.jasmineToString = function() { + return ''; + }; + + return StringMatching; +}; + +getJasmineRequireObj().Truthy = function(j$) { + function Truthy() {} + + Truthy.prototype.asymmetricMatch = function(other) { + return !!other; + }; + + Truthy.prototype.jasmineToString = function() { + return ''; + }; + + return Truthy; +}; + +//TODO: expectation result may make more sense as a presentation of an expectation. +getJasmineRequireObj().buildExpectationResult = function(j$) { + function buildExpectationResult(options) { + const exceptionFormatter = new j$.ExceptionFormatter(); + + /** + * @typedef Expectation + * @property {String} matcherName - The name of the matcher that was executed for this expectation. + * @property {String} message - The failure message for the expectation. + * @property {String} stack - The stack trace for the failure if available. + * @property {Boolean} passed - Whether the expectation passed or failed. + * @property {Object} expected - If the expectation failed, what was the expected value. + * @property {Object} actual - If the expectation failed, what actual value was produced. + * @property {String|undefined} globalErrorType - The type of an error that + * is reported on the top suite. Valid values are undefined, "afterAll", + * "load", "lateExpectation", and "lateError". + */ + const result = { + matcherName: options.matcherName, + message: message(), + stack: options.omitStackTrace ? '' : stack(), + passed: options.passed + }; + + if (!result.passed) { + result.expected = options.expected; + result.actual = options.actual; + + if (options.error && !j$.isString_(options.error)) { + if ('code' in options.error) { + result.code = options.error.code; + } + + if ( + options.error.code === 'ERR_ASSERTION' && + options.expected === '' && + options.actual === '' + ) { + result.expected = options.error.expected; + result.actual = options.error.actual; + result.matcherName = 'assert ' + options.error.operator; + } + } + } + + return result; + + function message() { + if (options.passed) { + return 'Passed.'; + } else if (options.message) { + return options.message; + } else if (options.error) { + return exceptionFormatter.message(options.error); + } + return ''; + } + + function stack() { + if (options.passed) { + return ''; + } + + let error = options.error; + + if (!error) { + if (options.errorForStack) { + error = options.errorForStack; + } else if (options.stack) { + error = options; + } else { + try { + throw new Error(message()); + } catch (e) { + error = e; + } + } + } + // Omit the message from the stack trace because it will be + // included elsewhere. + return exceptionFormatter.stack(error, { omitMessage: true }); + } + } + + return buildExpectationResult; +}; + +getJasmineRequireObj().CallTracker = function(j$) { + /** + * @namespace Spy#calls + * @since 2.0.0 + */ + function CallTracker() { + let calls = []; + const opts = {}; + + this.track = function(context) { + if (opts.cloneArgs) { + context.args = j$.util.cloneArgs(context.args); + } + calls.push(context); + }; + + /** + * Check whether this spy has been invoked. + * @name Spy#calls#any + * @since 2.0.0 + * @function + * @return {Boolean} + */ + this.any = function() { + return !!calls.length; + }; + + /** + * Get the number of invocations of this spy. + * @name Spy#calls#count + * @since 2.0.0 + * @function + * @return {Integer} + */ + this.count = function() { + return calls.length; + }; + + /** + * Get the arguments that were passed to a specific invocation of this spy. + * @name Spy#calls#argsFor + * @since 2.0.0 + * @function + * @param {Integer} index The 0-based invocation index. + * @return {Array} + */ + this.argsFor = function(index) { + const call = calls[index]; + return call ? call.args : []; + }; + + /** + * Get the "this" object that was passed to a specific invocation of this spy. + * @name Spy#calls#thisFor + * @since 3.8.0 + * @function + * @param {Integer} index The 0-based invocation index. + * @return {Object?} + */ + this.thisFor = function(index) { + const call = calls[index]; + return call ? call.object : undefined; + }; + + /** + * Get the raw calls array for this spy. + * @name Spy#calls#all + * @since 2.0.0 + * @function + * @return {Spy.callData[]} + */ + this.all = function() { + return calls; + }; + + /** + * Get all of the arguments for each invocation of this spy in the order they were received. + * @name Spy#calls#allArgs + * @since 2.0.0 + * @function + * @return {Array} + */ + this.allArgs = function() { + return calls.map(c => c.args); + }; + + /** + * Get the first invocation of this spy. + * @name Spy#calls#first + * @since 2.0.0 + * @function + * @return {ObjecSpy.callData} + */ + this.first = function() { + return calls[0]; + }; + + /** + * Get the most recent invocation of this spy. + * @name Spy#calls#mostRecent + * @since 2.0.0 + * @function + * @return {ObjecSpy.callData} + */ + this.mostRecent = function() { + return calls[calls.length - 1]; + }; + + /** + * Reset this spy as if it has never been called. + * @name Spy#calls#reset + * @since 2.0.0 + * @function + */ + this.reset = function() { + calls = []; + }; + + /** + * Set this spy to do a shallow clone of arguments passed to each invocation. + * @name Spy#calls#saveArgumentsByValue + * @since 2.5.0 + * @function + */ + this.saveArgumentsByValue = function() { + opts.cloneArgs = true; + }; + } + + return CallTracker; +}; + +getJasmineRequireObj().clearStack = function(j$) { + const maxInlineCallCount = 10; + + function browserQueueMicrotaskImpl(global) { + const { setTimeout, queueMicrotask } = global; + let currentCallCount = 0; + return function clearStack(fn) { + currentCallCount++; + + if (currentCallCount < maxInlineCallCount) { + queueMicrotask(fn); + } else { + currentCallCount = 0; + setTimeout(fn); + } + }; + } + + function nodeQueueMicrotaskImpl(global) { + const { queueMicrotask } = global; + + return function(fn) { + queueMicrotask(fn); + }; + } + + function messageChannelImpl(global) { + const { MessageChannel, setTimeout } = global; + const channel = new MessageChannel(); + let head = {}; + let tail = head; + + let taskRunning = false; + channel.port1.onmessage = function() { + head = head.next; + const task = head.task; + delete head.task; + + if (taskRunning) { + setTimeout(task, 0); + } else { + try { + taskRunning = true; + task(); + } finally { + taskRunning = false; + } + } + }; + + let currentCallCount = 0; + return function clearStack(fn) { + currentCallCount++; + + if (currentCallCount < maxInlineCallCount) { + tail = tail.next = { task: fn }; + channel.port2.postMessage(0); + } else { + currentCallCount = 0; + setTimeout(fn); + } + }; + } + + function getClearStack(global) { + const NODE_JS = + global.process && + global.process.versions && + typeof global.process.versions.node === 'string'; + + const SAFARI = + global.navigator && + /^((?!chrome|android).)*safari/i.test(global.navigator.userAgent); + + if (NODE_JS) { + // Unlike browsers, Node doesn't require us to do a periodic setTimeout + // so we avoid the overhead. + return nodeQueueMicrotaskImpl(global); + } else if ( + SAFARI || + j$.util.isUndefined(global.MessageChannel) /* tests */ + ) { + // queueMicrotask is dramatically faster than MessageChannel in Safari, + // at least through version 16. + // Some of our own integration tests provide a mock queueMicrotask in all + // environments because it's simpler to mock than MessageChannel. + return browserQueueMicrotaskImpl(global); + } else { + // MessageChannel is faster than queueMicrotask in supported browsers + // other than Safari. + return messageChannelImpl(global); + } + } + + return getClearStack; +}; + +getJasmineRequireObj().Clock = function() { + /* global process */ + const NODE_JS = + typeof process !== 'undefined' && + process.versions && + typeof process.versions.node === 'string'; + + /** + * @class Clock + * @since 1.3.0 + * @classdesc Jasmine's mock clock is used when testing time dependent code.
    + * _Note:_ Do not construct this directly. You can get the current clock with + * {@link jasmine.clock}. + * @hideconstructor + */ + function Clock(global, delayedFunctionSchedulerFactory, mockDate) { + const realTimingFunctions = { + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout, + setInterval: global.setInterval, + clearInterval: global.clearInterval + }; + const fakeTimingFunctions = { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval + }; + let installed = false; + let delayedFunctionScheduler; + let timer; + + this.FakeTimeout = FakeTimeout; + + /** + * Install the mock clock over the built-in methods. + * @name Clock#install + * @since 2.0.0 + * @function + * @return {Clock} + */ + this.install = function() { + if (!originalTimingFunctionsIntact()) { + throw new Error( + 'Jasmine Clock was unable to install over custom global timer functions. Is the clock already installed?' + ); + } + replace(global, fakeTimingFunctions); + timer = fakeTimingFunctions; + delayedFunctionScheduler = delayedFunctionSchedulerFactory(); + installed = true; + + return this; + }; + + /** + * Uninstall the mock clock, returning the built-in methods to their places. + * @name Clock#uninstall + * @since 2.0.0 + * @function + */ + this.uninstall = function() { + delayedFunctionScheduler = null; + mockDate.uninstall(); + replace(global, realTimingFunctions); + + timer = realTimingFunctions; + installed = false; + }; + + /** + * Execute a function with a mocked Clock + * + * The clock will be {@link Clock#install|install}ed before the function is called and {@link Clock#uninstall|uninstall}ed in a `finally` after the function completes. + * @name Clock#withMock + * @since 2.3.0 + * @function + * @param {Function} closure The function to be called. + */ + this.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; + + /** + * Instruct the installed Clock to also mock the date returned by `new Date()` + * @name Clock#mockDate + * @since 2.1.0 + * @function + * @param {Date} [initialDate=now] The `Date` to provide. + */ + this.mockDate = function(initialDate) { + mockDate.install(initialDate); + }; + + this.setTimeout = function(fn, delay, params) { + return Function.prototype.apply.apply(timer.setTimeout, [ + global, + arguments + ]); + }; + + this.setInterval = function(fn, delay, params) { + return Function.prototype.apply.apply(timer.setInterval, [ + global, + arguments + ]); + }; + + this.clearTimeout = function(id) { + return Function.prototype.call.apply(timer.clearTimeout, [global, id]); + }; + + this.clearInterval = function(id) { + return Function.prototype.call.apply(timer.clearInterval, [global, id]); + }; + + /** + * Tick the Clock forward, running any enqueued timeouts along the way + * @name Clock#tick + * @since 1.3.0 + * @function + * @param {int} millis The number of milliseconds to tick. + */ + this.tick = function(millis) { + if (installed) { + delayedFunctionScheduler.tick(millis, function(millis) { + mockDate.tick(millis); + }); + } else { + throw new Error( + 'Mock clock is not installed, use jasmine.clock().install()' + ); + } + }; + + return this; + + function originalTimingFunctionsIntact() { + return ( + global.setTimeout === realTimingFunctions.setTimeout && + global.clearTimeout === realTimingFunctions.clearTimeout && + global.setInterval === realTimingFunctions.setInterval && + global.clearInterval === realTimingFunctions.clearInterval + ); + } + + function replace(dest, source) { + for (const prop in source) { + dest[prop] = source[prop]; + } + } + + function setTimeout(fn, delay) { + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction( + fn, + delay, + argSlice(arguments, 2) + ); + } + + const timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction( + fn, + delay, + argSlice(arguments, 2), + false, + timeout + ); + + return timeout; + } + + function clearTimeout(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function setInterval(fn, interval) { + if (!NODE_JS) { + return delayedFunctionScheduler.scheduleFunction( + fn, + interval, + argSlice(arguments, 2), + true + ); + } + + const timeout = new FakeTimeout(); + + delayedFunctionScheduler.scheduleFunction( + fn, + interval, + argSlice(arguments, 2), + true, + timeout + ); + + return timeout; + } + + function clearInterval(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function argSlice(argsObj, n) { + return Array.prototype.slice.call(argsObj, n); + } + } + + /** + * Mocks Node.js Timeout class + */ + function FakeTimeout() {} + + FakeTimeout.prototype.ref = function() { + return this; + }; + + FakeTimeout.prototype.unref = function() { + return this; + }; + + return Clock; +}; + +getJasmineRequireObj().CompleteOnFirstErrorSkipPolicy = function(j$) { + function CompleteOnFirstErrorSkipPolicy(queueableFns) { + this.queueableFns_ = queueableFns; + this.erroredFnIx_ = null; + } + + CompleteOnFirstErrorSkipPolicy.prototype.skipTo = function(lastRanFnIx) { + let i; + + for ( + i = lastRanFnIx + 1; + i < this.queueableFns_.length && this.shouldSkip_(i); + i++ + ) {} + return i; + }; + + CompleteOnFirstErrorSkipPolicy.prototype.fnErrored = function(fnIx) { + this.erroredFnIx_ = fnIx; + }; + + CompleteOnFirstErrorSkipPolicy.prototype.shouldSkip_ = function(fnIx) { + if (this.erroredFnIx_ === null) { + return false; + } + + const fn = this.queueableFns_[fnIx]; + const candidateSuite = fn.suite; + const errorSuite = this.queueableFns_[this.erroredFnIx_].suite; + const wasCleanupFn = + fn.type === 'afterEach' || + fn.type === 'afterAll' || + fn.type === 'specCleanup'; + return ( + !wasCleanupFn || + (candidateSuite && isDescendent(candidateSuite, errorSuite)) + ); + }; + + function isDescendent(candidate, ancestor) { + if (!candidate.parentSuite) { + return false; + } else if (candidate.parentSuite === ancestor) { + return true; + } else { + return isDescendent(candidate.parentSuite, ancestor); + } + } + + return CompleteOnFirstErrorSkipPolicy; +}; + +getJasmineRequireObj().DelayedFunctionScheduler = function(j$) { + function DelayedFunctionScheduler() { + this.scheduledLookup_ = []; + this.scheduledFunctions_ = {}; + this.currentTime_ = 0; + this.delayedFnCount_ = 0; + this.deletedKeys_ = []; + + this.tick = function(millis, tickDate) { + millis = millis || 0; + const endTime = this.currentTime_ + millis; + + this.runScheduledFunctions_(endTime, tickDate); + }; + + this.scheduleFunction = function( + funcToCall, + millis, + params, + recurring, + timeoutKey, + runAtMillis + ) { + let f; + if (typeof funcToCall === 'string') { + f = function() { + // eslint-disable-next-line no-eval + return eval(funcToCall); + }; + } else { + f = funcToCall; + } + + millis = millis || 0; + timeoutKey = timeoutKey || ++this.delayedFnCount_; + runAtMillis = runAtMillis || this.currentTime_ + millis; + + const funcToSchedule = { + runAtMillis: runAtMillis, + funcToCall: f, + recurring: recurring, + params: params, + timeoutKey: timeoutKey, + millis: millis + }; + + if (runAtMillis in this.scheduledFunctions_) { + this.scheduledFunctions_[runAtMillis].push(funcToSchedule); + } else { + this.scheduledFunctions_[runAtMillis] = [funcToSchedule]; + this.scheduledLookup_.push(runAtMillis); + this.scheduledLookup_.sort(function(a, b) { + return a - b; + }); + } + + return timeoutKey; + }; + + this.removeFunctionWithId = function(timeoutKey) { + this.deletedKeys_.push(timeoutKey); + + for (const runAtMillis in this.scheduledFunctions_) { + const funcs = this.scheduledFunctions_[runAtMillis]; + const i = indexOfFirstToPass(funcs, function(func) { + return func.timeoutKey === timeoutKey; + }); + + if (i > -1) { + if (funcs.length === 1) { + delete this.scheduledFunctions_[runAtMillis]; + this.deleteFromLookup_(runAtMillis); + } else { + funcs.splice(i, 1); + } + + // intervals get rescheduled when executed, so there's never more + // than a single scheduled function with a given timeoutKey + break; + } + } + }; + + return this; + } + + DelayedFunctionScheduler.prototype.runScheduledFunctions_ = function( + endTime, + tickDate + ) { + tickDate = tickDate || function() {}; + if ( + this.scheduledLookup_.length === 0 || + this.scheduledLookup_[0] > endTime + ) { + if (endTime >= this.currentTime_) { + tickDate(endTime - this.currentTime_); + this.currentTime_ = endTime; + } + return; + } + + do { + this.deletedKeys_ = []; + const newCurrentTime = this.scheduledLookup_.shift(); + if (newCurrentTime >= this.currentTime_) { + tickDate(newCurrentTime - this.currentTime_); + this.currentTime_ = newCurrentTime; + } + + const funcsToRun = this.scheduledFunctions_[this.currentTime_]; + + delete this.scheduledFunctions_[this.currentTime_]; + + for (const fn of funcsToRun) { + if (fn.recurring) { + this.reschedule_(fn); + } + } + + for (const fn of funcsToRun) { + if (this.deletedKeys_.includes(fn.timeoutKey)) { + // skip a timeoutKey deleted whilst we were running + return; + } + fn.funcToCall.apply(null, fn.params || []); + } + this.deletedKeys_ = []; + } while ( + this.scheduledLookup_.length > 0 && + // checking first if we're out of time prevents setTimeout(0) + // scheduled in a funcToRun from forcing an extra iteration + this.currentTime_ !== endTime && + this.scheduledLookup_[0] <= endTime + ); + + // ran out of functions to call, but still time left on the clock + if (endTime >= this.currentTime_) { + tickDate(endTime - this.currentTime_); + this.currentTime_ = endTime; + } + }; + + DelayedFunctionScheduler.prototype.reschedule_ = function(scheduledFn) { + this.scheduleFunction( + scheduledFn.funcToCall, + scheduledFn.millis, + scheduledFn.params, + true, + scheduledFn.timeoutKey, + scheduledFn.runAtMillis + scheduledFn.millis + ); + }; + + DelayedFunctionScheduler.prototype.deleteFromLookup_ = function(key) { + const value = Number(key); + const i = indexOfFirstToPass(this.scheduledLookup_, function(millis) { + return millis === value; + }); + + if (i > -1) { + this.scheduledLookup_.splice(i, 1); + } + }; + + function indexOfFirstToPass(array, testFn) { + let index = -1; + + for (let i = 0; i < array.length; ++i) { + if (testFn(array[i])) { + index = i; + break; + } + } + + return index; + } + + return DelayedFunctionScheduler; +}; + +getJasmineRequireObj().Deprecator = function(j$) { + function Deprecator(topSuite) { + this.topSuite_ = topSuite; + this.verbose_ = false; + this.toSuppress_ = []; + } + + const verboseNote = + 'Note: This message will be shown only once. Set the verboseDeprecations ' + + 'config property to true to see every occurrence.'; + + Deprecator.prototype.verboseDeprecations = function(enabled) { + this.verbose_ = enabled; + }; + + // runnable is a spec or a suite. + // deprecation is a string or an Error. + // See Env#deprecated for a description of the options argument. + Deprecator.prototype.addDeprecationWarning = function( + runnable, + deprecation, + options + ) { + options = options || {}; + + if (!this.verbose_ && !j$.isError_(deprecation)) { + if (this.toSuppress_.indexOf(deprecation) !== -1) { + return; + } + this.toSuppress_.push(deprecation); + } + + this.log_(runnable, deprecation, options); + this.report_(runnable, deprecation, options); + }; + + Deprecator.prototype.log_ = function(runnable, deprecation, options) { + if (j$.isError_(deprecation)) { + console.error(deprecation); + return; + } + + let context; + + if (runnable === this.topSuite_ || options.ignoreRunnable) { + context = ''; + } else if (runnable.children) { + context = ' (in suite: ' + runnable.getFullName() + ')'; + } else { + context = ' (in spec: ' + runnable.getFullName() + ')'; + } + + if (!options.omitStackTrace) { + context += '\n' + this.stackTrace_(); + } + + if (!this.verbose_) { + context += '\n' + verboseNote; + } + + console.error('DEPRECATION: ' + deprecation + context); + }; + + Deprecator.prototype.stackTrace_ = function() { + const formatter = new j$.ExceptionFormatter(); + return formatter.stack(j$.util.errorWithStack()).replace(/^Error\n/m, ''); + }; + + Deprecator.prototype.report_ = function(runnable, deprecation, options) { + if (options.ignoreRunnable) { + runnable = this.topSuite_; + } + + if (j$.isError_(deprecation)) { + runnable.addDeprecationWarning(deprecation); + return; + } + + if (!this.verbose_) { + deprecation += '\n' + verboseNote; + } + + runnable.addDeprecationWarning({ + message: deprecation, + omitStackTrace: options.omitStackTrace || false + }); + }; + + return Deprecator; +}; + +getJasmineRequireObj().errors = function() { + function ExpectationFailed() {} + + ExpectationFailed.prototype = new Error(); + ExpectationFailed.prototype.constructor = ExpectationFailed; + + return { + ExpectationFailed: ExpectationFailed + }; +}; + +getJasmineRequireObj().ExceptionFormatter = function(j$) { + const ignoredProperties = [ + 'name', + 'message', + 'stack', + 'fileName', + 'sourceURL', + 'line', + 'lineNumber', + 'column', + 'description', + 'jasmineMessage' + ]; + + function ExceptionFormatter(options) { + const jasmineFile = + (options && options.jasmineFile) || j$.util.jasmineFile(); + this.message = function(error) { + let message = ''; + + if (error.jasmineMessage) { + message += error.jasmineMessage; + } else if (error.name && error.message) { + message += error.name + ': ' + error.message; + } else if (error.message) { + message += error.message; + } else { + message += error.toString() + ' thrown'; + } + + if (error.fileName || error.sourceURL) { + message += ' in ' + (error.fileName || error.sourceURL); + } + + if (error.line || error.lineNumber) { + message += ' (line ' + (error.line || error.lineNumber) + ')'; + } + + return message; + }; + + this.stack = function(error, { omitMessage } = {}) { + if (!error || !error.stack) { + return null; + } + + const lines = this.stack_(error, { + messageHandling: omitMessage ? 'omit' : undefined + }); + return lines.join('\n'); + }; + + // messageHandling can be falsy (unspecified), 'omit', or 'require' + this.stack_ = function(error, { messageHandling }) { + let lines = formatProperties(error).split('\n'); + + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + const stackTrace = new j$.StackTrace(error); + lines = lines.concat(filterJasmine(stackTrace)); + + if (messageHandling === 'require') { + lines.unshift(stackTrace.message || 'Error: ' + error.message); + } else if (messageHandling !== 'omit' && stackTrace.message) { + lines.unshift(stackTrace.message); + } + + if (error.cause) { + const substack = this.stack_(error.cause, { + messageHandling: 'require' + }); + substack[0] = 'Caused by: ' + substack[0]; + lines = lines.concat(substack); + } + + return lines; + }; + + function filterJasmine(stackTrace) { + const result = []; + const jasmineMarker = + stackTrace.style === 'webkit' ? '' : ' at '; + + stackTrace.frames.forEach(function(frame) { + if (frame.file !== jasmineFile) { + result.push(frame.raw); + } else if (result[result.length - 1] !== jasmineMarker) { + result.push(jasmineMarker); + } + }); + + return result; + } + + function formatProperties(error) { + if (!(error instanceof Object)) { + return; + } + + const result = {}; + let empty = true; + + for (const prop in error) { + if (ignoredProperties.includes(prop)) { + continue; + } + result[prop] = error[prop]; + empty = false; + } + + if (!empty) { + return 'error properties: ' + j$.basicPrettyPrinter_(result) + '\n'; + } + + return ''; + } + } + + return ExceptionFormatter; +}; + +getJasmineRequireObj().Expectation = function(j$) { + /** + * Matchers that come with Jasmine out of the box. + * @namespace matchers + */ + function Expectation(options) { + this.expector = new j$.Expector(options); + + const customMatchers = options.customMatchers || {}; + for (const matcherName in customMatchers) { + this[matcherName] = wrapSyncCompare( + matcherName, + customMatchers[matcherName] + ); + } + } + + /** + * Add some context for an {@link expect} + * @function + * @name matchers#withContext + * @since 3.3.0 + * @param {String} message - Additional context to show when the matcher fails + * @return {matchers} + */ + Expectation.prototype.withContext = function withContext(message) { + return addFilter(this, new ContextAddingFilter(message)); + }; + + /** + * Invert the matcher following this {@link expect} + * @member + * @name matchers#not + * @since 1.3.0 + * @type {matchers} + * @example + * expect(something).not.toBe(true); + */ + Object.defineProperty(Expectation.prototype, 'not', { + get: function() { + return addFilter(this, syncNegatingFilter); + } + }); + + /** + * Asynchronous matchers that operate on an actual value which is a promise, + * and return a promise. + * + * Most async matchers will wait indefinitely for the promise to be resolved + * or rejected, resulting in a spec timeout if that never happens. If you + * expect that the promise will already be resolved or rejected at the time + * the matcher is called, you can use the {@link async-matchers#already} + * modifier to get a faster failure with a more helpful message. + * + * Note: Specs must await the result of each async matcher, return the + * promise returned by the matcher, or return a promise that's derived from + * the one returned by the matcher. Otherwise the matcher will not be + * evaluated before the spec completes. + * + * @example + * // Good + * await expectAsync(aPromise).toBeResolved(); + * @example + * // Good + * return expectAsync(aPromise).toBeResolved(); + * @example + * // Good + * return expectAsync(aPromise).toBeResolved() + * .then(function() { + * // more spec code + * }); + * @example + * // Bad + * expectAsync(aPromise).toBeResolved(); + * @namespace async-matchers + */ + function AsyncExpectation(options) { + this.expector = new j$.Expector(options); + + const customAsyncMatchers = options.customAsyncMatchers || {}; + for (const matcherName in customAsyncMatchers) { + this[matcherName] = wrapAsyncCompare( + matcherName, + customAsyncMatchers[matcherName] + ); + } + } + + /** + * Add some context for an {@link expectAsync} + * @function + * @name async-matchers#withContext + * @since 3.3.0 + * @param {String} message - Additional context to show when the async matcher fails + * @return {async-matchers} + */ + AsyncExpectation.prototype.withContext = function withContext(message) { + return addFilter(this, new ContextAddingFilter(message)); + }; + + /** + * Invert the matcher following this {@link expectAsync} + * @member + * @name async-matchers#not + * @type {async-matchers} + * @example + * await expectAsync(myPromise).not.toBeResolved(); + * @example + * return expectAsync(myPromise).not.toBeResolved(); + */ + Object.defineProperty(AsyncExpectation.prototype, 'not', { + get: function() { + return addFilter(this, asyncNegatingFilter); + } + }); + + /** + * Fail as soon as possible if the actual is pending. + * Otherwise evaluate the matcher. + * @member + * @name async-matchers#already + * @since 3.8.0 + * @type {async-matchers} + * @example + * await expectAsync(myPromise).already.toBeResolved(); + * @example + * return expectAsync(myPromise).already.toBeResolved(); + */ + Object.defineProperty(AsyncExpectation.prototype, 'already', { + get: function() { + return addFilter(this, expectSettledPromiseFilter); + } + }); + + function wrapSyncCompare(name, matcherFactory) { + return function() { + const result = this.expector.compare(name, matcherFactory, arguments); + this.expector.processResult(result); + }; + } + + function wrapAsyncCompare(name, matcherFactory) { + return function() { + // Capture the call stack here, before we go async, so that it will contain + // frames that are relevant to the user instead of just parts of Jasmine. + const errorForStack = j$.util.errorWithStack(); + + return this.expector + .compare(name, matcherFactory, arguments) + .then(result => { + this.expector.processResult(result, errorForStack); + }); + }; + } + + function addCoreMatchers(prototype, matchers, wrapper) { + for (const matcherName in matchers) { + const matcher = matchers[matcherName]; + prototype[matcherName] = wrapper(matcherName, matcher); + } + } + + function addFilter(source, filter) { + const result = Object.create(source); + result.expector = source.expector.addFilter(filter); + return result; + } + + function negatedFailureMessage(result, matcherName, args, matchersUtil) { + if (result.message) { + if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } + } + + args = args.slice(); + args.unshift(true); + args.unshift(matcherName); + return matchersUtil.buildFailureMessage.apply(matchersUtil, args); + } + + function negate(result) { + result.pass = !result.pass; + return result; + } + + const syncNegatingFilter = { + selectComparisonFunc: function(matcher) { + function defaultNegativeCompare() { + return negate(matcher.compare.apply(null, arguments)); + } + + return matcher.negativeCompare || defaultNegativeCompare; + }, + buildFailureMessage: negatedFailureMessage + }; + + const asyncNegatingFilter = { + selectComparisonFunc: function(matcher) { + function defaultNegativeCompare() { + return matcher.compare.apply(this, arguments).then(negate); + } + + return matcher.negativeCompare || defaultNegativeCompare; + }, + buildFailureMessage: negatedFailureMessage + }; + + const expectSettledPromiseFilter = { + selectComparisonFunc: function(matcher) { + return function(actual) { + const matcherArgs = arguments; + + return j$.isPending_(actual).then(function(isPending) { + if (isPending) { + return { + pass: false, + message: + 'Expected a promise to be settled (via ' + + 'expectAsync(...).already) but it was pending.' + }; + } else { + return matcher.compare.apply(null, matcherArgs); + } + }); + }; + } + }; + + function ContextAddingFilter(message) { + this.message = message; + } + + ContextAddingFilter.prototype.modifyFailureMessage = function(msg) { + if (msg.indexOf('\n') === -1) { + return this.message + ': ' + msg; + } else { + return this.message + ':\n' + indent(msg); + } + }; + + function indent(s) { + return s.replace(/^/gm, ' '); + } + + return { + factory: function(options) { + return new Expectation(options || {}); + }, + addCoreMatchers: function(matchers) { + addCoreMatchers(Expectation.prototype, matchers, wrapSyncCompare); + }, + asyncFactory: function(options) { + return new AsyncExpectation(options || {}); + }, + addAsyncCoreMatchers: function(matchers) { + addCoreMatchers(AsyncExpectation.prototype, matchers, wrapAsyncCompare); + } + }; +}; + +getJasmineRequireObj().ExpectationFilterChain = function() { + function ExpectationFilterChain(maybeFilter, prev) { + this.filter_ = maybeFilter; + this.prev_ = prev; + } + + ExpectationFilterChain.prototype.addFilter = function(filter) { + return new ExpectationFilterChain(filter, this); + }; + + ExpectationFilterChain.prototype.selectComparisonFunc = function(matcher) { + return this.callFirst_('selectComparisonFunc', arguments).result; + }; + + ExpectationFilterChain.prototype.buildFailureMessage = function( + result, + matcherName, + args, + matchersUtil + ) { + return this.callFirst_('buildFailureMessage', arguments).result; + }; + + ExpectationFilterChain.prototype.modifyFailureMessage = function(msg) { + const result = this.callFirst_('modifyFailureMessage', arguments).result; + return result || msg; + }; + + ExpectationFilterChain.prototype.callFirst_ = function(fname, args) { + if (this.prev_) { + const prevResult = this.prev_.callFirst_(fname, args); + + if (prevResult.found) { + return prevResult; + } + } + + if (this.filter_ && this.filter_[fname]) { + return { + found: true, + result: this.filter_[fname].apply(this.filter_, args) + }; + } + + return { found: false }; + }; + + return ExpectationFilterChain; +}; + +getJasmineRequireObj().Expector = function(j$) { + function Expector(options) { + this.matchersUtil = options.matchersUtil || { + buildFailureMessage: function() {} + }; + this.actual = options.actual; + this.addExpectationResult = options.addExpectationResult || function() {}; + this.filters = new j$.ExpectationFilterChain(); + } + + Expector.prototype.instantiateMatcher = function( + matcherName, + matcherFactory, + args + ) { + this.matcherName = matcherName; + this.args = Array.prototype.slice.call(args, 0); + this.expected = this.args.slice(0); + + this.args.unshift(this.actual); + + const matcher = matcherFactory(this.matchersUtil); + + const comparisonFunc = this.filters.selectComparisonFunc(matcher); + return comparisonFunc || matcher.compare; + }; + + Expector.prototype.buildMessage = function(result) { + if (result.pass) { + return ''; + } + + const defaultMessage = () => { + if (!result.message) { + const args = this.args.slice(); + args.unshift(false); + args.unshift(this.matcherName); + return this.matchersUtil.buildFailureMessage.apply( + this.matchersUtil, + args + ); + } else if (j$.isFunction_(result.message)) { + return result.message(); + } else { + return result.message; + } + }; + + const msg = this.filters.buildFailureMessage( + result, + this.matcherName, + this.args, + this.matchersUtil, + defaultMessage + ); + return this.filters.modifyFailureMessage(msg || defaultMessage()); + }; + + Expector.prototype.compare = function(matcherName, matcherFactory, args) { + const matcherCompare = this.instantiateMatcher( + matcherName, + matcherFactory, + args + ); + return matcherCompare.apply(null, this.args); + }; + + Expector.prototype.addFilter = function(filter) { + const result = Object.create(this); + result.filters = this.filters.addFilter(filter); + return result; + }; + + Expector.prototype.processResult = function(result, errorForStack) { + const message = this.buildMessage(result); + + if (this.expected.length === 1) { + this.expected = this.expected[0]; + } + + this.addExpectationResult(result.pass, { + matcherName: this.matcherName, + passed: result.pass, + message: message, + error: errorForStack ? undefined : result.error, + errorForStack: errorForStack || undefined, + actual: this.actual, + expected: this.expected // TODO: this may need to be arrayified/sliced + }); + }; + + return Expector; +}; + +getJasmineRequireObj().formatErrorMsg = function() { + function generateErrorMsg(domain, usage) { + const usageDefinition = usage ? '\nUsage: ' + usage : ''; + + return function errorMsg(msg) { + return domain + ' : ' + msg + usageDefinition; + }; + } + + return generateErrorMsg; +}; + +getJasmineRequireObj().GlobalErrors = function(j$) { + function GlobalErrors(global) { + global = global || j$.getGlobal(); + + const handlers = []; + let overrideHandler = null, + onRemoveOverrideHandler = null; + + function onerror(message, source, lineno, colno, error) { + if (overrideHandler) { + overrideHandler(error || message); + return; + } + + const handler = handlers[handlers.length - 1]; + + if (handler) { + handler.apply(null, Array.prototype.slice.call(arguments, 0)); + } else { + throw arguments[0]; + } + } + + this.originalHandlers = {}; + this.jasmineHandlers = {}; + this.installOne_ = function installOne_(errorType, jasmineMessage) { + function taggedOnError(error) { + if (j$.isError_(error)) { + error.jasmineMessage = jasmineMessage + ': ' + error; + } else { + let substituteMsg; + + if (error) { + substituteMsg = jasmineMessage + ': ' + error; + } else { + substituteMsg = jasmineMessage + ' with no error or message'; + } + + if (errorType === 'unhandledRejection') { + substituteMsg += + '\n' + + '(Tip: to get a useful stack trace, use ' + + 'Promise.reject(new Error(...)) instead of Promise.reject(' + + (error ? '...' : '') + + ').)'; + } + + error = new Error(substituteMsg); + } + + const handler = handlers[handlers.length - 1]; + + if (overrideHandler) { + overrideHandler(error); + return; + } + + if (handler) { + handler(error); + } else { + throw error; + } + } + + this.originalHandlers[errorType] = global.process.listeners(errorType); + this.jasmineHandlers[errorType] = taggedOnError; + + global.process.removeAllListeners(errorType); + global.process.on(errorType, taggedOnError); + + this.uninstall = function uninstall() { + const errorTypes = Object.keys(this.originalHandlers); + for (const errorType of errorTypes) { + global.process.removeListener( + errorType, + this.jasmineHandlers[errorType] + ); + + for (let i = 0; i < this.originalHandlers[errorType].length; i++) { + global.process.on(errorType, this.originalHandlers[errorType][i]); + } + delete this.originalHandlers[errorType]; + delete this.jasmineHandlers[errorType]; + } + }; + }; + + this.install = function install() { + if ( + global.process && + global.process.listeners && + j$.isFunction_(global.process.on) + ) { + this.installOne_('uncaughtException', 'Uncaught exception'); + this.installOne_('unhandledRejection', 'Unhandled promise rejection'); + } else { + const originalHandler = global.onerror; + global.onerror = onerror; + + const browserRejectionHandler = function browserRejectionHandler( + event + ) { + if (j$.isError_(event.reason)) { + event.reason.jasmineMessage = + 'Unhandled promise rejection: ' + event.reason; + global.onerror(event.reason); + } else { + global.onerror('Unhandled promise rejection: ' + event.reason); + } + }; + + global.addEventListener('unhandledrejection', browserRejectionHandler); + + this.uninstall = function uninstall() { + global.onerror = originalHandler; + global.removeEventListener( + 'unhandledrejection', + browserRejectionHandler + ); + }; + } + }; + + this.pushListener = function pushListener(listener) { + handlers.push(listener); + }; + + this.popListener = function popListener(listener) { + if (!listener) { + throw new Error('popListener expects a listener'); + } + + handlers.pop(); + }; + + this.setOverrideListener = function(listener, onRemove) { + if (overrideHandler) { + throw new Error("Can't set more than one override listener at a time"); + } + + overrideHandler = listener; + onRemoveOverrideHandler = onRemove; + }; + + this.removeOverrideListener = function() { + if (onRemoveOverrideHandler) { + onRemoveOverrideHandler(); + } + + overrideHandler = null; + onRemoveOverrideHandler = null; + }; + } + + return GlobalErrors; +}; + +getJasmineRequireObj().toBePending = function(j$) { + /** + * Expect a promise to be pending, i.e. the promise is neither resolved nor rejected. + * @function + * @async + * @name async-matchers#toBePending + * @since 3.6 + * @example + * await expectAsync(aPromise).toBePending(); + */ + return function toBePending() { + return { + compare: function(actual) { + if (!j$.isPromiseLike(actual)) { + throw new Error('Expected toBePending to be called on a promise.'); + } + const want = {}; + return Promise.race([actual, Promise.resolve(want)]).then( + function(got) { + return { pass: want === got }; + }, + function() { + return { pass: false }; + } + ); + } + }; + }; +}; + +getJasmineRequireObj().toBeRejected = function(j$) { + /** + * Expect a promise to be rejected. + * @function + * @async + * @name async-matchers#toBeRejected + * @since 3.1.0 + * @example + * await expectAsync(aPromise).toBeRejected(); + * @example + * return expectAsync(aPromise).toBeRejected(); + */ + return function toBeRejected() { + return { + compare: function(actual) { + if (!j$.isPromiseLike(actual)) { + throw new Error('Expected toBeRejected to be called on a promise.'); + } + return actual.then( + function() { + return { pass: false }; + }, + function() { + return { pass: true }; + } + ); + } + }; + }; +}; + +getJasmineRequireObj().toBeRejectedWith = function(j$) { + /** + * Expect a promise to be rejected with a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeRejectedWith + * @since 3.3.0 + * @param {Object} expected - Value that the promise is expected to be rejected with + * @example + * await expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeRejectedWith({prop: 'value'}); + */ + return function toBeRejectedWith(matchersUtil) { + return { + compare: function(actualPromise, expectedValue) { + if (!j$.isPromiseLike(actualPromise)) { + throw new Error( + 'Expected toBeRejectedWith to be called on a promise.' + ); + } + + function prefix(passed) { + return ( + 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be rejected with ' + + matchersUtil.pp(expectedValue) + ); + } + + return actualPromise.then( + function() { + return { + pass: false, + message: prefix(false) + ' but it was resolved.' + }; + }, + function(actualValue) { + if (matchersUtil.equals(actualValue, expectedValue)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: + prefix(false) + + ' but it was rejected with ' + + matchersUtil.pp(actualValue) + + '.' + }; + } + } + ); + } + }; + }; +}; + +getJasmineRequireObj().toBeRejectedWithError = function(j$) { + /** + * Expect a promise to be rejected with a value matched to the expected + * @function + * @async + * @name async-matchers#toBeRejectedWithError + * @since 3.5.0 + * @param {Error} [expected] - `Error` constructor the object that was thrown needs to be an instance of. If not provided, `Error` will be used. + * @param {RegExp|String} [message] - The message that should be set on the thrown `Error` + * @example + * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, 'Error message'); + * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError, /Error message/); + * await expectAsync(aPromise).toBeRejectedWithError(MyCustomError); + * await expectAsync(aPromise).toBeRejectedWithError('Error message'); + * return expectAsync(aPromise).toBeRejectedWithError(/Error message/); + */ + return function toBeRejectedWithError(matchersUtil) { + return { + compare: function(actualPromise, arg1, arg2) { + if (!j$.isPromiseLike(actualPromise)) { + throw new Error( + 'Expected toBeRejectedWithError to be called on a promise.' + ); + } + + const expected = getExpectedFromArgs(arg1, arg2, matchersUtil); + + return actualPromise.then( + function() { + return { + pass: false, + message: 'Expected a promise to be rejected but it was resolved.' + }; + }, + function(actualValue) { + return matchError(actualValue, expected, matchersUtil); + } + ); + } + }; + }; + + function matchError(actual, expected, matchersUtil) { + if (!j$.isError_(actual)) { + return fail(expected, 'rejected with ' + matchersUtil.pp(actual)); + } + + if (!(actual instanceof expected.error)) { + return fail( + expected, + 'rejected with type ' + j$.fnNameFor(actual.constructor) + ); + } + + const actualMessage = actual.message; + + if ( + actualMessage === expected.message || + typeof expected.message === 'undefined' + ) { + return pass(expected); + } + + if ( + expected.message instanceof RegExp && + expected.message.test(actualMessage) + ) { + return pass(expected); + } + + return fail(expected, 'rejected with ' + matchersUtil.pp(actual)); + } + + function pass(expected) { + return { + pass: true, + message: + 'Expected a promise not to be rejected with ' + + expected.printValue + + ', but it was.' + }; + } + + function fail(expected, message) { + return { + pass: false, + message: + 'Expected a promise to be rejected with ' + + expected.printValue + + ' but it was ' + + message + + '.' + }; + } + + function getExpectedFromArgs(arg1, arg2, matchersUtil) { + let error, message; + + if (isErrorConstructor(arg1)) { + error = arg1; + message = arg2; + } else { + error = Error; + message = arg1; + } + + return { + error: error, + message: message, + printValue: + j$.fnNameFor(error) + + (typeof message === 'undefined' ? '' : ': ' + matchersUtil.pp(message)) + }; + } + + function isErrorConstructor(value) { + return ( + typeof value === 'function' && + (value === Error || j$.isError_(value.prototype)) + ); + } +}; + +getJasmineRequireObj().toBeResolved = function(j$) { + /** + * Expect a promise to be resolved. + * @function + * @async + * @name async-matchers#toBeResolved + * @since 3.1.0 + * @example + * await expectAsync(aPromise).toBeResolved(); + * @example + * return expectAsync(aPromise).toBeResolved(); + */ + return function toBeResolved(matchersUtil) { + return { + compare: function(actual) { + if (!j$.isPromiseLike(actual)) { + throw new Error('Expected toBeResolved to be called on a promise.'); + } + + return actual.then( + function() { + return { pass: true }; + }, + function(e) { + return { + pass: false, + message: + 'Expected a promise to be resolved but it was ' + + 'rejected with ' + + matchersUtil.pp(e) + + '.' + }; + } + ); + } + }; + }; +}; + +getJasmineRequireObj().toBeResolvedTo = function(j$) { + /** + * Expect a promise to be resolved to a value equal to the expected, using deep equality comparison. + * @function + * @async + * @name async-matchers#toBeResolvedTo + * @since 3.1.0 + * @param {Object} expected - Value that the promise is expected to resolve to + * @example + * await expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + * @example + * return expectAsync(aPromise).toBeResolvedTo({prop: 'value'}); + */ + return function toBeResolvedTo(matchersUtil) { + return { + compare: function(actualPromise, expectedValue) { + if (!j$.isPromiseLike(actualPromise)) { + throw new Error('Expected toBeResolvedTo to be called on a promise.'); + } + + function prefix(passed) { + return ( + 'Expected a promise ' + + (passed ? 'not ' : '') + + 'to be resolved to ' + + matchersUtil.pp(expectedValue) + ); + } + + return actualPromise.then( + function(actualValue) { + if (matchersUtil.equals(actualValue, expectedValue)) { + return { + pass: true, + message: prefix(true) + '.' + }; + } else { + return { + pass: false, + message: + prefix(false) + + ' but it was resolved to ' + + matchersUtil.pp(actualValue) + + '.' + }; + } + }, + function(e) { + return { + pass: false, + message: + prefix(false) + + ' but it was rejected with ' + + matchersUtil.pp(e) + + '.' + }; + } + ); + } + }; + }; +}; + +getJasmineRequireObj().DiffBuilder = function(j$) { + class DiffBuilder { + constructor(config) { + this.prettyPrinter_ = + (config || {}).prettyPrinter || j$.makePrettyPrinter(); + this.mismatches_ = new j$.MismatchTree(); + this.path_ = new j$.ObjectPath(); + this.actualRoot_ = undefined; + this.expectedRoot_ = undefined; + } + + setRoots(actual, expected) { + this.actualRoot_ = actual; + this.expectedRoot_ = expected; + } + + recordMismatch(formatter) { + this.mismatches_.add(this.path_, formatter); + } + + getMessage() { + const messages = []; + + this.mismatches_.traverse((path, isLeaf, formatter) => { + const { actual, expected } = this.dereferencePath_(path); + + if (formatter) { + messages.push(formatter(actual, expected, path, this.prettyPrinter_)); + return true; + } + + const actualCustom = this.prettyPrinter_.customFormat_(actual); + const expectedCustom = this.prettyPrinter_.customFormat_(expected); + const useCustom = !( + j$.util.isUndefined(actualCustom) && + j$.util.isUndefined(expectedCustom) + ); + + if (useCustom) { + messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path)); + return false; // don't recurse further + } + + if (isLeaf) { + messages.push(this.defaultFormatter_(actual, expected, path)); + } + + return true; + }); + + return messages.join('\n'); + } + + withPath(pathComponent, block) { + const oldPath = this.path_; + this.path_ = this.path_.add(pathComponent); + block(); + this.path_ = oldPath; + } + + dereferencePath_(objectPath) { + let actual = this.actualRoot_; + let expected = this.expectedRoot_; + + const handleAsymmetricExpected = () => { + if ( + j$.isAsymmetricEqualityTester_(expected) && + j$.isFunction_(expected.valuesForDiff_) + ) { + const asymmetricResult = expected.valuesForDiff_( + actual, + this.prettyPrinter_ + ); + expected = asymmetricResult.self; + actual = asymmetricResult.other; + } + }; + + handleAsymmetricExpected(); + + for (const pc of objectPath.components) { + actual = actual[pc]; + expected = expected[pc]; + handleAsymmetricExpected(); + } + + return { actual: actual, expected: expected }; + } + + defaultFormatter_(actual, expected, path) { + return wrapPrettyPrinted( + this.prettyPrinter_(actual), + this.prettyPrinter_(expected), + path + ); + } + } + + function wrapPrettyPrinted(actual, expected, path) { + return ( + 'Expected ' + + path + + (path.depth() ? ' = ' : '') + + actual + + ' to equal ' + + expected + + '.' + ); + } + + return DiffBuilder; +}; + +getJasmineRequireObj().MatchersUtil = function(j$) { + /** + * @class MatchersUtil + * @classdesc Utilities for use in implementing matchers.
    + * _Note:_ Do not construct this directly. Jasmine will construct one and + * pass it to matchers and asymmetric equality testers. + * @hideconstructor + */ + function MatchersUtil(options) { + options = options || {}; + this.customTesters_ = options.customTesters || []; + /** + * Formats a value for use in matcher failure messages and similar contexts, + * taking into account the current set of custom value formatters. + * @function + * @name MatchersUtil#pp + * @since 3.6.0 + * @param {*} value The value to pretty-print + * @return {string} The pretty-printed value + */ + this.pp = options.pp || function() {}; + } + + /** + * Determines whether `haystack` contains `needle`, using the same comparison + * logic as {@link MatchersUtil#equals}. + * @function + * @name MatchersUtil#contains + * @since 2.0.0 + * @param {*} haystack The collection to search + * @param {*} needle The value to search for + * @returns {boolean} True if `needle` was found in `haystack` + */ + MatchersUtil.prototype.contains = function(haystack, needle) { + if (!haystack) { + return false; + } + + if (j$.isSet(haystack)) { + // Try .has() first. It should be faster in cases where + // needle === something in haystack. Fall back to .equals() comparison + // if that fails. + if (haystack.has(needle)) { + return true; + } + } + + if (j$.isIterable_(haystack) && !j$.isString_(haystack)) { + // Arrays, Sets, etc. + for (const candidate of haystack) { + if (this.equals(candidate, needle)) { + return true; + } + } + + return false; + } + + if (haystack.indexOf) { + // Mainly strings + return haystack.indexOf(needle) >= 0; + } + + if (j$.isNumber_(haystack.length)) { + // Objects that are shaped like arrays but aren't iterable + for (let i = 0; i < haystack.length; i++) { + if (this.equals(haystack[i], needle)) { + return true; + } + } + } + + return false; + }; + + MatchersUtil.prototype.buildFailureMessage = function() { + const args = Array.prototype.slice.call(arguments, 0), + matcherName = args[0], + isNot = args[1], + actual = args[2], + expected = args.slice(3), + englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { + return ' ' + s.toLowerCase(); + }); + + let message = + 'Expected ' + + this.pp(actual) + + (isNot ? ' not ' : ' ') + + englishyPredicate; + + if (expected.length > 0) { + for (let i = 0; i < expected.length; i++) { + if (i > 0) { + message += ','; + } + message += ' ' + this.pp(expected[i]); + } + } + + return message + '.'; + }; + + MatchersUtil.prototype.asymmetricDiff_ = function( + a, + b, + aStack, + bStack, + diffBuilder + ) { + if (j$.isFunction_(b.valuesForDiff_)) { + const values = b.valuesForDiff_(a, this.pp); + this.eq_(values.other, values.self, aStack, bStack, diffBuilder); + } else { + diffBuilder.recordMismatch(); + } + }; + + MatchersUtil.prototype.asymmetricMatch_ = function( + a, + b, + aStack, + bStack, + diffBuilder + ) { + const asymmetricA = j$.isAsymmetricEqualityTester_(a); + const asymmetricB = j$.isAsymmetricEqualityTester_(b); + + if (asymmetricA === asymmetricB) { + return undefined; + } + + let result; + + if (asymmetricA) { + result = a.asymmetricMatch(b, this); + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + } + + if (asymmetricB) { + result = b.asymmetricMatch(a, this); + if (!result) { + this.asymmetricDiff_(a, b, aStack, bStack, diffBuilder); + } + return result; + } + }; + + /** + * Determines whether two values are deeply equal to each other. + * @function + * @name MatchersUtil#equals + * @since 2.0.0 + * @param {*} a The first value to compare + * @param {*} b The second value to compare + * @returns {boolean} True if the values are equal + */ + MatchersUtil.prototype.equals = function(a, b, diffBuilder) { + diffBuilder = diffBuilder || j$.NullDiffBuilder(); + diffBuilder.setRoots(a, b); + + return this.eq_(a, b, [], [], diffBuilder); + }; + + // Equality function lovingly adapted from isEqual in + // [Underscore](http://underscorejs.org) + MatchersUtil.prototype.eq_ = function(a, b, aStack, bStack, diffBuilder) { + let result = true; + + const asymmetricResult = this.asymmetricMatch_( + a, + b, + aStack, + bStack, + diffBuilder + ); + if (!j$.util.isUndefined(asymmetricResult)) { + return asymmetricResult; + } + + for (const tester of this.customTesters_) { + const customTesterResult = tester(a, b); + if (!j$.util.isUndefined(customTesterResult)) { + if (!customTesterResult) { + diffBuilder.recordMismatch(); + } + return customTesterResult; + } + } + + if (a instanceof Error && b instanceof Error) { + result = a.message == b.message; + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + } + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + result = a !== 0 || 1 / a == 1 / b; + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + } + // A strict comparison is necessary because `null == undefined`. + if (a === null || b === null) { + result = a === b; + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + } + const className = Object.prototype.toString.call(a); + if (className != Object.prototype.toString.call(b)) { + diffBuilder.recordMismatch(); + return false; + } + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + result = a == String(b); + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + result = + a != +a ? b != +b : a === 0 && b === 0 ? 1 / a == 1 / b : a == +b; + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + result = +a == +b; + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + case '[object ArrayBuffer]': + // If we have an instance of ArrayBuffer the Uint8Array ctor + // will be defined as well + return this.eq_( + new Uint8Array(a), + new Uint8Array(b), + aStack, + bStack, + diffBuilder + ); + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return ( + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase + ); + } + if (typeof a != 'object' || typeof b != 'object') { + diffBuilder.recordMismatch(); + return false; + } + + const aIsDomNode = j$.isDomNode(a); + const bIsDomNode = j$.isDomNode(b); + if (aIsDomNode && bIsDomNode) { + // At first try to use DOM3 method isEqualNode + result = a.isEqualNode(b); + if (!result) { + diffBuilder.recordMismatch(); + } + return result; + } + if (aIsDomNode || bIsDomNode) { + diffBuilder.recordMismatch(); + return false; + } + + const aIsPromise = j$.isPromise(a); + const bIsPromise = j$.isPromise(b); + if (aIsPromise && bIsPromise) { + return a === b; + } + + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + let length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) { + return bStack[length] == b; + } + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + let size = 0; + // Recursively compare objects and arrays. + // Compare array lengths to determine if a deep comparison is necessary. + if (className == '[object Array]') { + const aLength = a.length; + const bLength = b.length; + + diffBuilder.withPath('length', function() { + if (aLength !== bLength) { + diffBuilder.recordMismatch(); + result = false; + } + }); + + for (let i = 0; i < aLength || i < bLength; i++) { + diffBuilder.withPath(i, () => { + if (i >= bLength) { + diffBuilder.recordMismatch( + actualArrayIsLongerFormatter.bind(null, this.pp) + ); + result = false; + } else { + result = + this.eq_( + i < aLength ? a[i] : void 0, + i < bLength ? b[i] : void 0, + aStack, + bStack, + diffBuilder + ) && result; + } + }); + } + if (!result) { + return false; + } + } else if (j$.isMap(a) && j$.isMap(b)) { + if (a.size != b.size) { + diffBuilder.recordMismatch(); + return false; + } + + const keysA = []; + const keysB = []; + a.forEach(function(valueA, keyA) { + keysA.push(keyA); + }); + b.forEach(function(valueB, keyB) { + keysB.push(keyB); + }); + + // For both sets of keys, check they map to equal values in both maps. + // Keep track of corresponding keys (in insertion order) in order to handle asymmetric obj keys. + const mapKeys = [keysA, keysB]; + const cmpKeys = [keysB, keysA]; + for (let i = 0; result && i < mapKeys.length; i++) { + const mapIter = mapKeys[i]; + const cmpIter = cmpKeys[i]; + + for (let j = 0; result && j < mapIter.length; j++) { + const mapKey = mapIter[j]; + const cmpKey = cmpIter[j]; + const mapValueA = a.get(mapKey); + let mapValueB; + + // Only use the cmpKey when one of the keys is asymmetric and the corresponding key matches, + // otherwise explicitly look up the mapKey in the other Map since we want keys with unique + // obj identity (that are otherwise equal) to not match. + if ( + j$.isAsymmetricEqualityTester_(mapKey) || + (j$.isAsymmetricEqualityTester_(cmpKey) && + this.eq_(mapKey, cmpKey, aStack, bStack, j$.NullDiffBuilder())) + ) { + mapValueB = b.get(cmpKey); + } else { + mapValueB = b.get(mapKey); + } + result = this.eq_( + mapValueA, + mapValueB, + aStack, + bStack, + j$.NullDiffBuilder() + ); + } + } + + if (!result) { + diffBuilder.recordMismatch(); + return false; + } + } else if (j$.isSet(a) && j$.isSet(b)) { + if (a.size != b.size) { + diffBuilder.recordMismatch(); + return false; + } + + const valuesA = []; + a.forEach(function(valueA) { + valuesA.push(valueA); + }); + const valuesB = []; + b.forEach(function(valueB) { + valuesB.push(valueB); + }); + + // For both sets, check they are all contained in the other set + const setPairs = [[valuesA, valuesB], [valuesB, valuesA]]; + const stackPairs = [[aStack, bStack], [bStack, aStack]]; + for (let i = 0; result && i < setPairs.length; i++) { + const baseValues = setPairs[i][0]; + const otherValues = setPairs[i][1]; + const baseStack = stackPairs[i][0]; + const otherStack = stackPairs[i][1]; + // For each value in the base set... + for (const baseValue of baseValues) { + let found = false; + // ... test that it is present in the other set + for (let j = 0; !found && j < otherValues.length; j++) { + const otherValue = otherValues[j]; + const prevStackSize = baseStack.length; + // compare by value equality + found = this.eq_( + baseValue, + otherValue, + baseStack, + otherStack, + j$.NullDiffBuilder() + ); + if (!found && prevStackSize !== baseStack.length) { + baseStack.splice(prevStackSize); + otherStack.splice(prevStackSize); + } + } + result = result && found; + } + } + + if (!result) { + diffBuilder.recordMismatch(); + return false; + } + } else if (j$.isURL(a) && j$.isURL(b)) { + // URLs have no enumrable properties, so the default object comparison + // would consider any two URLs to be equal. + return a.toString() === b.toString(); + } else { + // Objects with different constructors are not equivalent, but `Object`s + // or `Array`s from different frames are. + const aCtor = a.constructor, + bCtor = b.constructor; + if ( + aCtor !== bCtor && + isFunction(aCtor) && + isFunction(bCtor) && + a instanceof aCtor && + b instanceof bCtor && + !(aCtor instanceof aCtor && bCtor instanceof bCtor) + ) { + diffBuilder.recordMismatch( + constructorsAreDifferentFormatter.bind(null, this.pp) + ); + return false; + } + } + + // Deep compare objects. + const aKeys = MatchersUtil.keys(a, className == '[object Array]'); + size = aKeys.length; + + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (MatchersUtil.keys(b, className == '[object Array]').length !== size) { + diffBuilder.recordMismatch( + objectKeysAreDifferentFormatter.bind(null, this.pp) + ); + return false; + } + + for (const key of aKeys) { + // Deep compare each member + if (!j$.util.has(b, key)) { + diffBuilder.recordMismatch( + objectKeysAreDifferentFormatter.bind(null, this.pp) + ); + result = false; + continue; + } + + diffBuilder.withPath(key, () => { + if (!this.eq_(a[key], b[key], aStack, bStack, diffBuilder)) { + result = false; + } + }); + } + + if (!result) { + return false; + } + + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + + return result; + }; + + MatchersUtil.keys = function(obj, isArray) { + const allKeys = (function(o) { + const keys = []; + for (const key in o) { + if (j$.util.has(o, key)) { + keys.push(key); + } + } + + const symbols = Object.getOwnPropertySymbols(o); + for (const sym of symbols) { + if (o.propertyIsEnumerable(sym)) { + keys.push(sym); + } + } + + return keys; + })(obj); + + if (!isArray) { + return allKeys; + } + + if (allKeys.length === 0) { + return allKeys; + } + + const extraKeys = []; + for (const k of allKeys) { + if (typeof k === 'symbol' || !/^[0-9]+$/.test(k)) { + extraKeys.push(k); + } + } + + return extraKeys; + }; + + function isFunction(obj) { + return typeof obj === 'function'; + } + + // Returns an array of [k, v] pairs for eacch property that's in objA + // and not in objB. + function extraKeysAndValues(objA, objB) { + return MatchersUtil.keys(objA) + .filter(key => !j$.util.has(objB, key)) + .map(key => [key, objA[key]]); + } + + function objectKeysAreDifferentFormatter(pp, actual, expected, path) { + const missingProperties = extraKeysAndValues(expected, actual), + extraProperties = extraKeysAndValues(actual, expected), + missingPropertiesMessage = formatKeyValuePairs(pp, missingProperties), + extraPropertiesMessage = formatKeyValuePairs(pp, extraProperties), + messages = []; + + if (!path.depth()) { + path = 'object'; + } + + if (missingPropertiesMessage.length) { + messages.push( + 'Expected ' + path + ' to have properties' + missingPropertiesMessage + ); + } + + if (extraPropertiesMessage.length) { + messages.push( + 'Expected ' + path + ' not to have properties' + extraPropertiesMessage + ); + } + + return messages.join('\n'); + } + + function constructorsAreDifferentFormatter(pp, actual, expected, path) { + if (!path.depth()) { + path = 'object'; + } + + return ( + 'Expected ' + + path + + ' to be a kind of ' + + j$.fnNameFor(expected.constructor) + + ', but was ' + + pp(actual) + + '.' + ); + } + + function actualArrayIsLongerFormatter(pp, actual, expected, path) { + return ( + 'Unexpected ' + + path + + (path.depth() ? ' = ' : '') + + pp(actual) + + ' in array.' + ); + } + + function formatKeyValuePairs(pp, keyValuePairs) { + let formatted = ''; + + for (const [key, value] of keyValuePairs) { + formatted += '\n ' + key.toString() + ': ' + pp(value); + } + + return formatted; + } + + return MatchersUtil; +}; + +/** + * @interface AsymmetricEqualityTester + * @classdesc An asymmetric equality tester is an object that can match multiple + * objects. Examples include jasmine.any() and jasmine.stringMatching(). Jasmine + * includes a number of built-in asymmetric equality testers, such as + * {@link jasmine.objectContaining}. User-defined asymmetric equality testers are + * also supported. + * + * Asymmetric equality testers work with any matcher, including user-defined + * custom matchers, that uses {@link MatchersUtil#equals} or + * {@link MatchersUtil#contains}. + * + * @example + * function numberDivisibleBy(divisor) { + * return { + * asymmetricMatch: function(n) { + * return typeof n === 'number' && n % divisor === 0; + * }, + * jasmineToString: function() { + * return ``; + * } + * }; + * } + * + * const actual = { + * n: 2, + * otherFields: "don't care" + * }; + * + * expect(actual).toEqual(jasmine.objectContaining({n: numberDivisibleBy(2)})); + * @see custom_asymmetric_equality_testers + * @since 2.0.0 + */ +/** + * Determines whether a value matches this tester + * @function + * @name AsymmetricEqualityTester#asymmetricMatch + * @param value {any} The value to test + * @param matchersUtil {MatchersUtil} utilities for testing equality, etc + * @return {Boolean} + */ +/** + * Returns a string representation of this tester to use in matcher failure messages + * @function + * @name AsymmetricEqualityTester#jasmineToString + * @param pp {function} Function that takes a value and returns a pretty-printed representation + * @return {String} + */ + +getJasmineRequireObj().MismatchTree = function(j$) { + /* + To be able to apply custom object formatters at all possible levels of an + object graph, DiffBuilder needs to be able to know not just where the + mismatch occurred but also all ancestors of the mismatched value in both + the expected and actual object graphs. MismatchTree maintains that context + and provides it via the traverse method. + */ + class MismatchTree { + constructor(path) { + this.path = path || new j$.ObjectPath([]); + this.formatter = undefined; + this.children = []; + this.isMismatch = false; + } + + add(path, formatter) { + if (path.depth() === 0) { + this.formatter = formatter; + this.isMismatch = true; + } else { + const key = path.components[0]; + path = path.shift(); + let child = this.child(key); + + if (!child) { + child = new MismatchTree(this.path.add(key)); + this.children.push(child); + } + + child.add(path, formatter); + } + } + + traverse(visit) { + const hasChildren = this.children.length > 0; + + if (this.isMismatch || hasChildren) { + if (visit(this.path, !hasChildren, this.formatter)) { + for (const child of this.children) { + child.traverse(visit); + } + } + } + } + + child(key) { + return this.children.find(child => { + const pathEls = child.path.components; + return pathEls[pathEls.length - 1] === key; + }); + } + } + + return MismatchTree; +}; + +getJasmineRequireObj().nothing = function() { + /** + * {@link expect} nothing explicitly. + * @function + * @name matchers#nothing + * @since 2.8.0 + * @example + * expect().nothing(); + */ + function nothing() { + return { + compare: function() { + return { + pass: true + }; + } + }; + } + + return nothing; +}; + +getJasmineRequireObj().NullDiffBuilder = function(j$) { + return function() { + return { + withPath: function(_, block) { + block(); + }, + setRoots: function() {}, + recordMismatch: function() {} + }; + }; +}; + +getJasmineRequireObj().ObjectPath = function(j$) { + class ObjectPath { + constructor(components) { + this.components = components || []; + } + + toString() { + if (this.components.length) { + return '$' + this.components.map(formatPropertyAccess).join(''); + } else { + return ''; + } + } + + add(component) { + return new ObjectPath(this.components.concat([component])); + } + + shift() { + return new ObjectPath(this.components.slice(1)); + } + + depth() { + return this.components.length; + } + } + + function formatPropertyAccess(prop) { + if (typeof prop === 'number' || typeof prop === 'symbol') { + return '[' + prop.toString() + ']'; + } + + if (isValidIdentifier(prop)) { + return '.' + prop; + } + + return `['${prop}']`; + } + + function isValidIdentifier(string) { + return /^[A-Za-z\$_][A-Za-z0-9\$_]*$/.test(string); + } + + return ObjectPath; +}; + +getJasmineRequireObj().requireAsyncMatchers = function(jRequire, j$) { + const availableMatchers = [ + 'toBePending', + 'toBeResolved', + 'toBeRejected', + 'toBeResolvedTo', + 'toBeRejectedWith', + 'toBeRejectedWithError' + ], + matchers = {}; + + for (const name of availableMatchers) { + matchers[name] = jRequire[name](j$); + } + + return matchers; +}; + +getJasmineRequireObj().toBe = function(j$) { + /** + * {@link expect} the actual value to be `===` to the expected value. + * @function + * @name matchers#toBe + * @since 1.3.0 + * @param {Object} expected - The expected value to compare against. + * @example + * expect(thing).toBe(realThing); + */ + function toBe(matchersUtil) { + const tip = + ' Tip: To check for deep equality, use .toEqual() instead of .toBe().'; + + return { + compare: function(actual, expected) { + const result = { + pass: actual === expected + }; + + if (typeof expected === 'object') { + result.message = + matchersUtil.buildFailureMessage( + 'toBe', + result.pass, + actual, + expected + ) + tip; + } + + return result; + } + }; + } + + return toBe; +}; + +getJasmineRequireObj().toBeCloseTo = function() { + /** + * {@link expect} the actual value to be within a specified precision of the expected value. + * @function + * @name matchers#toBeCloseTo + * @since 1.3.0 + * @param {Object} expected - The expected value to compare against. + * @param {Number} [precision=2] - The number of decimal points to check. + * @example + * expect(number).toBeCloseTo(42.2, 3); + */ + function toBeCloseTo() { + return { + compare: function(actual, expected, precision) { + if (precision !== 0) { + precision = precision || 2; + } + + if (expected === null || actual === null) { + throw new Error( + 'Cannot use toBeCloseTo with null. Arguments evaluated to: ' + + 'expect(' + + actual + + ').toBeCloseTo(' + + expected + + ').' + ); + } + + // Infinity is close to Infinity and -Infinity is close to -Infinity, + // regardless of the precision. + if (expected === Infinity || expected === -Infinity) { + return { + pass: actual === expected + }; + } + + const pow = Math.pow(10, precision + 1); + const delta = Math.abs(expected - actual); + const maxDelta = Math.pow(10, -precision) / 2; + + return { + pass: Math.round(delta * pow) <= maxDelta * pow + }; + } + }; + } + + return toBeCloseTo; +}; + +getJasmineRequireObj().toBeDefined = function() { + /** + * {@link expect} the actual value to be defined. (Not `undefined`) + * @function + * @name matchers#toBeDefined + * @since 1.3.0 + * @example + * expect(result).toBeDefined(); + */ + function toBeDefined() { + return { + compare: function(actual) { + return { + pass: void 0 !== actual + }; + } + }; + } + + return toBeDefined; +}; + +getJasmineRequireObj().toBeFalse = function() { + /** + * {@link expect} the actual value to be `false`. + * @function + * @name matchers#toBeFalse + * @since 3.5.0 + * @example + * expect(result).toBeFalse(); + */ + function toBeFalse() { + return { + compare: function(actual) { + return { + pass: actual === false + }; + } + }; + } + + return toBeFalse; +}; + +getJasmineRequireObj().toBeFalsy = function() { + /** + * {@link expect} the actual value to be falsy + * @function + * @name matchers#toBeFalsy + * @since 2.0.0 + * @example + * expect(result).toBeFalsy(); + */ + function toBeFalsy() { + return { + compare: function(actual) { + return { + pass: !actual + }; + } + }; + } + + return toBeFalsy; +}; + +getJasmineRequireObj().toBeGreaterThan = function() { + /** + * {@link expect} the actual value to be greater than the expected value. + * @function + * @name matchers#toBeGreaterThan + * @since 2.0.0 + * @param {Number} expected - The value to compare against. + * @example + * expect(result).toBeGreaterThan(3); + */ + function toBeGreaterThan() { + return { + compare: function(actual, expected) { + return { + pass: actual > expected + }; + } + }; + } + + return toBeGreaterThan; +}; + +getJasmineRequireObj().toBeGreaterThanOrEqual = function() { + /** + * {@link expect} the actual value to be greater than or equal to the expected value. + * @function + * @name matchers#toBeGreaterThanOrEqual + * @since 2.0.0 + * @param {Number} expected - The expected value to compare against. + * @example + * expect(result).toBeGreaterThanOrEqual(25); + */ + function toBeGreaterThanOrEqual() { + return { + compare: function(actual, expected) { + return { + pass: actual >= expected + }; + } + }; + } + + return toBeGreaterThanOrEqual; +}; + +getJasmineRequireObj().toBeInstanceOf = function(j$) { + const usageError = j$.formatErrorMsg( + '', + 'expect(value).toBeInstanceOf()' + ); + + /** + * {@link expect} the actual to be an instance of the expected class + * @function + * @name matchers#toBeInstanceOf + * @since 3.5.0 + * @param {Object} expected - The class or constructor function to check for + * @example + * expect('foo').toBeInstanceOf(String); + * expect(3).toBeInstanceOf(Number); + * expect(new Error()).toBeInstanceOf(Error); + */ + function toBeInstanceOf(matchersUtil) { + return { + compare: function(actual, expected) { + const actualType = + actual && actual.constructor + ? j$.fnNameFor(actual.constructor) + : matchersUtil.pp(actual); + const expectedType = expected + ? j$.fnNameFor(expected) + : matchersUtil.pp(expected); + let expectedMatcher; + let pass; + + try { + expectedMatcher = new j$.Any(expected); + pass = expectedMatcher.asymmetricMatch(actual); + } catch (error) { + throw new Error( + usageError('Expected value is not a constructor function') + ); + } + + if (pass) { + return { + pass: true, + message: + 'Expected instance of ' + + actualType + + ' not to be an instance of ' + + expectedType + }; + } else { + return { + pass: false, + message: + 'Expected instance of ' + + actualType + + ' to be an instance of ' + + expectedType + }; + } + } + }; + } + + return toBeInstanceOf; +}; + +getJasmineRequireObj().toBeLessThan = function() { + /** + * {@link expect} the actual value to be less than the expected value. + * @function + * @name matchers#toBeLessThan + * @since 2.0.0 + * @param {Number} expected - The expected value to compare against. + * @example + * expect(result).toBeLessThan(0); + */ + function toBeLessThan() { + return { + compare: function(actual, expected) { + return { + pass: actual < expected + }; + } + }; + } + + return toBeLessThan; +}; + +getJasmineRequireObj().toBeLessThanOrEqual = function() { + /** + * {@link expect} the actual value to be less than or equal to the expected value. + * @function + * @name matchers#toBeLessThanOrEqual + * @since 2.0.0 + * @param {Number} expected - The expected value to compare against. + * @example + * expect(result).toBeLessThanOrEqual(123); + */ + function toBeLessThanOrEqual() { + return { + compare: function(actual, expected) { + return { + pass: actual <= expected + }; + } + }; + } + + return toBeLessThanOrEqual; +}; + +getJasmineRequireObj().toBeNaN = function(j$) { + /** + * {@link expect} the actual value to be `NaN` (Not a Number). + * @function + * @name matchers#toBeNaN + * @since 1.3.0 + * @example + * expect(thing).toBeNaN(); + */ + function toBeNaN(matchersUtil) { + return { + compare: function(actual) { + const result = { + pass: actual !== actual + }; + + if (result.pass) { + result.message = 'Expected actual not to be NaN.'; + } else { + result.message = function() { + return 'Expected ' + matchersUtil.pp(actual) + ' to be NaN.'; + }; + } + + return result; + } + }; + } + + return toBeNaN; +}; + +getJasmineRequireObj().toBeNegativeInfinity = function(j$) { + /** + * {@link expect} the actual value to be `-Infinity` (-infinity). + * @function + * @name matchers#toBeNegativeInfinity + * @since 2.6.0 + * @example + * expect(thing).toBeNegativeInfinity(); + */ + function toBeNegativeInfinity(matchersUtil) { + return { + compare: function(actual) { + const result = { + pass: actual === Number.NEGATIVE_INFINITY + }; + + if (result.pass) { + result.message = 'Expected actual not to be -Infinity.'; + } else { + result.message = function() { + return 'Expected ' + matchersUtil.pp(actual) + ' to be -Infinity.'; + }; + } + + return result; + } + }; + } + + return toBeNegativeInfinity; +}; + +getJasmineRequireObj().toBeNull = function() { + /** + * {@link expect} the actual value to be `null`. + * @function + * @name matchers#toBeNull + * @since 1.3.0 + * @example + * expect(result).toBeNull(); + */ + function toBeNull() { + return { + compare: function(actual) { + return { + pass: actual === null + }; + } + }; + } + + return toBeNull; +}; + +getJasmineRequireObj().toBePositiveInfinity = function(j$) { + /** + * {@link expect} the actual value to be `Infinity` (infinity). + * @function + * @name matchers#toBePositiveInfinity + * @since 2.6.0 + * @example + * expect(thing).toBePositiveInfinity(); + */ + function toBePositiveInfinity(matchersUtil) { + return { + compare: function(actual) { + const result = { + pass: actual === Number.POSITIVE_INFINITY + }; + + if (result.pass) { + result.message = 'Expected actual not to be Infinity.'; + } else { + result.message = function() { + return 'Expected ' + matchersUtil.pp(actual) + ' to be Infinity.'; + }; + } + + return result; + } + }; + } + + return toBePositiveInfinity; +}; + +getJasmineRequireObj().toBeTrue = function() { + /** + * {@link expect} the actual value to be `true`. + * @function + * @name matchers#toBeTrue + * @since 3.5.0 + * @example + * expect(result).toBeTrue(); + */ + function toBeTrue() { + return { + compare: function(actual) { + return { + pass: actual === true + }; + } + }; + } + + return toBeTrue; +}; + +getJasmineRequireObj().toBeTruthy = function() { + /** + * {@link expect} the actual value to be truthy. + * @function + * @name matchers#toBeTruthy + * @since 2.0.0 + * @example + * expect(thing).toBeTruthy(); + */ + function toBeTruthy() { + return { + compare: function(actual) { + return { + pass: !!actual + }; + } + }; + } + + return toBeTruthy; +}; + +getJasmineRequireObj().toBeUndefined = function() { + /** + * {@link expect} the actual value to be `undefined`. + * @function + * @name matchers#toBeUndefined + * @since 1.3.0 + * @example + * expect(result).toBeUndefined(): + */ + function toBeUndefined() { + return { + compare: function(actual) { + return { + pass: void 0 === actual + }; + } + }; + } + + return toBeUndefined; +}; + +getJasmineRequireObj().toContain = function() { + /** + * {@link expect} the actual value to contain a specific value. + * @function + * @name matchers#toContain + * @since 2.0.0 + * @param {Object} expected - The value to look for. + * @example + * expect(array).toContain(anElement); + * expect(string).toContain(substring); + */ + function toContain(matchersUtil) { + return { + compare: function(actual, expected) { + return { + pass: matchersUtil.contains(actual, expected) + }; + } + }; + } + + return toContain; +}; + +getJasmineRequireObj().toEqual = function(j$) { + /** + * {@link expect} the actual value to be equal to the expected, using deep equality comparison. + * @function + * @name matchers#toEqual + * @since 1.3.0 + * @param {Object} expected - Expected value + * @example + * expect(bigObject).toEqual({"foo": ['bar', 'baz']}); + */ + function toEqual(matchersUtil) { + return { + compare: function(actual, expected) { + const result = { + pass: false + }, + diffBuilder = new j$.DiffBuilder({ prettyPrinter: matchersUtil.pp }); + + result.pass = matchersUtil.equals(actual, expected, diffBuilder); + + // TODO: only set error message if test fails + result.message = diffBuilder.getMessage(); + + return result; + } + }; + } + + return toEqual; +}; + +getJasmineRequireObj().toHaveBeenCalled = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveBeenCalled()' + ); + + /** + * {@link expect} the actual (a {@link Spy}) to have been called. + * @function + * @name matchers#toHaveBeenCalled + * @since 1.3.0 + * @example + * expect(mySpy).toHaveBeenCalled(); + * expect(mySpy).not.toHaveBeenCalled(); + */ + function toHaveBeenCalled(matchersUtil) { + return { + compare: function(actual) { + const result = {}; + + if (!j$.isSpy(actual)) { + throw new Error( + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(actual) + '.' + ) + ); + } + + if (arguments.length > 1) { + throw new Error( + getErrorMsg('Does not take arguments, use toHaveBeenCalledWith') + ); + } + + result.pass = actual.calls.any(); + + result.message = result.pass + ? 'Expected spy ' + actual.and.identity + ' not to have been called.' + : 'Expected spy ' + actual.and.identity + ' to have been called.'; + + return result; + } + }; + } + + return toHaveBeenCalled; +}; + +getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveBeenCalledBefore()' + ); + + /** + * {@link expect} the actual value (a {@link Spy}) to have been called before another {@link Spy}. + * @function + * @name matchers#toHaveBeenCalledBefore + * @since 2.6.0 + * @param {Spy} expected - {@link Spy} that should have been called after the `actual` {@link Spy}. + * @example + * expect(mySpy).toHaveBeenCalledBefore(otherSpy); + */ + function toHaveBeenCalledBefore(matchersUtil) { + return { + compare: function(firstSpy, latterSpy) { + if (!j$.isSpy(firstSpy)) { + throw new Error( + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(firstSpy) + '.' + ) + ); + } + if (!j$.isSpy(latterSpy)) { + throw new Error( + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(latterSpy) + '.' + ) + ); + } + + const result = { pass: false }; + + if (!firstSpy.calls.count()) { + result.message = + 'Expected spy ' + firstSpy.and.identity + ' to have been called.'; + return result; + } + if (!latterSpy.calls.count()) { + result.message = + 'Expected spy ' + latterSpy.and.identity + ' to have been called.'; + return result; + } + + const latest1stSpyCall = firstSpy.calls.mostRecent().invocationOrder; + const first2ndSpyCall = latterSpy.calls.first().invocationOrder; + + result.pass = latest1stSpyCall < first2ndSpyCall; + + if (result.pass) { + result.message = + 'Expected spy ' + + firstSpy.and.identity + + ' to not have been called before spy ' + + latterSpy.and.identity + + ', but it was'; + } else { + const first1stSpyCall = firstSpy.calls.first().invocationOrder; + const latest2ndSpyCall = latterSpy.calls.mostRecent().invocationOrder; + + if (first1stSpyCall < first2ndSpyCall) { + result.message = + 'Expected latest call to spy ' + + firstSpy.and.identity + + ' to have been called before first call to spy ' + + latterSpy.and.identity + + ' (no interleaved calls)'; + } else if (latest2ndSpyCall > latest1stSpyCall) { + result.message = + 'Expected first call to spy ' + + latterSpy.and.identity + + ' to have been called after latest call to spy ' + + firstSpy.and.identity + + ' (no interleaved calls)'; + } else { + result.message = + 'Expected spy ' + + firstSpy.and.identity + + ' to have been called before spy ' + + latterSpy.and.identity; + } + } + + return result; + } + }; + } + + return toHaveBeenCalledBefore; +}; + +getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveBeenCalledOnceWith(...arguments)' + ); + + /** + * {@link expect} the actual (a {@link Spy}) to have been called exactly once, and exactly with the particular arguments. + * @function + * @name matchers#toHaveBeenCalledOnceWith + * @since 3.6.0 + * @param {...Object} - The arguments to look for + * @example + * expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2); + */ + function toHaveBeenCalledOnceWith(util) { + return { + compare: function() { + const args = Array.prototype.slice.call(arguments, 0), + actual = args[0], + expectedArgs = args.slice(1); + + if (!j$.isSpy(actual)) { + throw new Error( + getErrorMsg('Expected a spy, but got ' + util.pp(actual) + '.') + ); + } + + const prettyPrintedCalls = actual.calls + .allArgs() + .map(function(argsForCall) { + return ' ' + util.pp(argsForCall); + }); + + if ( + actual.calls.count() === 1 && + util.contains(actual.calls.allArgs(), expectedArgs) + ) { + return { + pass: true, + message: + 'Expected spy ' + + actual.and.identity + + ' to have been called 0 times, multiple times, or once, but with arguments different from:\n' + + ' ' + + util.pp(expectedArgs) + + '\n' + + 'But the actual call was:\n' + + prettyPrintedCalls.join(',\n') + + '.\n\n' + }; + } + + function getDiffs() { + return actual.calls.allArgs().map(function(argsForCall, callIx) { + const diffBuilder = new j$.DiffBuilder(); + util.equals(argsForCall, expectedArgs, diffBuilder); + return diffBuilder.getMessage(); + }); + } + + function butString() { + switch (actual.calls.count()) { + case 0: + return 'But it was never called.\n\n'; + case 1: + return ( + 'But the actual call was:\n' + + prettyPrintedCalls.join(',\n') + + '.\n' + + getDiffs().join('\n') + + '\n\n' + ); + default: + return ( + 'But the actual calls were:\n' + + prettyPrintedCalls.join(',\n') + + '.\n\n' + ); + } + } + + return { + pass: false, + message: + 'Expected spy ' + + actual.and.identity + + ' to have been called only once, and with given args:\n' + + ' ' + + util.pp(expectedArgs) + + '\n' + + butString() + }; + } + }; + } + + return toHaveBeenCalledOnceWith; +}; + +getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveBeenCalledTimes()' + ); + + /** + * {@link expect} the actual (a {@link Spy}) to have been called the specified number of times. + * @function + * @name matchers#toHaveBeenCalledTimes + * @since 2.4.0 + * @param {Number} expected - The number of invocations to look for. + * @example + * expect(mySpy).toHaveBeenCalledTimes(3); + */ + function toHaveBeenCalledTimes(matchersUtil) { + return { + compare: function(actual, expected) { + if (!j$.isSpy(actual)) { + throw new Error( + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(actual) + '.' + ) + ); + } + + const args = Array.prototype.slice.call(arguments, 0), + result = { pass: false }; + + if (!j$.isNumber_(expected)) { + throw new Error( + getErrorMsg( + 'The expected times failed is a required argument and must be a number.' + ) + ); + } + + actual = args[0]; + const calls = actual.calls.count(); + const timesMessage = expected === 1 ? 'once' : expected + ' times'; + result.pass = calls === expected; + result.message = result.pass + ? 'Expected spy ' + + actual.and.identity + + ' not to have been called ' + + timesMessage + + '. It was called ' + + calls + + ' times.' + : 'Expected spy ' + + actual.and.identity + + ' to have been called ' + + timesMessage + + '. It was called ' + + calls + + ' times.'; + return result; + } + }; + } + + return toHaveBeenCalledTimes; +}; + +getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveBeenCalledWith(...arguments)' + ); + + /** + * {@link expect} the actual (a {@link Spy}) to have been called with particular arguments at least once. + * @function + * @name matchers#toHaveBeenCalledWith + * @since 1.3.0 + * @param {...Object} - The arguments to look for + * @example + * expect(mySpy).toHaveBeenCalledWith('foo', 'bar', 2); + */ + function toHaveBeenCalledWith(matchersUtil) { + return { + compare: function() { + const args = Array.prototype.slice.call(arguments, 0), + actual = args[0], + expectedArgs = args.slice(1), + result = { pass: false }; + + if (!j$.isSpy(actual)) { + throw new Error( + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(actual) + '.' + ) + ); + } + + if (!actual.calls.any()) { + result.message = function() { + return ( + 'Expected spy ' + + actual.and.identity + + ' to have been called with:\n' + + ' ' + + matchersUtil.pp(expectedArgs) + + '\nbut it was never called.' + ); + }; + return result; + } + + if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) { + result.pass = true; + result.message = function() { + return ( + 'Expected spy ' + + actual.and.identity + + ' not to have been called with:\n' + + ' ' + + matchersUtil.pp(expectedArgs) + + '\nbut it was.' + ); + }; + } else { + result.message = function() { + const prettyPrintedCalls = actual.calls + .allArgs() + .map(function(argsForCall) { + return ' ' + matchersUtil.pp(argsForCall); + }); + + const diffs = actual.calls + .allArgs() + .map(function(argsForCall, callIx) { + const diffBuilder = new j$.DiffBuilder(); + matchersUtil.equals(argsForCall, expectedArgs, diffBuilder); + return ( + 'Call ' + + callIx + + ':\n' + + diffBuilder.getMessage().replace(/^/gm, ' ') + ); + }); + + return ( + 'Expected spy ' + + actual.and.identity + + ' to have been called with:\n' + + ' ' + + matchersUtil.pp(expectedArgs) + + '\n' + + '' + + 'but actual calls were:\n' + + prettyPrintedCalls.join(',\n') + + '.\n\n' + + diffs.join('\n') + ); + }; + } + + return result; + } + }; + } + + return toHaveBeenCalledWith; +}; + +getJasmineRequireObj().toHaveClass = function(j$) { + /** + * {@link expect} the actual value to be a DOM element that has the expected class + * @function + * @name matchers#toHaveClass + * @since 3.0.0 + * @param {Object} expected - The class name to test for + * @example + * const el = document.createElement('div'); + * el.className = 'foo bar baz'; + * expect(el).toHaveClass('bar'); + */ + function toHaveClass(matchersUtil) { + return { + compare: function(actual, expected) { + if (!isElement(actual)) { + throw new Error(matchersUtil.pp(actual) + ' is not a DOM element'); + } + + return { + pass: actual.classList.contains(expected) + }; + } + }; + } + + function isElement(maybeEl) { + return ( + maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains) + ); + } + + return toHaveClass; +}; + +getJasmineRequireObj().toHaveSize = function(j$) { + /** + * {@link expect} the actual size to be equal to the expected, using array-like length or object keys size. + * @function + * @name matchers#toHaveSize + * @since 3.6.0 + * @param {Object} expected - Expected size + * @example + * array = [1,2]; + * expect(array).toHaveSize(2); + */ + function toHaveSize() { + return { + compare: function(actual, expected) { + const result = { + pass: false + }; + + if ( + j$.isA_('WeakSet', actual) || + j$.isWeakMap(actual) || + j$.isDataView(actual) + ) { + throw new Error('Cannot get size of ' + actual + '.'); + } + + if (j$.isSet(actual) || j$.isMap(actual)) { + result.pass = actual.size === expected; + } else if (isLength(actual.length)) { + result.pass = actual.length === expected; + } else { + result.pass = Object.keys(actual).length === expected; + } + + return result; + } + }; + } + + const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + function isLength(value) { + return ( + typeof value == 'number' && + value > -1 && + value % 1 === 0 && + value <= MAX_SAFE_INTEGER + ); + } + + return toHaveSize; +}; + +getJasmineRequireObj().toHaveSpyInteractions = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveSpyInteractions()' + ); + + /** + * {@link expect} the actual (a {@link SpyObj}) spies to have been called. + * @function + * @name matchers#toHaveSpyInteractions + * @since 4.1.0 + * @example + * expect(mySpyObj).toHaveSpyInteractions(); + * expect(mySpyObj).not.toHaveSpyInteractions(); + */ + function toHaveSpyInteractions(matchersUtil) { + return { + compare: function(actual) { + const result = {}; + + if (!j$.isObject_(actual)) { + throw new Error( + getErrorMsg('Expected a spy object, but got ' + typeof actual + '.') + ); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments')); + } + + result.pass = false; + let hasSpy = false; + const calledSpies = []; + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) continue; + hasSpy = true; + + if (spy.calls.any()) { + result.pass = true; + calledSpies.push([spy.and.identity, spy.calls.count()]); + } + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected a spy object with spies, but object has no spies.' + ) + ); + } + + let resultMessage; + if (result.pass) { + resultMessage = + 'Expected spy object spies not to have been called, ' + + 'but the following spies were called: '; + resultMessage += calledSpies + .map(([spyName, spyCount]) => { + return `${spyName} called ${spyCount} time(s)`; + }) + .join(', '); + } else { + resultMessage = + 'Expected spy object spies to have been called, ' + + 'but no spies were called.'; + } + result.message = resultMessage; + + return result; + } + }; + } + + return toHaveSpyInteractions; +}; + +getJasmineRequireObj().toMatch = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toMatch( || )' + ); + + /** + * {@link expect} the actual value to match a regular expression + * @function + * @name matchers#toMatch + * @since 1.3.0 + * @param {RegExp|String} expected - Value to look for in the string. + * @example + * expect("my string").toMatch(/string$/); + * expect("other string").toMatch("her"); + */ + function toMatch() { + return { + compare: function(actual, expected) { + if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) { + throw new Error(getErrorMsg('Expected is not a String or a RegExp')); + } + + const regexp = new RegExp(expected); + + return { + pass: regexp.test(actual) + }; + } + }; + } + + return toMatch; +}; + +getJasmineRequireObj().toThrow = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect(function() {}).toThrow()' + ); + + /** + * {@link expect} a function to `throw` something. + * @function + * @name matchers#toThrow + * @since 2.0.0 + * @param {Object} [expected] - Value that should be thrown. If not provided, simply the fact that something was thrown will be checked. + * @example + * expect(function() { return 'things'; }).toThrow('foo'); + * expect(function() { return 'stuff'; }).toThrow(); + */ + function toThrow(matchersUtil) { + return { + compare: function(actual, expected) { + const result = { pass: false }; + let threw = false; + let thrown; + + if (typeof actual != 'function') { + throw new Error(getErrorMsg('Actual is not a Function')); + } + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + result.message = 'Expected function to throw an exception.'; + return result; + } + + if (arguments.length == 1) { + result.pass = true; + result.message = function() { + return ( + 'Expected function not to throw, but it threw ' + + matchersUtil.pp(thrown) + + '.' + ); + }; + + return result; + } + + if (matchersUtil.equals(thrown, expected)) { + result.pass = true; + result.message = function() { + return ( + 'Expected function not to throw ' + + matchersUtil.pp(expected) + + '.' + ); + }; + } else { + result.message = function() { + return ( + 'Expected function to throw ' + + matchersUtil.pp(expected) + + ', but it threw ' + + matchersUtil.pp(thrown) + + '.' + ); + }; + } + + return result; + } + }; + } + + return toThrow; +}; + +getJasmineRequireObj().toThrowError = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect(function() {}).toThrowError(, )' + ); + + /** + * {@link expect} a function to `throw` an `Error`. + * @function + * @name matchers#toThrowError + * @since 2.0.0 + * @param {Error} [expected] - `Error` constructor the object that was thrown needs to be an instance of. If not provided, `Error` will be used. + * @param {RegExp|String} [message] - The message that should be set on the thrown `Error` + * @example + * expect(function() { return 'things'; }).toThrowError(MyCustomError, 'message'); + * expect(function() { return 'things'; }).toThrowError(MyCustomError, /bar/); + * expect(function() { return 'stuff'; }).toThrowError(MyCustomError); + * expect(function() { return 'other'; }).toThrowError(/foo/); + * expect(function() { return 'other'; }).toThrowError(); + */ + function toThrowError(matchersUtil) { + return { + compare: function(actual) { + const errorMatcher = getMatcher.apply(null, arguments); + + if (typeof actual != 'function') { + throw new Error(getErrorMsg('Actual is not a Function')); + } + + let thrown; + + try { + actual(); + return fail('Expected function to throw an Error.'); + } catch (e) { + thrown = e; + } + + if (!j$.isError_(thrown)) { + return fail(function() { + return ( + 'Expected function to throw an Error, but it threw ' + + matchersUtil.pp(thrown) + + '.' + ); + }); + } + + return errorMatcher.match(thrown); + } + }; + + function getMatcher() { + let expected, errorType; + + if (arguments[2]) { + errorType = arguments[1]; + expected = arguments[2]; + if (!isAnErrorType(errorType)) { + throw new Error(getErrorMsg('Expected error type is not an Error.')); + } + + return exactMatcher(expected, errorType); + } else if (arguments[1]) { + expected = arguments[1]; + + if (isAnErrorType(arguments[1])) { + return exactMatcher(null, arguments[1]); + } else { + return exactMatcher(arguments[1], null); + } + } else { + return anyMatcher(); + } + } + + function anyMatcher() { + return { + match: function(error) { + return pass( + 'Expected function not to throw an Error, but it threw ' + + j$.fnNameFor(error) + + '.' + ); + } + }; + } + + function exactMatcher(expected, errorType) { + if (expected && !isStringOrRegExp(expected)) { + if (errorType) { + throw new Error( + getErrorMsg('Expected error message is not a string or RegExp.') + ); + } else { + throw new Error( + getErrorMsg('Expected is not an Error, string, or RegExp.') + ); + } + } + + function messageMatch(message) { + if (typeof expected == 'string') { + return expected == message; + } else { + return expected.test(message); + } + } + + const errorTypeDescription = errorType + ? j$.fnNameFor(errorType) + : 'an exception'; + + function thrownDescription(thrown) { + const thrownName = errorType + ? j$.fnNameFor(thrown.constructor) + : 'an exception'; + let thrownMessage = ''; + + if (expected) { + thrownMessage = ' with message ' + matchersUtil.pp(thrown.message); + } + + return thrownName + thrownMessage; + } + + function messageDescription() { + if (expected === null) { + return ''; + } else if (expected instanceof RegExp) { + return ' with a message matching ' + matchersUtil.pp(expected); + } else { + return ' with message ' + matchersUtil.pp(expected); + } + } + + function matches(error) { + return ( + (errorType === null || error instanceof errorType) && + (expected === null || messageMatch(error.message)) + ); + } + + return { + match: function(thrown) { + if (matches(thrown)) { + return pass(function() { + return ( + 'Expected function not to throw ' + + errorTypeDescription + + messageDescription() + + '.' + ); + }); + } else { + return fail(function() { + return ( + 'Expected function to throw ' + + errorTypeDescription + + messageDescription() + + ', but it threw ' + + thrownDescription(thrown) + + '.' + ); + }); + } + } + }; + } + + function isStringOrRegExp(potential) { + return potential instanceof RegExp || typeof potential == 'string'; + } + + function isAnErrorType(type) { + if (typeof type !== 'function') { + return false; + } + + const Surrogate = function() {}; + Surrogate.prototype = type.prototype; + return j$.isError_(new Surrogate()); + } + } + + function pass(message) { + return { + pass: true, + message: message + }; + } + + function fail(message) { + return { + pass: false, + message: message + }; + } + + return toThrowError; +}; + +getJasmineRequireObj().toThrowMatching = function(j$) { + const usageError = j$.formatErrorMsg( + '', + 'expect(function() {}).toThrowMatching()' + ); + + /** + * {@link expect} a function to `throw` something matching a predicate. + * @function + * @name matchers#toThrowMatching + * @since 3.0.0 + * @param {Function} predicate - A function that takes the thrown exception as its parameter and returns true if it matches. + * @example + * expect(function() { throw new Error('nope'); }).toThrowMatching(function(thrown) { return thrown.message === 'nope'; }); + */ + function toThrowMatching(matchersUtil) { + return { + compare: function(actual, predicate) { + if (typeof actual !== 'function') { + throw new Error(usageError('Actual is not a Function')); + } + + if (typeof predicate !== 'function') { + throw new Error(usageError('Predicate is not a Function')); + } + + let thrown; + + try { + actual(); + return fail('Expected function to throw an exception.'); + } catch (e) { + thrown = e; + } + + if (predicate(thrown)) { + return pass( + 'Expected function not to throw an exception matching a predicate.' + ); + } else { + return fail(function() { + return ( + 'Expected function to throw an exception matching a predicate, ' + + 'but it threw ' + + thrownDescription(thrown) + + '.' + ); + }); + } + } + }; + + function thrownDescription(thrown) { + if (thrown && thrown.constructor) { + return ( + j$.fnNameFor(thrown.constructor) + + ' with message ' + + matchersUtil.pp(thrown.message) + ); + } else { + return matchersUtil.pp(thrown); + } + } + } + + function pass(message) { + return { + pass: true, + message: message + }; + } + + function fail(message) { + return { + pass: false, + message: message + }; + } + + return toThrowMatching; +}; + +getJasmineRequireObj().MockDate = function(j$) { + function MockDate(global) { + let currentTime = 0; + + if (!global || !global.Date) { + this.install = function() {}; + this.tick = function() {}; + this.uninstall = function() {}; + return this; + } + + const GlobalDate = global.Date; + + this.install = function(mockDate) { + if (mockDate instanceof GlobalDate) { + currentTime = mockDate.getTime(); + } else { + if (!j$.util.isUndefined(mockDate)) { + throw new Error( + 'The argument to jasmine.clock().mockDate(), if specified, ' + + 'should be a Date instance.' + ); + } + + currentTime = new GlobalDate().getTime(); + } + + global.Date = FakeDate; + }; + + this.tick = function(millis) { + millis = millis || 0; + currentTime = currentTime + millis; + }; + + this.uninstall = function() { + currentTime = 0; + global.Date = GlobalDate; + }; + + createDateProperties(); + + return this; + + function FakeDate() { + switch (arguments.length) { + case 0: + return new GlobalDate(currentTime); + case 1: + return new GlobalDate(arguments[0]); + case 2: + return new GlobalDate(arguments[0], arguments[1]); + case 3: + return new GlobalDate(arguments[0], arguments[1], arguments[2]); + case 4: + return new GlobalDate( + arguments[0], + arguments[1], + arguments[2], + arguments[3] + ); + case 5: + return new GlobalDate( + arguments[0], + arguments[1], + arguments[2], + arguments[3], + arguments[4] + ); + case 6: + return new GlobalDate( + arguments[0], + arguments[1], + arguments[2], + arguments[3], + arguments[4], + arguments[5] + ); + default: + return new GlobalDate( + arguments[0], + arguments[1], + arguments[2], + arguments[3], + arguments[4], + arguments[5], + arguments[6] + ); + } + } + + function createDateProperties() { + FakeDate.prototype = GlobalDate.prototype; + + FakeDate.now = function() { + return currentTime; + }; + + FakeDate.toSource = GlobalDate.toSource; + FakeDate.toString = GlobalDate.toString; + FakeDate.parse = GlobalDate.parse; + FakeDate.UTC = GlobalDate.UTC; + } + } + + return MockDate; +}; + +getJasmineRequireObj().NeverSkipPolicy = function(j$) { + function NeverSkipPolicy(queueableFns) {} + + NeverSkipPolicy.prototype.skipTo = function(lastRanFnIx) { + return lastRanFnIx + 1; + }; + + NeverSkipPolicy.prototype.fnErrored = function(fnIx) {}; + + return NeverSkipPolicy; +}; + +getJasmineRequireObj().makePrettyPrinter = function(j$) { + class SinglePrettyPrintRun { + constructor(customObjectFormatters, pp) { + this.customObjectFormatters_ = customObjectFormatters; + this.ppNestLevel_ = 0; + this.seen = []; + this.length = 0; + this.stringParts = []; + this.pp_ = pp; + } + + format(value) { + this.ppNestLevel_++; + try { + const customFormatResult = this.applyCustomFormatters_(value); + + if (customFormatResult) { + this.emitScalar(customFormatResult); + } else if (j$.util.isUndefined(value)) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === 0 && 1 / value === -Infinity) { + this.emitScalar('-0'); + } else if (value === j$.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString(this.pp_)); + } else if (j$.isString_(value)) { + this.emitString(value); + } else if (j$.isSpy(value)) { + this.emitScalar('spy on ' + value.and.identity); + } else if (j$.isSpy(value.toString)) { + this.emitScalar('spy on ' + value.toString.and.identity); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (j$.isDomNode(value)) { + if (value.tagName) { + this.emitDomElement(value); + } else { + this.emitScalar('HTMLNode'); + } + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (j$.isSet(value)) { + this.emitSet(value); + } else if (j$.isMap(value)) { + this.emitMap(value); + } else if (j$.isTypedArray_(value)) { + this.emitTypedArray(value); + } else if ( + value.toString && + typeof value === 'object' && + !j$.isArray_(value) && + hasCustomToString(value) + ) { + try { + this.emitScalar(value.toString()); + } catch (e) { + this.emitScalar('has-invalid-toString-method'); + } + } else if (this.seen.includes(value)) { + this.emitScalar( + '' + ); + } else if (j$.isArray_(value) || j$.isA_('Object', value)) { + this.seen.push(value); + if (j$.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + this.seen.pop(); + } else { + this.emitScalar(value.toString()); + } + } catch (e) { + if (this.ppNestLevel_ > 1 || !(e instanceof MaxCharsReachedError)) { + throw e; + } + } finally { + this.ppNestLevel_--; + } + } + + applyCustomFormatters_(value) { + return customFormat(value, this.customObjectFormatters_); + } + + iterateObject(obj, fn) { + const objKeys = j$.MatchersUtil.keys(obj, j$.isArray_(obj)); + const length = Math.min(objKeys.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + + for (let i = 0; i < length; i++) { + fn(objKeys[i]); + } + + return objKeys.length > length; + } + + emitScalar(value) { + this.append(value); + } + + emitString(value) { + this.append("'" + value + "'"); + } + + emitArray(array) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Array'); + return; + } + + const length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + this.append('[ '); + + for (let i = 0; i < length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + if (array.length > length) { + this.append(', ...'); + } + + let first = array.length === 0; + const wasTruncated = this.iterateObject(array, property => { + if (first) { + first = false; + } else { + this.append(', '); + } + + this.formatProperty(array, property); + }); + + if (wasTruncated) { + this.append(', ...'); + } + + this.append(' ]'); + } + + emitSet(set) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Set'); + return; + } + this.append('Set( '); + const size = Math.min(set.size, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + let i = 0; + set.forEach(function(value, key) { + if (i >= size) { + return; + } + if (i > 0) { + this.append(', '); + } + this.format(value); + + i++; + }, this); + if (set.size > size) { + this.append(', ...'); + } + this.append(' )'); + } + + emitMap(map) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Map'); + return; + } + this.append('Map( '); + const size = Math.min(map.size, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + let i = 0; + map.forEach(function(value, key) { + if (i >= size) { + return; + } + if (i > 0) { + this.append(', '); + } + this.format([key, value]); + + i++; + }, this); + if (map.size > size) { + this.append(', ...'); + } + this.append(' )'); + } + + emitObject(obj) { + const ctor = obj.constructor; + const constructorName = + typeof ctor === 'function' && obj instanceof ctor + ? j$.fnNameFor(obj.constructor) + : 'null'; + + this.append(constructorName); + + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + return; + } + + this.append('({ '); + let first = true; + + const wasTruncated = this.iterateObject(obj, property => { + if (first) { + first = false; + } else { + this.append(', '); + } + + this.formatProperty(obj, property); + }); + + if (wasTruncated) { + this.append(', ...'); + } + + this.append(' })'); + } + + emitTypedArray(arr) { + const constructorName = j$.fnNameFor(arr.constructor); + const limitedArray = Array.prototype.slice.call( + arr, + 0, + j$.MAX_PRETTY_PRINT_ARRAY_LENGTH + ); + let itemsString = Array.prototype.join.call(limitedArray, ', '); + + if (limitedArray.length !== arr.length) { + itemsString += ', ...'; + } + + this.append(constructorName + ' [ ' + itemsString + ' ]'); + } + + emitDomElement(el) { + const tagName = el.tagName.toLowerCase(); + let out = '<' + tagName; + + for (const attr of el.attributes) { + out += ' ' + attr.name; + + if (attr.value !== '') { + out += '="' + attr.value + '"'; + } + } + + out += '>'; + + if (el.childElementCount !== 0 || el.textContent !== '') { + out += '...'; + } + + this.append(out); + } + + formatProperty(obj, property) { + if (typeof property === 'symbol') { + this.append(property.toString()); + } else { + this.append(property); + } + + this.append(': '); + this.format(obj[property]); + } + + append(value) { + // This check protects us from the rare case where an object has overriden + // `toString()` with an invalid implementation (returning a non-string). + if (typeof value !== 'string') { + value = Object.prototype.toString.call(value); + } + + const result = truncate(value, j$.MAX_PRETTY_PRINT_CHARS - this.length); + this.length += result.value.length; + this.stringParts.push(result.value); + + if (result.truncated) { + throw new MaxCharsReachedError(); + } + } + } + + function hasCustomToString(value) { + // value.toString !== Object.prototype.toString if value has no custom toString but is from another context (e.g. + // iframe, web worker) + try { + return ( + j$.isFunction_(value.toString) && + value.toString !== Object.prototype.toString && + value.toString() !== Object.prototype.toString.call(value) + ); + } catch (e) { + // The custom toString() threw. + return true; + } + } + + function truncate(s, maxlen) { + if (s.length <= maxlen) { + return { value: s, truncated: false }; + } + + s = s.substring(0, maxlen - 4) + ' ...'; + return { value: s, truncated: true }; + } + + function MaxCharsReachedError() { + this.message = + 'Exceeded ' + + j$.MAX_PRETTY_PRINT_CHARS + + ' characters while pretty-printing a value'; + } + + MaxCharsReachedError.prototype = new Error(); + + function customFormat(value, customObjectFormatters) { + for (const formatter of customObjectFormatters) { + const result = formatter(value); + + if (result !== undefined) { + return result; + } + } + } + + return function(customObjectFormatters) { + customObjectFormatters = customObjectFormatters || []; + + const pp = function(value) { + const prettyPrinter = new SinglePrettyPrintRun( + customObjectFormatters, + pp + ); + prettyPrinter.format(value); + return prettyPrinter.stringParts.join(''); + }; + + pp.customFormat_ = function(value) { + return customFormat(value, customObjectFormatters); + }; + + return pp; + }; +}; + +getJasmineRequireObj().QueueRunner = function(j$) { + let nextid = 1; + + function StopExecutionError() {} + StopExecutionError.prototype = new Error(); + j$.StopExecutionError = StopExecutionError; + + function once(fn, onTwice) { + let called = false; + return function(arg) { + if (called) { + if (onTwice) { + onTwice(); + } + } else { + called = true; + // Direct call using single parameter, because cleanup/next does not need more + fn(arg); + } + return null; + }; + } + + function fallbackOnMultipleDone() { + console.error( + new Error( + "An asynchronous function called its 'done' " + + 'callback more than once, in a QueueRunner without a onMultipleDone ' + + 'handler.' + ) + ); + } + + function emptyFn() {} + + function QueueRunner(attrs) { + this.id_ = nextid++; + this.queueableFns = attrs.queueableFns || []; + this.onComplete = attrs.onComplete || emptyFn; + this.clearStack = + attrs.clearStack || + function(fn) { + fn(); + }; + this.onException = attrs.onException || emptyFn; + this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone; + this.userContext = attrs.userContext || new j$.UserContext(); + this.timeout = attrs.timeout || { + setTimeout: setTimeout, + clearTimeout: clearTimeout + }; + this.fail = attrs.fail || emptyFn; + this.globalErrors = attrs.globalErrors || { + pushListener: emptyFn, + popListener: emptyFn + }; + + const SkipPolicy = attrs.SkipPolicy || j$.NeverSkipPolicy; + this.skipPolicy_ = new SkipPolicy(this.queueableFns); + this.errored_ = false; + + if (typeof this.onComplete !== 'function') { + throw new Error('invalid onComplete ' + JSON.stringify(this.onComplete)); + } + this.deprecated = attrs.deprecated; + } + + QueueRunner.prototype.execute = function() { + this.handleFinalError = (message, source, lineno, colno, error) => { + // Older browsers would send the error as the first parameter. HTML5 + // specifies the the five parameters above. The error instance should + // be preffered, otherwise the call stack would get lost. + this.onException(error || message); + }; + this.globalErrors.pushListener(this.handleFinalError); + this.run(0); + }; + + QueueRunner.prototype.clearTimeout = function(timeoutId) { + Function.prototype.apply.apply(this.timeout.clearTimeout, [ + j$.getGlobal(), + [timeoutId] + ]); + }; + + QueueRunner.prototype.setTimeout = function(fn, timeout) { + return Function.prototype.apply.apply(this.timeout.setTimeout, [ + j$.getGlobal(), + [fn, timeout] + ]); + }; + + QueueRunner.prototype.attempt = function attempt(iterativeIndex) { + let timeoutId; + let timedOut; + let completedSynchronously = true; + + const onException = e => { + this.onException(e); + this.recordError_(iterativeIndex); + }; + + function handleError(error) { + // TODO probably shouldn't next() right away here. + // That makes debugging async failures much more confusing. + onException(error); + } + const cleanup = once(() => { + if (timeoutId !== void 0) { + this.clearTimeout(timeoutId); + } + this.globalErrors.popListener(handleError); + }); + const next = once( + err => { + cleanup(); + + if (typeof err !== 'undefined') { + if (!(err instanceof StopExecutionError) && !err.jasmineMessage) { + this.fail(err); + } + this.recordError_(iterativeIndex); + } + + const runNext = () => { + this.run(this.nextFnIx_(iterativeIndex)); + }; + + if (completedSynchronously) { + this.setTimeout(runNext); + } else { + runNext(); + } + }, + () => { + try { + if (!timedOut) { + this.onMultipleDone(); + } + } catch (error) { + // Any error we catch here is probably due to a bug in Jasmine, + // and it's not likely to end up anywhere useful if we let it + // propagate. Log it so it can at least show up when debugging. + console.error(error); + } + } + ); + timedOut = false; + const queueableFn = this.queueableFns[iterativeIndex]; + + next.fail = function nextFail() { + this.fail.apply(null, arguments); + this.recordError_(iterativeIndex); + next(); + }.bind(this); + + this.globalErrors.pushListener(handleError); + + if (queueableFn.timeout !== undefined) { + const timeoutInterval = + queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL; + timeoutId = this.setTimeout(function() { + timedOut = true; + const error = new Error( + 'Timeout - Async function did not complete within ' + + timeoutInterval + + 'ms ' + + (queueableFn.timeout + ? '(custom timeout)' + : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)') + ); + // TODO Need to decide what to do about a successful completion after a + // timeout. That should probably not be a deprecation, and maybe not + // an error in 4.0. (But a diagnostic of some sort might be helpful.) + onException(error); + next(); + }, timeoutInterval); + } + + try { + let maybeThenable; + + if (queueableFn.fn.length === 0) { + maybeThenable = queueableFn.fn.call(this.userContext); + + if (maybeThenable && j$.isFunction_(maybeThenable.then)) { + maybeThenable.then( + wrapInPromiseResolutionHandler(next), + onPromiseRejection + ); + completedSynchronously = false; + return { completedSynchronously: false }; + } + } else { + maybeThenable = queueableFn.fn.call(this.userContext, next); + this.diagnoseConflictingAsync_(queueableFn.fn, maybeThenable); + completedSynchronously = false; + return { completedSynchronously: false }; + } + } catch (e) { + onException(e); + this.recordError_(iterativeIndex); + } + + cleanup(); + return { completedSynchronously: true }; + + function onPromiseRejection(e) { + onException(e); + next(); + } + }; + + QueueRunner.prototype.run = function(recursiveIndex) { + const length = this.queueableFns.length; + + for ( + let iterativeIndex = recursiveIndex; + iterativeIndex < length; + iterativeIndex = this.nextFnIx_(iterativeIndex) + ) { + const result = this.attempt(iterativeIndex); + + if (!result.completedSynchronously) { + return; + } + } + + this.clearStack(() => { + this.globalErrors.popListener(this.handleFinalError); + + if (this.errored_) { + this.onComplete(new StopExecutionError()); + } else { + this.onComplete(); + } + }); + }; + + QueueRunner.prototype.nextFnIx_ = function(currentFnIx) { + const result = this.skipPolicy_.skipTo(currentFnIx); + + if (result === currentFnIx) { + throw new Error("Can't skip to the same queueable fn that just finished"); + } + + return result; + }; + + QueueRunner.prototype.recordError_ = function(currentFnIx) { + this.errored_ = true; + this.skipPolicy_.fnErrored(currentFnIx); + }; + + QueueRunner.prototype.diagnoseConflictingAsync_ = function(fn, retval) { + if (retval && j$.isFunction_(retval.then)) { + // Issue a warning that matches the user's code. + // Omit the stack trace because there's almost certainly no user code + // on the stack at this point. + if (j$.isAsyncFunction_(fn)) { + this.onException( + 'An asynchronous before/it/after ' + + 'function was defined with the async keyword but also took a ' + + 'done callback. Either remove the done callback (recommended) or ' + + 'remove the async keyword.' + ); + } else { + this.onException( + 'An asynchronous before/it/after ' + + 'function took a done callback but also returned a promise. ' + + 'Either remove the done callback (recommended) or change the ' + + 'function to not return a promise.' + ); + } + } + }; + + function wrapInPromiseResolutionHandler(fn) { + return function(maybeArg) { + if (j$.isError_(maybeArg)) { + fn(maybeArg); + } else { + fn(); + } + }; + } + + return QueueRunner; +}; + +getJasmineRequireObj().ReportDispatcher = function(j$) { + function ReportDispatcher(methods, queueRunnerFactory, onLateError) { + const dispatchedMethods = methods || []; + + for (const method of dispatchedMethods) { + this[method] = (function(m) { + return function() { + return dispatch(m, arguments); + }; + })(method); + } + + let reporters = []; + let fallbackReporter = null; + + this.addReporter = function(reporter) { + reporters.push(reporter); + }; + + this.provideFallbackReporter = function(reporter) { + fallbackReporter = reporter; + }; + + this.clearReporters = function() { + reporters = []; + }; + + return this; + + function dispatch(method, args) { + if (reporters.length === 0 && fallbackReporter !== null) { + reporters.push(fallbackReporter); + } + const fns = []; + for (const reporter of reporters) { + addFn(fns, reporter, method, args); + } + + return new Promise(function(resolve) { + queueRunnerFactory({ + queueableFns: fns, + onComplete: resolve, + isReporter: true, + onMultipleDone: function() { + onLateError( + new Error( + "An asynchronous reporter callback called its 'done' callback " + + 'more than once.' + ) + ); + } + }); + }); + } + + function addFn(fns, reporter, method, args) { + const fn = reporter[method]; + if (!fn) { + return; + } + + const thisArgs = j$.util.cloneArgs(args); + if (fn.length <= 1) { + fns.push({ + fn: function() { + return fn.apply(reporter, thisArgs); + } + }); + } else { + fns.push({ + fn: function(done) { + return fn.apply(reporter, thisArgs.concat([done])); + } + }); + } + } + } + + return ReportDispatcher; +}; + +getJasmineRequireObj().interface = function(jasmine, env) { + const jasmineInterface = { + /** + * Callback passed to parts of the Jasmine base interface. + * + * By default Jasmine assumes this function completes synchronously. + * If you have code that you need to test asynchronously, you can declare that you receive a `done` callback, return a Promise, or use the `async` keyword if it is supported in your environment. + * @callback implementationCallback + * @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on. + * @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion. + */ + + /** + * Create a group of specs (often called a suite). + * + * Calls to `describe` can be nested within other calls to compose your suite as a tree. + * @name describe + * @since 1.3.0 + * @function + * @global + * @param {String} description Textual description of the group + * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs + */ + describe: function(description, specDefinitions) { + return env.describe(description, specDefinitions); + }, + + /** + * A temporarily disabled [`describe`]{@link describe} + * + * Specs within an `xdescribe` will be marked pending and not executed + * @name xdescribe + * @since 1.3.0 + * @function + * @global + * @param {String} description Textual description of the group + * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs + */ + xdescribe: function(description, specDefinitions) { + return env.xdescribe(description, specDefinitions); + }, + + /** + * A focused [`describe`]{@link describe} + * + * If suites or specs are focused, only those that are focused will be executed + * @see fit + * @name fdescribe + * @since 2.1.0 + * @function + * @global + * @param {String} description Textual description of the group + * @param {Function} specDefinitions Function for Jasmine to invoke that will define inner suites and specs + */ + fdescribe: function(description, specDefinitions) { + return env.fdescribe(description, specDefinitions); + }, + + /** + * Define a single spec. A spec should contain one or more {@link expect|expectations} that test the state of the code. + * + * A spec whose expectations all succeed will be passing and a spec with any failures will fail. + * The name `it` is a pronoun for the test target, not an abbreviation of anything. It makes the + * spec more readable by connecting the function name `it` and the argument `description` as a + * complete sentence. + * @name it + * @since 1.3.0 + * @function + * @global + * @param {String} description Textual description of what this spec is checking + * @param {implementationCallback} [testFunction] Function that contains the code of your test. If not provided the test will be `pending`. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec. + * @see async + */ + it: function() { + return env.it.apply(env, arguments); + }, + + /** + * A temporarily disabled [`it`]{@link it} + * + * The spec will report as `pending` and will not be executed. + * @name xit + * @since 1.3.0 + * @function + * @global + * @param {String} description Textual description of what this spec is checking. + * @param {implementationCallback} [testFunction] Function that contains the code of your test. Will not be executed. + */ + xit: function() { + return env.xit.apply(env, arguments); + }, + + /** + * A focused [`it`]{@link it} + * + * If suites or specs are focused, only those that are focused will be executed. + * @name fit + * @since 2.1.0 + * @function + * @global + * @param {String} description Textual description of what this spec is checking. + * @param {implementationCallback} testFunction Function that contains the code of your test. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async spec. + * @see async + */ + fit: function() { + return env.fit.apply(env, arguments); + }, + + /** + * Run some shared setup before each of the specs in the {@link describe} in which it is called. + * @name beforeEach + * @since 1.3.0 + * @function + * @global + * @param {implementationCallback} [function] Function that contains the code to setup your specs. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeEach. + * @see async + */ + beforeEach: function() { + return env.beforeEach.apply(env, arguments); + }, + + /** + * Run some shared teardown after each of the specs in the {@link describe} in which it is called. + * @name afterEach + * @since 1.3.0 + * @function + * @global + * @param {implementationCallback} [function] Function that contains the code to teardown your specs. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterEach. + * @see async + */ + afterEach: function() { + return env.afterEach.apply(env, arguments); + }, + + /** + * Run some shared setup once before all of the specs in the {@link describe} are run. + * + * _Note:_ Be careful, sharing the setup from a beforeAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail. + * @name beforeAll + * @since 2.1.0 + * @function + * @global + * @param {implementationCallback} [function] Function that contains the code to setup your specs. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async beforeAll. + * @see async + */ + beforeAll: function() { + return env.beforeAll.apply(env, arguments); + }, + + /** + * Run some shared teardown once after all of the specs in the {@link describe} are run. + * + * _Note:_ Be careful, sharing the teardown from a afterAll makes it easy to accidentally leak state between your specs so that they erroneously pass or fail. + * @name afterAll + * @since 2.1.0 + * @function + * @global + * @param {implementationCallback} [function] Function that contains the code to teardown your specs. + * @param {Int} [timeout={@link jasmine.DEFAULT_TIMEOUT_INTERVAL}] Custom timeout for an async afterAll. + * @see async + */ + afterAll: function() { + return env.afterAll.apply(env, arguments); + }, + + /** + * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult} + * @name setSpecProperty + * @since 3.6.0 + * @function + * @param {String} key The name of the property + * @param {*} value The value of the property + */ + setSpecProperty: function(key, value) { + return env.setSpecProperty(key, value); + }, + + /** + * Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SuiteResult} + * @name setSuiteProperty + * @since 3.6.0 + * @function + * @param {String} key The name of the property + * @param {*} value The value of the property + */ + setSuiteProperty: function(key, value) { + return env.setSuiteProperty(key, value); + }, + + /** + * Create an expectation for a spec. + * @name expect + * @since 1.3.0 + * @function + * @global + * @param {Object} actual - Actual computed value to test expectations against. + * @return {matchers} + */ + expect: function(actual) { + return env.expect(actual); + }, + + /** + * Create an asynchronous expectation for a spec. Note that the matchers + * that are provided by an asynchronous expectation all return promises + * which must be either returned from the spec or waited for using `await` + * in order for Jasmine to associate them with the correct spec. + * @name expectAsync + * @since 3.3.0 + * @function + * @global + * @param {Object} actual - Actual computed value to test expectations against. + * @return {async-matchers} + * @example + * await expectAsync(somePromise).toBeResolved(); + * @example + * return expectAsync(somePromise).toBeResolved(); + */ + expectAsync: function(actual) { + return env.expectAsync(actual); + }, + + /** + * Mark a spec as pending, expectation results will be ignored. + * @name pending + * @since 2.0.0 + * @function + * @global + * @param {String} [message] - Reason the spec is pending. + */ + pending: function() { + return env.pending.apply(env, arguments); + }, + + /** + * Explicitly mark a spec as failed. + * @name fail + * @since 2.1.0 + * @function + * @global + * @param {String|Error} [error] - Reason for the failure. + */ + fail: function() { + return env.fail.apply(env, arguments); + }, + + /** + * Install a spy onto an existing object. + * @name spyOn + * @since 1.3.0 + * @function + * @global + * @param {Object} obj - The object upon which to install the {@link Spy}. + * @param {String} methodName - The name of the method to replace with a {@link Spy}. + * @returns {Spy} + */ + spyOn: function(obj, methodName) { + return env.spyOn(obj, methodName); + }, + + /** + * Install a spy on a property installed with `Object.defineProperty` onto an existing object. + * @name spyOnProperty + * @since 2.6.0 + * @function + * @global + * @param {Object} obj - The object upon which to install the {@link Spy} + * @param {String} propertyName - The name of the property to replace with a {@link Spy}. + * @param {String} [accessType=get] - The access type (get|set) of the property to {@link Spy} on. + * @returns {Spy} + */ + spyOnProperty: function(obj, methodName, accessType) { + return env.spyOnProperty(obj, methodName, accessType); + }, + + /** + * Installs spies on all writable and configurable properties of an object. + * @name spyOnAllFunctions + * @since 3.2.1 + * @function + * @global + * @param {Object} obj - The object upon which to install the {@link Spy}s + * @param {boolean} includeNonEnumerable - Whether or not to add spies to non-enumerable properties + * @returns {Object} the spied object + */ + spyOnAllFunctions: function(obj, includeNonEnumerable) { + return env.spyOnAllFunctions(obj, includeNonEnumerable); + }, + + jsApiReporter: new jasmine.JsApiReporter({ + timer: new jasmine.Timer() + }), + + /** + * @namespace jasmine + */ + jasmine: jasmine + }; + + /** + * Add a custom equality tester for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addCustomEqualityTester + * @since 2.0.0 + * @function + * @param {Function} tester - A function which takes two arguments to compare and returns a `true` or `false` comparison result if it knows how to compare them, and `undefined` otherwise. + * @see custom_equality + */ + jasmine.addCustomEqualityTester = function(tester) { + env.addCustomEqualityTester(tester); + }; + + /** + * Add custom matchers for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addMatchers + * @since 2.0.0 + * @function + * @param {Object} matchers - Keys from this object will be the new matcher names. + * @see custom_matcher + */ + jasmine.addMatchers = function(matchers) { + return env.addMatchers(matchers); + }; + + /** + * Add custom async matchers for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addAsyncMatchers + * @since 3.5.0 + * @function + * @param {Object} matchers - Keys from this object will be the new async matcher names. + * @see custom_matcher + */ + jasmine.addAsyncMatchers = function(matchers) { + return env.addAsyncMatchers(matchers); + }; + + /** + * Add a custom object formatter for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addCustomObjectFormatter + * @since 3.6.0 + * @function + * @param {Function} formatter - A function which takes a value to format and returns a string if it knows how to format it, and `undefined` otherwise. + * @see custom_object_formatters + */ + jasmine.addCustomObjectFormatter = function(formatter) { + return env.addCustomObjectFormatter(formatter); + }; + + /** + * Get the currently booted mock {Clock} for this Jasmine environment. + * @name jasmine.clock + * @since 2.0.0 + * @function + * @returns {Clock} + */ + jasmine.clock = function() { + return env.clock; + }; + + /** + * Create a bare {@link Spy} object. This won't be installed anywhere and will not have any implementation behind it. + * @name jasmine.createSpy + * @since 1.3.0 + * @function + * @param {String} [name] - Name to give the spy. This will be displayed in failure messages. + * @param {Function} [originalFn] - Function to act as the real implementation. + * @return {Spy} + */ + jasmine.createSpy = function(name, originalFn) { + return env.createSpy(name, originalFn); + }; + + /** + * Create an object with multiple {@link Spy}s as its members. + * @name jasmine.createSpyObj + * @since 1.3.0 + * @function + * @param {String} [baseName] - Base name for the spies in the object. + * @param {String[]|Object} methodNames - Array of method names to create spies for, or Object whose keys will be method names and values the {@link Spy#and#returnValue|returnValue}. + * @param {String[]|Object} [propertyNames] - Array of property names to create spies for, or Object whose keys will be propertynames and values the {@link Spy#and#returnValue|returnValue}. + * @return {Object} + */ + jasmine.createSpyObj = function(baseName, methodNames, propertyNames) { + return env.createSpyObj(baseName, methodNames, propertyNames); + }; + + /** + * Add a custom spy strategy for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.addSpyStrategy + * @since 3.5.0 + * @function + * @param {String} name - The name of the strategy (i.e. what you call from `and`) + * @param {Function} factory - Factory function that returns the plan to be executed. + */ + jasmine.addSpyStrategy = function(name, factory) { + return env.addSpyStrategy(name, factory); + }; + + /** + * Set the default spy strategy for the current scope of specs. + * + * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}. + * @name jasmine.setDefaultSpyStrategy + * @function + * @param {Function} defaultStrategyFn - a function that assigns a strategy + * @example + * beforeEach(function() { + * jasmine.setDefaultSpyStrategy(and => and.returnValue(true)); + * }); + */ + jasmine.setDefaultSpyStrategy = function(defaultStrategyFn) { + return env.setDefaultSpyStrategy(defaultStrategyFn); + }; + + return jasmineInterface; +}; + +getJasmineRequireObj().RunableResources = function(j$) { + class RunableResources { + constructor(options) { + this.byRunableId_ = {}; + this.getCurrentRunableId_ = options.getCurrentRunableId; + this.globalErrors_ = options.globalErrors; + + this.spyFactory = new j$.SpyFactory( + () => { + if (this.getCurrentRunableId_()) { + return this.customSpyStrategies(); + } else { + return {}; + } + }, + () => this.defaultSpyStrategy(), + () => this.makeMatchersUtil() + ); + + this.spyRegistry = new j$.SpyRegistry({ + currentSpies: () => this.spies(), + createSpy: (name, originalFn) => + this.spyFactory.createSpy(name, originalFn) + }); + } + + initForRunable(runableId, parentId) { + const newRes = (this.byRunableId_[runableId] = { + customEqualityTesters: [], + customMatchers: {}, + customAsyncMatchers: {}, + customSpyStrategies: {}, + customObjectFormatters: [], + defaultSpyStrategy: undefined, + spies: [] + }); + + const parentRes = this.byRunableId_[parentId]; + + if (parentRes) { + newRes.defaultSpyStrategy = parentRes.defaultSpyStrategy; + const toClone = [ + 'customEqualityTesters', + 'customMatchers', + 'customAsyncMatchers', + 'customObjectFormatters', + 'customSpyStrategies' + ]; + + for (const k of toClone) { + newRes[k] = j$.util.clone(parentRes[k]); + } + } + } + + clearForRunable(runableId) { + this.globalErrors_.removeOverrideListener(); + this.spyRegistry.clearSpies(); + delete this.byRunableId_[runableId]; + } + + spies() { + return this.forCurrentRunable_( + 'Spies must be created in a before function or a spec' + ).spies; + } + + defaultSpyStrategy() { + if (!this.getCurrentRunableId_()) { + return undefined; + } + + return this.byRunableId_[this.getCurrentRunableId_()].defaultSpyStrategy; + } + + setDefaultSpyStrategy(fn) { + this.forCurrentRunable_( + 'Default spy strategy must be set in a before function or a spec' + ).defaultSpyStrategy = fn; + } + + customSpyStrategies() { + return this.forCurrentRunable_( + 'Custom spy strategies must be added in a before function or a spec' + ).customSpyStrategies; + } + + customEqualityTesters() { + return this.forCurrentRunable_( + 'Custom Equalities must be added in a before function or a spec' + ).customEqualityTesters; + } + + customMatchers() { + return this.forCurrentRunable_( + 'Matchers must be added in a before function or a spec' + ).customMatchers; + } + + addCustomMatchers(matchersToAdd) { + const matchers = this.customMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customAsyncMatchers() { + return this.forCurrentRunable_( + 'Async Matchers must be added in a before function or a spec' + ).customAsyncMatchers; + } + + addCustomAsyncMatchers(matchersToAdd) { + const matchers = this.customAsyncMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customObjectFormatters() { + return this.forCurrentRunable_( + 'Custom object formatters must be added in a before function or a spec' + ).customObjectFormatters; + } + + makePrettyPrinter() { + return j$.makePrettyPrinter(this.customObjectFormatters()); + } + + makeMatchersUtil() { + if (this.getCurrentRunableId_()) { + return new j$.MatchersUtil({ + customTesters: this.customEqualityTesters(), + pp: this.makePrettyPrinter() + }); + } else { + return new j$.MatchersUtil({ pp: j$.basicPrettyPrinter_ }); + } + } + + forCurrentRunable_(errorMsg) { + const resources = this.byRunableId_[this.getCurrentRunableId_()]; + + if (!resources && errorMsg) { + throw new Error(errorMsg); + } + + return resources; + } + } + + return RunableResources; +}; + +getJasmineRequireObj().Runner = function(j$) { + class Runner { + constructor(options) { + this.topSuite_ = options.topSuite; + this.totalSpecsDefined_ = options.totalSpecsDefined; + this.focusedRunables_ = options.focusedRunables; + this.runableResources_ = options.runableResources; + this.queueRunnerFactory_ = options.queueRunnerFactory; + this.reporter_ = options.reporter; + this.getConfig_ = options.getConfig; + this.reportSpecDone_ = options.reportSpecDone; + this.hasFailures = false; + this.executedBefore_ = false; + + this.currentlyExecutingSuites_ = []; + this.currentSpec = null; + } + + currentRunable() { + return this.currentSpec || this.currentSuite(); + } + + currentSuite() { + return this.currentlyExecutingSuites_[ + this.currentlyExecutingSuites_.length - 1 + ]; + } + + // Although execute returns a promise, it isn't async for backwards + // compatibility: The "Invalid order" exception needs to be propagated + // synchronously from Env#execute. + // TODO: make this and Env#execute async in the next major release + execute(runablesToRun) { + if (this.executedBefore_) { + this.topSuite_.reset(); + } + this.executedBefore_ = true; + + this.hasFailures = false; + const focusedRunables = this.focusedRunables_(); + const config = this.getConfig_(); + + if (!runablesToRun) { + if (focusedRunables.length) { + runablesToRun = focusedRunables; + } else { + runablesToRun = [this.topSuite_.id]; + } + } + + const order = new j$.Order({ + random: config.random, + seed: j$.isNumber_(config.seed) ? config.seed + '' : config.seed + }); + + const processor = new j$.TreeProcessor({ + tree: this.topSuite_, + runnableIds: runablesToRun, + queueRunnerFactory: options => { + if (options.isLeaf) { + // A spec + options.SkipPolicy = j$.CompleteOnFirstErrorSkipPolicy; + } else { + // A suite + if (config.stopOnSpecFailure) { + options.SkipPolicy = j$.CompleteOnFirstErrorSkipPolicy; + } else { + options.SkipPolicy = j$.SkipAfterBeforeAllErrorPolicy; + } + } + + return this.queueRunnerFactory_(options); + }, + failSpecWithNoExpectations: config.failSpecWithNoExpectations, + nodeStart: (suite, next) => { + this.currentlyExecutingSuites_.push(suite); + this.runableResources_.initForRunable(suite.id, suite.parentSuite.id); + this.reporter_.suiteStarted(suite.result).then(next); + suite.startTimer(); + }, + nodeComplete: (suite, result, next) => { + if (suite !== this.currentSuite()) { + throw new Error('Tried to complete the wrong suite'); + } + + this.runableResources_.clearForRunable(suite.id); + this.currentlyExecutingSuites_.pop(); + + if (result.status === 'failed') { + this.hasFailures = true; + } + suite.endTimer(); + + if (suite.hadBeforeAllFailure) { + this.reportChildrenOfBeforeAllFailure_(suite).then(() => { + this.reportSuiteDone_(suite, result, next); + }); + } else { + this.reportSuiteDone_(suite, result, next); + } + }, + orderChildren: function(node) { + return order.sort(node.children); + }, + excludeNode: function(spec) { + return !config.specFilter(spec); + } + }); + + if (!processor.processTree().valid) { + throw new Error( + 'Invalid order: would cause a beforeAll or afterAll to be run multiple times' + ); + } + + return this.execute2_(runablesToRun, order, processor); + } + + async execute2_(runablesToRun, order, processor) { + const totalSpecsDefined = this.totalSpecsDefined_(); + + this.runableResources_.initForRunable(this.topSuite_.id); + const jasmineTimer = new j$.Timer(); + jasmineTimer.start(); + + /** + * Information passed to the {@link Reporter#jasmineStarted} event. + * @typedef JasmineStartedInfo + * @property {Int} totalSpecsDefined - The total number of specs defined in this suite. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + * @since 2.0.0 + */ + await this.reporter_.jasmineStarted({ + totalSpecsDefined, + order: order + }); + + this.currentlyExecutingSuites_.push(this.topSuite_); + await processor.execute(); + + if (this.topSuite_.hadBeforeAllFailure) { + await this.reportChildrenOfBeforeAllFailure_(this.topSuite_); + } + + this.runableResources_.clearForRunable(this.topSuite_.id); + this.currentlyExecutingSuites_.pop(); + let overallStatus, incompleteReason; + + if ( + this.hasFailures || + this.topSuite_.result.failedExpectations.length > 0 + ) { + overallStatus = 'failed'; + } else if (this.focusedRunables_().length > 0) { + overallStatus = 'incomplete'; + incompleteReason = 'fit() or fdescribe() was found'; + } else if (totalSpecsDefined === 0) { + overallStatus = 'incomplete'; + incompleteReason = 'No specs found'; + } else { + overallStatus = 'passed'; + } + + /** + * Information passed to the {@link Reporter#jasmineDone} event. + * @typedef JasmineDoneInfo + * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'. + * @property {Int} totalTime - The total time (in ms) that it took to execute the suite + * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level. + * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level. + * @since 2.4.0 + */ + const jasmineDoneInfo = { + overallStatus: overallStatus, + totalTime: jasmineTimer.elapsed(), + incompleteReason: incompleteReason, + order: order, + failedExpectations: this.topSuite_.result.failedExpectations, + deprecationWarnings: this.topSuite_.result.deprecationWarnings + }; + this.topSuite_.reportedDone = true; + await this.reporter_.jasmineDone(jasmineDoneInfo); + return jasmineDoneInfo; + } + + reportSuiteDone_(suite, result, next) { + suite.reportedDone = true; + this.reporter_.suiteDone(result).then(next); + } + + async reportChildrenOfBeforeAllFailure_(suite) { + for (const child of suite.children) { + if (child instanceof j$.Suite) { + await this.reporter_.suiteStarted(child.result); + await this.reportChildrenOfBeforeAllFailure_(child); + + // Marking the suite passed is consistent with how suites that + // contain failed specs but no suite-level failures are reported. + child.result.status = 'passed'; + + await this.reporter_.suiteDone(child.result); + } else { + /* a spec */ + await this.reporter_.specStarted(child.result); + + child.addExpectationResult( + false, + { + passed: false, + message: + 'Not run because a beforeAll function failed. The ' + + 'beforeAll failure will be reported on the suite that ' + + 'caused it.' + }, + true + ); + child.result.status = 'failed'; + + await new Promise(resolve => { + this.reportSpecDone_(child, child.result, resolve); + }); + } + } + } + } + + return Runner; +}; + +getJasmineRequireObj().SkipAfterBeforeAllErrorPolicy = function(j$) { + function SkipAfterBeforeAllErrorPolicy(queueableFns) { + this.queueableFns_ = queueableFns; + this.skipping_ = false; + } + + SkipAfterBeforeAllErrorPolicy.prototype.skipTo = function(lastRanFnIx) { + if (this.skipping_) { + return this.nextAfterAllAfter_(lastRanFnIx); + } else { + return lastRanFnIx + 1; + } + }; + + SkipAfterBeforeAllErrorPolicy.prototype.nextAfterAllAfter_ = function(i) { + for ( + i++; + i < this.queueableFns_.length && + this.queueableFns_[i].type !== 'afterAll'; + i++ + ) {} + return i; + }; + + SkipAfterBeforeAllErrorPolicy.prototype.fnErrored = function(fnIx) { + if (this.queueableFns_[fnIx].type === 'beforeAll') { + this.skipping_ = true; + // Failures need to be reported for each contained spec. But we can't do + // that from here because reporting is async. This function isn't async + // (and can't be without greatly complicating QueueRunner). Mark the + // failure so that the code that reports the suite result (which is + // already async) can detect the failure and report the specs. + this.queueableFns_[fnIx].suite.hadBeforeAllFailure = true; + } + }; + + return SkipAfterBeforeAllErrorPolicy; +}; + +getJasmineRequireObj().Spy = function(j$) { + const nextOrder = (function() { + let order = 0; + + return function() { + return order++; + }; + })(); + + /** + * @classdesc _Note:_ Do not construct this directly. Use {@link spyOn}, + * {@link spyOnProperty}, {@link jasmine.createSpy}, or + * {@link jasmine.createSpyObj} instead. + * @class Spy + * @hideconstructor + */ + function Spy(name, matchersUtil, optionals) { + const spy = function(context, args, invokeNew) { + /** + * @name Spy.callData + * @property {object} object - `this` context for the invocation. + * @property {number} invocationOrder - Order of the invocation. + * @property {Array} args - The arguments passed for this invocation. + * @property returnValue - The value that was returned from this invocation. + */ + const callData = { + object: context, + invocationOrder: nextOrder(), + args: Array.prototype.slice.apply(args) + }; + + callTracker.track(callData); + const returnValue = strategyDispatcher.exec(context, args, invokeNew); + callData.returnValue = returnValue; + + return returnValue; + }; + const { originalFn, customStrategies, defaultStrategyFn } = optionals || {}; + + const numArgs = typeof originalFn === 'function' ? originalFn.length : 0, + wrapper = makeFunc(numArgs, function(context, args, invokeNew) { + return spy(context, args, invokeNew); + }), + strategyDispatcher = new SpyStrategyDispatcher( + { + name: name, + fn: originalFn, + getSpy: function() { + return wrapper; + }, + customStrategies: customStrategies + }, + matchersUtil + ), + callTracker = new j$.CallTracker(); + + function makeFunc(length, fn) { + switch (length) { + case 1: + return function wrap1(a) { + return fn(this, arguments, this instanceof wrap1); + }; + case 2: + return function wrap2(a, b) { + return fn(this, arguments, this instanceof wrap2); + }; + case 3: + return function wrap3(a, b, c) { + return fn(this, arguments, this instanceof wrap3); + }; + case 4: + return function wrap4(a, b, c, d) { + return fn(this, arguments, this instanceof wrap4); + }; + case 5: + return function wrap5(a, b, c, d, e) { + return fn(this, arguments, this instanceof wrap5); + }; + case 6: + return function wrap6(a, b, c, d, e, f) { + return fn(this, arguments, this instanceof wrap6); + }; + case 7: + return function wrap7(a, b, c, d, e, f, g) { + return fn(this, arguments, this instanceof wrap7); + }; + case 8: + return function wrap8(a, b, c, d, e, f, g, h) { + return fn(this, arguments, this instanceof wrap8); + }; + case 9: + return function wrap9(a, b, c, d, e, f, g, h, i) { + return fn(this, arguments, this instanceof wrap9); + }; + default: + return function wrap() { + return fn(this, arguments, this instanceof wrap); + }; + } + } + + for (const prop in originalFn) { + if (prop === 'and' || prop === 'calls') { + throw new Error( + "Jasmine spies would overwrite the 'and' and 'calls' properties on the object being spied upon" + ); + } + + wrapper[prop] = originalFn[prop]; + } + + /** + * @member {SpyStrategy} - Accesses the default strategy for the spy. This strategy will be used + * whenever the spy is called with arguments that don't match any strategy + * created with {@link Spy#withArgs}. + * @name Spy#and + * @since 2.0.0 + * @example + * spyOn(someObj, 'func').and.returnValue(42); + */ + wrapper.and = strategyDispatcher.and; + /** + * Specifies a strategy to be used for calls to the spy that have the + * specified arguments. + * @name Spy#withArgs + * @since 3.0.0 + * @function + * @param {...*} args - The arguments to match + * @type {SpyStrategy} + * @example + * spyOn(someObj, 'func').withArgs(1, 2, 3).and.returnValue(42); + * someObj.func(1, 2, 3); // returns 42 + */ + wrapper.withArgs = function() { + return strategyDispatcher.withArgs.apply(strategyDispatcher, arguments); + }; + wrapper.calls = callTracker; + + if (defaultStrategyFn) { + defaultStrategyFn(wrapper.and); + } + + return wrapper; + } + + function SpyStrategyDispatcher(strategyArgs, matchersUtil) { + const baseStrategy = new j$.SpyStrategy(strategyArgs); + const argsStrategies = new StrategyDict(function() { + return new j$.SpyStrategy(strategyArgs); + }, matchersUtil); + + this.and = baseStrategy; + + this.exec = function(spy, args, invokeNew) { + let strategy = argsStrategies.get(args); + + if (!strategy) { + if (argsStrategies.any() && !baseStrategy.isConfigured()) { + throw new Error( + "Spy '" + + strategyArgs.name + + "' received a call with arguments " + + j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) + + ' but all configured strategies specify other arguments.' + ); + } else { + strategy = baseStrategy; + } + } + + return strategy.exec(spy, args, invokeNew); + }; + + this.withArgs = function() { + return { and: argsStrategies.getOrCreate(arguments) }; + }; + } + + function StrategyDict(strategyFactory, matchersUtil) { + this.strategies = []; + this.strategyFactory = strategyFactory; + this.matchersUtil = matchersUtil; + } + + StrategyDict.prototype.any = function() { + return this.strategies.length > 0; + }; + + StrategyDict.prototype.getOrCreate = function(args) { + let strategy = this.get(args); + + if (!strategy) { + strategy = this.strategyFactory(); + this.strategies.push({ + args: args, + strategy: strategy + }); + } + + return strategy; + }; + + StrategyDict.prototype.get = function(args) { + for (let i = 0; i < this.strategies.length; i++) { + if (this.matchersUtil.equals(args, this.strategies[i].args)) { + return this.strategies[i].strategy; + } + } + }; + + return Spy; +}; + +getJasmineRequireObj().SpyFactory = function(j$) { + function SpyFactory( + getCustomStrategies, + getDefaultStrategyFn, + getMatchersUtil + ) { + this.createSpy = function(name, originalFn) { + if (j$.isFunction_(name) && originalFn === undefined) { + originalFn = name; + name = originalFn.name; + } + + return j$.Spy(name, getMatchersUtil(), { + originalFn, + customStrategies: getCustomStrategies(), + defaultStrategyFn: getDefaultStrategyFn() + }); + }; + + this.createSpyObj = function(baseName, methodNames, propertyNames) { + const baseNameIsCollection = + j$.isObject_(baseName) || j$.isArray_(baseName); + + if (baseNameIsCollection) { + propertyNames = methodNames; + methodNames = baseName; + baseName = 'unknown'; + } + + const obj = {}; + + const methods = normalizeKeyValues(methodNames); + for (let i = 0; i < methods.length; i++) { + const spy = (obj[methods[i][0]] = this.createSpy( + baseName + '.' + methods[i][0] + )); + if (methods[i].length > 1) { + spy.and.returnValue(methods[i][1]); + } + } + + const properties = normalizeKeyValues(propertyNames); + for (let i = 0; i < properties.length; i++) { + const descriptor = { + enumerable: true, + get: this.createSpy(baseName + '.' + properties[i][0] + '.get'), + set: this.createSpy(baseName + '.' + properties[i][0] + '.set') + }; + if (properties[i].length > 1) { + descriptor.get.and.returnValue(properties[i][1]); + descriptor.set.and.returnValue(properties[i][1]); + } + Object.defineProperty(obj, properties[i][0], descriptor); + } + + if (methods.length === 0 && properties.length === 0) { + throw 'createSpyObj requires a non-empty array or object of method names to create spies for'; + } + + return obj; + }; + } + + function normalizeKeyValues(object) { + const result = []; + if (j$.isArray_(object)) { + for (let i = 0; i < object.length; i++) { + result.push([object[i]]); + } + } else if (j$.isObject_(object)) { + for (const key in object) { + if (object.hasOwnProperty(key)) { + result.push([key, object[key]]); + } + } + } + return result; + } + + return SpyFactory; +}; + +getJasmineRequireObj().SpyRegistry = function(j$) { + const spyOnMsg = j$.formatErrorMsg( + '', + 'spyOn(, )' + ); + const spyOnPropertyMsg = j$.formatErrorMsg( + '', + 'spyOnProperty(, , [accessType])' + ); + + function SpyRegistry(options) { + options = options || {}; + const global = options.global || j$.getGlobal(); + const createSpy = options.createSpy; + const currentSpies = + options.currentSpies || + function() { + return []; + }; + + this.allowRespy = function(allow) { + this.respy = allow; + }; + + this.spyOn = function(obj, methodName) { + const getErrorMsg = spyOnMsg; + + if (j$.util.isUndefined(obj) || obj === null) { + throw new Error( + getErrorMsg( + 'could not find an object to spy upon for ' + methodName + '()' + ) + ); + } + + if (j$.util.isUndefined(methodName) || methodName === null) { + throw new Error(getErrorMsg('No method name supplied')); + } + + if (j$.util.isUndefined(obj[methodName])) { + throw new Error(getErrorMsg(methodName + '() method does not exist')); + } + + if (obj[methodName] && j$.isSpy(obj[methodName])) { + if (this.respy) { + return obj[methodName]; + } else { + throw new Error( + getErrorMsg(methodName + ' has already been spied upon') + ); + } + } + + const descriptor = Object.getOwnPropertyDescriptor(obj, methodName); + + if (descriptor && !(descriptor.writable || descriptor.set)) { + throw new Error( + getErrorMsg(methodName + ' is not declared writable or has no setter') + ); + } + + const originalMethod = obj[methodName]; + const spiedMethod = createSpy(methodName, originalMethod); + let restoreStrategy; + + if ( + Object.prototype.hasOwnProperty.call(obj, methodName) || + (obj === global && methodName === 'onerror') + ) { + restoreStrategy = function() { + obj[methodName] = originalMethod; + }; + } else { + restoreStrategy = function() { + if (!delete obj[methodName]) { + obj[methodName] = originalMethod; + } + }; + } + + currentSpies().push({ + restoreObjectToOriginalState: restoreStrategy + }); + + obj[methodName] = spiedMethod; + + return spiedMethod; + }; + + this.spyOnProperty = function(obj, propertyName, accessType) { + const getErrorMsg = spyOnPropertyMsg; + + accessType = accessType || 'get'; + + if (j$.util.isUndefined(obj)) { + throw new Error( + getErrorMsg( + 'spyOn could not find an object to spy upon for ' + + propertyName + + '' + ) + ); + } + + if (j$.util.isUndefined(propertyName)) { + throw new Error(getErrorMsg('No property name supplied')); + } + + const descriptor = j$.util.getPropertyDescriptor(obj, propertyName); + + if (!descriptor) { + throw new Error(getErrorMsg(propertyName + ' property does not exist')); + } + + if (!descriptor.configurable) { + throw new Error( + getErrorMsg(propertyName + ' is not declared configurable') + ); + } + + if (!descriptor[accessType]) { + throw new Error( + getErrorMsg( + 'Property ' + + propertyName + + ' does not have access type ' + + accessType + ) + ); + } + + if (j$.isSpy(descriptor[accessType])) { + if (this.respy) { + return descriptor[accessType]; + } else { + throw new Error( + getErrorMsg( + propertyName + '#' + accessType + ' has already been spied upon' + ) + ); + } + } + + const originalDescriptor = j$.util.clone(descriptor); + const spy = createSpy(propertyName, descriptor[accessType]); + let restoreStrategy; + + if (Object.prototype.hasOwnProperty.call(obj, propertyName)) { + restoreStrategy = function() { + Object.defineProperty(obj, propertyName, originalDescriptor); + }; + } else { + restoreStrategy = function() { + delete obj[propertyName]; + }; + } + + currentSpies().push({ + restoreObjectToOriginalState: restoreStrategy + }); + + descriptor[accessType] = spy; + + Object.defineProperty(obj, propertyName, descriptor); + + return spy; + }; + + this.spyOnAllFunctions = function(obj, includeNonEnumerable) { + if (j$.util.isUndefined(obj)) { + throw new Error( + 'spyOnAllFunctions could not find an object to spy upon' + ); + } + + let pointer = obj, + propsToSpyOn = [], + properties, + propertiesToSkip = []; + + while ( + pointer && + (!includeNonEnumerable || pointer !== Object.prototype) + ) { + properties = getProps(pointer, includeNonEnumerable); + properties = properties.filter(function(prop) { + return propertiesToSkip.indexOf(prop) === -1; + }); + propertiesToSkip = propertiesToSkip.concat(properties); + propsToSpyOn = propsToSpyOn.concat( + getSpyableFunctionProps(pointer, properties) + ); + pointer = Object.getPrototypeOf(pointer); + } + + for (const prop of propsToSpyOn) { + this.spyOn(obj, prop); + } + + return obj; + }; + + this.clearSpies = function() { + const spies = currentSpies(); + for (let i = spies.length - 1; i >= 0; i--) { + const spyEntry = spies[i]; + spyEntry.restoreObjectToOriginalState(); + } + }; + } + + function getProps(obj, includeNonEnumerable) { + const enumerableProperties = Object.keys(obj); + + if (!includeNonEnumerable) { + return enumerableProperties; + } + + return Object.getOwnPropertyNames(obj).filter(function(prop) { + return ( + prop !== 'constructor' || + enumerableProperties.indexOf('constructor') > -1 + ); + }); + } + + function getSpyableFunctionProps(obj, propertiesToCheck) { + const props = []; + + for (const prop of propertiesToCheck) { + if ( + Object.prototype.hasOwnProperty.call(obj, prop) && + isSpyableProp(obj, prop) + ) { + props.push(prop); + } + } + return props; + } + + function isSpyableProp(obj, prop) { + let value; + try { + value = obj[prop]; + } catch (e) { + return false; + } + + if (value instanceof Function) { + const descriptor = Object.getOwnPropertyDescriptor(obj, prop); + return (descriptor.writable || descriptor.set) && descriptor.configurable; + } + return false; + } + + return SpyRegistry; +}; + +getJasmineRequireObj().SpyStrategy = function(j$) { + /** + * @interface SpyStrategy + */ + function SpyStrategy(options) { + options = options || {}; + + /** + * Get the identifying information for the spy. + * @name SpyStrategy#identity + * @since 3.0.0 + * @member + * @type {String} + */ + this.identity = options.name || 'unknown'; + this.originalFn = options.fn || function() {}; + this.getSpy = options.getSpy || function() {}; + this.plan = this._defaultPlan = function() {}; + + const cs = options.customStrategies || {}; + for (const k in cs) { + if (j$.util.has(cs, k) && !this[k]) { + this[k] = createCustomPlan(cs[k]); + } + } + + /** + * Tell the spy to return a promise resolving to the specified value when invoked. + * @name SpyStrategy#resolveTo + * @since 3.5.0 + * @function + * @param {*} value The value to return. + */ + this.resolveTo = function(value) { + this.plan = function() { + return Promise.resolve(value); + }; + return this.getSpy(); + }; + + /** + * Tell the spy to return a promise rejecting with the specified value when invoked. + * @name SpyStrategy#rejectWith + * @since 3.5.0 + * @function + * @param {*} value The value to return. + */ + this.rejectWith = function(value) { + this.plan = function() { + return Promise.reject(value); + }; + return this.getSpy(); + }; + } + + function createCustomPlan(factory) { + return function() { + const plan = factory.apply(null, arguments); + + if (!j$.isFunction_(plan)) { + throw new Error('Spy strategy must return a function'); + } + + this.plan = plan; + return this.getSpy(); + }; + } + + /** + * Execute the current spy strategy. + * @name SpyStrategy#exec + * @since 2.0.0 + * @function + */ + SpyStrategy.prototype.exec = function(context, args, invokeNew) { + const contextArgs = [context].concat( + args ? Array.prototype.slice.call(args) : [] + ); + const target = this.plan.bind.apply(this.plan, contextArgs); + + return invokeNew ? new target() : target(); + }; + + /** + * Tell the spy to call through to the real implementation when invoked. + * @name SpyStrategy#callThrough + * @since 2.0.0 + * @function + */ + SpyStrategy.prototype.callThrough = function() { + this.plan = this.originalFn; + return this.getSpy(); + }; + + /** + * Tell the spy to return the value when invoked. + * @name SpyStrategy#returnValue + * @since 2.0.0 + * @function + * @param {*} value The value to return. + */ + SpyStrategy.prototype.returnValue = function(value) { + this.plan = function() { + return value; + }; + return this.getSpy(); + }; + + /** + * Tell the spy to return one of the specified values (sequentially) each time the spy is invoked. + * @name SpyStrategy#returnValues + * @since 2.1.0 + * @function + * @param {...*} values - Values to be returned on subsequent calls to the spy. + */ + SpyStrategy.prototype.returnValues = function() { + const values = Array.prototype.slice.call(arguments); + this.plan = function() { + return values.shift(); + }; + return this.getSpy(); + }; + + /** + * Tell the spy to throw an error when invoked. + * @name SpyStrategy#throwError + * @since 2.0.0 + * @function + * @param {Error|Object|String} something Thing to throw + */ + SpyStrategy.prototype.throwError = function(something) { + const error = j$.isString_(something) ? new Error(something) : something; + this.plan = function() { + throw error; + }; + return this.getSpy(); + }; + + /** + * Tell the spy to call a fake implementation when invoked. + * @name SpyStrategy#callFake + * @since 2.0.0 + * @function + * @param {Function} fn The function to invoke with the passed parameters. + */ + SpyStrategy.prototype.callFake = function(fn) { + if ( + !( + j$.isFunction_(fn) || + j$.isAsyncFunction_(fn) || + j$.isGeneratorFunction_(fn) + ) + ) { + throw new Error( + 'Argument passed to callFake should be a function, got ' + fn + ); + } + this.plan = fn; + return this.getSpy(); + }; + + /** + * Tell the spy to do nothing when invoked. This is the default. + * @name SpyStrategy#stub + * @since 2.0.0 + * @function + */ + SpyStrategy.prototype.stub = function(fn) { + this.plan = function() {}; + return this.getSpy(); + }; + + SpyStrategy.prototype.isConfigured = function() { + return this.plan !== this._defaultPlan; + }; + + return SpyStrategy; +}; + +getJasmineRequireObj().StackTrace = function(j$) { + function StackTrace(error) { + let lines = error.stack.split('\n').filter(function(line) { + return line !== ''; + }); + + const extractResult = extractMessage(error.message, lines); + + if (extractResult) { + this.message = extractResult.message; + lines = extractResult.remainder; + } + + const parseResult = tryParseFrames(lines); + this.frames = parseResult.frames; + this.style = parseResult.style; + } + + const framePatterns = [ + // Node, Chrome, Edge + // e.g. " at QueueRunner.run (http://localhost:8888/__jasmine__/jasmine.js:4320:20)" + // Note that the "function name" can include a surprisingly large set of + // characters, including angle brackets and square brackets. + { + re: /^\s*at ([^\)]+) \(([^\)]+)\)$/, + fnIx: 1, + fileLineColIx: 2, + style: 'v8' + }, + + // NodeJS alternate form, often mixed in with the Chrome style + // e.g. " at /some/path:4320:20 + { re: /\s*at (.+)$/, fileLineColIx: 1, style: 'v8' }, + + // PhantomJS on OS X, Safari, Firefox + // e.g. "run@http://localhost:8888/__jasmine__/jasmine.js:4320:27" + // or "http://localhost:8888/__jasmine__/jasmine.js:4320:27" + { + re: /^(?:(([^@\s]+)@)|@)?([^\s]+)$/, + fnIx: 2, + fileLineColIx: 3, + style: 'webkit' + } + ]; + + // regexes should capture the function name (if any) as group 1 + // and the file, line, and column as group 2. + function tryParseFrames(lines) { + let style = null; + const frames = lines.map(function(line) { + const convertedLine = first(framePatterns, function(pattern) { + const overallMatch = line.match(pattern.re); + if (!overallMatch) { + return null; + } + + const fileLineColMatch = overallMatch[pattern.fileLineColIx].match( + /^(.*):(\d+):\d+$/ + ); + if (!fileLineColMatch) { + return null; + } + + style = style || pattern.style; + return { + raw: line, + file: fileLineColMatch[1], + line: parseInt(fileLineColMatch[2], 10), + func: overallMatch[pattern.fnIx] + }; + }); + + return convertedLine || { raw: line }; + }); + + return { + style: style, + frames: frames + }; + } + + function first(items, fn) { + for (const item of items) { + const result = fn(item); + + if (result) { + return result; + } + } + } + + function extractMessage(message, stackLines) { + const len = messagePrefixLength(message, stackLines); + + if (len > 0) { + return { + message: stackLines.slice(0, len).join('\n'), + remainder: stackLines.slice(len) + }; + } + } + + function messagePrefixLength(message, stackLines) { + if (!stackLines[0].match(/^\w*Error/)) { + return 0; + } + + const messageLines = message.split('\n'); + + for (let i = 1; i < messageLines.length; i++) { + if (messageLines[i] !== stackLines[i]) { + return 0; + } + } + + return messageLines.length; + } + + return StackTrace; +}; + +getJasmineRequireObj().Suite = function(j$) { + function Suite(attrs) { + this.env = attrs.env; + this.id = attrs.id; + this.parentSuite = attrs.parentSuite; + this.description = attrs.description; + this.expectationFactory = attrs.expectationFactory; + this.asyncExpectationFactory = attrs.asyncExpectationFactory; + this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; + this.onLateError = attrs.onLateError || function() {}; + + this.beforeFns = []; + this.afterFns = []; + this.beforeAllFns = []; + this.afterAllFns = []; + this.timer = attrs.timer || new j$.Timer(); + this.children = []; + + this.reset(); + } + + Suite.prototype.setSuiteProperty = function(key, value) { + this.result.properties = this.result.properties || {}; + this.result.properties[key] = value; + }; + + Suite.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Suite.prototype.expectAsync = function(actual) { + return this.asyncExpectationFactory(actual, this); + }; + + Suite.prototype.getFullName = function() { + const fullName = []; + for ( + let parentSuite = this; + parentSuite; + parentSuite = parentSuite.parentSuite + ) { + if (parentSuite.parentSuite) { + fullName.unshift(parentSuite.description); + } + } + return fullName.join(' '); + }; + + /* + * Mark the suite with "pending" status + */ + Suite.prototype.pend = function() { + this.markedPending = true; + }; + + /* + * Like {@link Suite#pend}, but pending state will survive {@link Spec#reset} + * Useful for fdescribe, xdescribe, where pending state should remain. + */ + Suite.prototype.exclude = function() { + this.pend(); + this.markedExcluding = true; + }; + + Suite.prototype.beforeEach = function(fn) { + this.beforeFns.unshift({ ...fn, suite: this }); + }; + + Suite.prototype.beforeAll = function(fn) { + this.beforeAllFns.push({ ...fn, type: 'beforeAll', suite: this }); + }; + + Suite.prototype.afterEach = function(fn) { + this.afterFns.unshift({ ...fn, suite: this, type: 'afterEach' }); + }; + + Suite.prototype.afterAll = function(fn) { + this.afterAllFns.unshift({ ...fn, type: 'afterAll' }); + }; + + Suite.prototype.startTimer = function() { + this.timer.start(); + }; + + Suite.prototype.endTimer = function() { + this.result.duration = this.timer.elapsed(); + }; + + function removeFns(queueableFns) { + for (const qf of queueableFns) { + qf.fn = null; + } + } + + Suite.prototype.cleanupBeforeAfter = function() { + if (this.autoCleanClosures) { + removeFns(this.beforeAllFns); + removeFns(this.afterAllFns); + removeFns(this.beforeFns); + removeFns(this.afterFns); + } + }; + + Suite.prototype.reset = function() { + /** + * @typedef SuiteResult + * @property {String} id - The unique id of this suite. + * @property {String} description - The description text passed to the {@link describe} that made this suite. + * @property {String} fullName - The full description including all ancestors of this suite. + * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. + * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. + * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty} + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + deprecationWarnings: [], + duration: null, + properties: null + }; + this.markedPending = this.markedExcluding; + this.children.forEach(function(child) { + child.reset(); + }); + this.reportedDone = false; + }; + + Suite.prototype.addChild = function(child) { + this.children.push(child); + }; + + Suite.prototype.status = function() { + if (this.markedPending) { + return 'pending'; + } + + if (this.result.failedExpectations.length > 0) { + return 'failed'; + } else { + return 'passed'; + } + }; + + Suite.prototype.canBeReentered = function() { + return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0; + }; + + Suite.prototype.getResult = function() { + this.result.status = this.status(); + return this.result; + }; + + Suite.prototype.sharedUserContext = function() { + if (!this.sharedContext) { + this.sharedContext = this.parentSuite + ? this.parentSuite.clonedSharedUserContext() + : new j$.UserContext(); + } + + return this.sharedContext; + }; + + Suite.prototype.clonedSharedUserContext = function() { + return j$.UserContext.fromExisting(this.sharedUserContext()); + }; + + Suite.prototype.handleException = function() { + if (arguments[0] instanceof j$.errors.ExpectationFailed) { + return; + } + + const data = { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: arguments[0] + }; + const failedExpectation = j$.buildExpectationResult(data); + + if (!this.parentSuite) { + failedExpectation.globalErrorType = 'afterAll'; + } + + if (this.reportedDone) { + this.onLateError(failedExpectation); + } else { + this.result.failedExpectations.push(failedExpectation); + } + }; + + Suite.prototype.onMultipleDone = function() { + let msg; + + // Issue a deprecation. Include the context ourselves and pass + // ignoreRunnable: true, since getting here always means that we've already + // moved on and the current runnable isn't the one that caused the problem. + if (this.parentSuite) { + msg = + "An asynchronous beforeAll or afterAll function called its 'done' " + + 'callback more than once.\n' + + '(in suite: ' + + this.getFullName() + + ')'; + } else { + msg = + 'A top-level beforeAll or afterAll function called its ' + + "'done' callback more than once."; + } + + this.onLateError(new Error(msg)); + }; + + Suite.prototype.addExpectationResult = function() { + if (isFailure(arguments)) { + const data = arguments[1]; + const expectationResult = j$.buildExpectationResult(data); + + if (this.reportedDone) { + this.onLateError(expectationResult); + } else { + this.result.failedExpectations.push(expectationResult); + + // TODO: refactor so that we don't need to override cached status + if (this.result.status) { + this.result.status = 'failed'; + } + } + + if (this.throwOnExpectationFailure) { + throw new j$.errors.ExpectationFailed(); + } + } + }; + + Suite.prototype.addDeprecationWarning = function(deprecation) { + if (typeof deprecation === 'string') { + deprecation = { message: deprecation }; + } + this.result.deprecationWarnings.push( + j$.buildExpectationResult(deprecation) + ); + }; + + Object.defineProperty(Suite.prototype, 'metadata', { + get: function() { + if (!this.metadata_) { + this.metadata_ = new SuiteMetadata(this); + } + + return this.metadata_; + } + }); + + /** + * @interface Suite + * @see Env#topSuite + * @since 2.0.0 + */ + function SuiteMetadata(suite) { + this.suite_ = suite; + /** + * The unique ID of this suite. + * @name Suite#id + * @readonly + * @type {string} + * @since 2.0.0 + */ + this.id = suite.id; + + /** + * The parent of this suite, or null if this is the top suite. + * @name Suite#parentSuite + * @readonly + * @type {Suite} + */ + this.parentSuite = suite.parentSuite ? suite.parentSuite.metadata : null; + + /** + * The description passed to the {@link describe} that created this suite. + * @name Suite#description + * @readonly + * @type {string} + * @since 2.0.0 + */ + this.description = suite.description; + } + + /** + * The full description including all ancestors of this suite. + * @name Suite#getFullName + * @function + * @returns {string} + * @since 2.0.0 + */ + SuiteMetadata.prototype.getFullName = function() { + return this.suite_.getFullName(); + }; + + /** + * The suite's children. + * @name Suite#children + * @type {Array.<(Spec|Suite)>} + * @since 2.0.0 + */ + Object.defineProperty(SuiteMetadata.prototype, 'children', { + get: function() { + return this.suite_.children.map(child => child.metadata); + } + }); + + function isFailure(args) { + return !args[0]; + } + + return Suite; +}; + +getJasmineRequireObj().SuiteBuilder = function(j$) { + class SuiteBuilder { + constructor(options) { + this.env_ = options.env; + this.expectationFactory_ = options.expectationFactory; + this.suiteAsyncExpectationFactory_ = function(actual, suite) { + return options.asyncExpectationFactory(actual, suite, 'Suite'); + }; + this.specAsyncExpectationFactory_ = function(actual, suite) { + return options.asyncExpectationFactory(actual, suite, 'Spec'); + }; + this.onLateError_ = options.onLateError; + this.specResultCallback_ = options.specResultCallback; + this.specStarted_ = options.specStarted; + + this.nextSuiteId_ = 0; + this.nextSpecId_ = 0; + + this.topSuite = this.suiteFactory_('Jasmine__TopLevel__Suite'); + this.currentDeclarationSuite_ = this.topSuite; + this.totalSpecsDefined = 0; + this.focusedRunables = []; + } + + describe(description, definitionFn) { + ensureIsFunction(definitionFn, 'describe'); + const suite = this.suiteFactory_(description); + if (definitionFn.length > 0) { + throw new Error('describe does not expect any arguments'); + } + if (this.currentDeclarationSuite_.markedExcluding) { + suite.exclude(); + } + this.addSpecsToSuite_(suite, definitionFn); + return suite; + } + + fdescribe(description, definitionFn) { + ensureIsFunction(definitionFn, 'fdescribe'); + const suite = this.suiteFactory_(description); + suite.isFocused = true; + + this.focusedRunables.push(suite.id); + this.unfocusAncestor_(); + this.addSpecsToSuite_(suite, definitionFn); + + return suite; + } + + xdescribe(description, definitionFn) { + ensureIsFunction(definitionFn, 'xdescribe'); + const suite = this.suiteFactory_(description); + suite.exclude(); + this.addSpecsToSuite_(suite, definitionFn); + + return suite; + } + + it(description, fn, timeout) { + // it() sometimes doesn't have a fn argument, so only check the type if + // it's given. + if (arguments.length > 1 && typeof fn !== 'undefined') { + ensureIsFunctionOrAsync(fn, 'it'); + } + + return this.it_(description, fn, timeout); + } + + xit(description, fn, timeout) { + // xit(), like it(), doesn't always have a fn argument, so only check the + // type when needed. + if (arguments.length > 1 && typeof fn !== 'undefined') { + ensureIsFunctionOrAsync(fn, 'xit'); + } + const spec = this.it_(description, fn, timeout); + spec.exclude('Temporarily disabled with xit'); + return spec; + } + + fit(description, fn, timeout) { + // Unlike it and xit, the function is required because it doesn't make + // sense to focus on nothing. + ensureIsFunctionOrAsync(fn, 'fit'); + + if (timeout) { + j$.util.validateTimeout(timeout); + } + const spec = this.specFactory_(description, fn, timeout); + this.currentDeclarationSuite_.addChild(spec); + this.focusedRunables.push(spec.id); + this.unfocusAncestor_(); + return spec; + } + + beforeEach(beforeEachFunction, timeout) { + ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach'); + + if (timeout) { + j$.util.validateTimeout(timeout); + } + + this.currentDeclarationSuite_.beforeEach({ + fn: beforeEachFunction, + timeout: timeout || 0 + }); + } + + beforeAll(beforeAllFunction, timeout) { + ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll'); + + if (timeout) { + j$.util.validateTimeout(timeout); + } + + this.currentDeclarationSuite_.beforeAll({ + fn: beforeAllFunction, + timeout: timeout || 0 + }); + } + + afterEach(afterEachFunction, timeout) { + ensureIsFunctionOrAsync(afterEachFunction, 'afterEach'); + + if (timeout) { + j$.util.validateTimeout(timeout); + } + + afterEachFunction.isCleanup = true; + this.currentDeclarationSuite_.afterEach({ + fn: afterEachFunction, + timeout: timeout || 0 + }); + } + + afterAll(afterAllFunction, timeout) { + ensureIsFunctionOrAsync(afterAllFunction, 'afterAll'); + + if (timeout) { + j$.util.validateTimeout(timeout); + } + + this.currentDeclarationSuite_.afterAll({ + fn: afterAllFunction, + timeout: timeout || 0 + }); + } + + it_(description, fn, timeout) { + if (timeout) { + j$.util.validateTimeout(timeout); + } + + const spec = this.specFactory_(description, fn, timeout); + if (this.currentDeclarationSuite_.markedExcluding) { + spec.exclude(); + } + this.currentDeclarationSuite_.addChild(spec); + + return spec; + } + + suiteFactory_(description) { + const config = this.env_.configuration(); + return new j$.Suite({ + id: 'suite' + this.nextSuiteId_++, + description, + parentSuite: this.currentDeclarationSuite_, + timer: new j$.Timer(), + expectationFactory: this.expectationFactory_, + asyncExpectationFactory: this.suiteAsyncExpectationFactory_, + throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, + onLateError: this.onLateError_ + }); + } + + addSpecsToSuite_(suite, definitionFn) { + const parentSuite = this.currentDeclarationSuite_; + parentSuite.addChild(suite); + this.currentDeclarationSuite_ = suite; + let threw = false; + + try { + definitionFn(); + } catch (e) { + suite.handleException(e); + threw = true; + } + + if (suite.parentSuite && !suite.children.length && !threw) { + throw new Error( + `describe with no children (describe() or it()): ${suite.getFullName()}` + ); + } + + this.currentDeclarationSuite_ = parentSuite; + } + + specFactory_(description, fn, timeout) { + this.totalSpecsDefined++; + const config = this.env_.configuration(); + const suite = this.currentDeclarationSuite_; + const spec = new j$.Spec({ + id: 'spec' + this.nextSpecId_++, + beforeAndAfterFns: beforeAndAfterFns(suite), + expectationFactory: this.expectationFactory_, + asyncExpectationFactory: this.specAsyncExpectationFactory_, + onLateError: this.onLateError_, + resultCallback: (result, next) => { + this.specResultCallback_(spec, result, next); + }, + getSpecName: function(spec) { + return getSpecName(spec, suite); + }, + onStart: (spec, next) => this.specStarted_(spec, suite, next), + description: description, + userContext: function() { + return suite.clonedSharedUserContext(); + }, + queueableFn: { + fn: fn, + timeout: timeout || 0 + }, + throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, + timer: new j$.Timer() + }); + return spec; + } + + unfocusAncestor_() { + const focusedAncestor = findFocusedAncestor( + this.currentDeclarationSuite_ + ); + + if (focusedAncestor) { + for (let i = 0; i < this.focusedRunables.length; i++) { + if (this.focusedRunables[i] === focusedAncestor) { + this.focusedRunables.splice(i, 1); + break; + } + } + } + } + } + + function findFocusedAncestor(suite) { + while (suite) { + if (suite.isFocused) { + return suite.id; + } + suite = suite.parentSuite; + } + + return null; + } + + function ensureIsFunction(fn, caller) { + if (!j$.isFunction_(fn)) { + throw new Error( + caller + ' expects a function argument; received ' + j$.getType_(fn) + ); + } + } + + function ensureIsFunctionOrAsync(fn, caller) { + if (!j$.isFunction_(fn) && !j$.isAsyncFunction_(fn)) { + throw new Error( + caller + ' expects a function argument; received ' + j$.getType_(fn) + ); + } + } + + function beforeAndAfterFns(targetSuite) { + return function() { + let befores = [], + afters = [], + suite = targetSuite; + + while (suite) { + befores = befores.concat(suite.beforeFns); + afters = afters.concat(suite.afterFns); + + suite = suite.parentSuite; + } + + return { + befores: befores.reverse(), + afters: afters + }; + }; + } + + function getSpecName(spec, suite) { + const fullName = [spec.description], + suiteFullName = suite.getFullName(); + + if (suiteFullName !== '') { + fullName.unshift(suiteFullName); + } + return fullName.join(' '); + } + + return SuiteBuilder; +}; + +getJasmineRequireObj().Timer = function() { + const defaultNow = (function(Date) { + return function() { + return new Date().getTime(); + }; + })(Date); + + function Timer(options) { + options = options || {}; + + const now = options.now || defaultNow; + let startTime; + + this.start = function() { + startTime = now(); + }; + + this.elapsed = function() { + return now() - startTime; + }; + } + + return Timer; +}; + +getJasmineRequireObj().TreeProcessor = function() { + function TreeProcessor(attrs) { + const tree = attrs.tree; + const runnableIds = attrs.runnableIds; + const queueRunnerFactory = attrs.queueRunnerFactory; + const nodeStart = attrs.nodeStart || function() {}; + const nodeComplete = attrs.nodeComplete || function() {}; + const failSpecWithNoExpectations = !!attrs.failSpecWithNoExpectations; + const orderChildren = + attrs.orderChildren || + function(node) { + return node.children; + }; + const excludeNode = + attrs.excludeNode || + function(node) { + return false; + }; + let stats = { valid: true }; + let processed = false; + const defaultMin = Infinity; + const defaultMax = 1 - Infinity; + + this.processTree = function() { + processNode(tree, true); + processed = true; + return stats; + }; + + this.execute = async function() { + if (!processed) { + this.processTree(); + } + + if (!stats.valid) { + throw 'invalid order'; + } + + const childFns = wrapChildren(tree, 0); + + await new Promise(function(resolve) { + queueRunnerFactory({ + queueableFns: childFns, + userContext: tree.sharedUserContext(), + onException: function() { + tree.handleException.apply(tree, arguments); + }, + onComplete: resolve, + onMultipleDone: tree.onMultipleDone + ? tree.onMultipleDone.bind(tree) + : null + }); + }); + }; + + function runnableIndex(id) { + for (let i = 0; i < runnableIds.length; i++) { + if (runnableIds[i] === id) { + return i; + } + } + } + + function processNode(node, parentExcluded) { + const executableIndex = runnableIndex(node.id); + + if (executableIndex !== undefined) { + parentExcluded = false; + } + + if (!node.children) { + const excluded = parentExcluded || excludeNode(node); + stats[node.id] = { + excluded: excluded, + willExecute: !excluded && !node.markedPending, + segments: [ + { + index: 0, + owner: node, + nodes: [node], + min: startingMin(executableIndex), + max: startingMax(executableIndex) + } + ] + }; + } else { + let hasExecutableChild = false; + + const orderedChildren = orderChildren(node); + + for (let i = 0; i < orderedChildren.length; i++) { + const child = orderedChildren[i]; + + processNode(child, parentExcluded); + + if (!stats.valid) { + return; + } + + const childStats = stats[child.id]; + + hasExecutableChild = hasExecutableChild || childStats.willExecute; + } + + stats[node.id] = { + excluded: parentExcluded, + willExecute: hasExecutableChild + }; + + segmentChildren(node, orderedChildren, stats[node.id], executableIndex); + + if (!node.canBeReentered() && stats[node.id].segments.length > 1) { + stats = { valid: false }; + } + } + } + + function startingMin(executableIndex) { + return executableIndex === undefined ? defaultMin : executableIndex; + } + + function startingMax(executableIndex) { + return executableIndex === undefined ? defaultMax : executableIndex; + } + + function segmentChildren( + node, + orderedChildren, + nodeStats, + executableIndex + ) { + let currentSegment = { + index: 0, + owner: node, + nodes: [], + min: startingMin(executableIndex), + max: startingMax(executableIndex) + }, + result = [currentSegment], + lastMax = defaultMax, + orderedChildSegments = orderChildSegments(orderedChildren); + + function isSegmentBoundary(minIndex) { + return ( + lastMax !== defaultMax && + minIndex !== defaultMin && + lastMax < minIndex - 1 + ); + } + + for (let i = 0; i < orderedChildSegments.length; i++) { + const childSegment = orderedChildSegments[i], + maxIndex = childSegment.max, + minIndex = childSegment.min; + + if (isSegmentBoundary(minIndex)) { + currentSegment = { + index: result.length, + owner: node, + nodes: [], + min: defaultMin, + max: defaultMax + }; + result.push(currentSegment); + } + + currentSegment.nodes.push(childSegment); + currentSegment.min = Math.min(currentSegment.min, minIndex); + currentSegment.max = Math.max(currentSegment.max, maxIndex); + lastMax = maxIndex; + } + + nodeStats.segments = result; + } + + function orderChildSegments(children) { + const specifiedOrder = [], + unspecifiedOrder = []; + + for (let i = 0; i < children.length; i++) { + const child = children[i], + segments = stats[child.id].segments; + + for (let j = 0; j < segments.length; j++) { + const seg = segments[j]; + + if (seg.min === defaultMin) { + unspecifiedOrder.push(seg); + } else { + specifiedOrder.push(seg); + } + } + } + + specifiedOrder.sort(function(a, b) { + return a.min - b.min; + }); + + return specifiedOrder.concat(unspecifiedOrder); + } + + function executeNode(node, segmentNumber) { + if (node.children) { + return { + fn: function(done) { + const onStart = { + fn: function(next) { + nodeStart(node, next); + } + }; + + queueRunnerFactory({ + onComplete: function() { + const args = Array.prototype.slice.call(arguments, [0]); + node.cleanupBeforeAfter(); + nodeComplete(node, node.getResult(), function() { + done.apply(undefined, args); + }); + }, + queueableFns: [onStart].concat(wrapChildren(node, segmentNumber)), + userContext: node.sharedUserContext(), + onException: function() { + node.handleException.apply(node, arguments); + }, + onMultipleDone: node.onMultipleDone + ? node.onMultipleDone.bind(node) + : null + }); + } + }; + } else { + return { + fn: function(done) { + node.execute( + queueRunnerFactory, + done, + stats[node.id].excluded, + failSpecWithNoExpectations + ); + } + }; + } + } + + function wrapChildren(node, segmentNumber) { + const result = [], + segmentChildren = stats[node.id].segments[segmentNumber].nodes; + + for (let i = 0; i < segmentChildren.length; i++) { + result.push( + executeNode(segmentChildren[i].owner, segmentChildren[i].index) + ); + } + + if (!stats[node.id].willExecute) { + return result; + } + + return node.beforeAllFns.concat(result).concat(node.afterAllFns); + } + } + + return TreeProcessor; +}; + +getJasmineRequireObj().UserContext = function(j$) { + function UserContext() {} + + UserContext.fromExisting = function(oldContext) { + const context = new UserContext(); + + for (const prop in oldContext) { + if (oldContext.hasOwnProperty(prop)) { + context[prop] = oldContext[prop]; + } + } + + return context; + }; + + return UserContext; +}; + +getJasmineRequireObj().version = function() { + return '4.5.0'; +}; diff --git a/ui/plugins/ui/jasmine/jasmine_favicon.png b/ui/plugins/ui/jasmine/jasmine_favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3b84583be4b9d5ae9cd5cae07b2dbaa5ebb0ad1c Binary files /dev/null and b/ui/plugins/ui/jasmine/jasmine_favicon.png differ diff --git a/ui/plugins/ui/jasmineSpec.js b/ui/plugins/ui/jasmineSpec.js new file mode 100644 index 0000000000000000000000000000000000000000..b97bbd4cafb11ad8567cd45f1c7fea6da6d109e4 --- /dev/null +++ b/ui/plugins/ui/jasmineSpec.js @@ -0,0 +1,412 @@ +"use strict" + +const JASMINE_SESSION_ID = `jasmine-${String(Date.now()).slice(8)}` + +beforeEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 60 * 1000 // Test timeout after 15 minutes + jasmine.addMatchers({ + toBeOneOf: function () { + return { + compare: function (actual, expected) { + return { + pass: expected.includes(actual) + } + } + } + } + }) +}) +describe('stable-diffusion-ui', function() { + beforeEach(function() { + expect(typeof SD).toBe('object') + expect(typeof SD.serverState).toBe('object') + expect(typeof SD.serverState.status).toBe('string') + }) + it('should be able to reach the backend', async function() { + expect(SD.serverState.status).toBe(SD.ServerStates.unavailable) + SD.sessionId = JASMINE_SESSION_ID + await SD.init() + expect(SD.isServerAvailable()).toBeTrue() + }) + + it('enfore the current task state', function() { + const task = new SD.Task() + expect(task.status).toBe(SD.TaskStatus.init) + expect(task.isPending).toBeTrue() + + task._setStatus(SD.TaskStatus.pending) + expect(task.status).toBe(SD.TaskStatus.pending) + expect(task.isPending).toBeTrue() + expect(function() { + task._setStatus(SD.TaskStatus.init) + }).toThrowError() + + task._setStatus(SD.TaskStatus.waiting) + expect(task.status).toBe(SD.TaskStatus.waiting) + expect(task.isPending).toBeTrue() + expect(function() { + task._setStatus(SD.TaskStatus.pending) + }).toThrowError() + + task._setStatus(SD.TaskStatus.processing) + expect(task.status).toBe(SD.TaskStatus.processing) + expect(task.isPending).toBeTrue() + expect(function() { + task._setStatus(SD.TaskStatus.pending) + }).toThrowError() + + task._setStatus(SD.TaskStatus.failed) + expect(task.status).toBe(SD.TaskStatus.failed) + expect(task.isPending).toBeFalse() + expect(function() { + task._setStatus(SD.TaskStatus.processing) + }).toThrowError() + expect(function() { + task._setStatus(SD.TaskStatus.completed) + }).toThrowError() + }) + it('should be able to run tasks', async function() { + expect(typeof SD.Task.run).toBe('function') + const promiseGenerator = (function*(val) { + expect(val).toBe('start') + expect(yield 1 + 1).toBe(4) + expect(yield 2 + 2).toBe(8) + yield asyncDelay(500) + expect(yield 3 + 3).toBe(12) + expect(yield 4 + 4).toBe(16) + return 8 + 8 + })('start') + const callback = function({value, done}) { + return {value: 2 * value, done} + } + expect(await SD.Task.run(promiseGenerator, {callback})).toBe(32) + }) + it('should be able to queue tasks', async function() { + expect(typeof SD.Task.enqueue).toBe('function') + const promiseGenerator = (function*(val) { + expect(val).toBe('start') + expect(yield 1 + 1).toBe(4) + expect(yield 2 + 2).toBe(8) + yield asyncDelay(500) + expect(yield 3 + 3).toBe(12) + expect(yield 4 + 4).toBe(16) + return 8 + 8 + })('start') + const callback = function({value, done}) { + return {value: 2 * value, done} + } + const gen = SD.Task.asGenerator({generator: promiseGenerator, callback}) + expect(await SD.Task.enqueue(gen)).toBe(32) + }) + it('should be able to chain handlers', async function() { + expect(typeof SD.Task.enqueue).toBe('function') + const promiseGenerator = (function*(val) { + expect(val).toBe('start') + expect(yield {test: '1'}).toEqual({test: '1', foo: 'bar'}) + expect(yield 2 + 2).toEqual(8) + yield asyncDelay(500) + expect(yield 3 + 3).toEqual(12) + expect(yield {test: 4}).toEqual({test: 8, foo: 'bar'}) + return {test: 8} + })('start') + const gen1 = SD.Task.asGenerator({generator: promiseGenerator, callback: function({value, done}) { + if (typeof value === "object") { + value['foo'] = 'bar' + } + return {value, done} + }}) + const gen2 = SD.Task.asGenerator({generator: gen1, callback: function({value, done}) { + if (typeof value === 'number') { + value = 2 * value + } + if (typeof value === 'object' && typeof value.test === 'number') { + value.test = 2 * value.test + } + return {value, done} + }}) + expect(await SD.Task.enqueue(gen2)).toEqual({test:32, foo: 'bar'}) + }) + describe('ServiceContainer', function() { + it('should be able to register providers', function() { + const cont = new ServiceContainer( + function foo() { + this.bar = '' + }, + function bar() { + return () => 0 + }, + { name: 'zero', definition: 0 }, + { name: 'ctx', definition: () => Object.create(null), singleton: true }, + { name: 'test', + definition: (ctx, missing, one, foo) => { + expect(ctx).toEqual({ran: true}) + expect(one).toBe(1) + expect(typeof foo).toBe('object') + expect(foo.bar).toBeDefined() + expect(typeof missing).toBe('undefined') + return {foo: 'bar'} + }, dependencies: ['ctx', 'missing', 'one', 'foo'] + } + ) + const fooObj = cont.get('foo') + expect(typeof fooObj).toBe('object') + fooObj.ran = true + + const ctx = cont.get('ctx') + expect(ctx).toEqual({}) + ctx.ran = true + + const bar = cont.get('bar') + expect(typeof bar).toBe('function') + expect(bar()).toBe(0) + + cont.register({name: 'one', definition: 1}) + const test = cont.get('test') + expect(typeof test).toBe('object') + expect(test.foo).toBe('bar') + }) + }) + it('should be able to stream data in chunks', async function() { + expect(SD.isServerAvailable()).toBeTrue() + const nbr_steps = 15 + let res = await fetch('/render', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + "prompt": "a photograph of an astronaut riding a horse", + "negative_prompt": "", + "width": 128, + "height": 128, + "seed": Math.floor(Math.random() * 10000000), + + "sampler": "plms", + "use_stable_diffusion_model": "sd-v1-4", + "num_inference_steps": nbr_steps, + "guidance_scale": 7.5, + + "numOutputsParallel": 1, + "stream_image_progress": true, + "show_only_filtered_image": true, + "output_format": "jpeg", + + "session_id": JASMINE_SESSION_ID, + }), + }) + expect(res.ok).toBeTruthy() + const renderRequest = await res.json() + expect(typeof renderRequest.stream).toBe('string') + expect(renderRequest.task).toBeDefined() + + // Wait for server status to update. + await SD.waitUntil(() => { + console.log('Waiting for %s to be received...', renderRequest.task) + return (!SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)]) + }, 250, 10 * 60 * 1000) + // Wait for task to start on server. + await SD.waitUntil(() => { + console.log('Waiting for %s to start...', renderRequest.task) + return !SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)] !== 'pending' + }, 250) + + const reader = new SD.ChunkedStreamReader(renderRequest.stream) + const parseToString = reader.parse + reader.parse = function(value) { + value = parseToString.call(this, value) + if (!value || value.length <= 0) { + return + } + return reader.readStreamAsJSON(value.join('')) + } + reader.onNext = function({done, value}) { + console.log(value) + if (typeof value === 'object' && 'status' in value) { + done = true + } + return {done, value} + } + let lastUpdate = undefined + let stepCount = 0 + let complete = false + //for await (const stepUpdate of reader) { + for await (const stepUpdate of reader.open()) { + console.log('ChunkedStreamReader received ', stepUpdate) + lastUpdate = stepUpdate + if (complete) { + expect(stepUpdate.status).toBe('succeeded') + expect(stepUpdate.output).toHaveSize(1) + } else { + expect(stepUpdate.total_steps).toBe(nbr_steps) + expect(stepUpdate.step).toBe(stepCount) + if (stepUpdate.step === stepUpdate.total_steps) { + complete = true + } else { + stepCount++ + } + } + } + for(let i=1; i <= 5; ++i) { + res = await fetch(renderRequest.stream) + expect(res.ok).toBeTruthy() + const cachedResponse = await res.json() + console.log('Cache test %s received %o', i, cachedResponse) + expect(lastUpdate).toEqual(cachedResponse) + } + }) + + describe('should be able to make renders', function() { + beforeEach(function() { + expect(SD.isServerAvailable()).toBeTrue() + }) + it('basic inline request', async function() { + let stepCount = 0 + let complete = false + const result = await SD.render({ + "prompt": "a photograph of an astronaut riding a horse", + "width": 128, + "height": 128, + "num_inference_steps": 10, + "show_only_filtered_image": false, + //"use_face_correction": 'GFPGANv1.3', + "use_upscale": "RealESRGAN_x4plus", + "session_id": JASMINE_SESSION_ID, + }, function(event) { + console.log(this, event) + if ('update' in event) { + const stepUpdate = event.update + if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) { + expect(stepUpdate.status).toBe('succeeded') + expect(stepUpdate.output).toHaveSize(2) + } else { + expect(stepUpdate.step).toBe(stepCount) + if (stepUpdate.step === stepUpdate.total_steps) { + complete = true + } else { + stepCount++ + } + } + } + }) + console.log(result) + expect(result.status).toBe('succeeded') + expect(result.output).toHaveSize(2) + }) + it('post and reader request', async function() { + const renderTask = new SD.RenderTask({ + "prompt": "a photograph of an astronaut riding a horse", + "width": 128, + "height": 128, + "seed": SD.MAX_SEED_VALUE, + "num_inference_steps": 10, + "session_id": JASMINE_SESSION_ID, + }) + expect(renderTask.status).toBe(SD.TaskStatus.init) + + const timeout = -1 + const renderRequest = await renderTask.post(timeout) + expect(typeof renderRequest.stream).toBe('string') + expect(renderTask.status).toBe(SD.TaskStatus.waiting) + expect(renderTask.streamUrl).toBe(renderRequest.stream) + + await renderTask.waitUntil({state: SD.TaskStatus.processing, callback: () => console.log('Waiting for render task to start...') }) + expect(renderTask.status).toBe(SD.TaskStatus.processing) + + let stepCount = 0 + let complete = false + //for await (const stepUpdate of renderTask.reader) { + for await (const stepUpdate of renderTask.reader.open()) { + console.log(stepUpdate) + if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) { + expect(stepUpdate.status).toBe('succeeded') + expect(stepUpdate.output).toHaveSize(1) + } else { + expect(stepUpdate.step).toBe(stepCount) + if (stepUpdate.step === stepUpdate.total_steps) { + complete = true + } else { + stepCount++ + } + } + } + expect(renderTask.status).toBe(SD.TaskStatus.completed) + expect(renderTask.result.status).toBe('succeeded') + expect(renderTask.result.output).toHaveSize(1) + }) + it('queued request', async function() { + let stepCount = 0 + let complete = false + const renderTask = new SD.RenderTask({ + "prompt": "a photograph of an astronaut riding a horse", + "width": 128, + "height": 128, + "num_inference_steps": 10, + "show_only_filtered_image": false, + //"use_face_correction": 'GFPGANv1.3', + "use_upscale": "RealESRGAN_x4plus", + "session_id": JASMINE_SESSION_ID, + }) + await renderTask.enqueue(function(event) { + console.log(this, event) + if ('update' in event) { + const stepUpdate = event.update + if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) { + expect(stepUpdate.status).toBe('succeeded') + expect(stepUpdate.output).toHaveSize(2) + } else { + expect(stepUpdate.step).toBe(stepCount) + if (stepUpdate.step === stepUpdate.total_steps) { + complete = true + } else { + stepCount++ + } + } + } + }) + console.log(renderTask.result) + expect(renderTask.result.status).toBe('succeeded') + expect(renderTask.result.output).toHaveSize(2) + }) + }) + describe('# Special cases', function() { + it('should throw an exception on set for invalid sessionId', function() { + expect(function() { + SD.sessionId = undefined + }).toThrowError("Can't set sessionId to undefined.") + }) + }) +}) + +const loadCompleted = window.onload +let loadEvent = undefined +window.onload = function(evt) { + loadEvent = evt +} +if (!PLUGINS.SELFTEST) { + PLUGINS.SELFTEST = {} +} +loadUIPlugins().then(function() { + console.log('loadCompleted', loadEvent) + describe('@Plugins', function() { + it('exposes hooks to overide', function() { + expect(typeof PLUGINS.IMAGE_INFO_BUTTONS).toBe('object') + expect(typeof PLUGINS.TASK_CREATE).toBe('object') + }) + describe('supports selftests', function() { // Hook to allow plugins to define tests. + const pluginsTests = Object.keys(PLUGINS.SELFTEST).filter((key) => PLUGINS.SELFTEST.hasOwnProperty(key)) + if (!pluginsTests || pluginsTests.length <= 0) { + it('but nothing loaded...', function() { + expect(true).toBeTruthy() + }) + return + } + for (const pTest of pluginsTests) { + describe(pTest, function() { + const testFn = PLUGINS.SELFTEST[pTest] + return Promise.resolve(testFn.call(jasmine, pTest)) + }) + } + }) + }) + loadCompleted.call(window, loadEvent) +}) diff --git a/ui/plugins/ui/merge.plugin.js b/ui/plugins/ui/merge.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..6ff97286fc17090f4fe8cfd534cee619a37a649e --- /dev/null +++ b/ui/plugins/ui/merge.plugin.js @@ -0,0 +1,458 @@ +(function() { + "use strict" + + ///////////////////// Function section + function smoothstep(x) { + return x * x * (3 - 2 * x) + } + + function smootherstep(x) { + return x * x * x * (x * (x * 6 - 15) + 10) + } + + function smootheststep(x) { + let y = -20 * Math.pow(x, 7) + y += 70 * Math.pow(x, 6) + y -= 84 * Math.pow(x, 5) + y += 35 * Math.pow(x, 4) + return y + } + function getCurrentTime() { + const now = new Date(); + let hours = now.getHours(); + let minutes = now.getMinutes(); + let seconds = now.getSeconds(); + + hours = hours < 10 ? `0${hours}` : hours; + minutes = minutes < 10 ? `0${minutes}` : minutes; + seconds = seconds < 10 ? `0${seconds}` : seconds; + + return `${hours}:${minutes}:${seconds}`; + } + + function addLogMessage(message) { + const logContainer = document.getElementById('merge-log'); + logContainer.innerHTML += `${getCurrentTime()} ${message}
    `; + + // Scroll to the bottom of the log + logContainer.scrollTop = logContainer.scrollHeight; + + document.querySelector('#merge-log-container').style.display = 'block' + } + + function addLogSeparator() { + const logContainer = document.getElementById('merge-log'); + logContainer.innerHTML += '
    ' + + logContainer.scrollTop = logContainer.scrollHeight; + } + + function drawDiagram(fn) { + const SIZE = 300 + const canvas = document.getElementById('merge-canvas'); + canvas.height = canvas.width = SIZE + const ctx = canvas.getContext('2d'); + + // Draw coordinate system + ctx.scale(1, -1); + ctx.translate(0, -canvas.height); + ctx.lineWidth = 1; + ctx.beginPath(); + + ctx.strokeStyle = 'white' + ctx.moveTo(0,0); ctx.lineTo(0,SIZE); ctx.lineTo(SIZE,SIZE); ctx.lineTo(SIZE,0); ctx.lineTo(0,0); ctx.lineTo(SIZE,SIZE); + ctx.stroke() + ctx.beginPath() + ctx.setLineDash([1,2]) + const n = SIZE / 10 + for (let i=n; i>0 + ctx.beginPath() + ctx.fillStyle = "yellow" + for (let i=0; i< iterations; i++) { + const alpha = ( start + i * step ) / 100 + const x = alpha*SIZE + const y = fn(alpha) * SIZE + if (x <= SIZE) { + ctx.rect(x-3,y-3,6,6) + ctx.fill() + } else { + ctx.strokeStyle = 'red' + ctx.moveTo(0,0); ctx.lineTo(0,SIZE); ctx.lineTo(SIZE,SIZE); ctx.lineTo(SIZE,0); ctx.lineTo(0,0); ctx.lineTo(SIZE,SIZE); + ctx.stroke() + addLogMessage('Warning: maximum ratio is ≥ 100%') + } + } + } + + function updateChart() { + let fn = (x) => x + switch (document.querySelector('#merge-interpolation').value) { + case 'SmoothStep': + fn = smoothstep + break + case 'SmootherStep': + fn = smootherstep + break + case 'SmoothestStep': + fn = smootheststep + break + } + drawDiagram(fn) + } + + /////////////////////// Tab implementation + document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` + + Merge models + + `) + + document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', ` +
    +
    + Loading.. +
    +
    + `) + + const tabMerge = document.querySelector('#tab-merge') + if (tabMerge) { + linkTabContents(tabMerge) + } + const merge = document.querySelector('#merge') + if (!merge) { + // merge tab not found, dont exec plugin code. + return + } + + document.querySelector('body').insertAdjacentHTML('beforeend', ` + + `) + + merge.innerHTML = ` +
    +
    +

    + +

    + +

    +

    Important: Please merge models of similar type.
    For e.g. SD 1.4 models with only SD 1.4/1.5 models,
    SD 2.0 with SD 2.0-type, and SD 2.1 with SD 2.1-type models.

    +
    + + + + + + + + + + + + + +
    Base name of the output file.
    Mix ratio and file suffix will be appended to this.
    + Image generation uses fp16, so it's a good choice.
    Use fp32 if you want to use the result models for more mixes
    +
    +
    +
    +
    +

    +
    +
    +
    +
    +
    + + Make a single file + + + Make multiple variations + +
    +
    +
    +
    + Saves a single merged model file, at the specified merge ratio.

    + + + % + Model A's contribution to the mix. The rest will be from Model B. +
    +
    +
    +
    + Saves multiple variations of the model, at different merge ratios.
    Each variation will be saved as a separate file.


    + + + + + + + + + + + + + +
    Number of models to create
    % Smallest share of model A in the mix
    % Share of model A added into the mix per step
    Sigmoid function to be applied to the model share before mixing
    +
    + Preview of variation ratios:
    + +
    +
    +
    +
    +
    + +
    +
    ` + + const tabSettingsSingle = document.querySelector('#tab-merge-opts-single') + const tabSettingsBatch = document.querySelector('#tab-merge-opts-batch') + linkTabContents(tabSettingsSingle) + linkTabContents(tabSettingsBatch) + + console.log('Activate') + let mergeModelAField = new ModelDropdown(document.querySelector('#mergeModelA'), 'stable-diffusion') + let mergeModelBField = new ModelDropdown(document.querySelector('#mergeModelB'), 'stable-diffusion') + updateChart() + + // slider + const singleMergeRatioField = document.querySelector('#single-merge-ratio') + const singleMergeRatioSlider = document.querySelector('#single-merge-ratio-slider') + + function updateSingleMergeRatio() { + singleMergeRatioField.value = singleMergeRatioSlider.value / 10 + singleMergeRatioField.dispatchEvent(new Event("change")) + } + + function updateSingleMergeRatioSlider() { + if (singleMergeRatioField.value < 0) { + singleMergeRatioField.value = 0 + } else if (singleMergeRatioField.value > 100) { + singleMergeRatioField.value = 100 + } + + singleMergeRatioSlider.value = singleMergeRatioField.value * 10 + singleMergeRatioSlider.dispatchEvent(new Event("change")) + } + + singleMergeRatioSlider.addEventListener('input', updateSingleMergeRatio) + singleMergeRatioField.addEventListener('input', updateSingleMergeRatioSlider) + updateSingleMergeRatio() + + document.querySelector('.merge-config').addEventListener('change', updateChart) + + document.querySelector('#merge-button').addEventListener('click', async function(e) { + // Build request template + let model0 = mergeModelAField.value + let model1 = mergeModelBField.value + let request = { model0: model0, model1: model1 } + request['use_fp16'] = document.querySelector('#merge-fp').value == 'fp16' + let iterations = document.querySelector('#merge-count').value>>0 + let start = parseFloat( document.querySelector('#merge-start').value ) + let step = parseFloat( document.querySelector('#merge-step').value ) + + if (isTabActive(tabSettingsSingle)) { + start = parseFloat(singleMergeRatioField.value) + step = 0 + iterations = 1 + addLogMessage(`merge ratio = ${start}%`) + } else { + addLogMessage(`start = ${start}%`) + addLogMessage(`step = ${step}%`) + } + + if (start + (iterations-1) * step >= 100) { + addLogMessage('Aborting: maximum ratio is ≥ 100%') + addLogMessage('Reduce the number of variations or the step size') + addLogSeparator() + document.querySelector('#merge-count').focus() + return + } + + if (document.querySelector('#merge-filename').value == "") { + addLogMessage('Aborting: No output file name specified') + addLogSeparator() + document.querySelector('#merge-filename').focus() + return + } + + // Disable merge button + e.target.disabled=true + e.target.classList.add('disabled') + let cursor = $("body").css("cursor"); + let label = document.querySelector('#merge-button').innerHTML + $("body").css("cursor", "progress"); + document.querySelector('#merge-button').innerHTML = 'Merging models ...' + + addLogMessage("Merging models") + addLogMessage("Model A: "+model0) + addLogMessage("Model B: "+model1) + + // Batch main loop + for (let i=0; iDone. The models have been saved to your models/stable-diffusion folder.") + addLogSeparator() + // Re-enable merge button + $("body").css("cursor", cursor); + document.querySelector('#merge-button').innerHTML = label + e.target.disabled=false + e.target.classList.remove('disabled') + + // Update model list + stableDiffusionModelField.innerHTML = '' + vaeModelField.innerHTML = '' + hypernetworkModelField.innerHTML = '' + await getModels() + }) + +})() diff --git a/ui/plugins/ui/modifiers-toggle.plugin.js b/ui/plugins/ui/modifiers-toggle.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..14eb56276dee74abac54eeff9eb1e482102a4b2b --- /dev/null +++ b/ui/plugins/ui/modifiers-toggle.plugin.js @@ -0,0 +1,53 @@ +(function () { + "use strict" + + var styleSheet = document.createElement("style"); + styleSheet.textContent = ` + .modifier-card-tiny.modifier-toggle-inactive { + background: transparent; + border: 2px dashed red; + opacity:0.2; + } + `; + document.head.appendChild(styleSheet); + + // observe for changes in tag list + var observer = new MutationObserver(function (mutations) { + // mutations.forEach(function (mutation) { + if (editorModifierTagsList.childNodes.length > 0) { + ModifierToggle() + } + // }) + }) + + observer.observe(editorModifierTagsList, { + childList: true + }) + + function ModifierToggle() { + let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay') + overlays.forEach (i => { + i.oncontextmenu = (e) => { + e.preventDefault() + + if (i.parentElement.classList.contains('modifier-toggle-inactive')) { + i.parentElement.classList.remove('modifier-toggle-inactive') + } + else + { + i.parentElement.classList.add('modifier-toggle-inactive') + } + // refresh activeTags + let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].dataset.fullName + activeTags = activeTags.map(obj => { + if (trimModifiers(obj.name) === trimModifiers(modifierName)) { + return {...obj, inactive: (obj.element.classList.contains('modifier-toggle-inactive'))}; + } + + return obj; + }); + document.dispatchEvent(new Event('refreshImageModifiers')) + } + }) + } +})() diff --git a/ui/plugins/ui/release-notes.plugin.js b/ui/plugins/ui/release-notes.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..da7b79de5e9db0bf9e28cfa6b5f1629c06f679dc --- /dev/null +++ b/ui/plugins/ui/release-notes.plugin.js @@ -0,0 +1,64 @@ +(function() { + // Register selftests when loaded by jasmine. + if (typeof PLUGINS?.SELFTEST === 'object') { + PLUGINS.SELFTEST["release-notes"] = function() { + it('should be able to fetch CHANGES.md', async function() { + let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/main/CHANGES.md`) + expect(releaseNotes.status).toBe(200) + }) + } + } + + document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` + + What's new? + + `) + + document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', ` +
    +
    + Loading.. +
    +
    + `) + + const tabNews = document.querySelector('#tab-news') + if (tabNews) { + linkTabContents(tabNews) + } + const news = document.querySelector('#news') + if (!news) { + // news tab not found, dont exec plugin code. + return + } + + document.querySelector('body').insertAdjacentHTML('beforeend', ` + + `) + + loadScript('/media/js/marked.min.js').then(async function() { + let appConfig = await fetch('/get/app_config') + if (!appConfig.ok) { + console.error('[release-notes] Failed to get app_config.') + return + } + appConfig = await appConfig.json() + + const updateBranch = appConfig.update_branch || 'main' + + let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`) + if (!releaseNotes.ok) { + console.error('[release-notes] Failed to get CHANGES.md.') + return + } + releaseNotes = await releaseNotes.text() + news.innerHTML = marked.parse(releaseNotes) + }) +})() \ No newline at end of file diff --git a/ui/plugins/ui/selftest.plugin.js b/ui/plugins/ui/selftest.plugin.js new file mode 100644 index 0000000000000000000000000000000000000000..f7c59eb136261df624b957dfa6a53849a33b4f7d --- /dev/null +++ b/ui/plugins/ui/selftest.plugin.js @@ -0,0 +1,25 @@ +/* SD-UI Selftest Plugin.js + */ +(function() { "use strict" + const ID_PREFIX = "selftest-plugin" + + const links = document.getElementById("community-links") + if (!links) { + console.error('%s the ID "community-links" cannot be found.', ID_PREFIX) + return + } + + // Add link to Jasmine SpecRunner + const pluginLink = document.createElement('li') + const options = { + 'stopSpecOnExpectationFailure': "true", + 'stopOnSpecFailure': 'false', + 'random': 'false', + 'hideDisabled': 'false' + } + const optStr = Object.entries(options).map(([key, val]) => `${key}=${val}`).join('&') + pluginLink.innerHTML = ` Start SelfTest` + links.appendChild(pluginLink) + + console.log('%s loaded!', ID_PREFIX) +})()