diff --git a/.gitignore b/.gitignore index 131f700..392aa51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,10 @@ node_modules/ -passwordreset*.json mail/*.html -scss/*.css* -scss/bs4/*.css* -scss/bs5/*.css* -data/static/*.css -data/static/*.js -data/static/*.js.map -data/static/ts/ -data/static/modules/ -!data/static/setup.js -data/config-base.json -data/config-default.ini -data/*.html -data/*.txt -data/docs/ -dist/* -jfa-go +dist/ build/ -pkg/ -old/ +data/ version.go notes docs/* +config-payload.json !docs/go.mod diff --git a/.goreleaser.yml b/.goreleaser.yml index f07b59b..1551ec0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,14 +7,20 @@ release: before: hooks: - go mod download + - rm -rf data/web + - mkdir -p data + - cp -r static data/web + - cp -r css data/web/ + - npm install + - cp node_modules/a17t/dist/a17t.css data/web/css/ + - cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/ + - cp -r html data/ + - cp -r lang data/ - python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json - python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini - - python3 -m pip install libsass - - npm install - - python3 scss/compile.py - - python3 mail/generate.py + - python3 mail/generate.py -o data/ - python3 version.py {{.Version}} version.go - - bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify' + - bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=./data/web/js/ --minify' - go get -u github.com/swaggo/swag/cmd/swag - swag init -g main.go builds: @@ -37,9 +43,8 @@ archives: amd64: x86_64 files: - data/* - - data/templates/* - - data/static/* - - data/static/modules/* + - data/**/* + - data/**/**/* checksum: name_template: 'checksums.txt' snapshot: diff --git a/Dockerfile b/Dockerfile index ea15e75..7ddbe32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN apt update -y \ && (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \ && apt install nodejs \ && (cd /opt/build; make all; make compress) \ - && sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/templates/setup.html + && sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/html/setup.html FROM golang:latest diff --git a/Makefile b/Makefile index d126cfb..6927345 100644 --- a/Makefile +++ b/Makefile @@ -1,33 +1,30 @@ +npm: + $(info installing npm dependencies) + npm install + configuration: $(info Fixing config-base) - python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json + -mkdir -p build/data + python3 config/fixconfig.py -i config/config-base.json -o build/data/config-base.json $(info Generating config-default.ini) - python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini - -sass: - $(info Getting libsass) - python3 -m pip install libsass - $(info Getting node dependencies) - npm install - $(info Compiling sass) - python3 scss/compile.py + python3 config/generate_ini.py -i config/config-base.json -o build/data/config-default.ini email: $(info Generating email html) - python3 mail/generate.py + python3 mail/generate.py -o build/data/ -typescript: - $(info Compiling typescript) - npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify - -rm -r data/static/ts - -rm -r data/static/typings - -rm data/static/*.map +ts: + $(info compiling typescript) + -mkdir -p build/data/web/js + -npx esbuild ts/*.ts ts/modules/*.ts --outdir=./build/data/web/js/ ts-debug: - -npx tsc -p ts/ --sourceMap - -rm -r data/static/ts - -rm -r data/static/typings - cp -r ts data/static/ + $(info compiling typescript w/ sourcemaps) + -mkdir -p build/data/web/js + -npx esbuild ts/*.ts ts/modules/*.ts --sourcemap --outdir=./build/data/web/js/ + -rm -r build/data/web/js/ts + $(info copying typescript) + cp -r ts build/data/web/js swagger: go get github.com/swaggo/swag/cmd/swag @@ -47,11 +44,22 @@ compress: upx --lzma build/jfa-go copy: - $(info Copying data) - cp -r data build/ + $(info copying css) + -mkdir -p build/data/web/css + cp -r css build/data/web/ + cp node_modules/a17t/dist/a17t.css build/data/web/css/ + cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 build/data/web/css/ + $(info copying html) + cp -r html build/data/ + $(info copying static data) + -mkdir -p build/data/web + cp -r static/* build/data/web/ + $(info copying language files) + cp -r lang build/data/ + install: cp -r build $(DESTDIR)/jfa-go -all: configuration sass email version typescript swagger compile copy -debug: configuration sass email version ts-debug swagger compile copy +all: configuration npm email version ts swagger compile copy +debug: configuration npm email version ts-debug swagger compile copy diff --git a/README.md b/README.md index d52f9b6..187d999 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![jfa-go](data/static/banner.svg) +# ![jfa-go](images/banner.svg) jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) that provides invite-based account creation as well as other features that make one's instance much easier to manage. @@ -21,16 +21,15 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly * 🌓 Customizable look * Specify contact and help messages to appear in emails and pages * Light and dark themes available - * Optionally provide custom CSS ## Interface

- +

- Invites tab - Accounts tab + Invites tab + Accounts tab

#### Install diff --git a/README.md.old b/README.md.old new file mode 100644 index 0000000..c27a77e --- /dev/null +++ b/README.md.old @@ -0,0 +1,38 @@ +This branch is for experimenting with [a17t](https://a17t.miles.land/) to replace bootstrap. Page structure is pretty much done (except setup.html), so i'm currently integrating this with the main app and existing web code. + +#### todo +**general** +* [x] modal implementation +* [x] animations +* [x] utilities +* [x] CSS for light & dark + +**admin** +* [x] invites tab +* [x] accounts tab +* [x] settings tab +* [x] modals +* [ ] integration with existing code + +**invites** +* [x] page design +* [ ] integration with existing code + +#### screenshots +##### dark +

+ invites + accounts + settings + login modal + modify user settings modal +

+ +##### light +

+ invites + accounts + settings + login modal + modify user settings modal +

diff --git a/api.go b/api.go index e01920f..9c46b78 100644 --- a/api.go +++ b/api.go @@ -667,6 +667,9 @@ func (app *appContext) DeleteProfile(gc *gin.Context) { gc.BindJSON(&req) name := req.Name if _, ok := app.storage.profiles[name]; ok { + if app.storage.defaultProfile == name { + app.storage.defaultProfile = "" + } delete(app.storage.profiles, name) } app.storage.storeProfiles() @@ -1072,13 +1075,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) { // @Summary Get jfa-go configuration. // @Produce json -// @Success 200 {object} configDTO "Uses the same format as config-base.json" +// @Success 200 {object} settings "Uses the same format as config-base.json" // @Router /config [get] // @Security Bearer // @tags Configuration func (app *appContext) GetConfig(gc *gin.Context) { app.info.Println("Config requested") - resp := map[string]interface{}{} + resp := app.configBase + // Load language options langPath := filepath.Join(app.localPath, "lang", "form") app.lang.langFiles, _ = ioutil.ReadDir(langPath) app.lang.langOptions = make([]string, len(app.lang.langFiles)) @@ -1095,37 +1099,25 @@ func (app *appContext) GetConfig(gc *gin.Context) { app.lang.langOptions[i] = meta.(map[string]interface{})["name"].(string) } } - for section, settings := range app.configBase { - if section == "order" { - resp[section] = settings.([]interface{}) - } else { - resp[section] = make(map[string]interface{}) - for key, values := range settings.(map[string]interface{}) { - if key == "order" { - resp[section].(map[string]interface{})[key] = values.([]interface{}) - } else { - resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) - if key != "meta" { - dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) - configKey := app.config.Section(section).Key(key) - if dataType == "number" { - if val, err := configKey.Int(); err == nil { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val - } - } else if dataType == "bool" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.MustBool(false) - } else if dataType == "select" && key == "language" { - resp[section].(map[string]interface{})[key].(map[string]interface{})["options"] = app.lang.langOptions - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = app.lang.langOptions[app.lang.chosenIndex] - } else { - resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = configKey.String() - } - } - } + s := resp.Sections["ui"].Settings["language"] + for sectName, section := range resp.Sections { + for settingName, setting := range section.Settings { + val := app.config.Section(sectName).Key(settingName) + s := resp.Sections[sectName].Settings[settingName] + switch setting.Type { + case "text", "email", "select", "password": + s.Value = val.MustString("") + case "number": + s.Value = val.MustInt(0) + case "bool": + s.Value = val.MustBool(false) } + resp.Sections[sectName].Settings[settingName] = s } } - // resp["jellyfin"].(map[string]interface{})["language"].(map[string]interface{})["options"].([]string) + s.Options = app.lang.langOptions + s.Value = app.lang.langOptions[app.lang.chosenIndex] + resp.Sections["ui"].Settings["language"] = s gc.JSON(200, resp) } @@ -1176,11 +1168,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { if _, ok := req["password_validation"]; ok { app.debug.Println("Reinitializing validator") validatorConf := ValidatorConf{ - "characters": app.config.Section("password_validation").Key("min_length").MustInt(0), - "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0), - "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0), - "numbers": app.config.Section("password_validation").Key("number").MustInt(0), - "special characters": app.config.Section("password_validation").Key("special").MustInt(0), + "length": app.config.Section("password_validation").Key("min_length").MustInt(0), + "uppercase": app.config.Section("password_validation").Key("upper").MustInt(0), + "lowercase": app.config.Section("password_validation").Key("lower").MustInt(0), + "number": app.config.Section("password_validation").Key("number").MustInt(0), + "special": app.config.Section("password_validation").Key("special").MustInt(0), } if !app.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { diff --git a/config/config-base.json b/config/config-base.json index 21e95a2..9d5c920 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1,668 +1,690 @@ { - "jellyfin": { - "meta": { - "name": "Jellyfin", - "description": "Settings for connecting to Jellyfin" + "order": [], + "sections": { + "jellyfin": { + "order": [], + "meta": { + "name": "Jellyfin", + "description": "Settings for connecting to Jellyfin" + }, + "settings": { + "username": { + "name": "Jellyfin Username", + "required": true, + "requires_restart": true, + "type": "text", + "value": "username", + "description": "It is recommended to create a limited admin account for this program." + }, + "password": { + "name": "Jellyfin Password", + "required": true, + "requires_restart": true, + "type": "password", + "value": "password" + }, + "server": { + "name": "Server address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "http://jellyfin.local:8096", + "description": "Jellyfin server address. Can be public, or local for security purposes." + }, + "public_server": { + "name": "Public address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://jellyf.in:443", + "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." + }, + "client": { + "name": "Client Name", + "required": true, + "requires_restart": true, + "type": "text", + "value": "jfa-go", + "description": "The name of the client that will show up in the Jellyfin dashboard." + }, + "cache_timeout": { + "name": "User cache timeout (minutes)", + "required": false, + "requires_restart": true, + "type": "number", + "value": 30, + "description": "Timeout of user cache in minutes. Set to 0 to disable." + } + } }, - "username": { - "name": "Jellyfin Username", - "required": true, - "requires_restart": true, - "type": "text", - "value": "username", - "description": "It is recommended to create a limited admin account for this program." + "ui": { + "order": [], + "meta": { + "name": "General", + "description": "Settings related to the UI and program functionality." + }, + "settings": { + "language": { + "name": "Language", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "en-us" + ], + "value": "en-US", + "description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." + }, + "theme": { + "name": "Default Look", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "Jellyfin (Dark)", + "Default (Light)" + ], + "value": "Jellyfin (Dark)", + "description": "Default appearance for all users." + }, + "host": { + "name": "Address", + "required": true, + "requires_restart": true, + "type": "text", + "value": "0.0.0.0", + "description": "Set 0.0.0.0 to run on localhost" + }, + "port": { + "name": "Port", + "required": true, + "requires_restart": true, + "type": "number", + "value": 8056 + }, + "jellyfin_login": { + "name": "Use Jellyfin for authentication", + "required": false, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable this to use Jellyfin users instead of the below username and pw." + }, + "admin_only": { + "name": "Allow admin users only", + "required": false, + "requires_restart": true, + "depends_true": "jellyfin_login", + "type": "bool", + "value": true, + "description": "Allows only admin users on Jellyfin to access the admin page." + }, + "username": { + "name": "Web Username", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "text", + "value": "your username", + "description": "Username for admin page (Leave blank if using jellyfin_login)" + }, + "password": { + "name": "Web Password", + "required": true, + "requires_restart": true, + "depends_false": "jellyfin_login", + "type": "password", + "value": "your password", + "description": "Password for admin page (Leave blank if using jellyfin_login)" + }, + "email": { + "name": "Admin email address", + "required": false, + "requires_restart": false, + "depends_false": "jellyfin_login", + "type": "text", + "value": "example@example.com", + "description": "Address to send notifications to (Leave blank if using jellyfin_login)" + }, + "debug": { + "name": "Debug logging", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Enables debug logging and exposes pprof as a route (Don't use in production!)" + }, + "contact_message": { + "name": "Contact message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Need help? contact me.", + "description": "Displayed at bottom of all pages except admin" + }, + "help_message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Enter your details to create an account.", + "description": "Displayed at top of invite form." + }, + "success_message": { + "name": "Success message", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Your account has been created. Click below to continue to Jellyfin.", + "description": "Displayed when a user creates an account" + }, + "url_base": { + "name": "URL Base", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "URL base for when running jfa-go with a reverse proxy in a subfolder." + } + } }, - "password": { - "name": "Jellyfin Password", - "required": true, - "requires_restart": true, - "type": "password", - "value": "password" - }, - "server": { - "name": "Server address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "http://jellyfin.local:8096", - "description": "Jellyfin server address. Can be public, or local for security purposes." - }, - "public_server": { - "name": "Public address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://jellyf.in:443", - "description": "Publicly accessible Jellyfin address for invite form. Leave blank to reuse the above address." - }, - "client": { - "name": "Client Name", - "required": true, - "requires_restart": true, - "type": "text", - "value": "jfa-go", - "description": "The name of the client that will show up in the Jellyfin dashboard." - }, - "cache_timeout": { - "name": "User cache timeout (minutes)", - "required": false, - "requires_restart": true, - "type": "number", - "value": 30, - "description": "Timeout of user cache in minutes. Set to 0 to disable." - } - }, - "ui": { - "meta": { - "name": "General", - "description": "Settings related to the UI and program functionality." - }, - "language": { - "name": "Language", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "en-us" - ], - "value": "en-US", - "description": "UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate." - }, - "theme": { - "name": "Default Look", - "required": false, - "requires_restart": true, - "type": "select", - "options": [ - "Bootstrap (Light)", - "Jellyfin (Dark)", - "Custom CSS" - ], - "value": "Jellyfin (Dark)", - "description": "Default appearance for all users." - }, - "host": { - "name": "Address", - "required": true, - "requires_restart": true, - "type": "text", - "value": "0.0.0.0", - "description": "Set 0.0.0.0 to run on localhost" - }, - "port": { - "name": "Port", - "required": true, - "requires_restart": true, - "type": "number", - "value": 8056 - }, - "jellyfin_login": { - "name": "Use Jellyfin for authentication", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable this to use Jellyfin users instead of the below username and pw." - }, - "admin_only": { - "name": "Allow admin users only", - "required": false, - "requires_restart": true, - "depends_true": "jellyfin_login", - "type": "bool", - "value": true, - "description": "Allows only admin users on Jellyfin to access the admin page." - }, - "username": { - "name": "Web Username", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "text", - "value": "your username", - "description": "Username for admin page (Leave blank if using jellyfin_login)" - }, - "password": { - "name": "Web Password", - "required": true, - "requires_restart": true, - "depends_false": "jellyfin_login", - "type": "password", - "value": "your password", - "description": "Password for admin page (Leave blank if using jellyfin_login)" + "password_validation": { + "order": [], + "meta": { + "name": "Password Validation", + "description": "Password validation (minimum length, etc.)" + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": false, + "type": "bool", + "value": true + }, + "min_length": { + "name": "Minimum Length", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "8" + }, + "upper": { + "name": "Minimum uppercase characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "lower": { + "name": "Minimum lowercase characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "0" + }, + "number": { + "name": "Minimum number count", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "1" + }, + "special": { + "name": "Minimum number of special characters", + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "0" + } + } }, "email": { - "name": "Admin email address", - "required": false, - "requires_restart": false, - "depends_false": "jellyfin_login", - "type": "text", - "value": "example@example.com", - "description": "Address to send notifications to (Leave blank if using jellyfin_login)" + "order": [], + "meta": { + "name": "Email", + "description": "General email settings. Ignore if not using email features." + }, + "settings": { + "no_username": { + "name": "Use email addresses as username", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": false, + "description": "Use email address from invite form as username on Jellyfin." + }, + "use_24h": { + "name": "Use 24h time", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "bool", + "value": true + }, + "date_format": { + "name": "Date format", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "%d/%m/%y", + "description": "Date format used in emails. Follows datetime.strftime format." + }, + "message": { + "name": "Help message", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Need help? contact me.", + "description": "Message displayed at bottom of emails." + }, + "method": { + "name": "Email method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "smtp", + "mailgun" + ], + "value": "smtp", + "description": "Method of sending email to use." + }, + "address": { + "name": "Sent from (address)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "email", + "value": "jellyfin@jellyf.in", + "description": "Address to send emails from" + }, + "from": { + "name": "Sent from (name)", + "required": false, + "requires_restart": false, + "depends_true": "method", + "type": "text", + "value": "Jellyfin", + "description": "The name of the sender" + } + } }, - "debug": { - "name": "Debug logging", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enables debug logging and exposes pprof as a route (Don't use in production!)" + "password_resets": { + "order": [], + "meta": { + "name": "Password Resets", + "description": "Settings for the password reset handler." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" + }, + "watch_directory": { + "name": "Jellyfin directory", + "required": false, + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "/path/to/jellyfin", + "description": "Path to the folder Jellyfin puts password-reset files." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Password Reset - Jellyfin", + "description": "Subject of password reset emails." + } + } }, - "contact_message": { - "name": "Contact message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Need help? contact me.", - "description": "Displayed at bottom of all pages except admin" + "invite_emails": { + "order": [], + "meta": { + "name": "Invite emails", + "description": "Settings for sending invites directly to users." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": false, + "type": "bool", + "value": true + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email HTML" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + }, + "subject": { + "name": "Email subject", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "Invite - Jellyfin", + "description": "Subject of invite emails." + }, + "url_base": { + "name": "URL Base", + "required": true, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "http://accounts.jellyf.in:8056/invite", + "description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." + } + } }, - "help_message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Enter your details to create an account.", - "description": "Displayed at top of invite form." + "notifications": { + "order": [], + "meta": { + "name": "Notifications", + "description": "Notification related settings." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": "false", + "requires_restart": true, + "type": "bool", + "value": true, + "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." + }, + "expiry_html": { + "name": "Expiry email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to expiry notification email HTML." + }, + "expiry_text": { + "name": "Expiry email (Plaintext)", + "required": false, + "requires_restart": "false", + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to expiry notification email in plaintext." + }, + "created_html": { + "name": "User created email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to user creation notification email HTML." + }, + "created_text": { + "name": "User created email (Plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Path to user creation notification email in plaintext." + } + } }, - "success_message": { - "name": "Success message", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account has been created. Click below to continue to Jellyfin.", - "description": "Displayed when a user creates an account" + "mailgun": { + "order": [], + "meta": { + "name": "Mailgun (Email)", + "description": "Mailgun API connection settings" + }, + "settings": { + "api_url": { + "name": "API URL", + "required": false, + "requires_restart": false, + "type": "text", + "value": "https://api.mailgun.net..." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": false, + "type": "text", + "value": "your api key" + } + } }, - "bs5": { - "name": "Use Bootstrap 5", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster." + "smtp": { + "order": [], + "meta": { + "name": "SMTP (Email)", + "description": "SMTP Server connection settings." + }, + "settings": { + "username": { + "name": "Username", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Username for SMTP. Leave blank to user send from address as username." + }, + "encryption": { + "name": "Encryption Method", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + "ssl_tls", + "starttls" + ], + "value": "starttls", + "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." + }, + "server": { + "name": "Server address", + "required": false, + "requires_restart": false, + "type": "text", + "value": "smtp.jellyf.in", + "description": "SMTP Server address." + }, + "port": { + "name": "Port", + "required": false, + "requires_restart": false, + "type": "number", + "value": 465 + }, + "password": { + "name": "Password", + "required": false, + "requires_restart": false, + "type": "password", + "value": "smtp password" + } + } }, - "url_base": { - "name": "URL Base", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "URL base for when running jfa-go with a reverse proxy in a subfolder." - } - }, - "password_validation": { - "meta": { - "name": "Password Validation", - "description": "Password validation (minimum length, etc.)" + "ombi": { + "order": [], + "meta": { + "name": "Ombi Integration", + "description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Enable to create an Ombi account for new Jellyfin users" + }, + "server": { + "name": "URL", + "required": false, + "requires_restart": true, + "type": "text", + "value": "localhost:5000", + "depends_true": "enabled", + "description": "Ombi server URL, including http(s)://." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "depends_true": "enabled", + "description": "API Key. Get this from the first tab in Ombi settings." + } + } }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true + "deletion": { + "order": [], + "meta": { + "name": "Account Deletion", + "description": "Subject/email files for account deletion emails." + }, + "settings": { + "subject": { + "name": "Email subject", + "required": false, + "requires_restart": false, + "type": "text", + "value": "Your account was deleted - Jellyfin", + "description": "Subject of account deletion emails." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + } + } }, - "min_length": { - "name": "Minimum Length", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "8" - }, - "upper": { - "name": "Minimum uppercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "lower": { - "name": "Minimum lowercase characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - }, - "number": { - "name": "Minimum number count", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "1" - }, - "special": { - "name": "Minimum number of special characters", - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "0" - } - }, - "email": { - "meta": { - "name": "Email", - "description": "General email settings. Ignore if not using email features." - }, - "no_username": { - "name": "Use email addresses as username", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": false, - "description": "Use email address from invite form as username on Jellyfin." - }, - "use_24h": { - "name": "Use 24h time", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "bool", - "value": true - }, - "date_format": { - "name": "Date format", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "%d/%m/%y", - "description": "Date format used in emails. Follows datetime.strftime format." - }, - "message": { - "name": "Help message", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Need help? contact me.", - "description": "Message displayed at bottom of emails." - }, - "method": { - "name": "Email method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "smtp", - "mailgun" - ], - "value": "smtp", - "description": "Method of sending email to use." - }, - "address": { - "name": "Sent from (address)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "email", - "value": "jellyfin@jellyf.in", - "description": "Address to send emails from" - }, - "from": { - "name": "Sent from (name)", - "required": false, - "requires_restart": false, - "depends_true": "method", - "type": "text", - "value": "Jellyfin", - "description": "The name of the sender" - } - }, - "password_resets": { - "meta": { - "name": "Password Resets", - "description": "Settings for the password reset handler." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enable to store provided email addresses, monitor Jellyfin directory for pw-resets, and send reset pins" - }, - "watch_directory": { - "name": "Jellyfin directory", - "required": false, - "requires_restart": true, - "depends_true": "enabled", - "type": "text", - "value": "/path/to/jellyfin", - "description": "Path to the folder Jellyfin puts password-reset files." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Password Reset - Jellyfin", - "description": "Subject of password reset emails." - } - }, - "invite_emails": { - "meta": { - "name": "Invite emails", - "description": "Settings for sending invites directly to users." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": false, - "type": "bool", - "value": true - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email HTML" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - }, - "subject": { - "name": "Email subject", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "Invite - Jellyfin", - "description": "Subject of invite emails." - }, - "url_base": { - "name": "URL Base", - "required": true, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "http://accounts.jellyf.in:8056/invite", - "description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself." - } - }, - "notifications": { - "meta": { - "name": "Notifications", - "description": "Notification related settings." - }, - "enabled": { - "name": "Enabled", - "required": "false", - "requires_restart": true, - "type": "bool", - "value": true, - "description": "Enabling adds optional toggles to invites to notify on expiry and user creation." - }, - "expiry_html": { - "name": "Expiry email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email HTML." - }, - "expiry_text": { - "name": "Expiry email (Plaintext)", - "required": false, - "requires_restart": "false", - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to expiry notification email in plaintext." - }, - "created_html": { - "name": "User created email (HTML)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email HTML." - }, - "created_text": { - "name": "User created email (Plaintext)", - "required": false, - "requires_restart": false, - "depends_true": "enabled", - "type": "text", - "value": "", - "description": "Path to user creation notification email in plaintext." - } - }, - "mailgun": { - "meta": { - "name": "Mailgun (Email)", - "description": "Mailgun API connection settings" - }, - "api_url": { - "name": "API URL", - "required": false, - "requires_restart": false, - "type": "text", - "value": "https://api.mailgun.net..." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": false, - "type": "text", - "value": "your api key" - } - }, - "smtp": { - "meta": { - "name": "SMTP (Email)", - "description": "SMTP Server connection settings." - }, - "username": { - "name": "Username", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Username for SMTP. Leave blank to user send from address as username." - }, - "encryption": { - "name": "Encryption Method", - "required": false, - "requires_restart": false, - "type": "select", - "options": [ - "ssl_tls", - "starttls" - ], - "value": "starttls", - "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." - }, - "server": { - "name": "Server address", - "required": false, - "requires_restart": false, - "type": "text", - "value": "smtp.jellyf.in", - "description": "SMTP Server address." - }, - "port": { - "name": "Port", - "required": false, - "requires_restart": false, - "type": "number", - "value": 465 - }, - "password": { - "name": "Password", - "required": false, - "requires_restart": false, - "type": "password", - "value": "smtp password" - } - }, - "ombi": { - "meta": { - "name": "Ombi Integration", - "description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this." - }, - "enabled": { - "name": "Enabled", - "required": false, - "requires_restart": true, - "type": "bool", - "value": false, - "description": "Enable to create an Ombi account for new Jellyfin users" - }, - "server": { - "name": "URL", - "required": false, - "requires_restart": true, - "type": "text", - "value": "localhost:5000", - "depends_true": "enabled", - "description": "Ombi server URL, including http(s)://." - }, - "api_key": { - "name": "API Key", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "depends_true": "enabled", - "description": "API Key. Get this from the first tab in Ombi settings." - } - }, - "deletion": { - "meta": { - "name": "Account Deletion", - "description": "Subject/email files for account deletion emails." - }, - "subject": { - "name": "Email subject", - "required": false, - "requires_restart": false, - "type": "text", - "value": "Your account was deleted - Jellyfin", - "description": "Subject of account deletion emails." - }, - "email_html": { - "name": "Custom email (HTML)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email html" - }, - "email_text": { - "name": "Custom email (plaintext)", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Path to custom email in plain text" - } - }, - "files": { - "meta": { - "name": "File Storage", - "description": "Optional settings for changing storage locations." - }, - "invites": { - "name": "Invite Storage", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored invites (json)." - }, - "emails": { - "name": "Email Addresses", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored email addresses (json)." - }, - "ombi_template": { - "name": "Ombi user template", - "required": false, - "requires_restart": false, - "type": "text", - "value": "", - "description": "Location of stored Ombi user template." - }, - "user_template": { - "name": "User Template (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)." - }, - "user_configuration": { - "name": "userConfiguration (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)" - }, - "user_displayprefs": { - "name": "displayPreferences (Deprecated)", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)" - }, - "user_profiles": { - "name": "User Profiles", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)" - }, - "custom_css": { - "name": "Custom CSS", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Location of custom bootstrap CSS." - }, - "html_templates": { - "name": "Custom HTML Template Directory", - "required": false, - "requires_restart": true, - "type": "text", - "value": "", - "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." + "files": { + "order": [], + "meta": { + "name": "File Storage", + "description": "Optional settings for changing storage locations." + }, + "settings": { + "invites": { + "name": "Invite Storage", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored invites (json)." + }, + "emails": { + "name": "Email Addresses", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored email addresses (json)." + }, + "ombi_template": { + "name": "Ombi user template", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Location of stored Ombi user template." + }, + "user_template": { + "name": "User Template (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)." + }, + "user_configuration": { + "name": "userConfiguration (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)" + }, + "user_displayprefs": { + "name": "displayPreferences (Deprecated)", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)" + }, + "user_profiles": { + "name": "User Profiles", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)" + }, + "html_templates": { + "name": "Custom HTML Template Directory", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "description": "Path to directory containing custom versions of web ui pages. See wiki for more info." + } + } } } } diff --git a/config/fixconfig.py b/config/fixconfig.py index 180f2e4..2cf90b4 100644 --- a/config/fixconfig.py +++ b/config/fixconfig.py @@ -9,17 +9,17 @@ args = parser.parse_args() with open(args.input, 'r') as f: config = json.load(f) -newconfig = {"order": []} +newconfig = {"sections": {}, "order": []} -for sect in config: +for sect in config["sections"]: newconfig["order"].append(sect) - newconfig[sect] = {} - newconfig[sect]["order"] = [] - newconfig[sect]["meta"] = config[sect]["meta"] - for setting in config[sect]: - if setting != "meta": - newconfig[sect]["order"].append(setting) - newconfig[sect][setting] = config[sect][setting] + newconfig["sections"][sect] = {} + newconfig["sections"][sect]["order"] = [] + newconfig["sections"][sect]["meta"] = config["sections"][sect]["meta"] + newconfig["sections"][sect]["settings"] = {} + for setting in config["sections"][sect]["settings"]: + newconfig["sections"][sect]["order"].append(setting) + newconfig["sections"][sect]["settings"][setting] = config["sections"][sect]["settings"][setting] with open(args.output, 'w') as f: f.write(json.dumps(newconfig, indent=4)) diff --git a/config/generate_ini.py b/config/generate_ini.py index efc8c0a..a801b8b 100644 --- a/config/generate_ini.py +++ b/config/generate_ini.py @@ -14,18 +14,19 @@ def generate_ini(base_file, ini_file): ini = configparser.RawConfigParser(allow_no_value=True) - for section in config_base: + for section in config_base["sections"]: ini.add_section(section) - for entry in config_base[section]: - if "description" in config_base[section][entry]: - ini.set(section, "; " + config_base[section][entry]["description"]) - if entry != "meta": - value = config_base[section][entry]["value"] - if isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - ini.set(section, entry, value) + if "meta" in config_base["sections"][section]: + ini.set(section, "; " + config_base["sections"][section]["meta"]["description"]) + for entry in config_base["sections"][section]["settings"]: + if "description" in config_base["sections"][section]["settings"][entry]: + ini.set(section, "; " + config_base["sections"][section]["settings"][entry]["description"]) + value = config_base["sections"][section]["settings"][entry]["value"] + if isinstance(value, bool): + value = str(value).lower() + else: + value = str(value) + ini.set(section, entry, value) with open(Path(ini_file), "w") as config_file: ini.write(config_file) diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..2b919d2 --- /dev/null +++ b/css/base.css @@ -0,0 +1,372 @@ +@import "a17t.css"; +@import "remixicon.css"; +@import "modal.css"; +@import "dark.css"; +@import "tooltip.css"; +@import "loader.css"; + +:root { + --border-width-default: 2px; + --border-width-2: 3px; + --border-width-4: 5px; + --border-width-8: 8px; +} + +.light-theme { + --settings-section-button-filter: 90%; +} + +.body { + background-color: #101010; +} + +.page-container { + margin: 5% 20% 5% 20%; +} + +@media (max-width: 1100px) { + .page-container { + margin: 2%; + } +} + +@media screen and (max-width: 750px) { + :root { + font-size: 0.9rem; + } + .table-responsive table { + min-width: 660px; + } +} + + +.tab-button { + font-size: 2rem; +} + +.mb-half { + margin-bottom: 0.5rem; +} + +.mb-1 { + margin-bottom: 1rem; +} + +.mb-2 { + margin-bottom: 2rem; +} + +.mt-half { + margin-top: 0.5rem; +} + +.mt-1 { + margin-top: 1rem; +} + +.ml-1 { + margin-left: 1rem; +} + +.ml-half { + margin-left: 0.5rem; +} + +.mr-1 { + margin-right: 1rem; +} + +.pb-1 { + padding-bottom: 1rem; +} + +.pl-1 { + padding-left: 1rem; +} + +.al { + text-align: left; +} + +.ar { + text-align: right; +} + +.inline-block { + display: inline-block; +} + +.align-top { + align-items: top; +} + +.flex { + display: flex; +} + +.flex-expand { + display: flex; + justify-content: space-between; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-row-group { + display: block; + flex-grow: 1; +} + +.row { + display: flex; + flex-wrap: wrap; +} + +.row.baseline { + align-items: baseline; +} + +.col { + flex: 1; + margin: 0.5rem; +} + +@media screen and (max-width: 400px) { + .row { + flex-direction: column; + } + .col { + flex: 45%; + } +} + +.fr { + float: right; +} + +.monospace { + background-color: inherit; /* so we can use a17t code blocks */ +} + +sup.\~critical, .text-critical { + color: var(--color-critical-normal-content); +} + +.grey { + color: var(--color-neutral-500); +} + +.aside.sm { + font-size: 0.8rem; + padding: 0.8rem; +} + +.support.lg { + font-size: 1rem; +} + +.badge.lg { + font-size: 1rem; +} + +.inv-created-users strong,p { + padding-left: 0.5rem; + padding-bottom: 0.2rem; +} + +.inv-created-users.empty strong,p { + padding: 0; +} + +.inv { + overflow: visible; +} + +.inv-table { + font-size: 0.8rem; +} + +.inv-profilearea { + min-width: 20%; +} + +.inv-profileselect { + min-width: 100%; +} + +.inv-codearea { + max-width: 40%; + min-width: 10rem; + display: flex; + justify-content: center; + align-items: center; +} + +.inv-empty .inv-codearea { + justify-content: start; +} + + +.invite-link { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: auto; +} + +.ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.no-pad { + padding: 0px 0px 0px 0px; +} + +.elem-pad > * { + margin: var(--spacing-4, 1rem); +} + +.icon.clickable { + padding: 0.5rem 0.6rem; +} + +.input { + box-sizing: border-box; /* fixes weird length issue with inputs */ +} + +.button.lg { + height: 2.5rem; +} + +.submit { + border: none; + outline: none; /* remove browser styling on submit buttons */ +} + +.full-width { /* full width inputs */ + box-sizing: border-box; /* TODO: maybe remove if we figure out input thing? */ + width: 100%; +} + +.center { + justify-content: center; +} + +.no-lp { + padding-left: 0px; +} + +.block { + display: block; +} + +.focused { + display: block; +} + +.unfocused { + display: none; +} + +.rotated { + transform: rotate(180deg); + -webkit-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -moz-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -o-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + transition: all 0.3s cubic-bezier(0,.89,.27,.92); +} + +.not-rotated { + transform: rotate(0deg); + -webkit-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -moz-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + -o-transition: all 0.3s cubic-bezier(0,.89,.27,.92); + transition: all 0.3s cubic-bezier(0,.89,.27,.92); +} + +.stealth-input { + font-size: 1rem; + padding-top: 0.1rem; + padding-bottom: 0.1rem; + margin-left: 0.5rem; + margin-right: 1rem; + max-width: 75%; +} + +.stealth-input-hidden { + border-style: none; + --fallback-box-shadow: none; + --field-hover-box-shadow: none; + --field-focus-box-shadow: none; + padding-top: 0.1rem; + padding-bottom: 0.1rem; +} + +.settings-section-button { + box-sizing: border-box; + width: 100%; + height: 2.5rem; + background-color: rgba(0,0,0,0); +} + +.settings-section-button:hover, .settings-section-button:focus { + box-sizing: border-box; + width: 100%; + height: 2.5rem; + background-color: var(--color-neutral-normal-fill); + filter: brightness(var(--settings-section-button-filter)) !important; +} + +.settings-section-button.selected { + background-color: var(--color-neutral-normal-fill); + --buton-filter-brightness: var(--settings-section-button-filter); + filter: brightness(var(--settings-section-button-filter)) !important; +} + +.setting { + margin-bottom: 0.25rem; +} + +.textarea { + resize: vertical; +} + +.overflow { + overflow: visible; +} + +.overflow-y { + overflow-y: visible; +} + +select, textarea { + color: inherit; + border: 0 solid var(--color-neutral-300); + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +input { + color: inherit; + border: 0 solid var(--color-neutral-300); +} + +p.top { + margin-top: 0px; +} + +.table-responsive { + overflow-x: auto; +} + +#notification-box { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 16; +} diff --git a/css/dark.css b/css/dark.css new file mode 100644 index 0000000..b21de54 --- /dev/null +++ b/css/dark.css @@ -0,0 +1,87 @@ +.dark-theme { + + --settings-section-button-filter: 110%; + + --color-neutral-900: rgba(255, 255, 255, 0.87); + --color-neutral-800: rgba(255, 255, 255, 0.8); + --color-neutral-700: rgba(255, 255, 255, 0.73); + --color-neutral-600: rgba(255, 255, 255, 0.66); + --color-neutral-500: rgb(153, 153, 153); + --color-neutral-400: #383838; + --color-neutral-300: #303030; + --color-neutral-200: #292929; + --color-neutral-100: #242424; + --color-neutral-50: #202020; + --color-neutral-000: #101010; + + --color-critical-900: #fef2f2; + --color-critical-800: #fee2e2; + --color-critical-700: #fecaca; + --color-critical-600: #fca5a5; + --color-critical-500: #f87171; + --color-critical-400: #ef4444; + --color-critical-300: #dc2626; + --color-critical-200: #b91c1c; + --color-critical-100: #991b1b; + --color-critical-50: #7f1d1d; + --color-critical-000: #441313; + + --color-warning-900: #fffbeb; + --color-warning-800: #fef3c7; + --color-warning-700: #fde68a; + --color-warning-600: #fcd34d; + --color-warning-500: #fbbf24; + --color-warning-400: #f59e0b; + --color-warning-300: #d97706; + --color-warning-200: #b45309; + --color-warning-100: #92400e; + --color-warning-50: #783900; + --color-warning-000: #411e01; + + --color-positive-900: #f0fdf4; + --color-positive-800: #dcfce7; + --color-positive-700: #bbf7d0; + --color-positive-600: #86efac; + --color-positive-500: #4ade80; + --color-positive-400: #22c55e; + --color-positive-300: #16a34a; + --color-positive-200: #15803d; + --color-positive-100: #166534; + --color-positive-50: #14532d; + --color-positive-000: #0f2e1b; + + --color-urge-900: #e0ffff; + --color-urge-800: #c0fbff; + --color-urge-700: #a0f4ff; + --color-urge-600: #80e9ff; + --color-urge-500: #60dbfb; + --color-urge-400: #40cbf3; + --color-urge-300: #20b9e9; + --color-urge-200: #00a4dc; /* tab buttons */ + --color-urge-100: #0054bc; + --color-urge-50: #00169a; + --color-urge-000: #050076; + + --color-info-900: #f5f3ff; + --color-info-800: #ede9fe; + --color-info-700: #ddd6fe; + --color-info-600: #c4b5fd; + --color-info-500: #a78bfa; + --color-info-400: #8b5cf6; + --color-info-300: #7c3aed; + --color-info-200: #6d28d9; + --color-info-100: #5b21b6; + --color-info-50: #4c1d95; + --color-info-000: #240e44; + + + --color-neutral-normal-content: #ffffff; +} + +.light-only { + display: none; +} + +.dark-only { + display: initial; +} diff --git a/css/loader.css b/css/loader.css new file mode 100644 index 0000000..2b7951a --- /dev/null +++ b/css/loader.css @@ -0,0 +1,40 @@ +.loader { + height: auto; + color: rgba(0, 0, 0, 0); +} + +.loader .dot { + --diameter: 0.5rem; + --radius: calc(var(--diameter) / 2); + --deviation: 20%; + height: var(--diameter); + width: var(--diameter); + background-color: var(--color-content); + border-radius: 50%; + position: absolute; + left: calc(50% - var(--radius)); + animation: osc 1s cubic-bezier(.72,.16,.31,.97) infinite; +} +.loader.loader-sm .dot { + --deviation: 10%; +} + +@keyframes osc { + 25% { + left: calc(50% + var(--deviation) - var(--radius)); + } + 75% { + left: calc(50% - var(--deviation)); + } + 0%, 50%, 100% { + left: calc(50% - var(--radius)); + } +/* + 0%, 100% { + left: calc(50% - var(--deviation)) + } + 50% { + left: calc(50% + var(--deviation) - var(--radius)); + } + */ +} diff --git a/css/modal.css b/css/modal.css new file mode 100644 index 0000000..3f27a99 --- /dev/null +++ b/css/modal.css @@ -0,0 +1,72 @@ +.modal { + display: none; + position: fixed; + z-index: 12; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,40%); +} + +.modal-shown { + display: block; +} + +@keyframes modal-hide { + from { opacity: 1; } + to { opacity: 0; } +} + +.modal-hiding { + animation: modal-hide 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +@keyframes modal-content-show { + from { + opacity: 0; + top: -6rem; + } + to { + opacity: 1; + top: 0; + } +} + +.modal-content { + position: relative; + margin: 10% auto; + width: 30%; +} + +.modal-content.wide { + width: 60%; +} + +.modal-shown .modal-content { + animation: modal-content-show 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +@media screen and (max-width: 1000px) { + .modal-content.wide { + width: 75%; + } +} + +@media screen and (max-width: 400px) { + .modal-content, .modal-content.wide { + width: 90%; + } +} + +.modal-close { + float: right; + color: #aaa; + font-weight: normal; +} + +.modal-close:hover, +.modal-close:focus { + filter: brightness(60%); +} diff --git a/css/tooltip.css b/css/tooltip.css new file mode 100644 index 0000000..4566f30 --- /dev/null +++ b/css/tooltip.css @@ -0,0 +1,36 @@ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .content { + visibility: hidden; + max-width: 10rem; + min-width: 6rem; + background-color: rgba(0, 0, 0, 0.6); + color: #fff; + padding: 0.5rem; + border-radius: 6px; + overflow-wrap: break-word; + text-align: center; + + position: absolute; + z-index: 1; + top: -1rem; +} + +.tooltip.right .content { + left: 120%; +} + +.tooltip.left .content { + right: 120%; +} + +.tooltip .content.sm { + font-size: 0.8rem; +} + +.tooltip:hover .content { + visibility: visible; +} diff --git a/data/static/setup.js b/data/static/setup.js deleted file mode 100644 index e9f5fc6..0000000 --- a/data/static/setup.js +++ /dev/null @@ -1,275 +0,0 @@ -document.getElementById('page-1').scrollIntoView({ - behavior: 'auto', - block: 'center', - inline: 'center' }); - -function checkAuthRadio() { - if (document.getElementById('manualAuthRadio').checked) { - document.getElementById('adminOnlyArea').style.display = 'none'; - document.getElementById('manualAuthArea').style.display = ''; - } else { - document.getElementById('manualAuthArea').style.display = 'none'; - document.getElementById('adminOnlyArea').style.display = ''; - }; -}; -var authRadios = ['manualAuthRadio', 'jfAuthRadio']; -for (var i = 0; i < authRadios.length; i++) { - document.getElementById(authRadios[i]).addEventListener('change', function() { - checkAuthRadio(); - }); -}; - -function checkEmailRadio() { - document.getElementById('emailNextButton').href = '#page-5'; - document.getElementById('valBackButton').href = '#page-7'; - if (document.getElementById('emailSMTPRadio').checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = ''; - document.getElementById('emailMailgunArea').style.display = 'none'; - document.getElementById('notificationsEnabled').checked = true; - } else if (document.getElementById('emailMailgunRadio').checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = ''; - document.getElementById('notificationsEnabled').checked = true; - } else if (document.getElementById('emailDisabledRadio').checked) { - document.getElementById('emailCommonArea').style.display = 'none'; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = 'none'; - document.getElementById('emailNextButton').href = '#page-8'; - document.getElementById('valBackButton').href = '#page-4'; - document.getElementById('notificationsEnabled').checked = false; - }; -}; -var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']; -for (var i = 0; i < emailRadios.length; i++) { - document.getElementById(emailRadios[i]).addEventListener('change', function() { - checkEmailRadio(); - }); -}; - -function checkSSL() { - var label = document.getElementById('emailSSL_TLSLabel'); - if (document.getElementById('emailSSL_TLS').checked) { - label.textContent = 'Use SSL/TLS'; - } else { - label.textContent = 'Use STARTTLS'; - }; -}; -document.getElementById('emailSSL_TLS').addEventListener('change', function() { - checkSSL(); -}); - -function checkPwrEnabled() { - if (document.getElementById('pwrEnabled').checked) { - document.getElementById('pwrArea').style.display = ''; - } else { - document.getElementById('pwrArea').style.display = 'none'; - }; -}; -var pwrEnabled = document.getElementById('pwrEnabled'); -pwrEnabled.addEventListener('change', function() { - checkPwrEnabled(); -}); - -function checkInvEnabled() { - if (document.getElementById('invEnabled').checked) { - document.getElementById('invArea').style.display = ''; - } else { - document.getElementById('invArea').style.display = 'none'; - }; -}; -document.getElementById('invEnabled').addEventListener('change', function() { - checkInvEnabled(); -}); - -function checkValEnabled() { - if (document.getElementById('valEnabled').checked) { - document.getElementById('valArea').style.display = ''; - } else { - document.getElementById('valArea').style.display = 'none'; - }; -}; -document.getElementById('valEnabled').addEventListener('change', function() { - checkValEnabled(); -}); -checkValEnabled(); -checkInvEnabled(); -checkSSL(); -checkAuthRadio(); -checkEmailRadio(); -checkPwrEnabled(); - -var jfValid = false -document.getElementById('jfTestButton').onclick = function() { - var testButton = document.getElementById('jfTestButton'); - var nextButton = document.getElementById('jfNextButton'); - var jfData = {}; - jfData['jfHost'] = document.getElementById('jfHost').value; - jfData['jfUser'] = document.getElementById('jfUser').value; - jfData['jfPassword'] = document.getElementById('jfPassword').value; - let valid = true; - for (val in jfData) { - if (jfData[val] == "") { - valid = false; - } - } - if (!valid) { - if (!testButton.classList.contains('btn-danger')) { - testButton.classList.add('btn-danger'); - testButton.textContent = 'Fill out fields above.'; - setTimeout(function() { - if (testButton.classList.contains('btn-danger')) { - testButton.classList.remove('btn-danger'); - testButton.textContent = 'Test'; - } - }, 2000); - } - } else { - testButton.disabled = true; - testButton.innerHTML = - '' + - 'Testing...'; - nextButton.classList.add('disabled'); - nextButton.setAttribute('aria-disabled', 'true'); - var req = new XMLHttpRequest(); - req.open("POST", "/jellyfin/test", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - testButton.disabled = false; - testButton.className = ''; - if (this.response['success'] == true) { - testButton.classList.add('btn', 'btn-success'); - testButton.textContent = 'Success'; - nextButton.classList.remove('disabled'); - nextButton.setAttribute('aria-disabled', 'false'); - } else { - testButton.classList.add('btn', 'btn-danger'); - testButton.textContent = 'Failed'; - }; - }; - }; - req.send(JSON.stringify(jfData)); - } -}; - -document.getElementById('submitButton').onclick = function() { - var submitButton = document.getElementById('submitButton'); - submitButton.disabled = true; - submitButton.innerHTML = - '' + - 'Submitting...'; - var config = {}; - config['jellyfin'] = {}; - config['ui'] = {}; - config['password_validation'] = {}; - config['email'] = {}; - config['password_resets'] = {}; - config['invite_emails'] = {}; - config['mailgun'] = {}; - config['smtp'] = {}; - config['notifications'] = {}; - // Page 2: Auth - if (document.getElementById('jfAuthRadio').checked) { - config['ui']['jellyfin_login'] = 'true'; - if (document.getElementById('jfAuthAdminOnly').checked) { - config['ui']['admin_only'] = 'true'; - } else { - config['ui']['admin_only'] = 'false' - }; - } else { - config['ui']['username'] = document.getElementById('manualAuthUsername').value; - config['ui']['password'] = document.getElementById('manualAuthPassword').value; - config['ui']['email'] = document.getElementById('manualAuthEmail').value; - }; - // Page 3: Connect to jellyfin - config['jellyfin']['server'] = document.getElementById('jfHost').value; - let publicAddress = document.getElementById('jfPublicHost').value; - if (publicAddress != "") { - config['jellyfin']['public_server'] = publicAddress; - } - config['jellyfin']['username'] = document.getElementById('jfUser').value; - config['jellyfin']['password'] = document.getElementById('jfPassword').value; - // Page 4: Email (Page 5, 6, 7 are only used if this is enabled) - if (document.getElementById('emailDisabledRadio').checked) { - config['password_resets']['enabled'] = 'false'; - config['invite_emails']['enabled'] = 'false'; - config['notifications']['enabled'] = 'false'; - } else { - if (document.getElementById('emailSMTPRadio').checked) { - if (document.getElementById('emailSSL_TLS').checked) { - config['smtp']['encryption'] = 'ssl_tls'; - } else { - config['smtp']['encryption'] = 'starttls'; - }; - config['email']['method'] = 'smtp'; - config['smtp']['server'] = document.getElementById('emailSMTPServer').value; - config['smtp']['port'] = document.getElementById('emailSMTPPort').value; - config['smtp']['password'] = document.getElementById('emailSMTPPassword').value; - config['email']['address'] = document.getElementById('emailSMTPAddress').value; - } else { - config['email']['method'] = 'mailgun'; - config['mailgun']['api_url'] = document.getElementById('emailMailgunURL').value; - config['mailgun']['api_key'] = document.getElementById('emailMailgunKey').value; - config['email']['address'] = document.getElementById('emailMailgunAddress').value; - }; - config['notifications']['enabled'] = document.getElementById('notificationsEnabled').checked.toString(); - // Page 5: Email formatting - config['email']['from'] = document.getElementById('emailSender').value; - config['email']['date_format'] = document.getElementById('emailDateFormat').value; - if (document.getElementById('email24hTimeRadio').checked) { - config['email']['use_24h'] = 'true'; - } else { - config['email']['use_24h'] = 'false'; - }; - config['email']['message'] = document.getElementById('emailMessage').value; - // Page 6: Password Resets - if (document.getElementById('pwrEnabled').checked) { - config['password_resets']['enabled'] = 'true'; - config['password_resets']['watch_directory'] = document.getElementById('pwrJfPath').value; - config['password_resets']['subject'] = document.getElementById('pwrSubject').value; - } else { - config['password_resets']['enabled'] = 'false'; - }; - // Page 7: Invite Emails - if (document.getElementById('invEnabled').checked) { - config['invite_emails']['enabled'] = 'true'; - config['invite_emails']['url_base'] = document.getElementById('invURLBase').value; - config['invite_emails']['subject'] = document.getElementById('invSubject').value; - } else { - config['invite_emails']['enabled'] = 'false'; - }; - }; - // Page 8: Password Validation - if (document.getElementById('valEnabled').checked) { - config['password_validation']['enabled'] = 'true'; - config['password_validation']['min_length'] = document.getElementById('valLength').value; - config['password_validation']['upper'] = document.getElementById('valUpper').value; - config['password_validation']['lower'] = document.getElementById('valLower').value; - config['password_validation']['number'] = document.getElementById('valNumber').value; - config['password_validation']['special'] = document.getElementById('valSpecial').value; - } else { - config['password_validation']['enabled'] = 'false'; - }; - // Page 9: Messages - config['ui']['contact_message'] = document.getElementById('msgContact').value; - config['ui']['help_message'] = document.getElementById('msgHelp').value; - config['ui']['success_message'] = document.getElementById('msgSuccess').value; - // Send it - config["restart-program"] = true; - var req = new XMLHttpRequest(); - req.open("POST", "/config", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - submitButton.disabled = false; - submitButton.className = ''; - submitButton.classList.add('btn', 'btn-success'); - submitButton.textContent = 'Success'; - }; - }; - req.send(JSON.stringify(config)); -}; diff --git a/data/templates/404.html b/data/templates/404.html deleted file mode 100644 index 22593ad..0000000 --- a/data/templates/404.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - 404 - jfa-go - - {{ template "header.html" . }} - - - -
-

Page not found.

-

- {{ .contactMessage }} -

-
- - diff --git a/data/templates/admin.html b/data/templates/admin.html deleted file mode 100644 index c1d7885..0000000 --- a/data/templates/admin.html +++ /dev/null @@ -1,473 +0,0 @@ - - - - - {{ template "header.html" . }} - Admin - jfa-go - - - - - - {{ if .ombiEnabled }} - - {{ end }} - - - - - -
- -
- -
-
-
-
Current Invites
-
    -
-
-
-
-
Generate Invite
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- -
-
- -
- -
-
-
- - - -
-
- - -
- {{ if .email_enabled }} -
- -
-
- -
- -
-
- {{ end }} -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
Accounts
-
- - - -
-
-
- - - - {{ if .bs5 }} - - {{ else }} - - {{ end }} - - - - - - - -
UsernameEmail AddressLast Active
-
-
-
-
-
-
Settings
-
- -
-
-
-
-
-
-
    -

    Note: * Indicates required field, R Indicates changes require a restart.

    - - - {{ if .ombiEnabled }} - - {{ end }} -
-
-
-
-
-
-
-
-
-

Profiles are applied to users when they create an account. They include things like access rights and homescreen layout. You can create them here.

- - - - - - - - - - - - - -
NameDefaultFromAdmin?Libraries
-
-
-
-
-
-
-
-
-

{{ .contactMessage }}

-
-
- - - - {{ if .ombiEnabled }} - - {{ end }} - - diff --git a/data/templates/form.html b/data/templates/form.html deleted file mode 100644 index 80c9635..0000000 --- a/data/templates/form.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - {{ template "header.html" . }} - - {{ .lang.pageTitle }} - - - -
-

- {{ .lang.createAccountHeader }} -

-

{{ .helpMessage }}

-

{{ .contactMessage }}

-
-
-
-
-
{{ .lang.accountDetails }}
-
-
-
- - -
- {{ if .username }} -
- - -
- {{ end }} -
- - -
-
- - -
-
- -
-
-
-
-
- {{ if .validate }} -
-
-
{{ .lang.passwordRequirementsHeader }}
-
-
    - {{ range $key, $value := .requirements }} -
  • -
    -
  • - {{ end }} -
-
-
-
- {{ end }} -
-
-
- - {{ template "form-base" . }} - - - diff --git a/data/templates/header.html b/data/templates/header.html deleted file mode 100644 index 5f2975b..0000000 --- a/data/templates/header.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - -{{ if not .bs5 }} - -{{ end }} - -{{ if .bs5 }} - -{{ else }} - -{{ end }} - diff --git a/data/templates/invalidCode.html b/data/templates/invalidCode.html deleted file mode 100644 index 99ed9e0..0000000 --- a/data/templates/invalidCode.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Invalid Code - jfa-go - - {{ template "header.html" . }} - - - -
-

Invalid Code.

-

The above code is either incorrect, or has expired.

-

{{ .contactMessage }}

-
- - diff --git a/esbuild.sh b/esbuild.sh deleted file mode 100755 index a9cc078..0000000 --- a/esbuild.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/bash -# set +e -# npx tsc -p ts/ -# set -e -npx esbuild ts/* --outdir=data/static --minify diff --git a/go.mod b/go.mod index d9eae46..017aba2 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/spec v0.20.0 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/gofrs/uuid v3.3.0+incompatible // indirect github.com/golang/protobuf v1.4.3 @@ -45,10 +46,9 @@ require ( github.com/ugorji/go v1.2.0 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect - golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba // indirect + golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect golang.org/x/text v0.3.4 // indirect + golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.62.0 - gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 421c4c0..c20e0ce 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= @@ -66,6 +67,8 @@ github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+j github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg= github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= @@ -79,6 +82,8 @@ github.com/go-openapi/spec v0.19.13 h1:AcZVcWsrfW7LqyHKVbTZYpFF7jQcMxmAsWrw2p/b9 github.com/go-openapi/spec v0.19.13/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= github.com/go-openapi/spec v0.19.14 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM= github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= +github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= @@ -89,6 +94,8 @@ github.com/go-openapi/swag v0.19.10 h1:A1SWXruroGP15P1sOiegIPbaKio+G9N5TwWTFaVPm github.com/go-openapi/swag v0.19.10/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc= github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= +github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -153,6 +160,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU= @@ -202,6 +210,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y= github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -314,6 +323,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTi golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -344,6 +355,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s= golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -382,6 +396,8 @@ golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5 golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0= golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee h1:5xKxdl/RhlelmSPaxyVeq5PYSmJ4H14yeQT58qP1F6o= +golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -407,6 +423,7 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= @@ -426,6 +443,9 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/html/404.html b/html/404.html new file mode 100644 index 0000000..171610a --- /dev/null +++ b/html/404.html @@ -0,0 +1,16 @@ + + + + + {{ template "header.html" . }} + 404 - jfa-go + + +
+

Page not found.

+

+ {{ .contactMessage }} +

+
+ + diff --git a/html/admin.html b/html/admin.html new file mode 100644 index 0000000..d86e90b --- /dev/null +++ b/html/admin.html @@ -0,0 +1,296 @@ + + + + + + + {{ template "header.html" . }} + Admin - jfa-go + + + + + + + + + + + + +
+
+
+
+
+ Invites + Accounts + Settings +
+
+
+
+
+ Logout + Theme +
+
+
+
+ Invites +
+
+
+ Create +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+ + +
+

Warning invites with infinite uses can be used abusively.

+ +
+ +
+
+ +
+ + +
+
+ Create +
+
+
+
+
+
+ Accounts +
+ Add User + Modify Settings + Delete User +
+
+ + + + + + + + + + +
UsernameEmail AddressLast Active
+
+
+
+
+
+ Settings +
+ Save +
+
+
+ + About + User profiles +
+
+
+
+
+
+ + + diff --git a/data/templates/form-base.html b/html/form-base.html similarity index 78% rename from data/templates/form-base.html rename to html/form-base.html index 35f3155..432d6a4 100644 --- a/data/templates/form-base.html +++ b/html/form-base.html @@ -1,10 +1,9 @@ {{ define "form-base" }} - + {{ end }} diff --git a/data/templates/form-loader.html b/html/form-loader.html similarity index 100% rename from data/templates/form-loader.html rename to html/form-loader.html diff --git a/html/form.html b/html/form.html new file mode 100644 index 0000000..88c5685 --- /dev/null +++ b/html/form.html @@ -0,0 +1,67 @@ + + + + + {{ template "header.html" . }} + {{ .lang.pageTitle }} + + + +
+
+
+
+ {{ .lang.createAccountHeader }} + {{ .helpMessage }} +
+
+
+
+ + + + + + + + + + + +
+
+
+
+ {{ .lang.passwordRequirementsHeader }} +
    + {{ range $key, $value := .requirements }} +
  • + +
  • + {{ end }} +
+
+ +
+
+
+
+ + {{ template "form-base" . }} + + + diff --git a/html/header.html b/html/header.html new file mode 100644 index 0000000..4a83009 --- /dev/null +++ b/html/header.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/html/invalidCode.html b/html/invalidCode.html new file mode 100644 index 0000000..c14ad1a --- /dev/null +++ b/html/invalidCode.html @@ -0,0 +1,17 @@ + + + + + {{ template "header.html" . }} + Invalid Code - jfa-go + + +
+

Invalid invite code.

+

The code above was either incorrect, or has expired.

+

+ {{ .contactMessage }} +

+
+ + diff --git a/data/templates/setup.html b/html/setup.html similarity index 99% rename from data/templates/setup.html rename to html/setup.html index 526d11f..ecffe4e 100644 --- a/data/templates/setup.html +++ b/html/setup.html @@ -3,7 +3,7 @@ - + @@ -369,6 +369,6 @@ - + diff --git a/images/accounts.png b/images/accounts.png index dc26621..d1a7321 100644 Binary files a/images/accounts.png and b/images/accounts.png differ diff --git a/data/static/banner.svg b/images/banner.svg similarity index 100% rename from data/static/banner.svg rename to images/banner.svg diff --git a/images/create.png b/images/create.png new file mode 100644 index 0000000..ef55a47 Binary files /dev/null and b/images/create.png differ diff --git a/images/demo.gif b/images/demo.gif index 6793691..d38e8c1 100644 Binary files a/images/demo.gif and b/images/demo.gif differ diff --git a/images/gengif.txt b/images/gengif.txt new file mode 100644 index 0000000..7fbda83 --- /dev/null +++ b/images/gengif.txt @@ -0,0 +1,3 @@ +Commands for making GIF: +ffmpeg -i demo.mkv -vf "palettegen" videoPalette.png +ffmpeg -i demo.mkv -i videoPalette.png -lavfi "fps=25 [x]; [x][1:v] paletteuse" -y demo.gif diff --git a/images/invites.png b/images/invites.png index fef7590..c0a08ee 100644 Binary files a/images/invites.png and b/images/invites.png differ diff --git a/images/jfa-go-banner-wide.svg b/images/jfa-go-banner-wide.svg deleted file mode 120000 index 7b6d367..0000000 --- a/images/jfa-go-banner-wide.svg +++ /dev/null @@ -1 +0,0 @@ -../data/static/banner.svg \ No newline at end of file diff --git a/jfa-go.service b/jfa-go.service deleted file mode 100644 index 8d50f27..0000000 --- a/jfa-go.service +++ /dev/null @@ -1,11 +0,0 @@ -# Systemd service file for jfa-go. Install to ~/.config/systemd/user. - -[Unit] -Description=A web app for managing users on Jellyfin - -[Service] -# Modify this to the path to your executable, if necessary. -ExecStart=/opt/jfa-go/jfa-go - -[Install] -WantedBy=default.target diff --git a/data/lang/form/en-us.json b/lang/form/en-us.json similarity index 100% rename from data/lang/form/en-us.json rename to lang/form/en-us.json diff --git a/data/lang/form/fr-fr.json b/lang/form/fr-fr.json similarity index 100% rename from data/lang/form/fr-fr.json rename to lang/form/fr-fr.json diff --git a/mail/generate.py b/mail/generate.py index b825c57..f5c7f37 100755 --- a/mail/generate.py +++ b/mail/generate.py @@ -1,8 +1,13 @@ import subprocess import shutil import os +import argparse from pathlib import Path +parser = argparse.ArgumentParser() +parser.add_argument("-o", "--output", help="output directory for .html and .txt files") + +args = parser.parse_args() def runcmd(cmd): if os.name == "nt": @@ -22,7 +27,8 @@ for mjml in [f for f in local_path.iterdir() if f.is_file() and "mjml" in f.suff html = [f for f in local_path.iterdir() if f.is_file() and "html" in f.suffix] -output = local_path.parent / "data" +output = Path(args.output) # local_path.parent / "build" / "data" +output.mkdir(parents=True, exist_ok=True) for f in html: shutil.copy(str(f), str(output / f.name)) diff --git a/main.go b/main.go index 3689c47..58bddea 100644 --- a/main.go +++ b/main.go @@ -48,11 +48,10 @@ type appContext struct { config *ini.File configPath string configBasePath string - configBase map[string]interface{} + configBase settings dataPath string localPath string - cssFile string - bsVersion int + cssClass string jellyfinLogin bool users []User invalidTokens []string @@ -82,10 +81,10 @@ type Languages struct { func (app *appContext) loadHTML(router *gin.Engine) { customPath := app.config.Section("files").Key("html_templates").MustString("") - templatePath := filepath.Join(app.localPath, "templates") + templatePath := filepath.Join(app.localPath, "html") htmlFiles, err := ioutil.ReadDir(templatePath) if err != nil { - app.err.Fatalf("Couldn't access template directory: \"%s\"", filepath.Join(app.localPath, "templates")) + app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath) return } loadFiles := make([]string, len(htmlFiles)) @@ -362,14 +361,6 @@ func start(asDaemon, firstCall bool) { app.debug.Printf("Loaded config file \"%s\"", app.configPath) - if app.config.Section("ui").Key("bs5").MustBool(false) { - app.cssFile = "bs5-jf.css" - app.bsVersion = 5 - } else { - app.cssFile = "bs4-jf.css" - app.bsVersion = 4 - } - app.debug.Println("Loading storage") app.storage.invite_path = app.config.Section("files").Key("invites").String() @@ -420,14 +411,15 @@ func start(asDaemon, firstCall bool) { json.Unmarshal(configBase, &app.configBase) themes := map[string]string{ - "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion), - "Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion), - "Custom CSS": "", + "Jellyfin (Dark)": "dark-theme", + "Default (Light)": "light-theme", + } + if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" { + app.config.Section("ui").Key("theme").SetValue("Default (Light)") } if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok { - app.cssFile = val + app.cssClass = val } - app.debug.Printf("Using css file \"%s\"", app.cssFile) secret, err := generateSecret(16) if err != nil { app.err.Fatal(err) @@ -559,7 +551,7 @@ func start(asDaemon, firstCall bool) { setGinLogger(router, debugMode) router.Use(gin.Recovery()) - router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.localPath, "static"), false))) + router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) app.loadHTML(router) router.NoRoute(app.NoRouteHandler) if debugMode { @@ -568,10 +560,13 @@ func start(asDaemon, firstCall bool) { } if !firstRun { router.GET("/", app.AdminPage) + router.GET("/accounts", app.AdminPage) + router.GET("/settings", app.AdminPage) + router.GET("/token/login", app.getTokenLogin) router.GET("/token/refresh", app.getTokenRefresh) router.POST("/newUser", app.NewUser) - router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.localPath, "static"), false))) + router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.localPath, "web"), false))) router.GET("/invite/:invCode", app.InviteProxy) if *SWAGGER { app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) diff --git a/models.go b/models.go index 9235317..d8d8381 100644 --- a/models.go +++ b/models.go @@ -127,3 +127,32 @@ type errorListDTO map[string]map[string]string type configDTO map[string]interface{} +// Below are for sending config + +type meta struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type setting struct { + Name string `json:"name"` + Description string `json:"description"` + Required bool `json:"required"` + RequiresRestart bool `json:"requires_restart"` + Type string `json:"type"` // Type (string, number, bool, etc.) + Value interface{} `json:"value"` + Options []string `json:"options,omitempty"` + DependsTrue string `json:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled. + DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue. +} + +type section struct { + Meta meta `json:"meta"` + Order []string `json:"order"` + Settings map[string]setting `json:"settings"` +} + +type settings struct { + Order []string `json:"order"` + Sections map[string]section `json:"sections"` +} diff --git a/package-lock.json b/package-lock.json index a595d43..a16ccc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,268 +1,86 @@ { - "name": "jellyfin-accounts", + "name": "jfa-go", "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/runtime": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", - "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", + "version": "7.12.5", + "resolved": "https://registry.npm.taobao.org/@babel/runtime/download/@babel/runtime-7.12.5.tgz?cache=0&sync_timestamp=1604441258461&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.12.5.tgz", + "integrity": "sha1-QQ5+SHRB4bNgwpvnFdhw2bmFiC4=", "requires": { "regenerator-runtime": "^0.13.4" } }, - "@babel/runtime-corejs3": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz", - "integrity": "sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==", + "@types/node": { + "version": "14.14.16", + "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz", + "integrity": "sha1-PMNR+NSBAd6t/tTJ5PEWBI1De0s=" + }, + "a17t": { + "version": "0.4.0", + "resolved": "https://registry.npm.taobao.org/a17t/download/a17t-0.4.0.tgz", + "integrity": "sha1-kDHaQ3iTT82Irj+/N3x5Lfy8Aw4=", "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" + "autoprefixer": "^10.0.2" + }, + "dependencies": { + "autoprefixer": { + "version": "10.1.0", + "resolved": "https://registry.npm.taobao.org/autoprefixer/download/autoprefixer-10.1.0.tgz?cache=0&sync_timestamp=1607411581276&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fautoprefixer%2Fdownload%2Fautoprefixer-10.1.0.tgz", + "integrity": "sha1-sZ/YUk7e+MhcnbO9sMmY3oThcvs=", + "requires": { + "browserslist": "^4.15.0", + "caniuse-lite": "^1.0.30001165", + "colorette": "^1.2.1", + "fraction.js": "^4.0.12", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + } + } } }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", - "requires": { - "@nodelib/fs.stat": "2.0.3", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" - }, - "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", - "requires": { - "@nodelib/fs.scandir": "2.1.3", - "fastq": "^1.6.0" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/jquery": { - "version": "3.5.3", - "resolved": "https://registry.npm.taobao.org/@types/jquery/download/@types/jquery-3.5.3.tgz?cache=0&sync_timestamp=1602524936372&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fjquery%2Fdownload%2F%40types%2Fjquery-3.5.3.tgz", - "integrity": "sha1-rcxkfkxnW9nrrn+5gOnKddWO6Mc=", - "requires": { - "@types/sizzle": "*" - } - }, - "@types/sizzle": { - "version": "2.3.2", - "resolved": "https://registry.npm.taobao.org/@types/sizzle/download/@types/sizzle-2.3.2.tgz", - "integrity": "sha1-qBG4wY4rq6t9VCszZYh64uTZ3kc=" - }, "abbrev": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "resolved": "https://registry.npm.taobao.org/abbrev/download/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" }, - "ajv": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", - "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "autoprefixer": { - "version": "9.8.5", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.5.tgz", - "integrity": "sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg==", - "requires": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001097", - "colorette": "^1.2.0", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - } - } + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npm.taobao.org/ansi-colors/download/ansi-colors-4.1.1.tgz", + "integrity": "sha1-y7muJWv3UK8eqzRPIpqif+lLo0g=" }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" - }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, - "bootstrap": { - "version": "5.0.0-alpha3", - "resolved": "https://registry.npm.taobao.org/bootstrap/download/bootstrap-5.0.0-alpha3.tgz?cache=0&sync_timestamp=1605115180548&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbootstrap%2Fdownload%2Fbootstrap-5.0.0-alpha3.tgz", - "integrity": "sha1-9au9OHQDg9a44EEhKu9eoiDn2qo=" - }, - "bootstrap4": { - "version": "npm:bootstrap@4.5.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", - "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" - }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "resolved": "https://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, "browserslist": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz", - "integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==", + "version": "4.16.0", + "resolved": "https://registry.npm.taobao.org/browserslist/download/browserslist-4.16.0.tgz?cache=0&sync_timestamp=1607665787496&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbrowserslist%2Fdownload%2Fbrowserslist-4.16.0.tgz", + "integrity": "sha1-QQJ3YnUAvjyyihv+A3WG++35SIs=", "requires": { - "caniuse-lite": "^1.0.30001093", - "electron-to-chromium": "^1.3.488", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" + "caniuse-lite": "^1.0.30001165", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.621", + "escalade": "^3.1.1", + "node-releases": "^1.1.67" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" - }, "camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -272,137 +90,55 @@ "upper-case": "^1.1.1" } }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, "caniuse-lite": { - "version": "1.0.30001100", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001100.tgz", - "integrity": "sha512-0eYdp1+wFCnMlCj2oudciuQn2B9xAFq3WpgpcBIZTxk/1HNA/O2YA7rpeYhnOqsqAJq1AHUgx6i1jtafg7m2zA==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } + "version": "1.0.30001170", + "resolved": "https://registry.npm.taobao.org/caniuse-lite/download/caniuse-lite-1.0.30001170.tgz?cache=0&sync_timestamp=1608444166670&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001170.tgz", + "integrity": "sha1-AIi/7MahRpSWnjkcwp1+tjYspqc=" }, "cheerio": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npm.taobao.org/cheerio/download/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha1-CUY21CWy6cD065GkbAVjDJoai/Y=", "requires": { "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", + "dom-serializer": "~0.1.1", "entities": "~1.1.1", "htmlparser2": "^3.9.1", - "lodash.assignin": "^4.0.9", - "lodash.bind": "^4.1.4", - "lodash.defaults": "^4.0.1", - "lodash.filter": "^4.4.0", - "lodash.flatten": "^4.2.0", - "lodash.foreach": "^4.3.0", - "lodash.map": "^4.4.0", - "lodash.merge": "^4.4.0", - "lodash.pick": "^4.2.1", - "lodash.reduce": "^4.4.0", - "lodash.reject": "^4.4.0", - "lodash.some": "^4.4.0" - } - }, - "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "lodash": "^4.15.0", + "parse5": "^3.0.1" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "clean-css": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "resolved": "https://registry.npm.taobao.org/clean-css/download/clean-css-4.2.3.tgz", + "integrity": "sha1-UHtd59l7SO5T2ErbAWD/YhY4D3g=", "requires": { "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" + } } }, - "clean-css-cli": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-4.3.0.tgz", - "integrity": "sha512-8GHZfr+mG3zB/Lgqrr27qHBFsPSn0fyEI3f2rIZpxPxUbn2J6A8xyyeBRVTW8duDuXigN0s80vsXiXJOEFIO5Q==", - "requires": { - "clean-css": "^4.2.1", - "commander": "2.x", - "glob": "7.x" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, "colorette": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } + "resolved": "https://registry.npm.taobao.org/colorette/download/colorette-1.2.1.tgz?cache=0&sync_timestamp=1593955937807&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolorette%2Fdownload%2Fcolorette-1.2.1.tgz", + "integrity": "sha1-TQuSEyXBT6+SYzCGpTbbbolWSxs=" }, "commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1607931421020&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz", + "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=" }, "concat-map": { "version": "0.0.1", @@ -411,51 +147,13 @@ }, "config-chain": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "resolved": "https://registry.npm.taobao.org/config-chain/download/config-chain-1.1.12.tgz", + "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" - }, - "core-js-pure": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", - "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", @@ -469,61 +167,18 @@ }, "css-what": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" + "resolved": "https://registry.npm.taobao.org/css-what/download/css-what-2.1.3.tgz?cache=0&sync_timestamp=1602570920759&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcss-what%2Fdownload%2Fcss-what-2.1.3.tgz", + "integrity": "sha1-ptdgRXM2X+dGhsPzEcVlE9iChfI=" }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "datauri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/datauri/-/datauri-2.0.0.tgz", - "integrity": "sha512-zS2HSf9pI5XPlNZgIqJg/wCJpecgU/HA6E/uv2EfaWnW1EiTGLfy/EexTIsC9c99yoCOTXlqeeWk4FkCSuO3/g==", - "requires": { - "image-size": "^0.7.3", - "mimer": "^1.0.0" - } - }, - "decamelize": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz", - "integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==", - "requires": { - "xregexp": "^4.2.4" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "dependency-graph": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz", - "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "requires": { - "path-type": "^4.0.0" - } + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npm.taobao.org/detect-node/download/detect-node-2.0.4.tgz", + "integrity": "sha1-AU7o+PZpxcWAI9pkuBecCDooxGw=" }, "dom-serializer": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "resolved": "https://registry.npm.taobao.org/dom-serializer/download/dom-serializer-0.1.1.tgz?cache=0&sync_timestamp=1607193128529&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-serializer%2Fdownload%2Fdom-serializer-0.1.1.tgz", + "integrity": "sha1-HsQFnihLq+027sKUHUqXChic58A=", "requires": { "domelementtype": "^1.3.0", "entities": "^1.1.1" @@ -531,13 +186,13 @@ }, "domelementtype": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-1.3.1.tgz?cache=0&sync_timestamp=1606866110836&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomelementtype%2Fdownload%2Fdomelementtype-1.3.1.tgz", + "integrity": "sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=" }, "domhandler": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-2.4.2.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-2.4.2.tgz", + "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=", "requires": { "domelementtype": "1" } @@ -551,19 +206,10 @@ "domelementtype": "1" } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "editorconfig": { "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "resolved": "https://registry.npm.taobao.org/editorconfig/download/editorconfig-0.15.3.tgz", + "integrity": "sha1-vvhMTnX7jcsM5c7o79UcFZmb78U=", "requires": { "commander": "^2.19.0", "lru-cache": "^4.1.5", @@ -572,165 +218,44 @@ } }, "electron-to-chromium": { - "version": "1.3.498", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.498.tgz", - "integrity": "sha512-W1hGwaQEU8j9su2jeAr3aabkPuuXw+j8t73eajGAkEJWbfWiwbxBwQN/8Qmv2qCy3uCDm2rOAaZneYQM8VGC4w==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "version": "1.3.633", + "resolved": "https://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.633.tgz?cache=0&sync_timestamp=1608750797791&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felectron-to-chromium%2Fdownload%2Felectron-to-chromium-1.3.633.tgz", + "integrity": "sha1-Ft1a7J3gOJTo0UodtM2oo2m5t/4=" }, "entities": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } + "resolved": "https://registry.npm.taobao.org/entities/download/entities-1.1.2.tgz?cache=0&sync_timestamp=1602897347667&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fentities%2Fdownload%2Fentities-1.1.2.tgz", + "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" }, "esbuild": { - "version": "0.7.8", - "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.7.8.tgz", - "integrity": "sha1-e6wXcYHv4MiTmnsVU2aCr+MtG3M=" + "version": "0.7.22", + "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.7.22.tgz", + "integrity": "sha1-kUm5A/gSi3xFp1QEbCQZnXa74I4=" }, "escalade": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", - "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==" + "version": "3.1.1", + "resolved": "https://registry.npm.taobao.org/escalade/download/escalade-3.1.1.tgz?cache=0&sync_timestamp=1602567437752&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fescalade%2Fdownload%2Fescalade-3.1.1.tgz", + "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA=" }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fs-extra": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", - "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^1.0.0" - } + "fraction.js": { + "version": "4.0.13", + "resolved": "https://registry.npm.taobao.org/fraction.js/download/fraction.js-4.0.13.tgz", + "integrity": "sha1-PBwxX6FrNchf/6lXJaNvpynGnf4=" }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "optional": true - }, "get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } + "resolved": "https://registry.npm.taobao.org/get-caller-file/download/get-caller-file-2.0.5.tgz", + "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=" }, "glob": { "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "resolved": "https://registry.npm.taobao.org/glob/download/glob-7.1.6.tgz", + "integrity": "sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -740,81 +265,29 @@ "path-is-absolute": "^1.0.0" } }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, "he": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + "resolved": "https://registry.npm.taobao.org/he/download/he-1.2.0.tgz", + "integrity": "sha1-hK5l+n6vsWX922FWauFLrwVmTw8=" }, "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/html-minifier/download/html-minifier-4.0.0.tgz", + "integrity": "sha1-zKmq2LzhF14C4XqMM+RtiYiIn1Y=", "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - }, - "dependencies": { - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" - } + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" } }, "htmlparser2": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "resolved": "https://registry.npm.taobao.org/htmlparser2/download/htmlparser2-3.10.1.tgz?cache=0&sync_timestamp=1607396725165&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhtmlparser2%2Fdownload%2Fhtmlparser2-3.10.1.tgz", + "integrity": "sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=", "requires": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", @@ -822,51 +295,18 @@ "entities": "^1.1.1", "inherits": "^2.0.1", "readable-stream": "^3.1.1" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" - }, - "image-size": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", - "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==" - }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", - "requires": { - "import-from": "^2.1.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", - "requires": { - "resolve-from": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-3.6.0.tgz", + "integrity": "sha1-M3u9o63AcGvT4CRCaihtS0sskZg=", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "inflight": { @@ -880,248 +320,72 @@ }, "inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" }, "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + "version": "1.3.7", + "resolved": "https://registry.npm.taobao.org/ini/download/ini-1.3.7.tgz?cache=0&sync_timestamp=1607907977465&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fini%2Fdownload%2Fini-1.3.7.tgz", + "integrity": "sha1-oJNj4ZEZcuoW16iFEAXYTPCamoQ=" }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, "is-glob": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-4.0.1.tgz", + "integrity": "sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=", "requires": { "is-extglob": "^2.1.1" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, "js-beautify": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.11.0.tgz", - "integrity": "sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A==", + "version": "1.13.0", + "resolved": "https://registry.npm.taobao.org/js-beautify/download/js-beautify-1.13.0.tgz?cache=0&sync_timestamp=1597939451698&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjs-beautify%2Fdownload%2Fjs-beautify-1.13.0.tgz", + "integrity": "sha1-oFbV06z9SRhUmq46sDn588Ue67I=", "requires": { "config-chain": "^1.1.12", "editorconfig": "^0.15.3", "glob": "^7.1.3", - "mkdirp": "~1.0.3", - "nopt": "^4.0.3" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", - "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^1.0.0" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "mkdirp": "^1.0.4", + "nopt": "^5.0.0" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npm.taobao.org/nopt/download/nopt-5.0.0.tgz?cache=0&sync_timestamp=1597649987736&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnopt%2Fdownload%2Fnopt-5.0.0.tgz", + "integrity": "sha1-UwlCu1ilEvzK/lP+IQ8TolNV3Ig=", + "requires": { + "abbrev": "1" + } + } } }, "juice": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/juice/-/juice-5.2.0.tgz", - "integrity": "sha512-0l6GZmT3efexyaaay3SchKT5kG311N59TEFP5lfvEy0nz9SNqjx311plJ3b4jze7arsmDsiHQLh/xnAuk0HFTQ==", + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/juice/download/juice-7.0.0.tgz", + "integrity": "sha1-UJvtatu25Luqf7+trE4ug+jIm6M=", "requires": { - "cheerio": "^0.22.0", - "commander": "^2.15.1", - "cross-spawn": "^6.0.5", - "deep-extend": "^0.6.0", - "mensch": "^0.3.3", + "cheerio": "^1.0.0-rc.3", + "commander": "^5.1.0", + "mensch": "^0.3.4", "slick": "^1.12.2", - "web-resource-inliner": "^4.3.1" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" + "web-resource-inliner": "^5.0.0" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npm.taobao.org/commander/download/commander-5.1.0.tgz?cache=0&sync_timestamp=1607931421020&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-5.1.0.tgz", + "integrity": "sha1-Rqu9FlL44Fm92u+Zu9yyrZzxea4=" + } } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" - }, - "lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" - }, - "lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" - }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" - }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" - }, - "lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" - }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" }, "lower-case": { "version": "1.1.4", @@ -1130,8 +394,8 @@ }, "lru-cache": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "resolved": "https://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.5.tgz?cache=0&sync_timestamp=1594427614275&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flru-cache%2Fdownload%2Flru-cache-4.1.5.tgz", + "integrity": "sha1-i75Q6oW+1ZvJ4z3KuCNe6bz0Q80=", "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -1139,606 +403,955 @@ }, "mensch": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", - "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "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" - } - }, - "mimer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mimer/-/mimer-1.1.0.tgz", - "integrity": "sha512-y9dVfy2uiycQvDNiAYW6zp49ZhFlXDMr5wfdOiMbdzGM/0N5LNR6HTUn3un+WUQcM0koaw8FMTG1bt5EnHJdvQ==" + "resolved": "https://registry.npm.taobao.org/mensch/download/mensch-0.3.4.tgz", + "integrity": "sha1-dw+RtGyxbqWyBO5zV2jD8MSR/s0=" }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "resolved": "https://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "^1.1.7" } }, "mjml": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.6.3.tgz", - "integrity": "sha512-nkkdB5lqDi2qHDOnAWCHXk5RmQ2phK6XB2eokpCW5KzK1A5Cbu61/Zw37VJU5gDvoab/ZD3FHBUj8O6alu4c4g==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml/download/mjml-4.8.0.tgz", + "integrity": "sha1-JVJsGuVOevJTSAs5KmJCuNfbCkM=", "requires": { - "mjml-accordion": "4.6.3", - "mjml-body": "4.6.3", - "mjml-button": "4.6.3", - "mjml-carousel": "4.6.3", - "mjml-cli": "4.6.3", - "mjml-column": "4.6.3", - "mjml-core": "4.6.3", - "mjml-divider": "4.6.3", - "mjml-group": "4.6.3", - "mjml-head": "4.6.3", - "mjml-head-attributes": "4.6.3", - "mjml-head-breakpoint": "4.6.3", - "mjml-head-font": "4.6.3", - "mjml-head-preview": "4.6.3", - "mjml-head-style": "4.6.3", - "mjml-head-title": "4.6.3", - "mjml-hero": "4.6.3", - "mjml-image": "4.6.3", - "mjml-migrate": "4.6.3", - "mjml-navbar": "4.6.3", - "mjml-raw": "4.6.3", - "mjml-section": "4.6.3", - "mjml-social": "4.6.3", - "mjml-spacer": "4.6.3", - "mjml-table": "4.6.3", - "mjml-text": "4.6.3", - "mjml-validator": "4.6.3", - "mjml-wrapper": "4.6.3" + "@babel/runtime": "^7.8.7", + "mjml-accordion": "4.8.0", + "mjml-body": "4.8.0", + "mjml-button": "4.8.0", + "mjml-carousel": "4.8.0", + "mjml-cli": "4.8.0", + "mjml-column": "4.8.0", + "mjml-core": "4.8.0", + "mjml-divider": "4.8.0", + "mjml-group": "4.8.0", + "mjml-head": "4.8.0", + "mjml-head-attributes": "4.8.0", + "mjml-head-breakpoint": "4.8.0", + "mjml-head-font": "4.8.0", + "mjml-head-html-attributes": "4.8.0", + "mjml-head-preview": "4.8.0", + "mjml-head-style": "4.8.0", + "mjml-head-title": "4.8.0", + "mjml-hero": "4.8.0", + "mjml-image": "4.8.0", + "mjml-migrate": "4.8.0", + "mjml-navbar": "4.8.0", + "mjml-raw": "4.8.0", + "mjml-section": "4.8.0", + "mjml-social": "4.8.0", + "mjml-spacer": "4.8.0", + "mjml-table": "4.8.0", + "mjml-text": "4.8.0", + "mjml-validator": "4.8.0", + "mjml-wrapper": "4.8.0" } }, "mjml-accordion": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.6.3.tgz", - "integrity": "sha512-fpX6Xc2xH++2xsixUv9EzIMz48wsxpEaRohh9IfUPo+q2OoA2eBwnIQFNmBLV3hvkQ5p/ESFP1kUDoC/Ey4diw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-accordion/download/mjml-accordion-4.8.0.tgz", + "integrity": "sha1-btO0jTYQcE5aBSfRD/+Y6UjJjeg=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-body": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.6.3.tgz", - "integrity": "sha512-nnYc2e/vjCzHNQ9h8FFYMyxM6QoJJL27AHUMghiUS1Hi4Pje65Ehisy5hWn9BF7kHyTb2PWg8kDM0qtBUpOfAA==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-body/download/mjml-body-4.8.0.tgz", + "integrity": "sha1-kMxPjRkzGZ64kb5703bYwwGnZKw=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-button": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.6.3.tgz", - "integrity": "sha512-P8C3xo1kkB96pP3ajsw/AHzorpN5xaA57CKm/A0mjyqGG43VZZS6NVW4cGEcOx7YNMT3DXTqsT6IlWBhNaFesQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-button/download/mjml-button-4.8.0.tgz", + "integrity": "sha1-yh4ncf5Q0FnbC89ezrtHM6yC5LU=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-carousel": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.6.3.tgz", - "integrity": "sha512-ITWVOdWAcBpKuMKiv1fcHwIhM7AM2b96l5+fbV8+NU7CD5YQutwgiWLvP4N6NVqr+WeH6fTajWD8//zr73B0Ew==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-carousel/download/mjml-carousel-4.8.0.tgz", + "integrity": "sha1-VTgkjBlolICENCwk+ljoaeKQulc=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-cli": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.6.3.tgz", - "integrity": "sha512-dmjkuPi3iK340gM6m1ruIrIB50R54MJbpZJ9JIJHrgQwzumSEJGqCVxQIqlChteBzYJ4vTnZFZiBR6ak8x8YSg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-cli/download/mjml-cli-4.8.0.tgz", + "integrity": "sha1-raSarndmcNIQv6ioYI4o0KsCAos=", "requires": { "@babel/runtime": "^7.8.7", "chokidar": "^3.0.0", "glob": "^7.1.1", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", "lodash": "^4.17.15", - "mjml-core": "4.6.3", - "mjml-migrate": "4.6.3", - "mjml-parser-xml": "4.6.3", - "mjml-validator": "4.6.3", - "yargs": "^13.3.0" + "mjml-core": "4.8.0", + "mjml-migrate": "4.8.0", + "mjml-parser-xml": "4.8.0", + "mjml-validator": "4.8.0", + "yargs": "^16.1.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "cliui": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-5.0.0.tgz", + "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792436886&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz", + "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "color-convert": "^2.0.1" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-3.1.1.tgz", + "integrity": "sha1-xV7PAhheJGklk5kxDBc84xIzsUI=", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-2.1.0.tgz?cache=0&sync_timestamp=1593261253160&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-2.1.0.tgz", + "integrity": "sha1-MPpAyef+B9vIlWeM0ocCTeokHdk=" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npm.taobao.org/braces/download/braces-3.0.2.tgz", + "integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=", + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-3.4.3.tgz?cache=0&sync_timestamp=1602585326091&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchokidar%2Fdownload%2Fchokidar-3.4.3.tgz", + "integrity": "sha1-wd84IxRI5FykrFiObHlXO6alfVs=", + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-7.0.4.tgz?cache=0&sync_timestamp=1604880226973&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcliui%2Fdownload%2Fcliui-7.0.4.tgz", + "integrity": "sha1-oCZe5lVHb8gHrqnfPfjfd4OAi08=", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz", + "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz", + "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=" }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "8.0.0", + "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-8.0.0.tgz?cache=0&sync_timestamp=1603212200036&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Femoji-regex%2Fdownload%2Femoji-regex-8.0.0.tgz", + "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=" }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-7.0.1.tgz", + "integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=", "requires": { - "locate-path": "^3.0.0" + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-2.1.3.tgz?cache=0&sync_timestamp=1608033993741&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffsevents%2Fdownload%2Ffsevents-2.1.3.tgz", + "integrity": "sha1-+3OHA66NL5/pAMM4Nt3r7ouX8j4=", + "optional": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-5.1.1.tgz?cache=0&sync_timestamp=1584835651798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglob-parent%2Fdownload%2Fglob-parent-5.1.1.tgz", + "integrity": "sha1-tsHvQXxOVmPqSY8cRa+saRa7wik=", + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-2.1.0.tgz", + "integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=", + "requires": { + "binary-extensions": "^2.0.0" } }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/is-number/download/is-number-7.0.0.tgz", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=" + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-3.5.0.tgz?cache=0&sync_timestamp=1602584469356&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freaddirp%2Fdownload%2Freaddirp-3.5.0.tgz", + "integrity": "sha1-m6dMAZsV02UnjS6Ru4xI17TULJ4=", "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "picomatch": "^2.2.1" } }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-4.2.0.tgz", + "integrity": "sha1-lSGCxGzHssMT0VluYjmSvRY7crU=", "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-6.0.0.tgz", + "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-5.0.1.tgz", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "requires": { + "is-number": "^7.0.0" } }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-7.0.0.tgz?cache=0&sync_timestamp=1589250991473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwrap-ansi%2Fdownload%2Fwrap-ansi-7.0.0.tgz", + "integrity": "sha1-Z+FFz/UQpqaYS98RUpEdadLrnkM=", "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npm.taobao.org/y18n/download/y18n-5.0.5.tgz", + "integrity": "sha1-h2nsCNA7HqLfJQCs71YXQ7u5qxg=" + }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npm.taobao.org/yargs/download/yargs-16.2.0.tgz?cache=0&sync_timestamp=1607208079987&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-16.2.0.tgz", + "integrity": "sha1-HIK/D2tqZur85+8w43b0mhJHf2Y=", "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-20.2.4.tgz", + "integrity": "sha1-tCiQ8UVmeW+Fro46JSkNIF8VSlQ=" } } }, "mjml-column": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.6.3.tgz", - "integrity": "sha512-LySMYdqMhEE49a6d7M0KOFmH6VYIQDLi8eReW7jozBU1v2mD1Leudm8VHXLc6wLJrUpHD5vNkjQ7ZTErUSwDWg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-column/download/mjml-column-4.8.0.tgz", + "integrity": "sha1-icwApZI61wRoUfODhsFuVScJB2w=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-core": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.6.3.tgz", - "integrity": "sha512-DWKh3wwO6y3nPkX29LmHuIynamwb3iDGk/WPu03yfDLYJAeJdVPJ35YCNV2Ap0WFmbSEihXjsGjMmz/g8OTRIg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-core/download/mjml-core-4.8.0.tgz", + "integrity": "sha1-sPLZoMhBeBIZHEEbHaoj/dirhMc=", "requires": { "@babel/runtime": "^7.8.7", - "html-minifier": "^3.5.3", + "cheerio": "1.0.0-rc.3", + "detect-node": "2.0.4", + "html-minifier": "^4.0.0", "js-beautify": "^1.6.14", - "juice": "^5.2.0", + "juice": "^7.0.0", "lodash": "^4.17.15", - "mjml-migrate": "4.6.3", - "mjml-parser-xml": "4.6.3", - "mjml-validator": "4.6.3" + "mjml-migrate": "4.8.0", + "mjml-parser-xml": "4.8.0", + "mjml-validator": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-divider": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.6.3.tgz", - "integrity": "sha512-yzErPHvnGRr+3Sc3i0AWBMfBLVXPGW6X6WjpntD8uFTDWzR70VHRV54CqnZv0pv7M4PUuMKT/AsJsFsXc1iw3A==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-divider/download/mjml-divider-4.8.0.tgz", + "integrity": "sha1-kYXaSDzPAx8kgNXaZee5W2Pcwds=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-group": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.6.3.tgz", - "integrity": "sha512-GI3SFDEY00xYXvYyjYHgTpmWnBKq2VKRxaOjybhZTmmHlIqWuJw/U5In2IhIJqjMEvbIaZBxcfUREIelogfRRQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-group/download/mjml-group-4.8.0.tgz", + "integrity": "sha1-wPCh5EXIpfFISTefP8CLX5KsshM=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.6.3.tgz", - "integrity": "sha512-db8d0/f8Li8JYIDvkrCzY23KH2lTQ5AqjTUwrOoG15eeSzlx8ugg5f/UaJvNkQsIXUDeVvGpwAz8gXG5x+1YNw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head/download/mjml-head-4.8.0.tgz", + "integrity": "sha1-1o7a5bfm7dxa/IXaHO27mBfzipQ=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-attributes": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.6.3.tgz", - "integrity": "sha512-7AS92bKSo664DQ26b0l8D/yAJ6yNsMbf2wX3rwH9S+hS3+Gyoi29LGtxjk+jlwWOjAsDo/VTZMhMHMquFEUn8w==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-attributes/download/mjml-head-attributes-4.8.0.tgz", + "integrity": "sha1-8AVmW8ybAb2UZK6m4KCXw5i/nT0=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-breakpoint": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.6.3.tgz", - "integrity": "sha512-La/H4pVyfjwbnq4r/JEBK+3ZMkjJYJGiGuDvdLHBP/cOBRfrtxEGEfMHiJzWAxCayrAd+kw/WlJyI34iLnB0kg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-breakpoint/download/mjml-head-breakpoint-4.8.0.tgz", + "integrity": "sha1-MVAnfQXTBIeqkX/iTk89ERYn5ZU=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-font": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.6.3.tgz", - "integrity": "sha512-H0TlnHrN+erBCwHw7BO57BquxLjj+/YleCzRWZIOmwjmtlq0ZfnzCyrH0cAOfxdM/VHBQcRGchQsiZfuACbtPA==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-font/download/mjml-head-font-4.8.0.tgz", + "integrity": "sha1-46/bsMdcPPy405Ok9KvQgLXaTZU=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } + } + }, + "mjml-head-html-attributes": { + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-html-attributes/download/mjml-head-html-attributes-4.8.0.tgz", + "integrity": "sha1-q5YdLDYxRod5PSzCRHCtfdFcVjw=", + "requires": { + "@babel/runtime": "^7.8.7", + "lodash": "^4.17.15", + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-preview": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.6.3.tgz", - "integrity": "sha512-pdSYu7T5dSeAVSQlafN9939hza3C4vy617xIyNGqTJEByvFJGCbo7lPEG0okJIua8+zVRQTzuOOqG2nhWqmrSw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-preview/download/mjml-head-preview-4.8.0.tgz", + "integrity": "sha1-ow+RDdJGzm8CdO6oIHFzGDh4wjY=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-style": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.6.3.tgz", - "integrity": "sha512-hnEyVeoYGNyme5maqJaeIkhrcj5j5pzOhUB3p2Ul3ENZfwhdUKyQyvqNAlqo5pVjn7c9otE4NLMOM6uv5lozqg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-style/download/mjml-head-style-4.8.0.tgz", + "integrity": "sha1-MULyxEmvCbdwIMOGeBrCZOb9tv4=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-head-title": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.6.3.tgz", - "integrity": "sha512-PU4lrT7Ci1O0CgKGE3nnQka0k3uuy8BR+O+qip1euHVev5C/UOr1RsMvOpwaVzVUwDfJkY07VafJIooN0/Ubpw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-head-title/download/mjml-head-title-4.8.0.tgz", + "integrity": "sha1-xUYxY3tPD0xWgnHNI+E33LtiZQc=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-hero": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.6.3.tgz", - "integrity": "sha512-Q74Hnwb8OtoghIZWzR1jrsmN6SfrBGoNH0f865SccrXqzZFnWyBygoHi2Cszi7OiF9LXF1NvOb8Q2bUmUsyIIw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-hero/download/mjml-hero-4.8.0.tgz", + "integrity": "sha1-k0QBYEt8pk+Q5Ui+1eYXGzzsLnk=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-image": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.6.3.tgz", - "integrity": "sha512-/CXBAuqRQ+JJEcIp7SNxnjVSLOmqAnnVsIypMJbENV7hS8o9OnaBPIgjAGB7x+8OVadXfR3RAO5b/XWOrO0CMw==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-image/download/mjml-image-4.8.0.tgz", + "integrity": "sha1-Jo3jucQZxgXbr5ozydipg1+ioqQ=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-migrate": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.6.3.tgz", - "integrity": "sha512-5vbL2n6Dx9M3vRItgPXm4E0LZw89b4YB4HtoxMquueAIgOkVIMja0FY48wPpIwow2wdKh+b31B6ise3Vmduczg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-migrate/download/mjml-migrate-4.8.0.tgz", + "integrity": "sha1-mR63m1zAUpYNayvLSW42NgicB2E=", "requires": { "@babel/runtime": "^7.8.7", - "commander": "^2.11.0", "js-beautify": "^1.6.14", "lodash": "^4.17.15", - "mjml-core": "4.5.0", - "mjml-parser-xml": "4.5.0" + "mjml-core": "4.8.0", + "mjml-parser-xml": "4.8.0", + "yargs": "^16.1.0" }, "dependencies": { - "mjml-core": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.5.0.tgz", - "integrity": "sha512-/9M4Dt0f7zaVzP7OJZlqaVWS1ijkoEoF6dKKeiXqRQ3oTvyiTEATHGA5xeifsU4dOzDFhdfFbu54LJOmHdPlVw==", + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npm.taobao.org/ansi-regex/download/ansi-regex-5.0.0.tgz", + "integrity": "sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792436886&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz", + "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", "requires": { - "babel-runtime": "^6.26.0", - "html-minifier": "^3.5.3", - "js-beautify": "^1.6.14", - "juice": "^5.2.0", - "lodash": "^4.17.15", - "mjml-migrate": "4.5.0", - "mjml-parser-xml": "4.5.0", - "mjml-validator": "4.5.0" - }, - "dependencies": { - "mjml-migrate": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.5.0.tgz", - "integrity": "sha512-zzAKSrGpF+OVoa3GHVS7O2A4WZPLBV/Nrc80MGaLS4hhBbuj2WeUdaugVlIMXRRuhQ+nP+k0fZSM8tonDDjd2w==", - "requires": { - "babel-runtime": "^6.26.0", - "commander": "^2.11.0", - "js-beautify": "^1.6.14", - "lodash": "^4.17.15", - "mjml-core": "4.5.0", - "mjml-parser-xml": "4.5.0" - } - } + "color-convert": "^2.0.1" } }, - "mjml-migrate": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.5.0.tgz", - "integrity": "sha512-zzAKSrGpF+OVoa3GHVS7O2A4WZPLBV/Nrc80MGaLS4hhBbuj2WeUdaugVlIMXRRuhQ+nP+k0fZSM8tonDDjd2w==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npm.taobao.org/cliui/download/cliui-7.0.4.tgz?cache=0&sync_timestamp=1604880226973&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcliui%2Fdownload%2Fcliui-7.0.4.tgz", + "integrity": "sha1-oCZe5lVHb8gHrqnfPfjfd4OAi08=", "requires": { - "babel-runtime": "^6.26.0", - "commander": "^2.11.0", - "js-beautify": "^1.6.14", - "lodash": "^4.17.15", - "mjml-core": "4.5.0", - "mjml-parser-xml": "4.5.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "mjml-parser-xml": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.5.0.tgz", - "integrity": "sha512-9NK9TnkDSJ0M7lMv1vuGjZumi1rqdv4Iwr9rBDpBPUvfv9ay7MoJrQjK28cu6PKcamOK6CHAFXihlV9Q6fbYaA==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz", + "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", "requires": { - "babel-runtime": "^6.26.0", - "htmlparser2": "^3.9.2", - "lodash": "^4.17.15" + "color-name": "~1.1.4" } }, - "mjml-validator": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.5.0.tgz", - "integrity": "sha512-Qbyf/VCk3U8ViLCu+VCwGYZVQaJAw5brKW/aXeRRHb10LdhaCF1S0JNIiNyutfnqn92QWdzYt6W+cbcEZIKa9A==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz", + "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npm.taobao.org/emoji-regex/download/emoji-regex-8.0.0.tgz?cache=0&sync_timestamp=1603212200036&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Femoji-regex%2Fdownload%2Femoji-regex-8.0.0.tgz", + "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=" + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npm.taobao.org/string-width/download/string-width-4.2.0.tgz", + "integrity": "sha1-lSGCxGzHssMT0VluYjmSvRY7crU=", "requires": { - "babel-runtime": "^6.26.0", - "lodash": "^4.17.15", - "warning": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npm.taobao.org/strip-ansi/download/strip-ansi-6.0.0.tgz", + "integrity": "sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-7.0.0.tgz?cache=0&sync_timestamp=1589250991473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwrap-ansi%2Fdownload%2Fwrap-ansi-7.0.0.tgz", + "integrity": "sha1-Z+FFz/UQpqaYS98RUpEdadLrnkM=", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npm.taobao.org/y18n/download/y18n-5.0.5.tgz", + "integrity": "sha1-h2nsCNA7HqLfJQCs71YXQ7u5qxg=" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npm.taobao.org/yargs/download/yargs-16.2.0.tgz?cache=0&sync_timestamp=1607208079987&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs%2Fdownload%2Fyargs-16.2.0.tgz", + "integrity": "sha1-HIK/D2tqZur85+8w43b0mhJHf2Y=", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-20.2.4.tgz", + "integrity": "sha1-tCiQ8UVmeW+Fro46JSkNIF8VSlQ=" } } }, "mjml-navbar": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.6.3.tgz", - "integrity": "sha512-Kqalb/YTicBuCwpUdMPQ+U/Sc6pOe35QWpixcEpIKK204+0+B/zx5vxo9EDDwOBaz4P9PbjSrH+8qHNKp7/i5w==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-navbar/download/mjml-navbar-4.8.0.tgz", + "integrity": "sha1-aygLhgPICjQ+JkJkFDQpbZISju4=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-parser-xml": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.6.3.tgz", - "integrity": "sha512-f4eptqzxAPM3YWvKg16QxiAlcR13jr5RX7x/JCSrV37Vx/Lr9tFe2utI2qEgsXvr2MQPud/fZ69XBCBgxPUvqQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-parser-xml/download/mjml-parser-xml-4.8.0.tgz", + "integrity": "sha1-IfUO4LgD2zWUiGGuthckyN3uS/4=", "requires": { "@babel/runtime": "^7.8.7", - "htmlparser2": "^3.9.2", + "detect-node": "2.0.4", + "htmlparser2": "^4.1.0", "lodash": "^4.17.15" + }, + "dependencies": { + "dom-serializer": { + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/dom-serializer/download/dom-serializer-1.2.0.tgz?cache=0&sync_timestamp=1607193128529&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-serializer%2Fdownload%2Fdom-serializer-1.2.0.tgz", + "integrity": "sha1-NDPZE2rrPGJ5gdqjhfx/MtJ8SPE=", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-4.0.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-4.0.0.tgz", + "integrity": "sha1-Aep4Id6ZbYX2kCnoH6hzwhgzCY4=", + "requires": { + "domelementtype": "^2.1.0" + } + } + } + }, + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-2.1.0.tgz?cache=0&sync_timestamp=1606866110836&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomelementtype%2Fdownload%2Fdomelementtype-2.1.0.tgz", + "integrity": "sha1-qFHAgKbRw9lDRK7RUdmfZp7fWF4=" + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-3.3.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-3.3.0.tgz", + "integrity": "sha1-bbfqRuRhfrFc+HXfaLK4UkzgA3o=", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.4.4", + "resolved": "https://registry.npm.taobao.org/domutils/download/domutils-2.4.4.tgz?cache=0&sync_timestamp=1607393197963&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomutils%2Fdownload%2Fdomutils-2.4.4.tgz", + "integrity": "sha1-KCc5xLFQ0CLTRpl5c2mq2NGbu9M=", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-4.0.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-4.0.0.tgz", + "integrity": "sha1-Aep4Id6ZbYX2kCnoH6hzwhgzCY4=", + "requires": { + "domelementtype": "^2.1.0" + } + } + } + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/entities/download/entities-2.1.0.tgz?cache=0&sync_timestamp=1602897347667&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fentities%2Fdownload%2Fentities-2.1.0.tgz", + "integrity": "sha1-mS0xKc999ocLlsV4WMJJoSD4uLU=" + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npm.taobao.org/htmlparser2/download/htmlparser2-4.1.0.tgz?cache=0&sync_timestamp=1607396725165&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhtmlparser2%2Fdownload%2Fhtmlparser2-4.1.0.tgz", + "integrity": "sha1-mk7xYfLkYl6/ffvmwKL1LRilnng=", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-raw": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.6.3.tgz", - "integrity": "sha512-Q3mU1VFg1iXM8088AAkmQa4I8yHBVjd3nd5ZyLq/s/Ye08NMlu1tgkxnbk8GH8lcVYOFha4BWAo+OhloLOQsYQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-raw/download/mjml-raw-4.8.0.tgz", + "integrity": "sha1-7w3inEIZLkraoCP2A5uO/+bmNmY=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-section": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.6.3.tgz", - "integrity": "sha512-pZqj7ZmCrpEkAwfGPAF+z6LqWuK3L+3vbCQP7DCeHfvinDufY7M2LE3ususXh9EGJ/RSuFd9q8lxHAtoUZ6OSg==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-section/download/mjml-section-4.8.0.tgz", + "integrity": "sha1-Dv81lcp8ZaK83rP3HU7MulnhHSw=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-social": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.6.3.tgz", - "integrity": "sha512-9RHSmjbIc0F4ps4mcKinFu0T08ECx6o7yzxyG2t2mBiI4DyKEhdR/5iaudTYifAt4a49jv8s/RVXFqjWLPP7RQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-social/download/mjml-social-4.8.0.tgz", + "integrity": "sha1-U3TXwSUo+NlvIBZVmca6E/aJ1uQ=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-spacer": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.6.3.tgz", - "integrity": "sha512-UhrzNFrK5QJ3AuO3lDzDlf7pTU07PVbwL2qMN8aqS81+wvoWPG3EvFj9JT8KBtAP1FXk+JsqdEqvinOJEQ9Z9Q==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-spacer/download/mjml-spacer-4.8.0.tgz", + "integrity": "sha1-qscNNUNN5fl5voBdJbxEL8Ll3Vg=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-table": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.6.3.tgz", - "integrity": "sha512-4Og9PJISmbNTU/lFXfWiCFm2bCbVCAEC+7EYyAt/S90KxJci8UikQOfFMupKUabR6DDooIT9X9NFLomOLGsJNQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-table/download/mjml-table-4.8.0.tgz", + "integrity": "sha1-M8AGZ78CbN7KDRO4f+drHHzSO/U=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-text": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.6.3.tgz", - "integrity": "sha512-G8P8KgY1rQeABc0+4EOJPo0riHQHFv4pWJnwxHZmK9eHFUMbnD7z5Qh/oSmvNXp5Vsf07l6QdGQN6NF7L0yU0Q==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-text/download/mjml-text-4.8.0.tgz", + "integrity": "sha1-LOvEn/iz+rMcgEd2E8MuwqDPWS8=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3" + "mjml-core": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mjml-validator": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.6.3.tgz", - "integrity": "sha512-cLAP7UpI6pXNHjvFYkyDDEc01stNX8rhtDFXTkZ+WDAzE0xUiFrIkiKqiHmYNl9Z5/FCak9+1H65eDYejW2igQ==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-validator/download/mjml-validator-4.8.0.tgz", + "integrity": "sha1-eUlj223s1ASLAH43Wd5O49R5SgU=", "requires": { - "@babel/runtime": "^7.8.7", - "lodash": "^4.17.15", - "warning": "^3.0.0" + "@babel/runtime": "^7.8.7" } }, "mjml-wrapper": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.6.3.tgz", - "integrity": "sha512-+TseKSGEzKXlx/E3T41WJg4YlL4qFp/kO/cZ6jau1cBYIxdxnR2f8RLh4wd8b39cnVGQJIYC2MO9H8S/uq+M1A==", + "version": "4.8.0", + "resolved": "https://registry.npm.taobao.org/mjml-wrapper/download/mjml-wrapper-4.8.0.tgz", + "integrity": "sha1-dYR5lY+47GbIsWKyIpJAeoNsVIo=", "requires": { "@babel/runtime": "^7.8.7", "lodash": "^4.17.15", - "mjml-core": "4.6.3", - "mjml-section": "4.6.3" + "mjml-core": "4.8.0", + "mjml-section": "4.8.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597335994883&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz", + "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=" + } } }, "mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "resolved": "https://registry.npm.taobao.org/mkdirp/download/mkdirp-1.0.4.tgz", + "integrity": "sha1-PrXtYmInVteaXw4qIh3+utdcL34=" }, "no-case": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "resolved": "https://registry.npm.taobao.org/no-case/download/no-case-2.3.2.tgz?cache=0&sync_timestamp=1606869671099&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fno-case%2Fdownload%2Fno-case-2.3.2.tgz", + "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=", "requires": { "lower-case": "^1.1.1" } }, - "node-releases": { - "version": "1.1.59", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.59.tgz", - "integrity": "sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw==" + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npm.taobao.org/node-fetch/download/node-fetch-2.6.1.tgz?cache=0&sync_timestamp=1599311968037&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnode-fetch%2Fdownload%2Fnode-fetch-2.6.1.tgz", + "integrity": "sha1-BFvTI2Mfdu0uK1VXM5RBa2OaAFI=" }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } + "node-releases": { + "version": "1.1.67", + "resolved": "https://registry.npm.taobao.org/node-releases/download/node-releases-1.1.67.tgz?cache=0&sync_timestamp=1605581679207&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnode-releases%2Fdownload%2Fnode-releases-1.1.67.tgz", + "integrity": "sha1-KOv8zNC6pqrY6NTY/ky8Sa4jnBI=" }, "normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz", + "integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=" }, "normalize-range": { "version": "0.1.2", @@ -1747,22 +1360,12 @@ }, "nth-check": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz?cache=0&sync_timestamp=1606861164153&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnth-check%2Fdownload%2Fnth-check-1.0.2.tgz", + "integrity": "sha1-sr0pXDfj3VijvwcAN2Zjuk2c8Fw=", "requires": { "boolbase": "~1.0.0" } }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1771,46 +1374,6 @@ "wrappy": "1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, "param-case": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", @@ -1819,154 +1382,28 @@ "no-case": "^2.2.0" } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npm.taobao.org/parse5/download/parse5-3.0.3.tgz?cache=0&sync_timestamp=1595850937464&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fparse5%2Fdownload%2Fparse5-3.0.3.tgz", + "integrity": "sha1-BC95L/3TaFFVHPTp4Gazh0q0W1w=", "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "@types/node": "*" } }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "picomatch": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "postcss": { - "version": "7.0.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", - "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "postcss-cli": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-7.1.1.tgz", - "integrity": "sha512-bYQy5ydAQJKCMSpvaMg0ThPBeGYqhQXumjbFOmWnL4u65CYXQ16RfS6afGQpit0dGv/fNzxbdDtx8dkqOhhIbg==", - "requires": { - "chalk": "^4.0.0", - "chokidar": "^3.3.0", - "dependency-graph": "^0.9.0", - "fs-extra": "^9.0.0", - "get-stdin": "^7.0.0", - "globby": "^11.0.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "postcss-reporter": "^6.0.0", - "pretty-hrtime": "^1.0.3", - "read-cache": "^1.0.0", - "yargs": "^15.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "postcss-load-config": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", - "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", - "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - } - }, - "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", - "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - } + "resolved": "https://registry.npm.taobao.org/picomatch/download/picomatch-2.2.2.tgz", + "integrity": "sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=" }, "postcss-value-parser": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + "resolved": "https://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-4.1.0.tgz?cache=0&sync_timestamp=1588083303810&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-value-parser%2Fdownload%2Fpostcss-value-parser-4.1.0.tgz", + "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=" }, "proto-list": { "version": "1.2.4", @@ -1978,429 +1415,170 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", - "requires": { - "pify": "^2.3.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + "version": "0.13.7", + "resolved": "https://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.13.7.tgz?cache=0&sync_timestamp=1595456023687&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fregenerator-runtime%2Fdownload%2Fregenerator-runtime-0.13.7.tgz", + "integrity": "sha1-ysLazIoepnX+qrrriugziYrkb1U=" }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } + "remixicon": { + "version": "2.5.0", + "resolved": "https://registry.npm.taobao.org/remixicon/download/remixicon-2.5.0.tgz", + "integrity": "sha1-teJFiUoVUKojeT+V2s6tv5atGkE=" }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" - }, "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "version": "5.1.2", + "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" }, "semver": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz?cache=0&sync_timestamp=1606851857382&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=" }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" - }, "slick": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "requires": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, "typescript": { - "version": "4.0.3", - "resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.0.3.tgz?cache=0&sync_timestamp=1600584904815&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.0.3.tgz", - "integrity": "sha1-FTu9Ro7wdyXB35x36LRT+NNqu6U=" + "version": "4.1.3", + "resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.1.3.tgz?cache=0&sync_timestamp=1609830171931&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.1.3.tgz", + "integrity": "sha1-UZ1YK9lMugz4k0x9joRn5HP1O7c=" }, "uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - } - } - }, - "universalify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" + "version": "3.12.3", + "resolved": "https://registry.npm.taobao.org/uglify-js/download/uglify-js-3.12.3.tgz?cache=0&sync_timestamp=1609162276146&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fuglify-js%2Fdownload%2Fuglify-js-3.12.3.tgz", + "integrity": "sha1-uybEq+DmjFXpd2vKm+2ZpN9z+s8=" }, "upper-case": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, "valid-data-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-2.0.0.tgz", - "integrity": "sha512-dyCZnv3aCey7yfTgIqdZanKl7xWAEEKCbgmR7SKqyK6QT/Z07ROactrgD1eA37C69ODRj7rNOjzKWVPh0EUjBA==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } + "version": "3.0.1", + "resolved": "https://registry.npm.taobao.org/valid-data-url/download/valid-data-url-3.0.1.tgz", + "integrity": "sha1-gmwXROcbVjLoR90V29Rbn7OKo08=" }, "web-resource-inliner": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.3.4.tgz", - "integrity": "sha512-agVAgRhOOi4GVlvKK34oM23tDgH8390HfLnZY2HZl8OFBwKNvUJkH7t89AT2iluQP8w9VHAAKX6Z8EN7/9tqKA==", + "version": "5.0.0", + "resolved": "https://registry.npm.taobao.org/web-resource-inliner/download/web-resource-inliner-5.0.0.tgz", + "integrity": "sha1-rDDbgJaTHyCnwbOt5U/0ROLiD3s=", "requires": { - "async": "^3.1.0", - "chalk": "^2.4.2", - "datauri": "^2.0.0", + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", "htmlparser2": "^4.0.0", - "lodash.unescape": "^4.0.1", - "request": "^2.88.0", - "safer-buffer": "^2.1.2", - "valid-data-url": "^2.0.0", - "xtend": "^4.0.2" + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" }, "dependencies": { "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "version": "1.2.0", + "resolved": "https://registry.npm.taobao.org/dom-serializer/download/dom-serializer-1.2.0.tgz?cache=0&sync_timestamp=1607193128529&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdom-serializer%2Fdownload%2Fdom-serializer-1.2.0.tgz", + "integrity": "sha1-NDPZE2rrPGJ5gdqjhfx/MtJ8SPE=", "requires": { "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-4.0.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-4.0.0.tgz", + "integrity": "sha1-Aep4Id6ZbYX2kCnoH6hzwhgzCY4=", + "requires": { + "domelementtype": "^2.1.0" + } + } } }, "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/domelementtype/download/domelementtype-2.1.0.tgz?cache=0&sync_timestamp=1606866110836&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomelementtype%2Fdownload%2Fdomelementtype-2.1.0.tgz", + "integrity": "sha1-qFHAgKbRw9lDRK7RUdmfZp7fWF4=" }, "domhandler": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", - "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", + "version": "3.3.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-3.3.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-3.3.0.tgz", + "integrity": "sha1-bbfqRuRhfrFc+HXfaLK4UkzgA3o=", "requires": { "domelementtype": "^2.0.1" } }, "domutils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.1.0.tgz", - "integrity": "sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg==", + "version": "2.4.4", + "resolved": "https://registry.npm.taobao.org/domutils/download/domutils-2.4.4.tgz?cache=0&sync_timestamp=1607393197963&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomutils%2Fdownload%2Fdomutils-2.4.4.tgz", + "integrity": "sha1-KCc5xLFQ0CLTRpl5c2mq2NGbu9M=", "requires": { - "dom-serializer": "^0.2.1", + "dom-serializer": "^1.0.1", "domelementtype": "^2.0.1", - "domhandler": "^3.0.0" + "domhandler": "^4.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npm.taobao.org/domhandler/download/domhandler-4.0.0.tgz?cache=0&sync_timestamp=1606872524192&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomhandler%2Fdownload%2Fdomhandler-4.0.0.tgz", + "integrity": "sha1-Aep4Id6ZbYX2kCnoH6hzwhgzCY4=", + "requires": { + "domelementtype": "^2.1.0" + } + } } }, "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" + "version": "2.1.0", + "resolved": "https://registry.npm.taobao.org/entities/download/entities-2.1.0.tgz?cache=0&sync_timestamp=1602897347667&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fentities%2Fdownload%2Fentities-2.1.0.tgz", + "integrity": "sha1-mS0xKc999ocLlsV4WMJJoSD4uLU=" + }, + "escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npm.taobao.org/escape-goat/download/escape-goat-3.0.0.tgz", + "integrity": "sha1-6LX7ZYVT/oo8SVnDFsbruMhCsZw=" }, "htmlparser2": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "resolved": "https://registry.npm.taobao.org/htmlparser2/download/htmlparser2-4.1.0.tgz?cache=0&sync_timestamp=1607396725165&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhtmlparser2%2Fdownload%2Fhtmlparser2-4.1.0.tgz", + "integrity": "sha1-mk7xYfLkYl6/ffvmwKL1LRilnng=", "requires": { "domelementtype": "^2.0.1", "domhandler": "^3.0.0", "domutils": "^2.0.0", "entities": "^2.0.0" } - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "mime": { + "version": "2.4.7", + "resolved": "https://registry.npm.taobao.org/mime/download/mime-2.4.7.tgz?cache=0&sync_timestamp=1608084213475&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.4.7.tgz", + "integrity": "sha1-lirtm+DtGckf19wuzl1/TompDXQ=" } } }, @@ -2409,62 +1587,10 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "xregexp": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", - "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", - "requires": { - "@babel/runtime-corejs3": "^7.8.3" - } - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.0.tgz", - "integrity": "sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^3.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - } - } } } } diff --git a/package.json b/package.json index 9eeda05..f6f6476 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "jellyfin-accounts", + "name": "jfa-go", "version": "1.0.0", "description": "This is only used for grabbing scss build dependencies, and isn't a real package.", "main": "index.js", @@ -8,24 +8,21 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/hrfee/jellyfin-accounts.git" + "url": "git+https://github.com/hrfee/jfa-go.git" }, "author": "", "license": "ISC", "bugs": { - "url": "https://github.com/hrfee/jellyfin-accounts/issues" + "url": "https://github.com/hrfee/jfa-go/issues" }, - "homepage": "https://github.com/hrfee/jellyfin-accounts#readme", + "homepage": "https://github.com/hrfee/jfa-go#readme", "dependencies": { - "@types/jquery": "^3.5.3", - "autoprefixer": "^9.8.5", - "bootstrap": "^5.0.0-alpha3", - "bootstrap4": "npm:bootstrap@^4.5.0", - "clean-css-cli": "^4.3.0", + "a17t": "^0.4.0", "esbuild": "^0.7.8", "lodash": "^4.17.19", - "mjml": "^4.6.3", - "postcss-cli": "^7.1.1", + "mjml": "^4.8.0", + "remixicon": "^2.5.0", "typescript": "^4.0.3" - } + }, + "devDependencies": {} } diff --git a/scss/README.md b/scss/README.md deleted file mode 100644 index 6928dc7..0000000 --- a/scss/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## SCSS - -* `bs<4/5>-jf.scss` contains the source for the customizations to bootstrap. To customize the UI, you can make modifications to this file and then compile it. - -**Note**: It is assumed that Bootstrap 5 is installed in `../../node_modules/bootstrap` relative to itself, and Bootstrap 4 in `../../node_modules/bootstrap4`. - -* Compilation requires dev dependencies (`poetry update`), bootstrap and some extra npm packages. -* If you're buildings from source, you can simply run `poetry run task compile-css` before building to automatically get deps and compile CSS. -* If you are creating custom css, run `poetry run task get-npm-deps` to only install the necessary dependencies. Follow along with the commands `scss/compile.py` runs to build your css and then set `custom_css` in your config as the path to your minified css and change the `theme` option to `Custom CSS`. - diff --git a/scss/base.scss b/scss/base.scss deleted file mode 100644 index 23edce9..0000000 --- a/scss/base.scss +++ /dev/null @@ -1,126 +0,0 @@ -.pageContainer { - margin: 5% 30% 5% 30%; -} -@media (max-width: 1900px) { - .pageContainer { - margin: 5% 20% 5% 20%; - } -} -@media (max-width: 1100px) { - .pageContainer { - margin: 2%; - } -} -h1 { - /*margin: 20%;*/ - margin-bottom: 5%; -} -.tabGroup { - /*margin: 20%;*/ - margin-bottom: 5%; - margin-top: 5%; -} -.linkForm { - /*margin: 20%;*/ - margin-top: 5%; - margin-bottom: 5%; -} -.contactBox { - /*margin: 20%;*/ - margin-top: 5%; - color: grey; -} -.circle { - /*margin-left: 1rem; - width: 1rem; - height: 1rem; - border-radius: 50%; - z-index: 5000;*/ --webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); --moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); - -o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); - transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeInCubic */ -} -.smooth-transition { --webkit-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); --moz-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); - -o-transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); - transition: all 300ms cubic-bezier(0.550, 0.055, 0.675, 0.190); /* easeincubic */ -} -.rotated { - transform: rotate(180deg); --webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); --moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); - -o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); - transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */ -} - -.not-rotated { - transform: rotate(0deg); --webkit-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); --moz-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); - -o-transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); - transition: all 150ms cubic-bezier(0.770, 0.000, 0.175, 1.000); /* easeInOutQuart */ -} - -.invite-link { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: auto; -} - -.settingIcon { - margin-left: 0.2rem; -} - -body.modal-open { - overflow: hidden; -} - -@mixin white-text { - &, &:visited, &:hover, &:active { - font-style: inherit; - color: inherit; - font-size: inherit; - text-decoration: none; - font-variant: inherit; - font-weight: inherit; - line-height: inherit; - font-family: inherit; - } -} - -%white-text { - @include white-text; -} - -%link-unstyled { - @include white-text; - background-color: transparent; - margin-right: 0.5rem; -} - -.text-button { - @extend %link-unstyled; -} - -.text-button:hover { - @extend %link-unstyled; -} - -.nl { - @extend %link-unstyled; -} - -.nl:hover { - @extend %white-text; -} - -.unfocused { - display: none; -} - -.text-monospace { - font-family: monospace; -} diff --git a/scss/bs4/bs4-jf.scss b/scss/bs4/bs4-jf.scss deleted file mode 100644 index a9d470c..0000000 --- a/scss/bs4/bs4-jf.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import "../jf-pre.scss"; -@import "../../node_modules/bootstrap4/scss/bootstrap"; -@import "../jf-post.scss"; -@import "../base.scss"; diff --git a/scss/bs4/bs4.scss b/scss/bs4/bs4.scss deleted file mode 100644 index bee395c..0000000 --- a/scss/bs4/bs4.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "../../node_modules/bootstrap4/scss/bootstrap"; - -.icon-button { - color: $text-muted; -} - -.icon-button:hover { - color: inherit; -} - -.icon-button:active { - color: $text-muted; -} - -.nav-link:hover { - background-color: $list-group-hover-bg; -} - -@import "../base.scss"; diff --git a/scss/bs5/bs5-jf.scss b/scss/bs5/bs5-jf.scss deleted file mode 100644 index 6b4a353..0000000 --- a/scss/bs5/bs5-jf.scss +++ /dev/null @@ -1,7 +0,0 @@ -.btn-close { - filter: invert(80%); -} -@import "../jf-pre.scss"; -@import "../../node_modules/bootstrap/scss/bootstrap"; -@import "../jf-post.scss"; -@import "../base.scss"; diff --git a/scss/bs5/bs5.scss b/scss/bs5/bs5.scss deleted file mode 100644 index 0784b32..0000000 --- a/scss/bs5/bs5.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "../../node_modules/bootstrap/scss/bootstrap"; - -.icon-button { - color: $text-muted; -} - -.icon-button:hover { - color: inherit; -} - -.icon-button:active { - color: $text-muted; -} - -.nav-link:hover { - background-color: $list-group-hover-bg; -} - -@import "../base.scss"; diff --git a/scss/compile.py b/scss/compile.py deleted file mode 100755 index 9d26a2f..0000000 --- a/scss/compile.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -import sass -import subprocess -import shutil -import os -from pathlib import Path - - -def runcmd(cmd): - if os.name == "nt": - return subprocess.check_output(cmd, shell=True) - proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) - return proc.communicate() - - -local_path = Path(__file__).resolve().parent - -for bsv in [d for d in local_path.iterdir() if "bs" in d.name]: - scss = [(bsv / f"{bsv.name}-jf.scss"), (bsv / f"{bsv.name}.scss")] - css = [(bsv / f"{bsv.name}-jf.css"), (bsv / f"{bsv.name}.css")] - min_css = [ - (bsv.parents[1] / "data" / "static" / f"{bsv.name}-jf.css"), - (bsv.parents[1] / "data" / "static" / f"{bsv.name}.css"), - ] - for i in range(2): - with open(css[i], "w") as f: - f.write( - sass.compile( - filename=str(scss[i].resolve()), - output_style="expanded", - precision=6, - omit_source_map_url=True, - ) - ) - if css[i].exists(): - print(f"{scss[i].name}: Compiled.") - # postcss only excepts forwards slashes? weird. - cssPath = str(css[i].resolve()) - if os.name == "nt": - cssPath = cssPath.replace("\\", "/") - runcmd(f"npx postcss {cssPath} --replace --use autoprefixer") - print(f"{scss[i].name}: Prefixed.") - runcmd( - f"npx cleancss --level 1 --format breakWith=lf --output {str(min_css[i].resolve())} {str(css[i].resolve())}" - ) - if min_css[i].exists(): - print( - f"{scss[i].name}: Minified and copied to {str(min_css[i].resolve())}." - ) diff --git a/scss/jf-post.scss b/scss/jf-post.scss deleted file mode 100644 index 5a82ff1..0000000 --- a/scss/jf-post.scss +++ /dev/null @@ -1,41 +0,0 @@ -.btn-primary, .btn-outline-primary:hover, .btn-outline-primary:active { - color: $jf-text-bold; -} - -.close { - color: $jf-text-secondary; -} - -.close:hover, .close:active { - color: $jf-text-primary; -} - -.icon-button { - color: $text-muted; -} - -.icon-button:hover { - color: $jf-text-bold; -} - -.icon-button:active { - color: $text-muted; -} - -.text-bright { - color: $jf-text-bold; -} - -.list-group-item-danger { - color: $jf-text-bold; - background-color: $danger; -} - -.list-group-item-success { - color: $jf-text-bold; - background-color: $success; -} - -.nav-link:hover { - background-color: $jf-blue-hover; -} diff --git a/scss/jf-pre.scss b/scss/jf-pre.scss deleted file mode 100644 index 205e77d..0000000 --- a/scss/jf-pre.scss +++ /dev/null @@ -1,101 +0,0 @@ -$jf-blue: rgb(0, 164, 220); -$jf-blue-hover: rgba(0, 164, 220, 0.2); -$jf-blue-focus: rgb(12, 176, 232); -$jf-blue-light: #4bb3dd; - -$jf-red: rgb(204, 0, 0); -$jf-red-light: #e12026; -$jf-yellower: #ffc107; -$jf-yellow: #e1b222; -$jf-orange: #ff870f; -$jf-green: #6fbd45; -$jf-green-dark: #008040; - - -$jf-black: #101010; // 16 16 16 -$jf-gray-90: #202020; // 32 32 32 -$jf-gray-80: #242424; // jf-card 36 36 36 -$jf-gray-70: #292929; // jf-input 41 41 41 -$jf-gray-60: #303030; // jf-button 48 48 48 -$jf-gray-50: #383838; // jf-button-focus 56 56 56 -$jf-text-bold: rgba(255, 255, 255, 0.87); -$jf-text-primary: rgba(255, 255, 255, 0.8); -$jf-text-secondary: rgb(153, 153, 153); - -$primary: $jf-blue; -$secondary: $jf-gray-50; -$success: $jf-green-dark; -$danger: $jf-red-light; -$light: $jf-text-primary; -$dark: $jf-gray-90; -$info: $jf-yellow; -$warning: $jf-yellower; - - - -$enable-gradients: false; -$enable-shadows: false; - -$enable-rounded: false; -$body-bg: $jf-black; -$body-color: $jf-text-primary; -$border-color: $jf-gray-60; -$component-active-color: $jf-text-bold; -$component-active-bg: $jf-blue-focus; -$text-muted: $jf-text-secondary; -$link-color: $jf-blue-focus; -$btn-link-disabled-color: $jf-text-secondary; -$input-bg: $jf-gray-90; -$input-color: $jf-text-primary; -$input-focus-bg: $jf-gray-60; -$input-focus-border-color: $jf-blue-focus; -$input-disabled-bg: $jf-gray-70; -input:disabled { - color: $text-muted; -} -$input-border-color: $jf-gray-60; -$input-placeholder-color: $text-muted; - -$form-check-input-bg: $jf-gray-60; -$form-check-input-border: $jf-gray-50; -$form-check-input-checked-color: $jf-blue-focus; -$form-check-input-checked-bg-color: $jf-blue-hover; - -$input-group-addon-bg: $input-bg; - -$form-select-disabled-color: $jf-text-secondary; -$form-select-disabled-bg: $input-disabled-bg; -$form-select-indicator-color: $jf-gray-50; - -$card-bg: $jf-gray-80; -$card-border-color: null; - -$tooltip-color: $jf-text-bold; -$tooltip-bg: $jf-gray-50; - -$modal-content-bg: $jf-gray-80; -$modal-content-border-color: $jf-gray-50; -$modal-header-border-color: null; -$modal-footer-border-color: null; - -$list-group-bg: $card-bg; -$list-group-border-color: $jf-gray-50; -$list-group-hover-bg: $jf-blue-hover; -$list-group-active-bg: $jf-blue-focus; -$list-group-action-color: $jf-text-primary; -$list-group-action-hover-color: $jf-text-bold; -$list-group-action-active-color: $jf-text-bold; -$list-group-action-active-bg: $jf-blue-focus; - -// idk why but i had to put these above and below the import -.list-group-item-danger { - color: $jf-text-bold; - background-color: $danger; -} - -.list-group-item-success { - color: $jf-text-bold; - background-color: $success; -} - -// @import "../../node_modules/bootstrap/scss/bootstrap"; diff --git a/data/static/android-chrome-192x192.png b/static/android-chrome-192x192.png similarity index 100% rename from data/static/android-chrome-192x192.png rename to static/android-chrome-192x192.png diff --git a/data/static/android-chrome-512x512.png b/static/android-chrome-512x512.png similarity index 100% rename from data/static/android-chrome-512x512.png rename to static/android-chrome-512x512.png diff --git a/data/static/apple-touch-icon.png b/static/apple-touch-icon.png similarity index 100% rename from data/static/apple-touch-icon.png rename to static/apple-touch-icon.png diff --git a/static/banner.svg b/static/banner.svg new file mode 100644 index 0000000..627a830 --- /dev/null +++ b/static/banner.svg @@ -0,0 +1,283 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/static/browserconfig.xml b/static/browserconfig.xml similarity index 100% rename from data/static/browserconfig.xml rename to static/browserconfig.xml diff --git a/data/static/favicon-16x16.png b/static/favicon-16x16.png similarity index 100% rename from data/static/favicon-16x16.png rename to static/favicon-16x16.png diff --git a/data/static/favicon-32x32.png b/static/favicon-32x32.png similarity index 100% rename from data/static/favicon-32x32.png rename to static/favicon-32x32.png diff --git a/data/static/favicon.ico b/static/favicon.ico similarity index 100% rename from data/static/favicon.ico rename to static/favicon.ico diff --git a/data/static/mstile-150x150.png b/static/mstile-150x150.png similarity index 100% rename from data/static/mstile-150x150.png rename to static/mstile-150x150.png diff --git a/data/static/safari-pinned-tab.svg b/static/safari-pinned-tab.svg similarity index 100% rename from data/static/safari-pinned-tab.svg rename to static/safari-pinned-tab.svg diff --git a/data/static/site.webmanifest b/static/site.webmanifest similarity index 100% rename from data/static/site.webmanifest rename to static/site.webmanifest diff --git a/ts/accounts.ts b/ts/accounts.ts deleted file mode 100644 index 5955537..0000000 --- a/ts/accounts.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { checkCheckboxes, populateUsers, populateRadios, changeEmail, validateEmail } from "./modules/accounts.js"; -import { _post, _get, _delete, rmAttr, addAttr, createEl } from "./modules/common.js"; -import { populateProfiles } from "./modules/settings.js"; -import { Focus, Unfocus, storeDefaults } from "./modules/admin.js"; - -interface aWindow extends Window { - changeEmail(icon: HTMLElement, id: string): void; -} - -declare var window: aWindow; - -window.changeEmail = changeEmail; - -(document.getElementById('selectAll')).onclick = function (): void { - const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]'); - for (let i = 0; i < checkboxes.length; i++) { - checkboxes[i].checked = (this).checked; - } - checkCheckboxes(); -}; - -(document.getElementById('deleteModalNotify')).onclick = function (): void { - const textbox: HTMLElement = document.getElementById('deleteModalReasonBox'); - if ((this).checked) { - Focus(textbox); - } else { - Unfocus(textbox); - } -}; - -(document.getElementById('accountsTabDelete')).onclick = function (): void { - const deleteButton = this as HTMLButtonElement; - const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); - let selected: Array = new Array(checkboxes.length); - for (let i = 0; i < checkboxes.length; i++) { - selected[i] = checkboxes[i].id.replace("select_", ""); - } - let title = " user"; - let msg = "Notify user"; - if (selected.length > 1) { - title += "s"; - msg += "s"; - } - title = `Delete ${selected.length} ${title}`; - msg += " of account deletion"; - - document.getElementById('deleteModalTitle').textContent = title; - const dmNotify = document.getElementById('deleteModalNotify') as HTMLInputElement; - dmNotify.checked = false; - document.getElementById('deleteModalNotifyLabel').textContent = msg; - const dmReason = document.getElementById('deleteModalReason') as HTMLTextAreaElement; - dmReason.value = ''; - Unfocus(document.getElementById('deleteModalReasonBox')); - const dmSend = document.getElementById('deleteModalSend') as HTMLButtonElement; - dmSend.textContent = 'Delete'; - dmSend.onclick = function (): void { - const button = this as HTMLButtonElement; - const send = { - 'users': selected, - 'notify': dmNotify.checked, - 'reason': dmReason.value - }; - _delete("/users", send, function (): void { - if (this.readyState == 4) { - if (this.status == 500) { - if ("error" in this.reponse) { - button.textContent = 'Failed'; - } else { - button.textContent = 'Partial fail (check console)'; - console.log(this.response); - } - setTimeout((): void => { - Unfocus(deleteButton); - window.Modals.delete.hide(); - }, 4000); - } else { - Unfocus(deleteButton); - window.Modals.delete.hide() - } - populateUsers(); - checkCheckboxes(); - } - }); - }; - window.Modals.delete.show(); -}; - -(document.getElementById('selectAll')).checked = false; - -(document.getElementById('accountsTabSetDefaults')).onclick = function (): void { - const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); - let userIDs: Array = new Array(checkboxes.length); - for (let i = 0; i < checkboxes.length; i++){ - userIDs[i] = checkboxes[i].id.replace("select_", ""); - } - if (userIDs.length == 0) { - return; - } - populateRadios(); - let userString = 'user'; - if (userIDs.length > 1) { - userString += "s"; - } - populateProfiles(true); - const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement; - profileSelect.textContent = ''; - for (let i = 0; i < window.availableProfiles.length; i++) { - profileSelect.innerHTML += ` - - `; - } - document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`; - document.getElementById('userDefaultsDescription').textContent = ` - Apply settings from an existing profile or source settings from a user. - `; - document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`; - Focus(document.getElementById('defaultsSourceSection')); - (document.getElementById('defaultsSource')).value = 'profile'; - Focus(document.getElementById('profileSelectBox')); - Unfocus(document.getElementById('defaultUserRadiosBox')); - Unfocus(document.getElementById('newProfileBox')); - document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs); - window.Modals.userDefaults.show(); -}; - -(document.getElementById('defaultsSource')).addEventListener('change', function (): void { - const radios = document.getElementById('defaultUserRadiosBox'); - const profileBox = document.getElementById('profileSelectBox'); - if (this.value == 'profile') { - Unfocus(radios); - Focus(profileBox); - } else { - Unfocus(profileBox); - Focus(radios); - } -}); - -(document.getElementById('newUserCreate')).onclick = function (): void { - const button = this as HTMLButtonElement; - const ogText = button.textContent; - button.innerHTML = ` - Creating... - `; - const email: string = (document.getElementById('newUserEmail')).value; - var username: string = email; - if (document.getElementById('newUserName') != null) { - username = (document.getElementById('newUserName')).value; - } - const password: string = (document.getElementById('newUserPassword')).value; - if (!validateEmail(email) && email != "") { - return; - } - const send = { - 'username': username, - 'password': password, - 'email': email - }; - _post("/users", send, function (): void { - if (this.readyState == 4) { - rmAttr(button, 'btn-primary'); - if (this.status == 200) { - addAttr(button, 'btn-success'); - button.textContent = 'Success'; - setTimeout((): void => { - rmAttr(button, 'btn-success'); - addAttr(button, 'btn-primary'); - button.textContent = ogText; - window.Modals.newUser.hide(); - }, 1000); - populateUsers(); - } else { - addAttr(button, 'btn-danger'); - if ("error" in this.response) { - button.textContent = this.response["error"]; - } else { - button.textContent = 'Failed'; - } - setTimeout((): void => { - rmAttr(button, 'btn-danger'); - addAttr(button, 'btn-primary'); - button.textContent = ogText; - }, 2000); - populateUsers(); - } - } - }); -}; - -(document.getElementById('accountsTabAddUser')).onclick = function (): void { - (document.getElementById('newUserEmail')).value = ''; - (document.getElementById('newUserPassword')).value = ''; - if (document.getElementById('newUserName') != null) { - (document.getElementById('newUserName')).value = ''; - } - window.Modals.newUser.show(); -}; diff --git a/ts/admin.ts b/ts/admin.ts index 33eb546..f44a4a4 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -1,124 +1,99 @@ -import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js"; -import { Focus, Unfocus } from "./modules/admin.js"; -import { toggleCSS } from "./modules/animation.js"; -import { populateUsers, checkCheckboxes } from "./modules/accounts.js"; -import { generateInvites, addOptions, checkDuration } from "./modules/invites.js"; -import { showSetting, openSettings } from "./modules/settings.js"; -import { BS4 } from "./modules/bs4.js"; -import { BS5 } from "./modules/bs5.js"; -import "./accounts.js"; -import "./settings.js"; +import { toggleTheme, loadTheme } from "./modules/theme.js"; +import { Modal } from "./modules/modal.js"; +import { Tabs } from "./modules/tabs.js"; +import { inviteList, createInvite } from "./modules/invites.js"; +import { accountsList } from "./modules/accounts.js"; +import { settingsList } from "./modules/settings.js"; +import { ProfileEditor } from "./modules/profiles.js"; +import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; -interface aWindow extends Window { - toClipboard(str: string): void; -} +loadTheme(); +(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; -declare var window: aWindow; +window.animationEvent = whichAnimationEvent(); -interface TabSwitcher { - els: Array; - tabButtons: Array; - focus: (el: number) => void; - invites: () => void; - accounts: () => void; - settings: () => void; -} +window.token = ""; -const tabs: TabSwitcher = { - els: [document.getElementById('invitesTab') as HTMLDivElement, document.getElementById('accountsTab') as HTMLDivElement, document.getElementById('settingsTab') as HTMLDivElement], - tabButtons: [document.getElementById('invitesTabButton') as HTMLAnchorElement, document.getElementById('accountsTabButton') as HTMLAnchorElement, document.getElementById('settingsTabButton') as HTMLAnchorElement], - focus: (el: number): void => { - for (let i = 0; i < tabs.els.length; i++) { - if (i == el) { - Focus(tabs.els[i]); - addAttr(tabs.tabButtons[i], "active"); - } else { - Unfocus(tabs.els[i]); - rmAttr(tabs.tabButtons[i], "active"); - } - } - }, - invites: (): void => tabs.focus(0), - accounts: (): void => { - populateUsers(); - (document.getElementById('selectAll') as HTMLInputElement).checked = false; - checkCheckboxes(); - tabs.focus(1); - }, - settings: (): void => openSettings(document.getElementById('settingsSections'), document.getElementById('settingsContent'), (): void => { - window.BS.triggerTooltips(); - showSetting("ui"); - tabs.focus(2); - }) -}; +window.availableProfiles = window.availableProfiles || []; -window.bsVersion = window.bs5 ? 5 : 4 +// load modals +(() => { + window.modals = {} as Modals; -if (window.bs5) { - window.BS = new BS5; -} else { - window.BS = new BS4; - window.BS.Compat(); -} + window.modals.login = new Modal(document.getElementById('modal-login'), true); -window.Modals = {} as BSModals; + window.modals.addUser = new Modal(document.getElementById('modal-add-user')); -window.Modals.login = window.BS.newModal('login'); -window.Modals.userDefaults = window.BS.newModal('userDefaults'); -window.Modals.users = window.BS.newModal('users'); -window.Modals.restart = window.BS.newModal('restartModal'); -window.Modals.refresh = window.BS.newModal('refreshModal'); -window.Modals.about = window.BS.newModal('aboutModal'); -window.Modals.delete = window.BS.newModal('deleteModal'); -window.Modals.newUser = window.BS.newModal('newUserModal'); + window.modals.about = new Modal(document.getElementById('modal-about')); + (document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle; -tabs.tabButtons[0].onclick = tabs.invites; -tabs.tabButtons[1].onclick = tabs.accounts; -tabs.tabButtons[2].onclick = tabs.settings; + window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user')); -tabs.invites(); + window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user')); -// Predefined colors for the theme button. -var buttonColor: string = "custom"; -if (window.cssFile.includes("jf")) { - buttonColor = "rgb(255,255,255)"; -} else if (window.cssFile == ("bs" + window.bsVersion + ".css")) { - buttonColor = "rgb(16,16,16)"; -} + window.modals.settingsRestart = new Modal(document.getElementById('modal-restart')); -if (buttonColor != "custom") { - const switchButton = document.createElement('button') as HTMLButtonElement; - switchButton.classList.add('btn', 'btn-secondary'); - switchButton.innerHTML = ` - Theme - - `; - switchButton.onclick = (): void => toggleCSS(document.getElementById('fakeButton')); - document.getElementById('headerButtons').appendChild(switchButton); -} + window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh')); -var availableProfiles: Array; + window.modals.ombiDefaults = new Modal(document.getElementById('modal-ombi-defaults')); + document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close); -window["token"] = ""; + window.modals.profiles = new Modal(document.getElementById("modal-user-profiles")); -window.toClipboard = (str: string): void => { - const el = document.createElement('textarea') as HTMLTextAreaElement; - el.value = str; - el.readOnly = true; - el.style.position = "absolute"; - el.style.left = "-9999px"; - document.body.appendChild(el); - const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; - el.select(); - document.execCommand("copy"); - document.body.removeChild(el); - if (selected) { - document.getSelection().removeAllRanges(); - document.getSelection().addRange(selected); + window.modals.addProfile = new Modal(document.getElementById("modal-add-profile")); +})(); + +var inviteCreator = new createInvite(); +var accounts = new accountsList(); + +window.invites = new inviteList(); + +var settings = new settingsList(); + +var profiles = new ProfileEditor(); + +window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); + +/*const modifySettingsSource = function () { + const profile = document.getElementById('radio-use-profile') as HTMLInputElement; + const user = document.getElementById('radio-use-user') as HTMLInputElement; + const profileSelect = document.getElementById('modify-user-profiles') as HTMLDivElement; + const userSelect = document.getElementById('modify-user-users') as HTMLDivElement; + (user.nextElementSibling as HTMLSpanElement).classList.toggle('!normal'); + (user.nextElementSibling as HTMLSpanElement).classList.toggle('!high'); + (profile.nextElementSibling as HTMLSpanElement).classList.toggle('!normal'); + (profile.nextElementSibling as HTMLSpanElement).classList.toggle('!high'); + profileSelect.classList.toggle('unfocused'); + userSelect.classList.toggle('unfocused'); +}*/ + +// load tabs +window.tabs = new Tabs(); +window.tabs.addTab("invites", null, window.invites.reload); +window.tabs.addTab("accounts", null, accounts.reload); +window.tabs.addTab("settings", null, settings.reload); + +for (let tab of ["invites", "accounts", "settings"]) { + if (window.location.pathname == "/" + tab) { + window.tabs.switch(tab, true); } } -function login(username: string, password: string, modal: boolean, button?: HTMLButtonElement, run?: (arg0: number) => void): void { +if (window.location.pathname == "/") { + window.tabs.switch("invites", true); +} + +document.addEventListener("tab-change", (event: CustomEvent) => { + let tab = "/" + event.detail; + if (tab == "/invites") { + if (window.location.pathname == "/") { + tab = "/"; + } else { tab = "../"; } + } + window.history.replaceState("", "Admin - jfa-go", tab); +}); + +function login(username: string, password: string, run?: (state?: number) => void) { const req = new XMLHttpRequest(); req.responseType = 'json'; let url = window.URLBase; @@ -135,77 +110,63 @@ function login(username: string, password: string, modal: boolean, button?: HTML req.onreadystatechange = function (): void { if (this.readyState == 4) { if (this.status != 200) { - let errorMsg = this.response["error"]; + let errorMsg = "Connection error."; + if (this.response) { + errorMsg = this.response["error"]; + } if (!errorMsg) { errorMsg = "Unknown error"; } - if (modal) { - button.disabled = false; - button.textContent = errorMsg; - addAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - setTimeout((): void => { - addAttr(button, "btn-primary"); - rmAttr(button, "btn-danger"); - button.textContent = "Login"; - }, 4000); + if (!refresh) { + window.notifications.customError("loginError", errorMsg); } else { - window.Modals.login.show(); + window.modals.login.show(); } } else { const data = this.response; window.token = data["token"]; - generateInvites(); - setInterval((): void => generateInvites(), 60 * 1000); - addOptions(30, document.getElementById('days') as HTMLSelectElement); - addOptions(24, document.getElementById('hours') as HTMLSelectElement); - const minutes = document.getElementById('minutes') as HTMLSelectElement; - addOptions(59, minutes); - minutes.value = "30"; - checkDuration(); - if (modal) { - window.Modals.login.hide(); + window.modals.login.close(); + setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000); + const currentTab = window.tabs.current; + switch (currentTab) { + case "invites": + window.invites.reload(); + break; + case "accounts": + accounts.reload(); + break; + case "settings": + settings.reload(); + break; } - Focus(document.getElementById('logoutButton')); - } - if (run) { - run(+this.status); + document.getElementById("logout-button").classList.remove("unfocused"); } + if (run) { run(+this.status); } } }; req.send(); } -(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean { - window.token = ""; - const details = serializeForm('loginForm'); - const button = document.getElementById('loginSubmit') as HTMLButtonElement; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-danger"); - button.disabled = true; - button.innerHTML = ` - - Loading...`; - login(details["username"], details["password"], true, button); - return false; +(document.getElementById('form-login') as HTMLFormElement).onsubmit = (event: SubmitEvent) => { + event.preventDefault(); + const button = (event.target as HTMLElement).querySelector(".submit") as HTMLSpanElement; + const username = (document.getElementById("login-user") as HTMLInputElement).value; + const password = (document.getElementById("login-password") as HTMLInputElement).value; + if (!username || !password) { + window.notifications.customError("loginError", "The username and/or password were left blank."); + return; + } + toggleLoader(button); + login(username, password, () => toggleLoader(button)); }; -generateInvites(true); +login("", ""); -login("", "", false, null, (status: number): void => { - if (!(status == 200 || status == 204)) { - window.Modals.login.show(); +(document.getElementById('logout-button') as HTMLButtonElement).onclick = () => _post("/logout", null, (req: XMLHttpRequest): boolean => { + if (req.readyState == 4 && req.status == 200) { + window.token = ""; + location.reload(); + return false; } }); -(document.getElementById('logoutButton') as HTMLButtonElement).onclick = function (): void { - _post("/logout", null, function (): boolean { - if (this.readyState == 4 && this.status == 200) { - window.token = ""; - location.reload(); - return false; - } - }); -}; - - diff --git a/ts/form.ts b/ts/form.ts index 973e153..714bee7 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -1,25 +1,29 @@ -import { serializeForm, _post, _get, _delete, addAttr, rmAttr } from "./modules/common.js"; -import { BS5 } from "./modules/bs5.js"; -import { BS4 } from "./modules/bs4.js"; +import { Modal } from "./modules/modal.js"; +import { _post, toggleLoader } from "./modules/common.js"; interface formWindow extends Window { - usernameEnabled: boolean; validationStrings: pwValStrings; - checkPassword(): void; invalidPassword: string; + modal: Modal; } -declare var window: formWindow; - interface pwValString { singular: string; plural: string; } interface pwValStrings { - length, uppercase, lowercase, number, special: pwValString; + length: pwValString; + uppercase: pwValString; + lowercase: pwValString; + number: pwValString; + special: pwValString; + [ type: string ]: pwValString; } +window.modal = new Modal(document.getElementById("modal-success")); +declare var window: formWindow; + var defaultPwValStrings: pwValStrings = { length: { singular: "Must have at least {n} character", @@ -43,111 +47,132 @@ var defaultPwValStrings: pwValStrings = { } } -const toggleSpinner = (ogText?: string): string => { - const submitButton = document.getElementById('submitButton') as HTMLButtonElement; - if (document.getElementById('createAccountSpinner')) { - submitButton.innerHTML = ogText ? ogText : `Create Account`; - submitButton.disabled = false; - return ""; +const form = document.getElementById("form-create") as HTMLFormElement; +const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement; +const submitSpan = form.querySelector("span.submit") as HTMLSpanElement; +let usernameField = document.getElementById("create-username") as HTMLInputElement; +const emailField = document.getElementById("create-email") as HTMLInputElement; +if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameField = emailField; } +const passwordField = document.getElementById("create-password") as HTMLInputElement; +const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement; + +const checkPasswords = () => { + if (passwordField.value != rePasswordField.value) { + rePasswordField.setCustomValidity(window.invalidPassword); + submitButton.disabled = true; + submitSpan.setAttribute("disabled", ""); } else { - let ogText = submitButton.innerHTML; - submitButton.innerHTML = ` - Creating... - `; - return ogText; + rePasswordField.setCustomValidity(""); + submitButton.disabled = false; + submitSpan.removeAttribute("disabled"); } }; +rePasswordField.addEventListener("keyup", checkPasswords); +passwordField.addEventListener("keyup", checkPasswords); -for (let key in window.validationStrings) { - if (window.validationStrings[key].singular == "" || !(window.validationStrings[key].plural.includes("{n}"))) { - window.validationStrings[key].singular = defaultPwValStrings[key].singular; - } - if (window.validationStrings[key].plural == "" || !(window.validationStrings[key].plural.includes("{n}"))) { - window.validationStrings[key].plural = defaultPwValStrings[key].plural; - } - let el = document.getElementById(key) as HTMLUListElement; - if (el) { - const min: number = +el.getAttribute("min"); - let text = ""; - if (min == 1) { - text = window.validationStrings[key].singular.replace("{n}", "1"); - } else { - text = window.validationStrings[key].plural.replace("{n}", min.toString()); - } - (document.getElementById(key).children[0] as HTMLDivElement).textContent = text; - } +interface respDTO { + [ type: string ]: boolean; } -window.BS = window.bs5 ? new BS5 : new BS4; -var successBox: BSModal = window.BS.newModal('successBox');; +interface sendDTO { + code: string; + email: string; + username: string; + password: string; +} -var code = window.location.href.split('/').pop(); - -(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => { +const create = (event: SubmitEvent) => { event.preventDefault(); - const el = document.getElementById('errorMessage'); - if (el) { - el.remove(); - } - const ogText = toggleSpinner(); - let send: Object = serializeForm('accountForm'); - send["code"] = code; - if (!window.usernameEnabled) { - send["email"] = send["username"]; - } - _post("/newUser", send, function (): void { - if (this.readyState == 4) { - toggleSpinner(ogText); - let data: Object = this.response; - const errorGiven = ("error" in data) - if (errorGiven || data["success"] === false) { - let errorMessage = "Unknown Error"; - if (errorGiven && errorGiven != true) { - errorMessage = data["error"]; - } - document.getElementById('errorBox').innerHTML += ` - - `; + toggleLoader(submitSpan); + let send: sendDTO = { + code: window.location.href.split('/').pop(), + username: usernameField.value, + email: emailField.value, + password: passwordField.value + }; + _post("/newUser", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + let vals = JSON.parse(req.response) as respDTO; + let valid = true; + for (let type in vals) { + if (requirements[type]) { requirements[type].valid = vals[type]; } + if (!vals[type]) { valid = false; } + } + toggleLoader(submitSpan); + if (req.status == 200 && valid) { + window.modal.show(); } else { - let valid = true; - for (let key in data) { - if (data.hasOwnProperty(key)) { - const criterion = document.getElementById(key); - if (criterion) { - if (data[key] === false) { - valid = false; - addAttr(criterion, "list-group-item-danger"); - rmAttr(criterion, "list-group-item-success"); - } else { - addAttr(criterion, "list-group-item-success"); - rmAttr(criterion, "list-group-item-danger"); - } - } - } - } - if (valid) { - successBox.show(); - } + submitSpan.classList.add("~critical"); + submitSpan.classList.remove("~urge"); + setTimeout(() => { + submitSpan.classList.add("~urge"); + submitSpan.classList.remove("~critical"); + }, 1000); } } - }, true); - return false; -}); + }); +}; -window.checkPassword = (): void => { - const entry = document.getElementById('inputPassword') as HTMLInputElement; - if (entry.value != "") { - const reentry = document.getElementById('reInputPassword') as HTMLInputElement; - const identical = (entry.value == reentry.value); - const submitButton = document.getElementById('submitButton') as HTMLButtonElement; - if (identical) { - reentry.setCustomValidity(''); - rmAttr(submitButton, "btn-outline-danger"); - addAttr(submitButton, "btn-outline-primary"); +form.onsubmit = create; + +class Requirement { + private _name: string; + private _minCount: number; + private _content: HTMLSpanElement; + private _valid: HTMLSpanElement; + private _li: HTMLLIElement; + + get valid(): boolean { return this._valid.classList.contains("~positive"); } + set valid(state: boolean) { + if (state) { + this._valid.classList.add("~positive"); + this._valid.classList.remove("~critical"); + this._valid.innerHTML = ``; } else { - reentry.setCustomValidity(window.invalidPassword); - addAttr(submitButton, "btn-outline-danger"); - rmAttr(submitButton, "btn-outline-primary"); + this._valid.classList.add("~critical"); + this._valid.classList.remove("~positive"); + this._valid.innerHTML = ``; + } + } + + constructor(name: string, el: HTMLLIElement) { + this._name = name; + this._li = el; + this._content = this._li.querySelector("span.requirement-content") as HTMLSpanElement; + this._valid = this._li.querySelector("span.requirement-valid") as HTMLSpanElement; + this.valid = false; + this._minCount = +this._li.getAttribute("min"); + + let text = ""; + if (this._minCount == 1) { + text = window.validationStrings[this._name].singular.replace("{n}", "1"); + } else { + text = window.validationStrings[this._name].plural.replace("{n}", ""+this._minCount); + } + this._content.textContent = text; + } +} + +const testStrings = (f: pwValString): boolean => { + const testString = (s: string): boolean => { + if (s == "" || !s.includes("{n}")) { return false; } + return true; + } + return testString(f.singular) && testString(f.plural); +} + +var requirements: { [category: string]: Requirement} = {}; + +if (!window.validationStrings) { + window.validationStrings = defaultPwValStrings; +} else { + for (let category in window.validationStrings) { + if (!testStrings(window.validationStrings[category])) { + window.validationStrings[category] = defaultPwValStrings[category]; + } + const el = document.getElementById("requirement-" + category); + if (el) { + requirements[category] = new Requirement(category, el as HTMLLIElement); } } } diff --git a/ts/invites.ts b/ts/invites.ts deleted file mode 100644 index 0b2efd7..0000000 --- a/ts/invites.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js"; -import { generateInvites, checkDuration } from "./modules/invites.js"; - -interface aWindow extends Window { - setProfile(el: HTMLElement): void; -} - -declare var window: aWindow; - -function fixCheckboxes(): void { - const send_to_address: Array = [document.getElementById('send_to_address') as HTMLInputElement, document.getElementById('send_to_address_enabled') as HTMLInputElement]; - if (send_to_address[0] != null) { - send_to_address[0].disabled = !send_to_address[1].checked; - } - const multiUseEnabled = document.getElementById('multiUseEnabled') as HTMLInputElement; - const multiUseCount = document.getElementById('multiUseCount') as HTMLInputElement; - const noUseLimit = document.getElementById('noUseLimit') as HTMLInputElement; - multiUseCount.disabled = !multiUseEnabled.checked; - noUseLimit.checked = false; - noUseLimit.disabled = !multiUseEnabled.checked; -} - -fixCheckboxes(); - -(document.getElementById('inviteForm') as HTMLFormElement).onsubmit = function (): boolean { - const button = document.getElementById('generateSubmit') as HTMLButtonElement; - button.disabled = true; - button.innerHTML = ` - - Loading...`; - let send = serializeForm('inviteForm'); - send["remaining-uses"] = +send["remaining-uses"]; - if (!send['multiple-uses'] || send['no-limit']) { - delete send['remaining-uses']; - } - if (send["profile"] == "NoProfile") { - send["profile"] = ""; - } - const sendToAddress: any = document.getElementById('send_to_address'); - const sendToAddressEnabled: any = document.getElementById('send_to_address_enabled'); - if (sendToAddress && sendToAddressEnabled) { - send['email'] = send['send_to_address']; - delete send['send_to_address']; - delete send['send_to_address_enabled']; - } - _post("/invites", send, function (): void { - if (this.readyState == 4) { - button.textContent = 'Generate'; - button.disabled = false; - generateInvites(); - } - }); - return false; -}; - -window.BS.triggerTooltips(); - -window.setProfile= (select: HTMLSelectElement): void => { - if (!select.value) { - return; - } - let val = select.value; - if (select.value == "NoProfile") { - val = "" - } - const invite = select.id.replace("profile_", ""); - const send = { - "invite": invite, - "profile": val - }; - _post("/invites/profile", send, function (): void { - if (this.readyState == 4 && this.status != 200) { - generateInvites(); - } - }); -} - -const nE: Array = ["days", "hours", "minutes"]; -for (const i in nE) { - document.getElementById(nE[i]).addEventListener("change", checkDuration); -} diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 1d5e661..80c1593 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -1,163 +1,409 @@ -import { _get, _post, _delete, createEl } from "../modules/common.js"; -import { Focus, Unfocus } from "../modules/admin.js"; +import { _get, _post, _delete, toggleLoader } from "../modules/common.js"; -interface aWindow extends Window { - checkCheckboxes: () => void; +interface User { + id: string; + name: string; + email: string | undefined; + last_active: string; + admin: boolean; } -declare var window: aWindow; +class user implements User { + private _row: HTMLTableRowElement; + private _check: HTMLInputElement; + private _username: HTMLSpanElement; + private _admin: HTMLSpanElement; + private _email: HTMLInputElement; + private _emailAddress: string; + private _emailEditButton: HTMLElement; + private _lastActive: HTMLTableDataCellElement; + id: string; + private _selected: boolean; -export const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email); + get selected(): boolean { return this._selected; } + set selected(state: boolean) { + this._selected = state; + this._check.checked = state; + state ? document.dispatchEvent(this._checkEvent) : document.dispatchEvent(this._uncheckEvent); + } -export const checkCheckboxes = (): void => { - const defaultsButton = document.getElementById('accountsTabSetDefaults'); - const deleteButton = document.getElementById('accountsTabDelete'); - const checkboxes: NodeListOf = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked'); - let checked = checkboxes.length; - if (checked == 0) { - Unfocus(defaultsButton); - Unfocus(deleteButton); - } else { - Focus(defaultsButton); - Focus(deleteButton); - if (checked == 1) { - deleteButton.textContent = 'Delete User'; + get name(): string { return this._username.textContent; } + set name(value: string) { this._username.textContent = value; } + + get admin(): boolean { return this._admin.classList.contains("chip"); } + set admin(state: boolean) { + if (state) { + this._admin.classList.add("chip", "~info", "ml-1"); + this._admin.textContent = "Admin"; } else { - deleteButton.textContent = 'Delete Users'; + this._admin.classList.remove("chip", "~info", "ml-1"); + this._admin.textContent = "" } } -} -window.checkCheckboxes = checkCheckboxes; + get email(): string { return this._emailAddress; } + set email(value: string) { this._email.value = value; this._emailAddress = value; } + + get last_active(): string { return this._lastActive.textContent; } + set last_active(value: string) { this._lastActive.textContent = value; } -export function changeEmail(icon: HTMLElement, id: string): void { - const iconContent = icon.outerHTML; - icon.setAttribute('class', ''); - const entry = icon.nextElementSibling as HTMLInputElement; - const ogEmail = entry.value; - entry.readOnly = false; - entry.classList.remove('form-control-plaintext'); - entry.classList.add('form-control'); - if (ogEmail == "") { - entry.placeholder = 'Address'; + private _checkEvent = new CustomEvent("accountCheckEvent"); + private _uncheckEvent = new CustomEvent("accountUncheckEvent"); + + constructor(user: User) { + this._row = document.createElement("tr") as HTMLTableRowElement; + this._row.innerHTML = ` + + + + + `; + this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement; + this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; + this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement; + this._email = this._row.querySelector(".accounts-email") as HTMLInputElement; + this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; + this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; + this._check.onchange = () => { this.selected = this._check.checked; } + + const toggleStealthInput = () => { + this._email.classList.toggle("stealth-input-hidden"); + this._email.readOnly = !this._email.readOnly; + this._emailEditButton.classList.toggle("ri-check-line"); + this._emailEditButton.classList.toggle("ri-edit-line"); + }; + const outerClickListener = (event: Event) => { + if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) { + toggleStealthInput(); + this.email = this.email; + document.removeEventListener("click", outerClickListener); + } + }; + this._emailEditButton.onclick = () => { + if (this._email.classList.contains("stealth-input-hidden")) { + document.addEventListener('click', outerClickListener); + } else { + this._updateEmail(); + document.removeEventListener('click', outerClickListener); + } + toggleStealthInput(); + }; + + this.update(user); } - const tick = createEl(` - - `); - tick.onclick = (): void => { - const newEmail = entry.value; - if (!validateEmail(newEmail) || newEmail == ogEmail) { - return; - } - cross.remove(); - const spinner = createEl(` -
- Saving... -
- `); - tick.replaceWith(spinner); + + private _updateEmail = () => { + let oldEmail = this.email; + this.email = this._email.value; let send = {}; - send[id] = newEmail; - _post("/users/emails", send, function (): void { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - entry.nextElementSibling.remove(); + send[this.id] = this.email; + _post("/users/emails", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`); } else { - entry.value = ogEmail; + this.email = oldEmail; + window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`); } } }); - icon.outerHTML = iconContent; - entry.readOnly = true; - entry.classList.remove('form-control'); - entry.classList.add('form-control-plaintext'); - entry.placeholder = ''; - }; - const cross = createEl(` - - `); - cross.onclick = (): void => { - tick.remove(); - cross.remove(); - icon.outerHTML = iconContent; - entry.readOnly = true; - entry.classList.remove('form-control'); - entry.classList.add('form-control-plaintext'); - entry.placeholder = ''; - entry.value = ogEmail; - }; - icon.parentNode.appendChild(tick); - icon.parentNode.appendChild(cross); -}; - -export function populateUsers(): void { - const acList = document.getElementById('accountsList'); - acList.innerHTML = ` -
- Getting Users... - -
- `; - Unfocus(acList.parentNode.querySelector('thead')); - const accountsList = document.createElement('tbody'); - accountsList.id = 'accountsList'; - const generateEmail = (id: string, name: string, email: string): string => { - let entry: HTMLDivElement = document.createElement('div'); - entry.id = 'email_' + id; - let emailValue: string = email; - if (emailValue == undefined) { - emailValue = ""; - } - entry.innerHTML = ` - - - `; - return entry.outerHTML; - }; - const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => { - let fci = "form-check-input"; - if (window.bsVersion != 5) { - fci = ""; - } - return ` - - ${username}${admin ? 'Admin' : ''} - ${generateEmail(id, name, email)} - ${lastActive} - `; - }; - - _get("/users", null, function (): void { - if (this.readyState == 4 && this.status == 200) { - window.jfUsers = this.response['users']; - for (const user of window.jfUsers) { - let tr = document.createElement('tr'); - tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']); - accountsList.appendChild(tr); - } - Focus(acList.parentNode.querySelector('thead')); - acList.replaceWith(accountsList); - } - }); -} - -export function populateRadios(): void { - const radioList = document.getElementById('defaultUserRadios'); - radioList.textContent = ''; - let first = true; - for (const i in window.jfUsers) { - const user = window.jfUsers[i]; - const radio = document.createElement('div'); - radio.classList.add('form-check'); - let checked = ''; - if (first) { - checked = 'checked'; - first = false; - } - radio.innerHTML = ` - - `; - radioList.appendChild(radio); } -} + update = (user: User) => { + this.id = user.id; + this.name = user.name; + this.email = user.email || ""; + this.last_active = user.last_active; + this.admin = user.admin; + } + + asElement = (): HTMLTableRowElement => { return this._row; } + remove = () => { + if (this.selected) { + document.dispatchEvent(this._uncheckEvent); + } + this._row.remove(); + } +} + + + + +export class accountsList { + private _table = document.getElementById("accounts-list") as HTMLTableSectionElement; + + private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement; + private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement; + private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement; + private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; + private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement; + private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement; + private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement; + private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement; + private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement; + + private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; + private _users: { [id: string]: user }; + private _checkCount: number = 0; + + private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement; + private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement; + private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement; + private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement; + + get selectAll(): boolean { return this._selectAll.checked; } + set selectAll(state: boolean) { + for (let id in this._users) { + this._users[id].selected = state; + } + this._selectAll.checked = state; + this._selectAll.indeterminate = false; + state ? this._checkCount = Object.keys(this._users).length : 0; + + } + + add = (u: User) => { + let domAccount = new user(u); + this._users[u.id] = domAccount; + this._table.appendChild(domAccount.asElement()); + } + + private _checkCheckCount = () => { + if (this._checkCount == 0) { + this._selectAll.indeterminate = false; + this._selectAll.checked = false; + this._modifySettings.classList.add("unfocused"); + this._deleteUser.classList.add("unfocused"); + } else { + if (this._checkCount == Object.keys(this._users).length) { + this._selectAll.checked = true; + this._selectAll.indeterminate = false; + } else { + this._selectAll.checked = false; + this._selectAll.indeterminate = true; + } + this._modifySettings.classList.remove("unfocused"); + this._deleteUser.classList.remove("unfocused"); + (this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users"; + } + } + + private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; } + + private _collectUsers = (): string[] => { + let list: string[] = []; + for (let id in this._users) { + if (this._users[id].selected) { list.push(id); } + } + return list; + } + + private _addUser = (event: Event) => { + event.preventDefault(); + const button = this._addUserForm.querySelector("span.submit") as HTMLSpanElement; + const send = { + "username": this._addUserName.value, + "email": this._addUserEmail.value, + "password": this._addUserPassword.value + }; + for (let field in send) { + if (!send[field]) { + window.notifications.customError("addUserBlankField", "Fields were left blank."); + return; + } + } + toggleLoader(button); + _post("/users", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 200) { + window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`); + } + this.reload(); + window.modals.addUser.close(); + } + }); + } + + deleteUsers = () => { + const modalHeader = document.getElementById("header-delete-user"); + modalHeader.textContent = this._genCountString(); + let list = this._collectUsers(); + const form = document.getElementById("form-delete-user") as HTMLFormElement; + const button = form.querySelector("span.submit") as HTMLSpanElement; + this._deleteNotify.checked = false; + this._deleteReason.value = ""; + this._deleteReason.classList.add("unfocused"); + form.onsubmit = (event: Event) => { + event.preventDefault(); + toggleLoader(button); + let send = { + "users": list, + "notify": this._deleteNotify.checked, + "reason": this._deleteNotify ? this._deleteReason.value : "" + }; + _delete("/users", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + window.modals.deleteUser.close(); + if (req.status != 200 && req.status != 204) { + let errorMsg = "Failed (check console/logs)."; + if (!("error" in req.response)) { + errorMsg = "Partial failure (check console/logs)."; + } + window.notifications.customError("deleteUserError", errorMsg); + } else { + window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`); + } + this.reload(); + } + }); + }; + window.modals.deleteUser.show(); + } + + modifyUsers = () => { + const modalHeader = document.getElementById("header-modify-user"); + modalHeader.textContent = this._genCountString(); + let list = this._collectUsers(); + (() => { + let innerHTML = ""; + for (const profile of window.availableProfiles) { + innerHTML += ``; + } + this._profileSelect.innerHTML = innerHTML; + })(); + + (() => { + let innerHTML = ""; + for (let id in this._users) { + innerHTML += ``; + } + this._userSelect.innerHTML = innerHTML; + })(); + + const form = document.getElementById("form-modify-user") as HTMLFormElement; + const button = form.querySelector("span.submit") as HTMLSpanElement; + this._modifySettingsProfile.checked = true; + this._modifySettingsUser.checked = false; + form.onsubmit = (event: Event) => { + event.preventDefault(); + toggleLoader(button); + let send = { + "apply_to": list, + "homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked + }; + if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) { + send["from"] = "profile"; + send["profile"] = this._profileSelect.value; + } else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) { + send["from"] = "user"; + send["id"] = this._userSelect.value; + } + _post("/users/settings", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 500) { + let response = JSON.parse(req.response); + let errorMsg = ""; + if ("homescreen" in response && "policy" in response) { + const homescreen = Object.keys(response["homescreen"]).length; + const policy = Object.keys(response["policy"]).length; + if (homescreen != 0 && policy == 0) { + errorMsg = "Settings were applied, but applying homescreen layout may have failed."; + } else if (policy != 0 && homescreen == 0) { + errorMsg = "Homescreen layout was applied, but applying settings may have failed."; + } else if (policy != 0 && homescreen != 0) { + errorMsg = "Application failed."; + } + } else if ("error" in response) { + errorMsg = response["error"]; + } + window.notifications.customError("modifySettingsError", errorMsg); + } else if (req.status == 200 || req.status == 204) { + window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`); + } + this.reload(); + window.modals.modifyUser.close(); + } + }); + }; + window.modals.modifyUser.show(); + } + + + + constructor() { + this._users = {}; + this._selectAll.checked = false; + this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked }; + document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); }); + document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); }); + this._addUserButton.onclick = window.modals.addUser.toggle; + this._addUserForm.addEventListener("submit", this._addUser); + + this._deleteNotify.onchange = () => { + if (this._deleteNotify.checked) { + this._deleteReason.classList.remove("unfocused"); + } else { + this._deleteReason.classList.add("unfocused"); + } + }; + this._modifySettings.onclick = this.modifyUsers; + this._modifySettings.classList.add("unfocused"); + const checkSource = () => { + const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement; + const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement; + if (this._modifySettingsProfile.checked) { + this._userSelect.parentElement.classList.add("unfocused"); + this._profileSelect.parentElement.classList.remove("unfocused") + profileSpan.classList.add("!high"); + profileSpan.classList.remove("!normal"); + userSpan.classList.remove("!high"); + userSpan.classList.add("!normal"); + } else { + this._userSelect.parentElement.classList.remove("unfocused"); + this._profileSelect.parentElement.classList.add("unfocused"); + userSpan.classList.add("!high"); + userSpan.classList.remove("!normal"); + profileSpan.classList.remove("!high"); + profileSpan.classList.add("!normal"); + } + }; + this._modifySettingsProfile.onchange = checkSource; + this._modifySettingsUser.onchange = checkSource; + + this._deleteUser.onclick = this.deleteUsers; + this._deleteUser.classList.add("unfocused"); + + if (!window.usernameEnabled) { + this._addUserName.classList.add("unfocused"); + this._addUserName = this._addUserEmail; + } + /*if (!window.emailEnabled) { + this._deleteNotify.parentElement.classList.add("unfocused"); + this._deleteNotify.checked = false; + }*/ + } + + reload = () => _get("/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + // same method as inviteList.reload() + let accountsOnDOM: { [id: string]: boolean } = {}; + for (let id in this._users) { accountsOnDOM[id] = true; } + for (let u of (req.response["users"] as User[])) { + if (u.id in this._users) { + this._users[u.id].update(u); + delete accountsOnDOM[u.id]; + } else { + this.add(u); + } + } + for (let id in accountsOnDOM) { + this._users[id].remove(); + delete this._users[id]; + } + this._checkCheckCount; + } + }) +} diff --git a/ts/modules/admin.ts b/ts/modules/admin.ts deleted file mode 100644 index ade7cb8..0000000 --- a/ts/modules/admin.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { rmAttr, addAttr, _post, _get, _delete, createEl } from "../modules/common.js"; - -export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused'); -export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused'); - -export function storeDefaults(users: string | Array): void { - const button = document.getElementById('storeDefaults') as HTMLButtonElement; - button.disabled = true; - button.innerHTML = - '' + - 'Loading...'; - let data = { "homescreen": false }; - if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') { - data["from"] = "profile"; - data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value; - } else { - const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement - let id = radio.id.replace("default_", ""); - data["from"] = "user"; - data["id"] = id; - } - if (users != "all") { - data["apply_to"] = users; - } - if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) { - data["homescreen"] = true; - } - _post("/users/settings", data, function (): void { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - button.textContent = "Success"; - addAttr(button, "btn-success"); - rmAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - button.disabled = false; - setTimeout((): void => { - button.textContent = "Submit"; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-success"); - button.disabled = false; - window.Modals.userDefaults.hide(); - }, 1000); - } else { - if ("error" in this.response) { - button.textContent = this.response["error"]; - } else if (("policy" in this.response) || ("homescreen" in this.response)) { - button.textContent = "Failed (check console)"; - } else { - button.textContent = "Failed"; - } - addAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - setTimeout((): void => { - button.textContent = "Submit"; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-danger"); - button.disabled = false; - }, 1000); - } - } - }); -} diff --git a/ts/modules/animation.ts b/ts/modules/animation.ts deleted file mode 100644 index 248d753..0000000 --- a/ts/modules/animation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { rmAttr, addAttr } from "../modules/common.js"; - -interface aWindow extends Window { - rotateButton(el: HTMLElement): void; -} - -declare var window: aWindow; - -// Used for animation on theme change -const whichTransitionEvent = (): string => { - const el = document.createElement('fakeElement'); - const transitions = { - 'transition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'MozTransition': 'transitionend', - 'WebkitTransition': 'webkitTransitionEnd' - }; - for (const t in transitions) { - if (el.style[t] !== undefined) { - return transitions[t]; - } - } - return ''; -}; - -var transitionEndEvent = whichTransitionEvent(); - -// Toggles between light and dark themes -const _toggleCSS = (): void => { - const els: NodeListOf = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]'); - let cssEl = 0; - let remove = false; - if (els.length != 1) { - cssEl = 1; - remove = true - } - let href: string = "bs" + window.bsVersion; - if (!els[cssEl].href.includes(href + "-jf")) { - href += "-jf"; - } - href += ".css"; - let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement; - newEl.href = href; - els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling); - if (remove) { - els[0].remove(); - } - document.cookie = "css=" + href; -} - -// Toggles between light and dark themes, but runs animation if window small enough. -window.buttonWidth = 0; -export const toggleCSS = (el: HTMLElement): void => { - const switchToColor = window.getComputedStyle(document.body, null).backgroundColor; - // Max page width for animation to take place - let maxWidth = 1500; - if (window.innerWidth < maxWidth) { - // Calculate minimum radius to cover screen - const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2)); - const currentRadius = el.getBoundingClientRect().width / 2; - const scale = radius / currentRadius; - window.buttonWidth = +window.getComputedStyle(el, null).width; - document.body.classList.remove('smooth-transition'); - el.style.transform = `scale(${scale})`; - el.style.color = switchToColor; - el.addEventListener(transitionEndEvent, function (): void { - if (this.style.transform.length != 0) { - _toggleCSS(); - this.style.removeProperty('transform'); - document.body.classList.add('smooth-transition'); - } - }, false); - } else { - _toggleCSS(); - el.style.color = switchToColor; - } -}; - -window.rotateButton = (el: HTMLElement): void => { - if (el.classList.contains("rotated")) { - rmAttr(el, "rotated") - addAttr(el, "not-rotated"); - } else { - rmAttr(el, "not-rotated"); - addAttr(el, "rotated"); - } -}; diff --git a/ts/modules/bs4.ts b/ts/modules/bs4.ts deleted file mode 100644 index 58c7023..0000000 --- a/ts/modules/bs4.ts +++ /dev/null @@ -1,45 +0,0 @@ -declare var $: any; - -class Modal implements BSModal { - el: HTMLDivElement; - modal: any; - - constructor(id: string, find?: boolean) { - this.el = document.getElementById(id) as HTMLDivElement; - this.modal = $(this.el) as any; - this.modal.on("shown.b.modal", (): void => document.body.classList.add('modal-open')); - }; - - show(): void { this.modal.modal("show"); }; - hide(): void { this.modal.modal("hide"); }; -} - -export class BS4 implements Bootstrap { - triggerTooltips: tooltipTrigger = function (): void { - const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]')); - for (const i in checkboxes) { - checkboxes[i].click(); - checkboxes[i].click(); - } - const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]')); - tooltips.map((el: HTMLAnchorElement): any => { - return ($(el) as any).tooltip(); - }); - }; - - Compat(): void { - console.log('Fixing BS4 Compatability'); - const send_to_address_enabled = document.getElementById('send_to_address_enabled'); - if (send_to_address_enabled) { - send_to_address_enabled.classList.remove("form-check-input"); - } - const multiUseEnabled = document.getElementById('multiUseEnabled'); - if (multiUseEnabled) { - multiUseEnabled.classList.remove("form-check-input"); - } - } - - newModal: ModalConstructor = function (id: string, find?: boolean): BSModal { - return new Modal(id, find); - }; -} diff --git a/ts/modules/bs5.ts b/ts/modules/bs5.ts deleted file mode 100644 index 2c2e166..0000000 --- a/ts/modules/bs5.ts +++ /dev/null @@ -1,37 +0,0 @@ -declare var bootstrap: any; - -class Modal implements BSModal { - el: HTMLDivElement; - modal: any; - - constructor(id: string, find?: boolean) { - this.el = document.getElementById(id) as HTMLDivElement; - if (find) { - this.modal = bootstrap.Modal.getInstance(this.el); - } else { - this.modal = new bootstrap.Modal(this.el); - } - this.el.addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open")); - }; - - show(): void { this.modal.show(); }; - hide(): void { this.modal.hide(); }; -} - -export class BS5 implements Bootstrap { - triggerTooltips: tooltipTrigger = function (): void { - const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]')); - for (const i in checkboxes) { - checkboxes[i].click(); - checkboxes[i].click(); - } - const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]')); - tooltips.map((el: HTMLAnchorElement): any => { - return new bootstrap.Tooltip(el); - }); - }; - - newModal: ModalConstructor = function (id: string, find?: boolean): BSModal { - return new Modal(id, find); - }; -}; diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 39ba0d2..7dfe918 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -49,18 +49,25 @@ export const rmAttr = (el: HTMLElement, attr: string): void => { }; export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr); - -export const _get = (url: string, data: Object, onreadystatechange: () => void): void => { +export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => { let req = new XMLHttpRequest(); req.open("GET", window.URLBase + url, true); req.responseType = 'json'; req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = onreadystatechange; + req.onreadystatechange = () => { + if (req.status == 0) { + window.notifications.connectionError(); + return; + } else if (req.status == 401) { + window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + } + onreadystatechange(req); + }; req.send(JSON.stringify(data)); }; -export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => { +export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean): void => { let req = new XMLHttpRequest(); req.open("POST", window.URLBase + url, true); if (response) { @@ -68,16 +75,131 @@ export const _post = (url: string, data: Object, onreadystatechange: () => void, } req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = onreadystatechange; + req.onreadystatechange = () => { + if (req.status == 0) { + window.notifications.connectionError(); + return; + } else if (req.status == 401) { + window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + } + onreadystatechange(req); + }; req.send(JSON.stringify(data)); }; -export function _delete(url: string, data: Object, onreadystatechange: () => void): void { +export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void { let req = new XMLHttpRequest(); req.open("DELETE", window.URLBase + url, true); req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.onreadystatechange = onreadystatechange; + req.onreadystatechange = () => { + if (req.status == 0) { + window.notifications.connectionError(); + return; + } else if (req.status == 401) { + window.notifications.customError("401Error", "Unauthorized. Try logging back in."); + } + onreadystatechange(req); + }; req.send(JSON.stringify(data)); } +export function toClipboard (str: string) { + const el = document.createElement('textarea') as HTMLTextAreaElement; + el.value = str; + el.readOnly = true; + el.style.position = "absolute"; + el.style.left = "-9999px"; + document.body.appendChild(el); + const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); + if (selected) { + document.getSelection().removeAllRanges(); + document.getSelection().addRange(selected); + } +} + +export class notificationBox implements NotificationBox { + private _box: HTMLDivElement; + private _errorTypes: { [type: string]: boolean } = {}; + private _positiveTypes: { [type: string]: boolean } = {}; + timeout: number; + constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; } + + private _error = (message: string): HTMLElement => { + const noti = document.createElement('aside'); + noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error"); + noti.innerHTML = `Error: ${message}`; + const closeButton = document.createElement('span') as HTMLSpanElement; + closeButton.classList.add("button", "~critical", "!low", "ml-1"); + closeButton.innerHTML = ``; + closeButton.onclick = () => { this._box.removeChild(noti); }; + noti.appendChild(closeButton); + return noti; + } + + private _positive = (bold: string, message: string): HTMLElement => { + const noti = document.createElement('aside'); + noti.classList.add("aside", "~positive", "!normal", "mt-half", "notification-positive"); + noti.innerHTML = `${bold} ${message}`; + const closeButton = document.createElement('span') as HTMLSpanElement; + closeButton.classList.add("button", "~positive", "!low", "ml-1"); + closeButton.innerHTML = ``; + closeButton.onclick = () => { this._box.removeChild(noti); }; + noti.appendChild(closeButton); + return noti; + } + + connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); } + + customError = (type: string, message: string) => { + this._errorTypes[type] = this._errorTypes[type] || false; + const noti = this._error(message); + noti.classList.add("error-" + type); + const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type); + if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) { + previousNoti.remove(); + } + this._box.appendChild(noti); + this._errorTypes[type] = true; + setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000); + } + + customPositive = (type: string, bold: string, message: string) => { + this._positiveTypes[type] = this._positiveTypes[type] || false; + const noti = this._positive(bold, message); + noti.classList.add("positive-" + type); + const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type); + if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) { + previousNoti.remove(); + } + this._box.appendChild(noti); + this._positiveTypes[type] = true; + setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); + } +} + +export const whichAnimationEvent = () => { + const el = document.createElement("fakeElement"); + if (el.style["animation"] !== void 0) { + return "animationend"; + } + return "webkitAnimationEnd"; +} + +export function toggleLoader(el: HTMLElement, small: boolean = true) { + if (el.classList.contains("loader")) { + el.classList.remove("loader"); + el.classList.remove("loader-sm"); + const dot = el.querySelector("span.dot"); + if (dot) { dot.remove(); } + } else { + el.classList.add("loader"); + if (small) { el.classList.add("loader-sm"); } + const dot = document.createElement("span") as HTMLSpanElement; + dot.classList.add("dot") + el.appendChild(dot); + } +} diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index 8e1e2b1..afbdff6 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -1,297 +1,630 @@ -import { _get, _post, _delete } from "../modules/common.js"; +import { _get, _post, _delete, toClipboard, toggleLoader } from "../modules/common.js"; -interface aWindow extends Window { - setNotify(el: HTMLElement): void; - deleteInvite(code: string): void; -} - -declare var window: aWindow; - -const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; } - -function genUsedBy(usedBy: Array>): string { - let uB = ""; - if (usedBy && usedBy.length != 0) { - uB = ` -
    -
  • Users created:
  • - `; - for (const i in usedBy) { - uB += ` -
  • -
    ${usedBy[i][0]}
    -
    ${usedBy[i][1]}
    -
  • - `; +export class DOMInvite implements Invite { + updateNotify = (checkbox: HTMLInputElement) => { + let state: { [code: string]: { [type: string]: boolean } } = {}; + let revertChanges: () => void; + if (checkbox.classList.contains("inv-notify-expiry")) { + revertChanges = () => { this.notifyExpiry = !this.notifyExpiry }; + state[this.code] = { "notify-expiry": this.notifyExpiry }; + } else { + revertChanges = () => { this.notifyCreation = !this.notifyCreation }; + state[this.code] = { "notify-creation": this.notifyCreation }; } - uB += `
` + _post("/invites/notify", state, (req: XMLHttpRequest) => { + if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) { + revertChanges(); + } + }); } - return uB; -} -function addItem(invite: Invite): void { - const links = document.getElementById('invites'); - const container = document.createElement('div') as HTMLDivElement; - container.id = invite.code; - const item = document.createElement('div') as HTMLDivElement; - item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block'); - let link = ""; - let innerHTML = `None`; - if (invite.empty) { - item.innerHTML = ` -
- ${innerHTML} -
- `; - container.appendChild(item); - links.appendChild(container); - return; + delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => { + if (req.readyState == 4 && (req.status == 200 || req.status == 204)) { + this.remove(); + const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code }); + document.dispatchEvent(inviteDeletedEvent); + } + }) + + private _code: string = "None"; + get code(): string { return this._code; } + set code(code: string) { + this._code = code; + this._codeLink = window.location.href.split("#")[0] + "invite/" + code; + const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement; + linkEl.textContent = code.replace(/-/g, '-'); + linkEl.href = this._codeLink; } - link = window.location.href.split('#')[0] + "invite/" + invite.code; - innerHTML = ` -
- ${invite.code.replace(/-/g, '-')} - - `; - if (invite.email) { - let email = invite.email; - if (!invite.email.includes("Failed to send to")) { - email = `Sent to ${email}`; + private _codeLink: string; + + private _expiresIn: string; + get expiresIn(): string { return this._expiresIn } + set expiresIn(expiry: string) { + this._expiresIn = expiry; + this._infoArea.querySelector("span.inv-expiry").textContent = expiry; + } + + private _remainingUses: string = "1"; + get remainingUses(): string { return this._remainingUses; } + set remainingUses(remaining: string) { + this._remainingUses = remaining; + this._middle.querySelector("strong.inv-remaining").textContent = remaining; + } + + private _email: string = ""; + get email(): string { return this._email }; + set email(address: string) { + this._email = address; + const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement; + const icon = container.querySelector("i"); + const chip = container.querySelector("span.inv-email-chip"); + const tooltip = container.querySelector("span.content") as HTMLSpanElement; + if (address == "") { + container.classList.remove("mr-1"); + icon.classList.remove("ri-mail-line"); + icon.classList.remove("ri-mail-close-line"); + chip.classList.remove("~neutral"); + chip.classList.remove("~critical"); + chip.classList.remove("chip"); + } else { + container.classList.add("mr-1"); + chip.classList.add("chip"); + if (address.includes("Failed to send to")) { + icon.classList.remove("ri-mail-line"); + icon.classList.add("ri-mail-close-line"); + chip.classList.remove("~neutral"); + chip.classList.add("~critical"); + } else { + address = "Sent to " + address; + icon.classList.remove("ri-mail-close-line"); + icon.classList.add("ri-mail-line"); + chip.classList.remove("~critical"); + chip.classList.add("~neutral"); + } + } + tooltip.textContent = address; + } + + private _usedBy: string[][]; + get usedBy(): string[][] { return this._usedBy; } + set usedBy(uB: string[][]) { + // ub[i][0]: username, ub[i][1]: date + this._usedBy = uB; + if (uB.length == 0) { + this._right.classList.add("empty"); + this._userTable.innerHTML = `

None yet!

`; + return; + } + this._right.classList.remove("empty"); + let innerHTML = ` + + + + + + + + + `; + for (let user of uB) { + innerHTML += ` + + + + + `; } innerHTML += ` - ${email} + +
NameDate
${user[0]}${user[1]}
`; + this._userTable.innerHTML = innerHTML; } - innerHTML += ` -
-
- ${invite.expiresIn} -
- - -
-
- `; - item.innerHTML = innerHTML; - container.appendChild(item); + private _created: string; + get created(): string { return this._created; } + set created(created: string) { + this._created = created; + this._middle.querySelector("strong.inv-created").textContent = created; + } - let profiles = ` - - `; - - let dateCreated: string; - if (invite.created) { - dateCreated = `
  • Created: ${invite.created}
  • `; - } - - let middle: string; - if (window.notifications_enabled) { - middle = ` -
    -
      - Notify on: -
    • - - -
    • -
    • - - -
    • -
    -
    - `; - } - - let right: string = genUsedBy(invite.usedBy) - - const dropdown = document.createElement('div') as HTMLDivElement; - dropdown.id = `${CSS.escape(invite.code)}_collapse`; - dropdown.classList.add("collapse"); - dropdown.innerHTML = ` -
    -
    -
      -
    • - ${profiles} -
    • - ${dateCreated} -
    • Remaining uses: ${invite.remainingUses}
    • -
    -
    - ${middle} -
    - ${right} -
    -
    - `; - - container.appendChild(dropdown); - links.appendChild(container); -} - -function parseInvite(invite: Object): Invite { - let inv: Invite = { code: invite["code"], empty: false, }; - if (invite["email"]) { - inv.email = invite["email"]; - } - let time = "" - const f = ["days", "hours", "minutes"]; - for (const i in f) { - if (invite[f[i]] != 0) { - time += `${invite[f[i]]}${f[i][0]} `; + let innerHTML = ``; + for (let profile of window.availableProfiles) { + innerHTML += ``; } - } - inv.expiresIn = `Expires in ${time.slice(0, -1)}`; - if (invite["no-limit"]) { - inv.remainingUses = "∞"; - } else if ("remaining-uses" in invite) { - inv.remainingUses = invite["remaining-uses"]; - } - if ("used-by" in invite) { - inv.usedBy = invite["used-by"]; - } - if ("created" in invite) { - inv.created = invite["created"]; - } - if ("notify-expiry" in invite) { - inv.notifyExpiry = invite["notify-expiry"]; - } - if ("notify-creation" in invite) { - inv.notifyCreation = invite["notify-creation"]; - } - if ("profile" in invite) { - inv.profile = invite["profile"]; - } - return inv; -} - -window.setNotify = (el: HTMLElement): void => { - let send = {}; - let code: string; - let notifyType: string; - if (el.id.includes("Expiry")) { - code = el.id.replace("_notifyExpiry", ""); - notifyType = "notify-expiry"; - } else if (el.id.includes("Creation")) { - code = el.id.replace("_notifyCreation", ""); - notifyType = "notify-creation"; - } - send[code] = {}; - send[code][notifyType] = (el as HTMLInputElement).checked; - _post("/invites/notify", send, function (): void { - if (this.readyState == 4 && this.status != 200) { - (el as HTMLInputElement).checked = !(el as HTMLInputElement).checked; - } - }); -} - -function updateInvite(invite: Invite): void { - document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn; - const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses"); - if (remainingUses) { - remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`; - } - document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy); -} - -// delete invite from DOM -const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove(); - -// delete invite from jfa-go -window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void { - if (this.readyState == 4) { - generateInvites(); - } -}); - -export function generateInvites(empty?: boolean): void { - if (empty) { - document.getElementById('invites').textContent = ''; - addItem(emptyInvite()); - return; - } - _get("/invites", null, function (): void { - if (this.readyState == 4) { - let data = this.response; - window.availableProfiles = data['profiles']; - const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement; - let innerHTML = ""; - for (let i = 0; i < window.availableProfiles.length; i++) { - const profile = window.availableProfiles[i]; - innerHTML += ` - - `; + select.innerHTML = innerHTML; + this._profile = selected; + }; + updateProfile = () => { + const select = this._left.querySelector("select") as HTMLSelectElement; + const previous = this.profile; + let profile = select.value; + if (profile == "noProfile") { profile = ""; } + _post("/invites/profile", { "invite": this.code, "profile": profile }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (!(req.status == 200 || req.status == 204)) { + select.value = previous || "noProfile"; + } else { + this._profile = profile; + } } + }); + } + + private _container: HTMLDivElement; + + private _header: HTMLDivElement; + private _codeArea: HTMLDivElement; + private _infoArea: HTMLDivElement; + + private _details: HTMLDivElement; + private _left: HTMLDivElement; + private _middle: HTMLDivElement; + private _right: HTMLDivElement; + private _userTable: HTMLDivElement; + + // whether the details card is expanded. + get expanded(): boolean { + return this._details.classList.contains("focused"); + } + set expanded(state: boolean) { + const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement); + if (state) { + this._details.classList.remove("unfocused"); + this._details.classList.add("focused"); + toggle.previousElementSibling.classList.add("rotated"); + toggle.previousElementSibling.classList.remove("not-rotated"); + } else { + this._details.classList.add("unfocused"); + this._details.classList.remove("focused"); + toggle.previousElementSibling.classList.remove("rotated"); + toggle.previousElementSibling.classList.add("not-rotated"); + } + } + + constructor(invite: Invite) { + // first create the invite structure, then use our setter methods to fill in the data. + this._container = document.createElement('div') as HTMLDivElement; + this._container.classList.add("inv"); + + this._header = document.createElement('div') as HTMLDivElement; + this._container.appendChild(this._header); + this._header.classList.add("card", "~neutral", "!normal", "inv-header", "elem-pad", "no-pad", "flex-expand", "row", "mt-half", "overflow-y"); + + this._codeArea = document.createElement('div') as HTMLDivElement; + this._header.appendChild(this._codeArea); + this._codeArea.classList.add("inv-codearea"); + this._codeArea.innerHTML = ` + + + `; + const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement; + copyButton.onclick = () => { + toClipboard(this._codeLink); + const icon = copyButton.children[0]; + icon.classList.remove("ri-file-copy-line"); + icon.classList.add("ri-check-line"); + copyButton.classList.remove("~info"); + copyButton.classList.add("~positive"); + setTimeout(() => { + icon.classList.remove("ri-check-line"); + icon.classList.add("ri-file-copy-line"); + copyButton.classList.remove("~positive"); + copyButton.classList.add("~info"); + }, 800); + }; + + this._infoArea = document.createElement('div') as HTMLDivElement; + this._header.appendChild(this._infoArea); + this._infoArea.classList.add("inv-infoarea"); + this._infoArea.innerHTML = ` +
    + + +
    + + Delete + + `; + + (this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete; + + const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement); + toggle.onchange = () => { this.expanded = !this.expanded; }; + + this._details = document.createElement('div') as HTMLDivElement; + this._container.appendChild(this._details); + this._details.classList.add("card", "~neutral", "!normal", "mt-half", "no-pad", "inv-details"); + const detailsInner = document.createElement('div') as HTMLDivElement; + this._details.appendChild(detailsInner); + detailsInner.classList.add("inv-row", "flex-expand", "row", "elem-pad", "align-top"); + + this._left = document.createElement('div') as HTMLDivElement; + detailsInner.appendChild(this._left); + this._left.classList.add("inv-profilearea"); + let innerHTML = ` +

    Profile

    +
    + +
    + `; + if (window.notificationsEnabled) { innerHTML += ` - +

    Notify on:

    + + `; - Profiles.innerHTML = innerHTML; - if (data['invites'] == null || data['invites'].length == 0) { - document.getElementById('invites').textContent = ''; - addItem(emptyInvite()); + } + this._left.innerHTML = innerHTML; + (this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile; + + if (window.notificationsEnabled) { + const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement; + notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); }; + + const notifyCreation = this._left.querySelector("input.inv-notify-creation") as HTMLInputElement; + notifyCreation.onchange = () => { this._notifyCreation = notifyCreation.checked; this.updateNotify(notifyCreation); }; + } + + this._middle = document.createElement('div') as HTMLDivElement; + detailsInner.appendChild(this._middle); + this._middle.classList.add("block"); + this._middle.innerHTML = ` +

    Created

    +

    Remaining uses

    + `; + + this._right = document.createElement('div') as HTMLDivElement; + detailsInner.appendChild(this._right); + this._right.classList.add("card", "~neutral", "!low", "inv-created-users"); + this._right.innerHTML = `Created users`; + this._userTable = document.createElement('div') as HTMLDivElement; + this._right.appendChild(this._userTable); + + + this.expanded = false; + this.update(invite); + + document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); + } + + update = (invite: Invite) => { + this.code = invite.code; + this.created = invite.created; + this.email = invite.email; + this.expiresIn = invite.expiresIn; + if (window.notificationsEnabled) { + this.notifyCreation = invite.notifyCreation; + this.notifyExpiry = invite.notifyExpiry; + } + this.profile = invite.profile; + this.remainingUses = invite.remainingUses; + this.usedBy = invite.usedBy; + } + + asElement = (): HTMLDivElement => { return this._container; } + + remove = () => { this._container.remove(); } +} + +export class inviteList implements inviteList { + private _list: HTMLDivElement; + private _empty: boolean; + // since invite reload sends profiles, this event it broadcast so the createInvite object can load them. + private _profileLoadEvent = new CustomEvent("profileLoadEvent"); + + invites: { [code: string]: DOMInvite }; + + constructor() { + this._list = document.getElementById('invites') as HTMLDivElement; + this.empty = true; + this.invites = {}; + document.addEventListener("newInviteEvent", () => { this.reload(); }, false); + document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => { + const code = event.detail; + const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys + delete this.invites[code]; + if (length == 0) { + this.empty = true; + } + }, false); + } + + get empty(): boolean { return this._empty; } + set empty(state: boolean) { + this._empty = state; + if (state) { + this.invites = {}; + this._list.classList.add("empty"); + this._list.innerHTML = ` +
    +
    +
    + None +
    +
    +
    + `; + } else { + this._list.classList.remove("empty"); + if (this._list.querySelector(".inv-empty")) { + this._list.textContent = ''; + } + } + } + + add = (invite: Invite) => { + let domInv = new DOMInvite(invite); + this.invites[invite.code] = domInv; + if (this.empty) { this.empty = false; } + this._list.appendChild(domInv.asElement()); + } + + reload = () => _get("/invites", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + let data = req.response; + if (req.status == 200) { + window.availableProfiles = data["profiles"]; + document.dispatchEvent(this._profileLoadEvent); + } + if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) { + this.empty = true; return; } - let items = document.getElementById('invites').children; - for (const i in data['invites']) { - let match = false; - const inv = parseInvite(data['invites'][i]); - for (const x in items) { - if (items[x].id == inv.code) { - match = true; - updateInvite(inv); - break; - } - } - if (!match) { - addItem(inv); + // get a list of all current inv codes on dom + // every time we find a match in resp, delete from list + // at end delete all remaining in list from dom + let invitesOnDOM: { [code: string]: boolean } = {}; + for (let code in this.invites) { invitesOnDOM[code] = true; } + for (let inv of (data["invites"] as Array)) { + const invite = parseInvite(inv); + if (invite.code in this.invites) { + this.invites[invite.code].update(invite); + delete invitesOnDOM[invite.code]; + } else { + this.add(invite); } } - // second pass to check for expired invites - items = document.getElementById('invites').children; - for (let i = 0; i < items.length; i++) { - let exists = false; - for (const x in data['invites']) { - if (items[i].id == data['invites'][x]['code']) { - exists = true; - break; - } - } - if (!exists) { - hideInvite(items[i].id); - } + for (let code in invitesOnDOM) { + this.invites[code].remove(); + delete this.invites[code]; } } - }); + }) +} + + +function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite { + let parsed: Invite = {}; + parsed.code = invite["code"] as string; + parsed.email = invite["email"] as string || ""; + let time = ""; + const fields = ["days", "hours", "minutes"]; + for (let i = 0; i < fields.length; i++) { + if (invite[fields[i]] != 0) { + time += `${invite[fields[i]]}${fields[i][0]} `; + } + } + parsed.expiresIn = `Expires in ${time.slice(0, -1)}`; + parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"]) + parsed.usedBy = invite["used-by"] as string[][] || []; + parsed.created = invite["created"] as string || "Unknown"; + parsed.profile = invite["profile"] as string || ""; + parsed.notifyExpiry = invite["notify-expiry"] as boolean || false; + parsed.notifyCreation = invite["notify-creation"] as boolean || false; + return parsed; } -export const addOptions = (length: number, el: HTMLSelectElement): void => { - for (let v = 0; v <= length; v++) { - const opt = document.createElement('option'); - opt.textContent = ""+v; - opt.value = ""+v; - el.appendChild(opt); - } - el.value = "0"; -}; +export class createInvite { + private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement; + private _sendTo = document.getElementById("create-send-to") as HTMLInputElement; + private _uses = document.getElementById('create-uses') as HTMLInputElement; + private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; + private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement; + private _createButton = document.getElementById("create-submit") as HTMLSpanElement; + private _profile = document.getElementById("create-profile") as HTMLSelectElement; -export function checkDuration(): void { - const boxVals: Array = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value]; - const submit = document.getElementById('generateSubmit') as HTMLButtonElement; - if (boxVals.reduce((a: number, b: number): number => a + b) == 0) { - submit.disabled = true; - } else { - submit.disabled = false; + private _days = document.getElementById("create-days") as HTMLSelectElement; + private _hours = document.getElementById("create-hours") as HTMLSelectElement; + private _minutes = document.getElementById("create-minutes") as HTMLSelectElement; + + // Broadcast when new invite created + private _newInviteEvent = new CustomEvent("newInviteEvent"); + private _firstLoad = true; + + private _count: Number = 30; + private _populateNumbers = () => { + const fieldIDs = ["create-days", "create-hours", "create-minutes"]; + for (let i = 0; i < fieldIDs.length; i++) { + const field = document.getElementById(fieldIDs[i]); + field.textContent = ''; + for (let n = 0; n <= this._count; n++) { + const opt = document.createElement("option") as HTMLOptionElement; + opt.textContent = ""+n; + opt.value = ""+n; + field.appendChild(opt); + } + } + } + + get sendToEnabled(): boolean { + return this._sendToEnabled.checked; + } + set sendToEnabled(state: boolean) { + this._sendToEnabled.checked = state; + this._sendTo.disabled = !state; + if (state) { + this._sendToEnabled.parentElement.classList.remove("~neutral"); + this._sendToEnabled.parentElement.classList.add("~urge"); + } else { + this._sendToEnabled.parentElement.classList.remove("~urge"); + this._sendToEnabled.parentElement.classList.add("~neutral"); + } + } + + get infiniteUses(): boolean { + return this._infUses.checked; + } + set infiniteUses(state: boolean) { + this._infUses.checked = state; + this._uses.disabled = state; + if (state) { + this._infUses.parentElement.classList.remove("~neutral"); + this._infUses.parentElement.classList.add("~urge"); + this._infUsesWarning.classList.remove("unfocused"); + } else { + this._infUses.parentElement.classList.remove("~urge"); + this._infUses.parentElement.classList.add("~neutral"); + this._infUsesWarning.classList.add("unfocused"); + } + } + + get uses(): number { return this._uses.valueAsNumber; } + set uses(n: number) { this._uses.valueAsNumber = n; } + + private _checkDurationValidity = () => { + if (this.days + this.hours + this.minutes == 0) { + this._createButton.setAttribute("disabled", ""); + this._createButton.onclick = null; + } else { + this._createButton.removeAttribute("disabled"); + this._createButton.onclick = this.create; + } + } + + get days(): number { + return +this._days.value; + } + set days(n: number) { + this._days.value = ""+n; + this._checkDurationValidity(); + } + get hours(): number { + return +this._hours.value; + } + set hours(n: number) { + this._hours.value = ""+n; + this._checkDurationValidity(); + } + get minutes(): number { + return +this._minutes.value; + } + set minutes(n: number) { + this._minutes.value = ""+n; + this._checkDurationValidity(); + } + + get sendTo(): string { return this._sendTo.value; } + set sendTo(address: string) { this._sendTo.value = address; } + + get profile(): string { + const val = this._profile.value; + if (val == "noProfile") { + return ""; + } + return val; + } + set profile(p: string) { + if (p == "") { p = "noProfile"; } + this._profile.value = p; + } + + loadProfiles = () => { + let innerHTML = ``; + for (let profile of window.availableProfiles) { + innerHTML += ``; + } + let selected = this.profile; + this._profile.innerHTML = innerHTML; + if (this._firstLoad) { + this.profile = window.availableProfiles[0] || ""; + this._firstLoad = false; + } else { + this.profile = selected; + } + } + + create = () => { + toggleLoader(this._createButton); + let send = { + "days": this.days, + "hours": this.hours, + "minutes": this.minutes, + "multiple-uses": (this.uses > 1 || this.infiniteUses), + "no-limit": this.infiniteUses, + "remaining-uses": this.uses, + "email": this.sendToEnabled ? this.sendTo : "", + "profile": this.profile + }; + _post("/invites", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + document.dispatchEvent(this._newInviteEvent); + } + toggleLoader(this._createButton); + } + }); + } + + constructor() { + this._populateNumbers(); + this.days = 0; + this.hours = 0; + this.minutes = 30; + this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; }; + this.infiniteUses = false; + this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; }; + this.sendToEnabled = false; + this._createButton.onclick = this.create; + this.sendTo = ""; + this.uses = 1; + + this._days.onchange = this._checkDurationValidity; + this._hours.onchange = this._checkDurationValidity; + this._minutes.onchange = this._checkDurationValidity; + document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); + + if (!window.emailEnabled) { + document.getElementById("create-send-to-container").classList.add("unfocused"); + } } } + + + diff --git a/ts/modules/modal.ts b/ts/modules/modal.ts new file mode 100644 index 0000000..398df93 --- /dev/null +++ b/ts/modules/modal.ts @@ -0,0 +1,42 @@ +declare var window: Window; + +export class Modal implements Modal { + modal: HTMLElement; + closeButton: HTMLSpanElement; + constructor(modal: HTMLElement, important: boolean = false) { + this.modal = modal; + const closeButton = this.modal.querySelector('span.modal-close') + if (closeButton !== null) { + this.closeButton = closeButton as HTMLSpanElement; + this.closeButton.onclick = this.close; + } + if (!important) { + window.addEventListener('click', (event: Event) => { + if (event.target == this.modal) { this.close(); } + }); + } + } + close = (event?: Event) => { + if (event) { + event.preventDefault(); + } + this.modal.classList.add('modal-hiding'); + const modal = this.modal; + const listenerFunc = function () { + modal.classList.remove('modal-shown'); + modal.classList.remove('modal-hiding'); + modal.removeEventListener(window.animationEvent, listenerFunc); + }; + this.modal.addEventListener(window.animationEvent, listenerFunc, false); + } + show = () => { + this.modal.classList.add('modal-shown'); + } + toggle = () => { + if (this.modal.classList.contains('modal-shown')) { + this.close(); + } else { + this.show(); + } + } +} diff --git a/ts/modules/profiles.ts b/ts/modules/profiles.ts new file mode 100644 index 0000000..c2f5296 --- /dev/null +++ b/ts/modules/profiles.ts @@ -0,0 +1,204 @@ +import { _get, _post, _delete, toggleLoader } from "../modules/common.js"; + +interface Profile { + admin: boolean; + libraries: string; + fromUser: string; +} + +class profile implements Profile { + private _row: HTMLTableRowElement; + private _name: HTMLElement; + private _adminChip: HTMLSpanElement; + private _libraries: HTMLTableDataCellElement; + private _fromUser: HTMLTableDataCellElement; + private _defaultRadio: HTMLInputElement; + + get name(): string { return this._name.textContent; } + set name(v: string) { this._name.textContent = v; } + + get admin(): boolean { return this._adminChip.classList.contains("chip"); } + set admin(state: boolean) { + if (state) { + this._adminChip.classList.add("chip", "~info", "ml-half"); + this._adminChip.textContent = "Admin"; + } else { + this._adminChip.classList.remove("chip", "~info", "ml-half"); + this._adminChip.textContent = ""; + } + } + + get libraries(): string { return this._libraries.textContent; } + set libraries(v: string) { this._libraries.textContent = v; } + + get fromUser(): string { return this._fromUser.textContent; } + set fromUser(v: string) { this._fromUser.textContent = v; } + + get default(): boolean { return this._defaultRadio.checked; } + set default(v: boolean) { this._defaultRadio.checked = v; } + + constructor(name: string, p: Profile) { + this._row = document.createElement("tr") as HTMLTableRowElement; + this._row.innerHTML = ` + + + + + Delete + `; + this._name = this._row.querySelector("b.profile-name"); + this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; + this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement; + this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement; + this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement; + this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name })); + (this._row.querySelector("span.button") as HTMLSpanElement).onclick = this.delete; + + this.update(name, p); + } + + update = (name: string, p: Profile) => { + this.name = name; + this.admin = p.admin; + this.fromUser = p.fromUser; + this.libraries = p.libraries; + } + + remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); } + + delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + this.remove(); + } else { + window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`); + } + } + }) + + asElement = (): HTMLTableRowElement => { return this._row; } +} + +interface profileResp { + default_profile: string; + profiles: { [name: string]: Profile }; +} + +export class ProfileEditor { + private _table = document.getElementById("table-profiles") as HTMLTableElement; + private _createButton = document.getElementById("button-profile-create") as HTMLSpanElement; + private _profiles: { [name: string]: profile } = {}; + private _default: string; + + private _createForm = document.getElementById("form-add-profile") as HTMLFormElement; + private _profileName = document.getElementById("add-profile-name") as HTMLInputElement; + private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement; + private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement; + + get empty(): boolean { return (Object.keys(this._table.children).length == 0) } + set empty(state: boolean) { + if (state) { + this._table.innerHTML = `None` + } else if (this._table.querySelector("td.empty")) { + this._table.textContent = ``; + } + } + + get default(): string { return this._default; } + set default(v: string) { + this._default = v; + if (v != "") { this._profiles[v].default = true; } + for (let name in this._profiles) { + if (name != v) { this._profiles[name].default = false; } + } + } + + load = () => _get("/profiles", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + let resp = req.response as profileResp; + if (Object.keys(resp.profiles).length == 0) { + this.empty = true; + } else { + this.empty = false; + for (let name in resp.profiles) { + if (name in this._profiles) { + this._profiles[name].update(name, resp.profiles[name]); + } else { + this._profiles[name] = new profile(name, resp.profiles[name]); + this._table.appendChild(this._profiles[name].asElement()); + } + } + } + this.default = resp.default_profile; + window.modals.profiles.show(); + } else { + window.notifications.customError("profileEditor", "Failed to load profiles."); + } + } + }) + + constructor() { + (document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load; + document.addEventListener("profiles-default", (event: CustomEvent) => { + const prevDefault = this.default; + const newDefault = event.detail; + _post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + this.default = newDefault; + } else { + this.default = prevDefault; + window.notifications.customError("profileDefault", "Failed to set default profile."); + } + } + }); + }); + document.addEventListener("profiles-delete", (event: CustomEvent) => { + delete this._profiles[event.detail]; + this.load(); + }); + + this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + let innerHTML = ``; + for (let user of req.response["users"]) { + innerHTML += ``; + } + this._userSelect.innerHTML = innerHTML; + this._storeHomescreen.checked = true; + window.modals.profiles.close(); + window.modals.addProfile.show(); + } else { + window.notifications.customError("loadUsers", "Failed to load users."); + } + } + }); + + this._createForm.onsubmit = (event: SubmitEvent) => { + event.preventDefault(); + const button = this._createForm.querySelector("span.submit") as HTMLSpanElement; + toggleLoader(button); + let send = { + "homescreen": this._storeHomescreen.checked, + "id": this._userSelect.value, + "name": this._profileName.value + } + _post("/profiles", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + window.modals.addProfile.close(); + if (req.status == 200 || req.status == 204) { + this.load(); + window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`); + } else { + window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`); + } + window.modals.profiles.show(); + } + }) + }; + + } +} diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 642b148..0467dcd 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -1,164 +1,614 @@ -import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js"; -import { Focus, Unfocus } from "../modules/admin.js"; +import { _get, _post, toggleLoader } from "../modules/common.js"; -interface Profile { - Admin: boolean; - LibraryAccess: string; - FromUser: string; +interface settingsBoolEvent extends Event { + detail: boolean; } -export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void { - if (this.readyState == 4 && this.status == 200) { - const profileList = document.getElementById('profileList'); - profileList.textContent = ''; - window.availableProfiles = [this.response["default_profile"]]; - for (let name in this.response["profiles"]) { - if (name != window.availableProfiles[0]) { - window.availableProfiles.push(name); - } - const reqProfile = this.response["profiles"][name]; - if (!noTable && name != "default_profile") { - const profile: Profile = { - Admin: reqProfile["admin"], - LibraryAccess: reqProfile["libraries"], - FromUser: reqProfile["fromUser"] - }; - profileList.innerHTML += ` - ${name} - - ${profile.FromUser} - ${profile.Admin ? "Yes" : "No"} - ${profile.LibraryAccess} - - `; - } +interface Meta { + name: string; + description: string; +} + +interface Setting { + name: string; + description: string; + required: boolean; + requires_restart: boolean; + type: string; + value: string | boolean | number; + depends_true?: Setting; + depends_false?: Setting; + + asElement: () => HTMLElement; + update: (s: Setting) => void; +} + +class DOMInput { + protected _input: HTMLInputElement; + private _container: HTMLDivElement; + private _tooltip: HTMLDivElement; + private _required: HTMLSpanElement; + private _restart: HTMLSpanElement; + + get name(): string { return this._container.querySelector("span.setting-label").textContent; } + set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; } + + get description(): string { return this._tooltip.querySelector("span.content").textContent; } + set description(d: string) { + const content = this._tooltip.querySelector("span.content") as HTMLSpanElement; + content.textContent = d; + if (d == "") { + this._tooltip.classList.add("unfocused"); + } else { + this._tooltip.classList.remove("unfocused"); } } -}); -export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void { - if (this.readyState == 4 && this.status == 200) { - settingsList.textContent = ''; - window.config = this.response; - for (const i in window.config["order"]) { - const section: string = window.config["order"][i] - const sectionCollapse = document.createElement('div') as HTMLDivElement; - Unfocus(sectionCollapse); - sectionCollapse.id = section; + get required(): boolean { return this._required.classList.contains("badge"); } + set required(state: boolean) { + if (state) { + this._required.classList.add("badge", "~critical"); + this._required.textContent = "*"; + } else { + this._required.classList.remove("badge", "~critical"); + this._required.textContent = ""; + } + } + + get requires_restart(): boolean { return this._restart.classList.contains("badge"); } + set requires_restart(state: boolean) { + if (state) { + this._restart.classList.add("badge", "~info"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~info"); + this._restart.textContent = ""; + } + } - const title: string = window.config[section]["meta"]["name"]; - const description: string = window.config[section]["meta"]["description"]; - const entryListID: string = `${section}_entryList`; - // const footerID: string = `${section}_footer`; - - sectionCollapse.innerHTML = ` -
    - ${description} -
    -
    + constructor(inputType: string, setting: Setting, section: string, name: string) { + this._container = document.createElement("div"); + this._container.classList.add("setting"); + this._container.innerHTML = ` + + `; + this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement; + this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement; + this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement; + this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement; + if (setting.depends_false || setting.depends_true) { + let dependant = setting.depends_true || setting.depends_false; + let state = true; + if (setting.depends_false) { state = false; } + document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => { + this._input.disabled = (event.detail !== state); + }); + } + const onValueChange = () => { + const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value }) + document.dispatchEvent(event); + if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } + }; + this._input.onchange = onValueChange; + this.update(setting); + } - for (const x in config[section]["order"]) { - const entry: string = config[section]["order"][x]; - if (entry == "meta") { - continue; + get value(): any { return this._input.value; } + set value(v: any) { this._input.value = v; } + + update = (s: Setting) => { + this.name = s.name; + this.description = s.description; + this.required = s.required; + this.requires_restart = s.requires_restart; + this.value = s.value; + } + + asElement = (): HTMLDivElement => { return this._container; } +} + +interface SText extends Setting { + value: string; +} +class DOMText extends DOMInput implements SText { + constructor(setting: Setting, section: string, name: string) { super("text", setting, section, name); } + type: string = "text"; + get value(): string { return this._input.value } + set value(v: string) { this._input.value = v; } +} + +interface SPassword extends Setting { + value: string; +} +class DOMPassword extends DOMInput implements SPassword { + constructor(setting: Setting, section: string, name: string) { super("password", setting, section, name); } + type: string = "password"; + get value(): string { return this._input.value } + set value(v: string) { this._input.value = v; } +} + +interface SEmail extends Setting { + value: string; +} +class DOMEmail extends DOMInput implements SEmail { + constructor(setting: Setting, section: string, name: string) { super("email", setting, section, name); } + type: string = "email"; + get value(): string { return this._input.value } + set value(v: string) { this._input.value = v; } +} + +interface SNumber extends Setting { + value: number; +} +class DOMNumber extends DOMInput implements SNumber { + constructor(setting: Setting, section: string, name: string) { super("number", setting, section, name); } + type: string = "number"; + get value(): number { return +this._input.value; } + set value(v: number) { this._input.value = ""+v; } +} + +interface SBool extends Setting { + value: boolean; +} +class DOMBool implements SBool { + protected _input: HTMLInputElement; + private _container: HTMLDivElement; + private _tooltip: HTMLDivElement; + private _required: HTMLSpanElement; + private _restart: HTMLSpanElement; + type: string = "bool"; + + get name(): string { return this._container.querySelector("span.setting-label").textContent; } + set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; } + + get description(): string { return this._tooltip.querySelector("span.content").textContent; } + set description(d: string) { + const content = this._tooltip.querySelector("span.content") as HTMLSpanElement; + content.textContent = d; + if (d == "") { + this._tooltip.classList.add("unfocused"); + } else { + this._tooltip.classList.remove("unfocused"); + } + } + + get required(): boolean { return this._required.classList.contains("badge"); } + set required(state: boolean) { + if (state) { + this._required.classList.add("badge", "~critical"); + this._required.textContent = "*"; + } else { + this._required.classList.remove("badge", "~critical"); + this._required.textContent = ""; + } + } + + get requires_restart(): boolean { return this._restart.classList.contains("badge"); } + set requires_restart(state: boolean) { + if (state) { + this._restart.classList.add("badge", "~info"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~info"); + this._restart.textContent = ""; + } + } + get value(): boolean { return this._input.checked; } + set value(state: boolean) { this._input.checked = state; } + constructor(setting: SBool, section: string, name: string) { + this._container = document.createElement("div"); + this._container.classList.add("setting"); + this._container.innerHTML = ` + + `; + this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement; + this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement; + this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement; + this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement; + const onValueChange = () => { + const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value }) + document.dispatchEvent(event); + }; + this._input.onchange = () => { + onValueChange(); + if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } + }; + document.addEventListener(`settings-loaded`, onValueChange); + + if (setting.depends_false || setting.depends_true) { + let dependant = setting.depends_true || setting.depends_false; + let state = true; + if (setting.depends_false) { state = false; } + document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => { + this._input.disabled = (event.detail !== state); + }); + } + this.update(setting); + } + update = (s: SBool) => { + this.name = s.name; + this.description = s.description; + this.required = s.required; + this.requires_restart = s.requires_restart; + this.value = s.value; + } + + asElement = (): HTMLDivElement => { return this._container; } +} + +interface SSelect extends Setting { + options: string[]; + value: string; +} +class DOMSelect implements SSelect { + protected _select: HTMLSelectElement; + private _container: HTMLDivElement; + private _tooltip: HTMLDivElement; + private _required: HTMLSpanElement; + private _restart: HTMLSpanElement; + private _options: string[]; + type: string = "bool"; + + get name(): string { return this._container.querySelector("span.setting-label").textContent; } + set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; } + + get description(): string { return this._tooltip.querySelector("span.content").textContent; } + set description(d: string) { + const content = this._tooltip.querySelector("span.content") as HTMLSpanElement; + content.textContent = d; + if (d == "") { + this._tooltip.classList.add("unfocused"); + } else { + this._tooltip.classList.remove("unfocused"); + } + } + + get required(): boolean { return this._required.classList.contains("badge"); } + set required(state: boolean) { + if (state) { + this._required.classList.add("badge", "~critical"); + this._required.textContent = "*"; + } else { + this._required.classList.remove("badge", "~critical"); + this._required.textContent = ""; + } + } + + get requires_restart(): boolean { return this._restart.classList.contains("badge"); } + set requires_restart(state: boolean) { + if (state) { + this._restart.classList.add("badge", "~info"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~info"); + this._restart.textContent = ""; + } + } + get value(): string { return this._select.value; } + set value(v: string) { this._select.value = v; } + + get options(): string[] { return this._options; } + set options(opt: string[]) { + this._options = opt; + let innerHTML = ""; + for (let option of this._options) { + innerHTML += ``; + } + this._select.innerHTML = innerHTML; + } + + constructor(setting: SSelect, section: string, name: string) { + this._options = []; + this._container = document.createElement("div"); + this._container.classList.add("setting"); + this._container.innerHTML = ` + + `; + this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement; + this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement; + this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement; + this._select = this._container.querySelector("select.settings-select") as HTMLSelectElement; + if (setting.depends_false || setting.depends_true) { + let dependant = setting.depends_true || setting.depends_false; + let state = true; + if (setting.depends_false) { state = false; } + document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => { + this._input.disabled = (event.detail !== state); + }); + } + const onValueChange = () => { + const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value }) + document.dispatchEvent(event); + if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } + }; + this._select.onchange = onValueChange; + this.update(setting); + } + update = (s: SSelect) => { + this.name = s.name; + this.description = s.description; + this.required = s.required; + this.requires_restart = s.requires_restart; + this.options = s.options; + this.value = s.value; + } + + asElement = (): HTMLDivElement => { return this._container; } +} + +interface Section { + meta: Meta; + order: string[]; + settings: { [settingName: string]: Setting }; +} + +class sectionPanel { + private _section: HTMLDivElement; + private _settings: { [name: string]: Setting }; + private _sectionName: string; + values: { [field: string]: string } = {}; + + constructor(s: Section, sectionName: string) { + this._sectionName = sectionName; + this._settings = {}; + this._section = document.createElement("div") as HTMLDivElement; + this._section.classList.add("settings-section", "unfocused"); + this._section.innerHTML = ` + ${s.meta.name} +

    ${s.meta.description}

    + `; + this.update(s); + } + update = (s: Section) => { + for (let name of s.order) { + let setting: Setting = s.settings[name]; + if (name in this._settings) { + this._settings[name].update(setting); + } else { + switch (setting.type) { + case "text": + setting = new DOMText(setting, this._sectionName, name); + break; + case "password": + setting = new DOMPassword(setting, this._sectionName, name); + break; + case "email": + setting = new DOMEmail(setting, this._sectionName, name); + break; + case "number": + setting = new DOMNumber(setting, this._sectionName, name); + break; + case "bool": + setting = new DOMBool(setting as SBool, this._sectionName, name); + break; + case "select": + setting = new DOMSelect(setting as SSelect, this._sectionName, name); + break; } - let entryName: string = window.config[section][entry]["name"]; - let required = false; - if (window.config[section][entry]["required"]) { - entryName += ` *`; - required = true; - } - if (window.config[section][entry]["requires_restart"]) { - entryName += ` R`; - } - if ("description" in window.config[section][entry]) { - entryName +=` - - `; - } - const entryValue: boolean | string = window.config[section][entry]["value"]; - const entryType: string = window.config[section][entry]["type"]; - const entryGroup = document.createElement('div'); - if (entryType == "bool") { - entryGroup.classList.add("form-check"); - entryGroup.innerHTML = ` - - - `; - (entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void { - const me = this as HTMLInputElement; - for (const y in window.config["order"]) { - const sect: string = window.config["order"][y]; - for (const z in window.config[sect]["order"]) { - const ent: string = window.config[sect]["order"][z]; - if (`${sect}_${window.config[sect][ent]['depends_true']}` == me.id) { - (document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked); - } else if (`${sect}_${window.config[sect][ent]['depends_false']}` == me.id) { - (document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked; - } - } - } - }; - } else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) { - entryGroup.classList.add("form-group"); - entryGroup.innerHTML = ` - - - `; - } else if (entryType == 'select') { - entryGroup.classList.add("form-group"); - const entryOptions: Array = window.config[section][entry]["options"]; - let innerGroup = ` - - `; - entryGroup.innerHTML = innerGroup; - } - sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup); + this.values[name] = ""+setting.value; + document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => { + const oldValue = this.values[name]; + this.values[name] = ""+event.detail; + document.dispatchEvent(new CustomEvent("settings-section-changed")); + }); + this._section.appendChild(setting.asElement()); + this._settings[name] = setting; } - - settingsList.innerHTML += ` - - `; - settingsContent.appendChild(sectionCollapse); - } - if (callback) { - callback(); } } -}); + + get visible(): boolean { return !this._section.classList.contains("unfocused"); } + set visible(s: boolean) { + if (s) { + this._section.classList.remove("unfocused"); + } else { + this._section.classList.add("unfocused"); + } + } -export function showSetting(id: string, runBefore?: () => void): void { - const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf; - for (let i = 0; i < els.length; i++) { - const el = els[i]; - if (el.id != `${id}_button`) { - rmAttr(el, "active"); - } - const sectEl = document.getElementById(el.id.replace("_button", "")); - if (sectEl.id != id) { - Unfocus(sectEl); + asElement = (): HTMLDivElement => { return this._section; } +} + + + +interface Settings { + order: string[]; + sections: { [sectionName: string]: Section }; +} + +export class settingsList { + private _saveButton = document.getElementById("settings-save") as HTMLSpanElement; + private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement; + private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement; + + private _panel = document.getElementById("settings-panel") as HTMLDivElement; + private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement; + private _sections: { [name: string]: sectionPanel } + private _buttons: { [name: string]: HTMLSpanElement } + private _needsRestart: boolean = false; + + addSection = (name: string, s: Section) => { + const section = new sectionPanel(s, name); + this._sections[name] = section; + this._panel.appendChild(this._sections[name].asElement()); + const button = document.createElement("span") as HTMLSpanElement; + button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half"); + button.textContent = s.meta.name; + button.onclick = () => { this._showPanel(name); }; + this._buttons[name] = button; + this._sidebar.appendChild(this._buttons[name]); + } + + private _showPanel = (name: string) => { + for (let n in this._sections) { + if (n == name) { + this._sections[name].visible = true; + this._buttons[name].classList.add("selected"); + } else { + this._sections[n].visible = false; + this._buttons[n].classList.remove("selected"); + } } } - addAttr(document.getElementById(`${id}_button`), "active"); - const section = document.getElementById(id); - if (runBefore) { - runBefore(); + + private _save = () => { + let config = {}; + for (let name in this._sections) { + config[name] = this._sections[name].values; + } + if (this._needsRestart) { + this._saveRestart.onclick = () => { + config["restart-program"] = true; + this._send(config, () => { + window.modals.settingsRestart.close(); + window.modals.settingsRefresh.show(); + }); + }; + this._saveNoRestart.onclick = () => { + config["restart-program"] = false; + this._send(config, window.modals.settingsRestart.close); + } + window.modals.settingsRestart.show(); + } else { + this._send(config); + } + // console.log(config); } - Focus(section); - if (screen.width <= 1100) { - // ugly - setTimeout((): void => section.scrollIntoView({ block: "center", behavior: "smooth" }), 200); + + private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + window.notifications.customPositive("settingsSaved", "Success:", "settings were saved."); + } else { + window.notifications.customError("settingsSaved", "Couldn't save settings."); + } + this.reload(); + if (run) { run(); } + } + }); + + constructor() { + this._sections = {}; + this._buttons = {}; + document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused")); + this._saveButton.onclick = this._save; + document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; }); + + if (window.ombiEnabled) { + let ombi = new ombiDefaults(); + this._sidebar.appendChild(ombi.button()); + } + } + + reload = () => _get("/config", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status != 200) { + window.notifications.customError("settingsLoadError", "Failed to load settings."); + return; + } + let settings = req.response as Settings; + for (let name of settings.order) { + if (name in this._sections) { + this._sections[name].update(settings.sections[name]); + } else { + this.addSection(name, settings.sections[name]); + } + } + this._showPanel(settings.order[0]); + this._needsRestart = false; + document.dispatchEvent(new CustomEvent("settings-loaded")); + this._saveButton.classList.add("unfocused"); + } + }) +} + +interface ombiUser { + id: string; + name: string; +} + +class ombiDefaults { + private _form: HTMLFormElement; + private _button: HTMLSpanElement; + private _select: HTMLSelectElement; + private _users: { [id: string]: string } = {}; + constructor() { + this._button = document.createElement("span") as HTMLSpanElement; + this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half"); + this._button.innerHTML = `Ombi user defaults `; + this._button.onclick = this.load; + this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement; + this._form.onsubmit = this.send; + this._select = this._form.querySelector("select") as HTMLSelectElement; + } + button = (): HTMLSpanElement => { return this._button; } + send = () => { + const button = this._form.querySelector("span.submit") as HTMLSpanElement; + toggleLoader(button); + let resp = {} as ombiUser; + resp.id = this._select.value; + resp.name = this._users[resp.id]; + _post("/ombi/defaults", resp, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 200 || req.status == 204) { + window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults."); + } else { + window.notifications.customError("ombiDefaults", "Failed to store ombi defaults."); + } + window.modals.ombiDefaults.close(); + } + }); + } + + load = () => { + toggleLoader(this._button); + _get("/ombi/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 && "users" in req.response) { + const users = req.response["users"] as ombiUser[]; + let innerHTML = ""; + for (let user of users) { + this._users[user.id] = user.name; + innerHTML += ``; + } + this._select.innerHTML = innerHTML; + toggleLoader(this._button); + window.modals.ombiDefaults.show(); + } else { + toggleLoader(this._button); + window.notifications.customError("ombiLoadError", "Failed to load ombi users.") + } + } + }); } } + + + + + diff --git a/ts/modules/tabs.ts b/ts/modules/tabs.ts new file mode 100644 index 0000000..465d670 --- /dev/null +++ b/ts/modules/tabs.ts @@ -0,0 +1,40 @@ +export class Tabs implements Tabs { + private _current: string = ""; + tabs: Array; + + constructor() { + this.tabs = []; + } + + addTab = (tabID: string, preFunc = () => void {}, postFunc = () => void {}) => { + let tab = {} as Tab; + tab.tabID = tabID; + tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement; + tab.buttonEl = document.getElementById("button-tab-" + tabID) as HTMLSpanElement; + tab.buttonEl.onclick = () => { this.switch(tabID); }; + tab.preFunc = preFunc; + tab.postFunc = postFunc; + this.tabs.push(tab); + } + + get current(): string { return this._current; } + set current(tabID: string) { this.switch(tabID); } + + switch = (tabID: string, noRun: boolean = false) => { + this._current = tabID; + for (let t of this.tabs) { + if (t.tabID == tabID) { + t.buttonEl.classList.add("active", "~urge"); + if (t.preFunc && !noRun) { t.preFunc(); } + t.tabEl.classList.remove("unfocused"); + if (t.postFunc && !noRun) { t.postFunc(); } + document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID })); + } else { + t.buttonEl.classList.remove("active"); + t.buttonEl.classList.remove("~urge"); + t.tabEl.classList.add("unfocused"); + } + } + } +} + diff --git a/ts/modules/theme.ts b/ts/modules/theme.ts new file mode 100644 index 0000000..60d4e64 --- /dev/null +++ b/ts/modules/theme.ts @@ -0,0 +1,16 @@ +export function toggleTheme() { + document.documentElement.classList.toggle('dark-theme'); + document.documentElement.classList.toggle('light-theme'); + localStorage.setItem('theme', document.documentElement.classList.contains('dark-theme') ? "dark" : "light"); +} + +export function loadTheme() { + const theme = localStorage.getItem("theme"); + if (theme == "dark") { + document.documentElement.classList.add('dark-theme'); + document.documentElement.classList.remove('light-theme'); + } else if (theme == "light") { + document.documentElement.classList.add('light-theme'); + document.documentElement.classList.remove('dark-theme'); + } +} diff --git a/ts/ombi.ts b/ts/ombi.ts deleted file mode 100644 index b3ac076..0000000 --- a/ts/ombi.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { _get, _post, _delete, rmAttr, addAttr } from "./modules/common.js"; - -const ombiDefaultsModal = window.BS.newModal('ombiDefaults'); - -(document.getElementById('openOmbiDefaults') as HTMLButtonElement).onclick = function (): void { - let button = this as HTMLButtonElement; - button.disabled = true; - const ogHTML = button.innerHTML; - button.innerHTML = - '' + - 'Loading...'; - _get("/ombi/users", null, function (): void { - if (this.readyState == 4) { - if (this.status == 200) { - const users = this.response['users']; - const radioList = document.getElementById('ombiUserRadios'); - radioList.textContent = ''; - let first = true; - for (const i in users) { - const user = users[i]; - const radio = document.createElement('div') as HTMLDivElement; - radio.classList.add('form-check'); - let checked = ''; - if (first) { - checked = 'checked'; - first = false; - } - radio.innerHTML = ` - - - `; - radioList.appendChild(radio); - } - button.disabled = false; - button.innerHTML = ogHTML; - const submitButton = document.getElementById('storeOmbiDefaults') as HTMLButtonElement; - submitButton.disabled = false; - submitButton.textContent = 'Submit'; - addAttr(submitButton, "btn-primary"); - rmAttr(submitButton, "btn-success"); - rmAttr(submitButton, "btn-danger"); - ombiDefaultsModal.show(); - } - } - }); -}; - -(document.getElementById('storeOmbiDefaults') as HTMLButtonElement).onclick = function (): void { - let button = this as HTMLButtonElement; - button.disabled = true; - button.innerHTML = - '' + - 'Loading...'; - const radio = document.querySelector('input[name=ombiRadios]:checked') as HTMLInputElement; - const data = { - "id": radio.id.replace("ombiDefault_", "") - }; - _post("/ombi/defaults", data, function (): void { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - button.textContent = "Success"; - addAttr(button, "btn-success"); - rmAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - button.disabled = false; - setTimeout((): void => ombiDefaultsModal.hide(), 1000); - } else { - button.textContent = "Failed"; - rmAttr(button, "btn-primary"); - addAttr(button, "btn-danger"); - setTimeout((): void => { - button.textContent = "Submit"; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-danger"); - button.disabled = false; - }, 1000); - } - } - }); -}; - - - - diff --git a/ts/settings.ts b/ts/settings.ts deleted file mode 100644 index e5da911..0000000 --- a/ts/settings.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { _post, _get, _delete, rmAttr, addAttr } from "./modules/common.js"; -import { generateInvites } from "./modules/invites.js"; -import { populateRadios } from "./modules/accounts.js"; -import { Focus, Unfocus } from "./modules/admin.js"; -import { showSetting, populateProfiles } from "./modules/settings.js"; - -interface aWindow extends Window { - setDefaultProfile(name: string): void; - deleteProfile(name: string): void; - createProfile(): void; - showSetting(id: string, runBefore?: () => void): void; - config: Object; - modifiedConfig: Object; -} - -declare var window: aWindow; - -window.config = {}; -window.modifiedConfig = {}; - -window.showSetting = showSetting; - -function sendConfig(restart?: boolean): void { - window.modifiedConfig["restart-program"] = restart; - _post("/config", window.modifiedConfig, function (): void { - if (this.readyState == 4) { - const save = document.getElementById("settingsSave") as HTMLButtonElement - if (this.status == 200 || this.status == 204) { - save.textContent = "Success"; - addAttr(save, "btn-success"); - rmAttr(save, "btn-primary"); - setTimeout((): void => { - save.textContent = "Save"; - addAttr(save, "btn-primary"); - rmAttr(save, "btn-success"); - }, 1000); - } else { - save.textContent = "Save"; - } - if (restart) { - window.Modals.refresh.show(); - } - } - }); -} - -(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => { - window.Modals.about.show(); -}; - -(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles); - -window.setDefaultProfile = (name: string): void => _post("/profiles/default", { "name": name }, function (): void { - if (this.readyState == 4) { - if (this.status != 200) { - (document.getElementById(`defaultProfile_${window.availableProfiles[0]}`) as HTMLInputElement).checked = true; - (document.getElementById(`defaultProfile_${name}`) as HTMLInputElement).checked = false; - } else { - generateInvites(); - } - } -}); - -window.deleteProfile = (name: string): void => _delete("/profiles", { "name": name }, function (): void { - if (this.readyState == 4 && this.status == 200) { - populateProfiles(); - } -}); - -const createProfile = (): void => _get("/users", null, function (): void { - if (this.readyState == 4 && this.status == 200) { - window.jfUsers = this.response["users"]; - populateRadios(); - const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement; - submitButton.disabled = false; - submitButton.textContent = 'Create'; - addAttr(submitButton, "btn-primary"); - rmAttr(submitButton, "btn-danger"); - rmAttr(submitButton, "btn-success"); - document.getElementById('defaultsTitle').textContent = `Create Profile`; - document.getElementById('userDefaultsDescription').textContent = ` - Create an account and configure it to your liking, then choose it from below to store the settings as a profile. Profiles can be specified per invite, so that any new user on that invite will have the settings applied.`; - document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`; - (document.getElementById('defaultsSource') as HTMLSelectElement).value = 'fromUser'; - document.getElementById('defaultsSourceSection').classList.add('unfocused'); - (document.getElementById('storeDefaults') as HTMLButtonElement).onclick = storeProfile; - Focus(document.getElementById('newProfileBox')); - (document.getElementById('newProfileName') as HTMLInputElement).value = ''; - Focus(document.getElementById('defaultUserRadiosBox')); - window.Modals.userDefaults.show(); - } -}); - -window.createProfile = createProfile; - -function storeProfile(): void { - this.disabled = true; - this.innerHTML = - '' + - 'Loading...'; - const button = document.getElementById('storeDefaults') as HTMLButtonElement; - const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement - const name = (document.getElementById('newProfileName') as HTMLInputElement).value; - let id = radio.id.replace("default_", ""); - let data = { - "name": name, - "id": id, - "homescreen": false - } - if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) { - data["homescreen"] = true; - } - _post("/profiles", data, function (): void { - if (this.readyState == 4) { - if (this.status == 200 || this.status == 204) { - button.textContent = "Success"; - addAttr(button, "btn-success"); - rmAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - button.disabled = false; - setTimeout((): void => { - button.textContent = "Create"; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-success"); - button.disabled = false; - window.Modals.userDefaults.hide(); - - }, 1000); - populateProfiles(); - generateInvites(); - } else { - if ("error" in this.response) { - button.textContent = this.response["error"]; - } else if (("policy" in this.response) || ("homescreen" in this.response)) { - button.textContent = "Failed (check console)"; - } else { - button.textContent = "Failed"; - } - addAttr(button, "btn-danger"); - rmAttr(button, "btn-primary"); - setTimeout((): void => { - button.textContent = "Create"; - addAttr(button, "btn-primary"); - rmAttr(button, "btn-danger"); - button.disabled = false; - }, 1000); - } - } - }); -} - -// (document.getElementById('openSettings') as HTMLButtonElement).onclick = (): void => openSettings(document.getElementById('settingsList'), document.getElementById('settingsList'), (): void => settingsModal.show()); - -(document.getElementById('settingsSave') as HTMLButtonElement).onclick = function (): void { - window.modifiedConfig = {}; - const save = this as HTMLButtonElement; - let restartSettingsChanged = false; - let settingsChanged = false; - for (const i in window.config["order"]) { - const section = window.config["order"][i]; - for (const x in window.config[section]["order"]) { - const entry = window.config[section]["order"][x]; - if (entry == "meta") { - continue; - } - let val: string; - const entryID = `${section}_${entry}`; - const el = document.getElementById(entryID) as HTMLInputElement; - if (el.type == "checkbox") { - val = el.checked.toString(); - } else { - val = el.value.toString(); - } - if (val != window.config[section][entry]["value"].toString()) { - if (!(section in window.modifiedConfig)) { - window.modifiedConfig[section] = {}; - } - window.modifiedConfig[section][entry] = val; - settingsChanged = true; - if (window.config[section][entry]["requires_restart"]) { - restartSettingsChanged = true; - } - } - } - } - const spinnerHTML = ` - - Loading...`; - if (restartSettingsChanged) { - save.innerHTML = spinnerHTML; - (document.getElementById('applyRestarts') as HTMLButtonElement).onclick = (): void => sendConfig(); - const restartButton = document.getElementById('applyAndRestart') as HTMLButtonElement; - if (restartButton) { - restartButton.onclick = (): void => sendConfig(true); - } - window.Modals.restart.show(); - } else if (settingsChanged) { - save.innerHTML = spinnerHTML; - sendConfig(); - } -}; - -(document.getElementById('restartModalCancel') as HTMLButtonElement).onclick = (): void => { - document.getElementById('settingsSave').textContent = "Save"; -}; diff --git a/ts/setup.ts b/ts/setup.ts new file mode 100644 index 0000000..daa4a17 --- /dev/null +++ b/ts/setup.ts @@ -0,0 +1,260 @@ +// Lord forgive me for this mess, i'll fix it one day i swear + +document.getElementById("page-1").scrollIntoView({ + behavior: "auto", + block: "center", + inline: "center" +}); + +const checkAuthRadio = () => { + if ((document.getElementById('manualAuthRadio') as HTMLInputElement).checked) { + document.getElementById('adminOnlyArea').style.display = 'none'; + document.getElementById('manualAuthArea').style.display = ''; + } else { + document.getElementById('manualAuthArea').style.display = 'none'; + document.getElementById('adminOnlyArea').style.display = ''; + } +}; + +for (let radio of ['manualAuthRadio', 'jfAuthRadio']) { + document.getElementById(radio).addEventListener('change', checkAuthRadio); +}; + +const checkEmailRadio = () => { + (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-5'; + (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-7'; + if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) { + document.getElementById('emailCommonArea').style.display = ''; + document.getElementById('emailSMTPArea').style.display = ''; + document.getElementById('emailMailgunArea').style.display = 'none'; + (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true; + } else if ((document.getElementById('emailMailgunRadio') as HTMLInputElement).checked) { + document.getElementById('emailCommonArea').style.display = ''; + document.getElementById('emailSMTPArea').style.display = 'none'; + document.getElementById('emailMailgunArea').style.display = ''; + (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true; + } else if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) { + document.getElementById('emailCommonArea').style.display = 'none'; + document.getElementById('emailSMTPArea').style.display = 'none'; + document.getElementById('emailMailgunArea').style.display = 'none'; + (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-8'; + (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-4'; + (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = false; + } +}; + +for (let radio of ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']) { + document.getElementById(radio).addEventListener('change', checkEmailRadio); +} + +const checkSSL = () => { + var label = document.getElementById('emailSSL_TLSLabel'); + if ((document.getElementById('emailSSL_TLS') as HTMLInputElement).checked) { + label.textContent = 'Use SSL/TLS'; + } else { + label.textContent = 'Use STARTTLS'; + } +}; +document.getElementById('emailSSL_TLS').addEventListener('change', checkSSL); + +var pwrEnabled = document.getElementById('pwrEnabled') as HTMLInputElement; +const checkPwrEnabled = () => { + if (pwrEnabled.checked) { + document.getElementById('pwrArea').style.display = ''; + } else { + document.getElementById('pwrArea').style.display = 'none'; + } +}; +pwrEnabled.addEventListener('change', checkPwrEnabled); + +var invEnabled = document.getElementById("invEnabled") as HTMLInputElement; +const checkInvEnabled = () => { + if (invEnabled.checked) { + document.getElementById('invArea').style.display = ''; + } else { + document.getElementById('invArea').style.display = 'none'; + } +}; +invEnabled.addEventListener('change', checkInvEnabled); + +var valEnabled = document.getElementById("valEnabled") as HTMLInputElement; +const checkValEnabled = () => { + const valArea = document.getElementById("valArea"); + if (valEnabled.checked) { + valArea.style.display = ''; + } else { + valArea.style.display = 'none'; + } +}; +valEnabled.addEventListener('change', checkValEnabled); + +checkValEnabled(); +checkInvEnabled(); +checkSSL(); +checkAuthRadio(); +checkEmailRadio(); +checkPwrEnabled(); + +var jfValid = false +document.getElementById('jfTestButton').onclick = () => { + let testButton = document.getElementById('jfTestButton') as HTMLInputElement; + let nextButton = document.getElementById('jfNextButton') as HTMLAnchorElement; + let jfData = {}; + jfData['jfHost'] = (document.getElementById('jfHost') as HTMLInputElement).value; + jfData['jfUser'] = (document.getElementById('jfUser') as HTMLInputElement).value; + jfData['jfPassword'] = (document.getElementById('jfPassword') as HTMLInputElement).value; + let valid = true; + for (let val in jfData) { + if (jfData[val] == "") { + valid = false; + } + } + if (!valid) { + if (!testButton.classList.contains('btn-danger')) { + testButton.classList.add('btn-danger'); + testButton.textContent = 'Fill out fields above.'; + setTimeout(function() { + if (testButton.classList.contains('btn-danger')) { + testButton.classList.remove('btn-danger'); + testButton.textContent = 'Test'; + } + }, 2000); + } + } else { + testButton.disabled = true; + testButton.innerHTML = + '' + + 'Testing...'; + nextButton.classList.add('disabled'); + nextButton.setAttribute('aria-disabled', 'true'); + var req = new XMLHttpRequest(); + req.open("POST", "/jellyfin/test", true); + req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.responseType = 'json'; + req.onreadystatechange = function() { + if (this.readyState == 4) { + testButton.disabled = false; + testButton.className = ''; + if (this.response['success'] == true) { + testButton.classList.add('btn', 'btn-success'); + testButton.textContent = 'Success'; + nextButton.classList.remove('disabled'); + nextButton.setAttribute('aria-disabled', 'false'); + } else { + testButton.classList.add('btn', 'btn-danger'); + testButton.textContent = 'Failed'; + }; + }; + }; + req.send(JSON.stringify(jfData)); + } +}; + +document.getElementById('submitButton').onclick = () => { + const submitButton = document.getElementById('submitButton') as HTMLInputElement; + submitButton.disabled = true; + submitButton.innerHTML =` + + Submitting... + `; + let config = {}; + config['jellyfin'] = {}; + config['ui'] = {}; + config['password_validation'] = {}; + config['email'] = {}; + config['password_resets'] = {}; + config['invite_emails'] = {}; + config['mailgun'] = {}; + config['smtp'] = {}; + config['notifications'] = {}; + // Page 2: Auth + if ((document.getElementById('jfAuthRadio') as HTMLInputElement).checked) { + config['ui']['jellyfin_login'] = 'true'; + config['ui']['admin_only'] = ""+(document.getElementById("jfAuthAdminOnly") as HTMLInputElement).checked; + } else { + config['ui']['username'] = (document.getElementById('manualAuthUsername') as HTMLInputElement).value; + config['ui']['password'] = (document.getElementById('manualAuthPassword') as HTMLInputElement).value; + config['ui']['email'] = (document.getElementById('manualAuthEmail') as HTMLInputElement).value; + }; + // Page 3: Connect to jellyfin + config['jellyfin']['server'] = (document.getElementById('jfHost') as HTMLInputElement).value; + let publicAddress = (document.getElementById('jfPublicHost') as HTMLInputElement).value; + if (publicAddress != "") { + config['jellyfin']['public_server'] = publicAddress; + } + config['jellyfin']['username'] = (document.getElementById('jfUser') as HTMLInputElement).value; + config['jellyfin']['password'] = (document.getElementById('jfPassword') as HTMLInputElement).value; + // Page 4: Email (Page 5, 6, 7 are only used if this is enabled) + if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) { + config['password_resets']['enabled'] = 'false'; + config['invite_emails']['enabled'] = 'false'; + config['notifications']['enabled'] = 'false'; + } else { + if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) { + config['smtp']['encryption'] = (document.getElementById('emailSSL_TLS') as HTMLInputElement).checked ? "ssl_tls" : "starttls"; + config['email']['method'] = 'smtp'; + config['smtp']['server'] = (document.getElementById('emailSMTPServer') as HTMLInputElement).value; + config['smtp']['port'] = (document.getElementById('emailSMTPPort') as HTMLInputElement).value; + config['smtp']['password'] = (document.getElementById('emailSMTPPassword') as HTMLInputElement).value; + config['email']['address'] = (document.getElementById('emailSMTPAddress') as HTMLInputElement).value; + } else { + config['email']['method'] = 'mailgun'; + config['mailgun']['api_url'] = (document.getElementById('emailMailgunURL') as HTMLInputElement).value; + config['mailgun']['api_key'] = (document.getElementById('emailMailgunKey') as HTMLInputElement).value; + config['email']['address'] = (document.getElementById('emailMailgunAddress') as HTMLInputElement).value; + }; + config['notifications']['enabled'] = ""+(document.getElementById('notificationsEnabled') as HTMLInputElement).checked; + // Page 5: Email formatting + config['email']['from'] = (document.getElementById('emailSender') as HTMLInputElement).value; + config['email']['date_format'] = (document.getElementById('emailDateFormat') as HTMLInputElement).value; + config['email']['use_24h'] = ""+(document.getElementById('email24hTimeRadio') as HTMLInputElement).checked; + config['email']['message'] = (document.getElementById('emailMessage') as HTMLInputElement).value; + // Page 6: Password Resets + if (pwrEnabled.checked) { + config['password_resets']['enabled'] = 'true'; + config['password_resets']['watch_directory'] = (document.getElementById('pwrJfPath') as HTMLInputElement).value; + config['password_resets']['subject'] = (document.getElementById('pwrSubject') as HTMLInputElement).value; + } else { + config['password_resets']['enabled'] = 'false'; + }; + // Page 7: Invite Emails + if ((document.getElementById('invEnabled') as HTMLInputElement).checked) { + config['invite_emails']['enabled'] = 'true'; + config['invite_emails']['url_base'] = (document.getElementById('invURLBase') as HTMLInputElement).value; + config['invite_emails']['subject'] = (document.getElementById('invSubject') as HTMLInputElement).value; + } else { + config['invite_emails']['enabled'] = 'false'; + }; + }; + // Page 8: Password Validation + if ((document.getElementById('valEnabled') as HTMLInputElement).checked) { + config['password_validation']['enabled'] = 'true'; + config['password_validation']['min_length'] = (document.getElementById('valLength') as HTMLInputElement).value; + config['password_validation']['upper'] = (document.getElementById('valUpper') as HTMLInputElement).value; + config['password_validation']['lower'] = (document.getElementById('valLower') as HTMLInputElement).value; + config['password_validation']['number'] = (document.getElementById('valNumber') as HTMLInputElement).value; + config['password_validation']['special'] = (document.getElementById('valSpecial') as HTMLInputElement).value; + } else { + config['password_validation']['enabled'] = 'false'; + }; + // Page 9: Messages + config['ui']['contact_message'] = (document.getElementById('msgContact') as HTMLInputElement).value; + config['ui']['help_message'] = (document.getElementById('msgHelp') as HTMLInputElement).value; + config['ui']['success_message'] = (document.getElementById('msgSuccess') as HTMLInputElement).value; + // Send it + config["restart-program"] = true; + let req = new XMLHttpRequest(); + req.open("POST", "/config", true); + req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + req.responseType = 'json'; + req.onreadystatechange = function() { + if (this.readyState == 4) { + submitButton.disabled = false; + submitButton.className = ''; + submitButton.classList.add('btn', 'btn-success'); + submitButton.textContent = 'Success'; + }; + }; + req.send(JSON.stringify(config)); +}; + diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 0905779..95903f5 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "outDir": "../data/static", + "outDir": "../js", "target": "es6", "lib": ["dom", "es2017"], "typeRoots": ["./node_modules/@types", "./typings"] diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 5be1863..7c2b8c3 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -1,62 +1,91 @@ -declare interface ModalConstructor { - (id: string, find?: boolean): BSModal; +declare interface Modal { + modal: HTMLElement; + closeButton: HTMLSpanElement + show: () => void; + close: (event?: Event) => void; + toggle: () => void; } -declare interface BSModal { - el: HTMLDivElement; - modal: any; - show: () => void; - hide: () => void; +interface ArrayConstructor { + from(arrayLike: any, mapFn?, thisArg?): Array; } declare interface Window { - getComputedStyle(element: HTMLElement, pseudoElt: HTMLElement): any; - bsVersion: number; - bs5: boolean; - BS: Bootstrap; URLBase: string; - Modals: BSModals; + modals: Modals; cssFile: string; - availableProfiles: Array; + availableProfiles: string[]; jfUsers: Array; - notifications_enabled: boolean; + notificationsEnabled: boolean; + emailEnabled: boolean; + ombiEnabled: boolean; + usernameEnabled: boolean; token: string; buttonWidth: number; + transitionEvent: string; + animationEvent: string; + tabs: Tabs; + invites: inviteList; + notifications: NotificationBox; } -declare interface tooltipTrigger { - (): void; +declare interface NotificationBox { + connectionError: () => void; + customError: (type: string, message: string) => void; + customPositive: (type: string, bold: string, message: string) => void; } -declare interface Bootstrap { - newModal: ModalConstructor; - triggerTooltips: tooltipTrigger; - Compat?(): void; +declare interface Tabs { + current: string; + tabs: Array; + addTab: (tabID: string, preFunc?: () => void, postFunc?: () => void) => void; + switch: (tabID: string, noRun?: boolean) => void; } -declare interface BSModals { - login: BSModal; - userDefaults: BSModal; - users: BSModal; - restart: BSModal; - refresh: BSModal; - about: BSModal; - delete: BSModal; - newUser: BSModal; +declare interface Tab { + tabID: string; + tabEl: HTMLDivElement; + buttonEl: HTMLSpanElement; + preFunc?: () => void; + postFunc?: () => void; +} + + +declare interface Modals { + about: Modal; + login: Modal; + addUser: Modal; + modifyUser: Modal; + deleteUser: Modal; + settingsRestart: Modal; + settingsRefresh: Modal; + ombiDefaults?: Modal; + profiles: Modal; + addProfile: Modal; } interface Invite { code?: string; expiresIn?: string; - empty: boolean; remainingUses?: string; email?: string; - usedBy?: Array>; + usedBy?: string[][]; created?: string; notifyExpiry?: boolean; notifyCreation?: boolean; profile?: string; } +interface inviteList { + empty: boolean; + invites: { [code: string]: Invite } + add: (invite: Invite) => void; + reload: () => void; +} + +declare interface SubmitEvent extends Event { + submitter: HTMLInputElement; +} + declare var config: Object; declare var modifiedConfig: Object; diff --git a/views.go b/views.go index 86e455b..7575312 100644 --- a/views.go +++ b/views.go @@ -13,14 +13,12 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) { } func (app *appContext) AdminPage(gc *gin.Context) { - bs5 := app.config.Section("ui").Key("bs5").MustBool(false) emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) gcHTML(gc, http.StatusOK, "admin.html", gin.H{ "urlBase": app.URLBase, - "bs5": bs5, - "cssFile": app.cssFile, + "cssClass": app.cssClass, "contactMessage": "", "email_enabled": emailEnabled, "notifications": notificationsEnabled, @@ -42,7 +40,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { } gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{ "urlBase": app.URLBase, - "cssFile": app.cssFile, + "cssClass": app.cssClass, "contactMessage": app.config.Section("ui").Key("contact_message").String(), "helpMessage": app.config.Section("ui").Key("help_message").String(), "successMessage": app.config.Section("ui").Key("success_message").String(), @@ -50,14 +48,12 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "validate": app.config.Section("password_validation").Key("enabled").MustBool(false), "requirements": app.validator.getCriteria(), "email": email, - "bs5": app.config.Section("ui").Key("bs5").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false), "lang": app.storage.lang.Form["strings"], }) } else { gcHTML(gc, 404, "invalidCode.html", gin.H{ - "bs5": app.config.Section("ui").Key("bs5").MustBool(false), - "cssFile": app.cssFile, + "cssClass": app.cssClass, "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) } @@ -65,8 +61,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { func (app *appContext) NoRouteHandler(gc *gin.Context) { gcHTML(gc, 404, "404.html", gin.H{ - "bs5": app.config.Section("ui").Key("bs5").MustBool(false), - "cssFile": app.cssFile, + "cssClass": app.cssClass, "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) }