Merge a17t-redesign, kinda ts-ify setup.js
the web ui has been redesigned with the a17t toolkit, which imo looks a lot better than bootstrap. This also brought a complete rework of the web code, which now makes a lot more sense hopefully. the setup page is still stuck with bootstrap, its not much of a priority but i'll rewrite it eventually.
22
.gitignore
vendored
@ -1,26 +1,10 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
passwordreset*.json
|
|
||||||
mail/*.html
|
mail/*.html
|
||||||
scss/*.css*
|
dist/
|
||||||
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
|
|
||||||
build/
|
build/
|
||||||
pkg/
|
data/
|
||||||
old/
|
|
||||||
version.go
|
version.go
|
||||||
notes
|
notes
|
||||||
docs/*
|
docs/*
|
||||||
|
config-payload.json
|
||||||
!docs/go.mod
|
!docs/go.mod
|
||||||
|
@ -7,14 +7,20 @@ release:
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod download
|
- 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/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 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||||
- python3 -m pip install libsass
|
- python3 mail/generate.py -o data/
|
||||||
- npm install
|
|
||||||
- python3 scss/compile.py
|
|
||||||
- python3 mail/generate.py
|
|
||||||
- python3 version.py {{.Version}} version.go
|
- 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
|
- go get -u github.com/swaggo/swag/cmd/swag
|
||||||
- swag init -g main.go
|
- swag init -g main.go
|
||||||
builds:
|
builds:
|
||||||
@ -37,9 +43,8 @@ archives:
|
|||||||
amd64: x86_64
|
amd64: x86_64
|
||||||
files:
|
files:
|
||||||
- data/*
|
- data/*
|
||||||
- data/templates/*
|
- data/**/*
|
||||||
- data/static/*
|
- data/**/**/*
|
||||||
- data/static/modules/*
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
snapshot:
|
snapshot:
|
||||||
|
@ -7,7 +7,7 @@ RUN apt update -y \
|
|||||||
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
||||||
&& apt install nodejs \
|
&& apt install nodejs \
|
||||||
&& (cd /opt/build; make all; make compress) \
|
&& (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
|
FROM golang:latest
|
||||||
|
|
||||||
|
58
Makefile
@ -1,33 +1,30 @@
|
|||||||
|
npm:
|
||||||
|
$(info installing npm dependencies)
|
||||||
|
npm install
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
$(info Fixing config-base)
|
$(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)
|
$(info Generating config-default.ini)
|
||||||
python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
python3 config/generate_ini.py -i config/config-base.json -o build/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
|
|
||||||
|
|
||||||
email:
|
email:
|
||||||
$(info Generating email html)
|
$(info Generating email html)
|
||||||
python3 mail/generate.py
|
python3 mail/generate.py -o build/data/
|
||||||
|
|
||||||
typescript:
|
ts:
|
||||||
$(info Compiling typescript)
|
$(info compiling typescript)
|
||||||
npx esbuild ts/*.ts ts/modules/*.ts --outdir=data/static --minify
|
-mkdir -p build/data/web/js
|
||||||
-rm -r data/static/ts
|
-npx esbuild ts/*.ts ts/modules/*.ts --outdir=./build/data/web/js/
|
||||||
-rm -r data/static/typings
|
|
||||||
-rm data/static/*.map
|
|
||||||
|
|
||||||
ts-debug:
|
ts-debug:
|
||||||
-npx tsc -p ts/ --sourceMap
|
$(info compiling typescript w/ sourcemaps)
|
||||||
-rm -r data/static/ts
|
-mkdir -p build/data/web/js
|
||||||
-rm -r data/static/typings
|
-npx esbuild ts/*.ts ts/modules/*.ts --sourcemap --outdir=./build/data/web/js/
|
||||||
cp -r ts data/static/
|
-rm -r build/data/web/js/ts
|
||||||
|
$(info copying typescript)
|
||||||
|
cp -r ts build/data/web/js
|
||||||
|
|
||||||
swagger:
|
swagger:
|
||||||
go get github.com/swaggo/swag/cmd/swag
|
go get github.com/swaggo/swag/cmd/swag
|
||||||
@ -47,11 +44,22 @@ compress:
|
|||||||
upx --lzma build/jfa-go
|
upx --lzma build/jfa-go
|
||||||
|
|
||||||
copy:
|
copy:
|
||||||
$(info Copying data)
|
$(info copying css)
|
||||||
cp -r data build/
|
-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:
|
install:
|
||||||
cp -r build $(DESTDIR)/jfa-go
|
cp -r build $(DESTDIR)/jfa-go
|
||||||
|
|
||||||
all: configuration sass email version typescript swagger compile copy
|
all: configuration npm email version ts swagger compile copy
|
||||||
debug: configuration sass email version ts-debug swagger compile copy
|
debug: configuration npm email version ts-debug swagger compile copy
|
||||||
|
@ -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.
|
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
|
* 🌓 Customizable look
|
||||||
* Specify contact and help messages to appear in emails and pages
|
* Specify contact and help messages to appear in emails and pages
|
||||||
* Light and dark themes available
|
* Light and dark themes available
|
||||||
* Optionally provide custom CSS
|
|
||||||
|
|
||||||
## Interface
|
## Interface
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/demo.gif" width="100%"></img>
|
<img src="images/demo.gif" width="100%"></img>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
<img src="images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
||||||
<img src="https://github.com/hrfee/jfa-go/blob/main/images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
<img src="images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
#### Install
|
#### Install
|
||||||
|
38
README.md.old
Normal file
@ -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
|
||||||
|
<p>
|
||||||
|
<img src="images/dark/invites.png" alt="invites" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/dark/accounts.png" alt="accounts" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/dark/settings.png" alt="settings" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/dark/login-modal.png" alt="login modal" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/dark/modify-settings.png" alt="modify user settings modal" style="width: 32%; height: auto;">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
##### light
|
||||||
|
<p>
|
||||||
|
<img src="images/light/invites.png" alt="invites" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/light/accounts.png" alt="accounts" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/light/settings.png" alt="settings" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/light/login-modal.png" alt="login modal" style="width: 32%; height: auto;">
|
||||||
|
<img src="images/light/modify-settings.png" alt="modify user settings modal" style="width: 32%; height: auto;">
|
||||||
|
</p>
|
62
api.go
@ -667,6 +667,9 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
|
|||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
name := req.Name
|
name := req.Name
|
||||||
if _, ok := app.storage.profiles[name]; ok {
|
if _, ok := app.storage.profiles[name]; ok {
|
||||||
|
if app.storage.defaultProfile == name {
|
||||||
|
app.storage.defaultProfile = ""
|
||||||
|
}
|
||||||
delete(app.storage.profiles, name)
|
delete(app.storage.profiles, name)
|
||||||
}
|
}
|
||||||
app.storage.storeProfiles()
|
app.storage.storeProfiles()
|
||||||
@ -1072,13 +1075,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
|
|
||||||
// @Summary Get jfa-go configuration.
|
// @Summary Get jfa-go configuration.
|
||||||
// @Produce json
|
// @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]
|
// @Router /config [get]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||||
app.info.Println("Config requested")
|
app.info.Println("Config requested")
|
||||||
resp := map[string]interface{}{}
|
resp := app.configBase
|
||||||
|
// Load language options
|
||||||
langPath := filepath.Join(app.localPath, "lang", "form")
|
langPath := filepath.Join(app.localPath, "lang", "form")
|
||||||
app.lang.langFiles, _ = ioutil.ReadDir(langPath)
|
app.lang.langFiles, _ = ioutil.ReadDir(langPath)
|
||||||
app.lang.langOptions = make([]string, len(app.lang.langFiles))
|
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)
|
app.lang.langOptions[i] = meta.(map[string]interface{})["name"].(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for section, settings := range app.configBase {
|
s := resp.Sections["ui"].Settings["language"]
|
||||||
if section == "order" {
|
for sectName, section := range resp.Sections {
|
||||||
resp[section] = settings.([]interface{})
|
for settingName, setting := range section.Settings {
|
||||||
} else {
|
val := app.config.Section(sectName).Key(settingName)
|
||||||
resp[section] = make(map[string]interface{})
|
s := resp.Sections[sectName].Settings[settingName]
|
||||||
for key, values := range settings.(map[string]interface{}) {
|
switch setting.Type {
|
||||||
if key == "order" {
|
case "text", "email", "select", "password":
|
||||||
resp[section].(map[string]interface{})[key] = values.([]interface{})
|
s.Value = val.MustString("")
|
||||||
} else {
|
case "number":
|
||||||
resp[section].(map[string]interface{})[key] = values.(map[string]interface{})
|
s.Value = val.MustInt(0)
|
||||||
if key != "meta" {
|
case "bool":
|
||||||
dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string)
|
s.Value = val.MustBool(false)
|
||||||
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.Sections[sectName].Settings[settingName] = s
|
||||||
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.Options = app.lang.langOptions
|
||||||
}
|
s.Value = app.lang.langOptions[app.lang.chosenIndex]
|
||||||
}
|
resp.Sections["ui"].Settings["language"] = s
|
||||||
}
|
|
||||||
// resp["jellyfin"].(map[string]interface{})["language"].(map[string]interface{})["options"].([]string)
|
|
||||||
gc.JSON(200, resp)
|
gc.JSON(200, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1176,11 +1168,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
if _, ok := req["password_validation"]; ok {
|
if _, ok := req["password_validation"]; ok {
|
||||||
app.debug.Println("Reinitializing validator")
|
app.debug.Println("Reinitializing validator")
|
||||||
validatorConf := ValidatorConf{
|
validatorConf := ValidatorConf{
|
||||||
"characters": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||||
"uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0),
|
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||||
"lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0),
|
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||||
"numbers": app.config.Section("password_validation").Key("number").MustInt(0),
|
"number": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||||
"special characters": app.config.Section("password_validation").Key("special").MustInt(0),
|
"special": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||||
}
|
}
|
||||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||||
for key := range validatorConf {
|
for key := range validatorConf {
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"order": [],
|
||||||
|
"sections": {
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Jellyfin",
|
"name": "Jellyfin",
|
||||||
"description": "Settings for connecting to Jellyfin"
|
"description": "Settings for connecting to Jellyfin"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"username": {
|
"username": {
|
||||||
"name": "Jellyfin Username",
|
"name": "Jellyfin Username",
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -51,12 +55,15 @@
|
|||||||
"value": 30,
|
"value": 30,
|
||||||
"description": "Timeout of user cache in minutes. Set to 0 to disable."
|
"description": "Timeout of user cache in minutes. Set to 0 to disable."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "General",
|
"name": "General",
|
||||||
"description": "Settings related to the UI and program functionality."
|
"description": "Settings related to the UI and program functionality."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"language": {
|
"language": {
|
||||||
"name": "Language",
|
"name": "Language",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -74,9 +81,8 @@
|
|||||||
"requires_restart": true,
|
"requires_restart": true,
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"options": [
|
"options": [
|
||||||
"Bootstrap (Light)",
|
|
||||||
"Jellyfin (Dark)",
|
"Jellyfin (Dark)",
|
||||||
"Custom CSS"
|
"Default (Light)"
|
||||||
],
|
],
|
||||||
"value": "Jellyfin (Dark)",
|
"value": "Jellyfin (Dark)",
|
||||||
"description": "Default appearance for all users."
|
"description": "Default appearance for all users."
|
||||||
@ -172,14 +178,6 @@
|
|||||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||||
"description": "Displayed when a user creates an account"
|
"description": "Displayed when a user creates an account"
|
||||||
},
|
},
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"url_base": {
|
"url_base": {
|
||||||
"name": "URL Base",
|
"name": "URL Base",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -188,12 +186,15 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "URL base for when running jfa-go with a reverse proxy in a subfolder."
|
"description": "URL base for when running jfa-go with a reverse proxy in a subfolder."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"password_validation": {
|
"password_validation": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Password Validation",
|
"name": "Password Validation",
|
||||||
"description": "Password validation (minimum length, etc.)"
|
"description": "Password validation (minimum length, etc.)"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -236,12 +237,15 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "0"
|
"value": "0"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Email",
|
"name": "Email",
|
||||||
"description": "General email settings. Ignore if not using email features."
|
"description": "General email settings. Ignore if not using email features."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"no_username": {
|
"no_username": {
|
||||||
"name": "Use email addresses as username",
|
"name": "Use email addresses as username",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -307,12 +311,15 @@
|
|||||||
"value": "Jellyfin",
|
"value": "Jellyfin",
|
||||||
"description": "The name of the sender"
|
"description": "The name of the sender"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"password_resets": {
|
"password_resets": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Password Resets",
|
"name": "Password Resets",
|
||||||
"description": "Settings for the password reset handler."
|
"description": "Settings for the password reset handler."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -357,12 +364,15 @@
|
|||||||
"value": "Password Reset - Jellyfin",
|
"value": "Password Reset - Jellyfin",
|
||||||
"description": "Subject of password reset emails."
|
"description": "Subject of password reset emails."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"invite_emails": {
|
"invite_emails": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Invite emails",
|
"name": "Invite emails",
|
||||||
"description": "Settings for sending invites directly to users."
|
"description": "Settings for sending invites directly to users."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -406,12 +416,15 @@
|
|||||||
"value": "http://accounts.jellyf.in:8056/invite",
|
"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."
|
"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": {
|
"notifications": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Notifications",
|
"name": "Notifications",
|
||||||
"description": "Notification related settings."
|
"description": "Notification related settings."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": "false",
|
"required": "false",
|
||||||
@ -456,12 +469,15 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Path to user creation notification email in plaintext."
|
"description": "Path to user creation notification email in plaintext."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"mailgun": {
|
"mailgun": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Mailgun (Email)",
|
"name": "Mailgun (Email)",
|
||||||
"description": "Mailgun API connection settings"
|
"description": "Mailgun API connection settings"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"api_url": {
|
"api_url": {
|
||||||
"name": "API URL",
|
"name": "API URL",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -476,12 +492,15 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "your api key"
|
"value": "your api key"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"smtp": {
|
"smtp": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "SMTP (Email)",
|
"name": "SMTP (Email)",
|
||||||
"description": "SMTP Server connection settings."
|
"description": "SMTP Server connection settings."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"username": {
|
"username": {
|
||||||
"name": "Username",
|
"name": "Username",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -524,12 +543,15 @@
|
|||||||
"type": "password",
|
"type": "password",
|
||||||
"value": "smtp password"
|
"value": "smtp password"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ombi": {
|
"ombi": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Ombi Integration",
|
"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."
|
"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": {
|
"enabled": {
|
||||||
"name": "Enabled",
|
"name": "Enabled",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -556,12 +578,15 @@
|
|||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"description": "API Key. Get this from the first tab in Ombi settings."
|
"description": "API Key. Get this from the first tab in Ombi settings."
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"deletion": {
|
"deletion": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Account Deletion",
|
"name": "Account Deletion",
|
||||||
"description": "Subject/email files for account deletion emails."
|
"description": "Subject/email files for account deletion emails."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"subject": {
|
"subject": {
|
||||||
"name": "Email subject",
|
"name": "Email subject",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -586,12 +611,15 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Path to custom email in plain text"
|
"description": "Path to custom email in plain text"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "File Storage",
|
"name": "File Storage",
|
||||||
"description": "Optional settings for changing storage locations."
|
"description": "Optional settings for changing storage locations."
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
"invites": {
|
"invites": {
|
||||||
"name": "Invite Storage",
|
"name": "Invite Storage",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -648,14 +676,6 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)"
|
"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": {
|
"html_templates": {
|
||||||
"name": "Custom HTML Template Directory",
|
"name": "Custom HTML Template Directory",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -666,3 +686,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,17 +9,17 @@ args = parser.parse_args()
|
|||||||
with open(args.input, 'r') as f:
|
with open(args.input, 'r') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
|
|
||||||
newconfig = {"order": []}
|
newconfig = {"sections": {}, "order": []}
|
||||||
|
|
||||||
for sect in config:
|
for sect in config["sections"]:
|
||||||
newconfig["order"].append(sect)
|
newconfig["order"].append(sect)
|
||||||
newconfig[sect] = {}
|
newconfig["sections"][sect] = {}
|
||||||
newconfig[sect]["order"] = []
|
newconfig["sections"][sect]["order"] = []
|
||||||
newconfig[sect]["meta"] = config[sect]["meta"]
|
newconfig["sections"][sect]["meta"] = config["sections"][sect]["meta"]
|
||||||
for setting in config[sect]:
|
newconfig["sections"][sect]["settings"] = {}
|
||||||
if setting != "meta":
|
for setting in config["sections"][sect]["settings"]:
|
||||||
newconfig[sect]["order"].append(setting)
|
newconfig["sections"][sect]["order"].append(setting)
|
||||||
newconfig[sect][setting] = config[sect][setting]
|
newconfig["sections"][sect]["settings"][setting] = config["sections"][sect]["settings"][setting]
|
||||||
|
|
||||||
with open(args.output, 'w') as f:
|
with open(args.output, 'w') as f:
|
||||||
f.write(json.dumps(newconfig, indent=4))
|
f.write(json.dumps(newconfig, indent=4))
|
||||||
|
@ -14,13 +14,14 @@ def generate_ini(base_file, ini_file):
|
|||||||
|
|
||||||
ini = configparser.RawConfigParser(allow_no_value=True)
|
ini = configparser.RawConfigParser(allow_no_value=True)
|
||||||
|
|
||||||
for section in config_base:
|
for section in config_base["sections"]:
|
||||||
ini.add_section(section)
|
ini.add_section(section)
|
||||||
for entry in config_base[section]:
|
if "meta" in config_base["sections"][section]:
|
||||||
if "description" in config_base[section][entry]:
|
ini.set(section, "; " + config_base["sections"][section]["meta"]["description"])
|
||||||
ini.set(section, "; " + config_base[section][entry]["description"])
|
for entry in config_base["sections"][section]["settings"]:
|
||||||
if entry != "meta":
|
if "description" in config_base["sections"][section]["settings"][entry]:
|
||||||
value = config_base[section][entry]["value"]
|
ini.set(section, "; " + config_base["sections"][section]["settings"][entry]["description"])
|
||||||
|
value = config_base["sections"][section]["settings"][entry]["value"]
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
value = str(value).lower()
|
value = str(value).lower()
|
||||||
else:
|
else:
|
||||||
|
372
css/base.css
Normal file
@ -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;
|
||||||
|
}
|
87
css/dark.css
Normal file
@ -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;
|
||||||
|
}
|
40
css/loader.css
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
72
css/modal.css
Normal file
@ -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%);
|
||||||
|
}
|
36
css/tooltip.css
Normal file
@ -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;
|
||||||
|
}
|
@ -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 =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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 =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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));
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>404 - jfa-go</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
|
|
||||||
{{ template "header.html" . }}
|
|
||||||
<style>
|
|
||||||
.messageBox {
|
|
||||||
margin: 20%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="messageBox">
|
|
||||||
<h1>Page not found.</h1>
|
|
||||||
<p>
|
|
||||||
{{ .contactMessage }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,473 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
// To grab theme preference
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for (let c of ca) {
|
|
||||||
while(c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
window.URLBase = "{{ .urlBase }}";
|
|
||||||
{{ if .bs5 }}
|
|
||||||
window.bsVersion = 5;
|
|
||||||
{{ else }}
|
|
||||||
window.bsVersion = 4;
|
|
||||||
{{ end }}
|
|
||||||
window.cssFile = "{{ .cssFile }}";
|
|
||||||
var css = document.createElement('link');
|
|
||||||
css.setAttribute('rel', 'stylesheet');
|
|
||||||
css.setAttribute('type', 'text/css');
|
|
||||||
var cssCookie = getCookie("css");
|
|
||||||
if (cssCookie.includes('bs' + bsVersion)) {
|
|
||||||
cssFile = cssCookie;
|
|
||||||
} else if (cssCookie.includes('bs')) {
|
|
||||||
if (cssCookie.includes('jf')) {
|
|
||||||
cssFile = 'bs' + bsVersion + '-jf.css';
|
|
||||||
} else {
|
|
||||||
cssFile = 'bs' + bsVersion + '.css';
|
|
||||||
}
|
|
||||||
document.cookie = 'css=' + cssFile;
|
|
||||||
}
|
|
||||||
css.setAttribute('href', cssFile);
|
|
||||||
document.head.appendChild(css);
|
|
||||||
</script>
|
|
||||||
{{ template "header.html" . }}
|
|
||||||
<title>Admin - jfa-go</title>
|
|
||||||
</head>
|
|
||||||
<body class="smooth-transition">
|
|
||||||
<div class="modal fade" id="login" role="dialog" aria-labelledby="login" aria-hidden="true" data-backdrop="static">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="loginTitle">Login</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="formBody">
|
|
||||||
<form action="#" method="POST" id="loginForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Username</label>
|
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" id="loginSubmit" class="btn btn-primary" form="loginForm">Login</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="users" role="dialog" aria-labelledby="users" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="usersTitle">Users</h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<ul class="list-group list-group-flush" id="userList">
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" id="userFooter">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="userDefaults" role="dialog" aria-labelledby="users" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="defaultsTitle"></h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p id="userDefaultsDescription"></p>
|
|
||||||
<div class="mb-3" id="defaultsSourceSection">
|
|
||||||
<label for="defaultsSource" class="form-label">Use settings from:</label>
|
|
||||||
<select class="form-select" id="defaultsSource" aria-label="User settings source">
|
|
||||||
<option value="profile" selected>Profile</option>
|
|
||||||
<option value="fromUser">Source from existing user</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 unfocused" id="profileSelectBox">
|
|
||||||
<label for="profileSelect" class="form-label">Profile</label>
|
|
||||||
<select class="form-select" id="profileSelect" aria-label="Profile to apply">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 unfocused" id="newProfileBox">
|
|
||||||
<label for="newProfileName" class="form-label">Name</label>
|
|
||||||
<input type="text" class="form-control" id="newProfileName" aria-describedby="Profile Name">
|
|
||||||
</div>
|
|
||||||
<div id="defaultUserRadiosBox">
|
|
||||||
<label for="defaultUserRadios" class="form-label">Get settings from</label>
|
|
||||||
<div id="defaultUserRadios"></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-check" style="margin-top: 1rem;">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="storeDefaultHomescreen" checked>
|
|
||||||
<label class="form-check-label" for="storeDefaultHomescreen" id="storeHomescreenLabel"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" id="defaultsFooter">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="storeDefaults">Submit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ if .ombiEnabled }}
|
|
||||||
<div class="modal fade" id="ombiDefaults" role="dialog" aria-labelledby="Ombi Users" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="ombiTitle">Ombi user defaults</h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Create an Ombi user and configure it to your liking, then choose it from below to store the settings and permissions as a template for all new users.</p>
|
|
||||||
<div id="ombiUserRadios"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" id="ombiFooter">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="storeOmbiDefaults">Submit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Warning</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-light" data-dismiss="modal" id="restartModalCancel">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply & Restart</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="refreshModal" role="dialog" aria-labelledby="Refresh page notice" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Settings applied.</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Refresh the page in a few seconds.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="aboutModal" role="dialog" aria-labelledby="About jfa-go" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">About</h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<img src="banner.svg" alt="jfa-go banner">
|
|
||||||
<p><a href="https://github.com/hrfee/jfa-go"><i class="fa fa-github"></i> jfa-go</a></p>
|
|
||||||
<p>Version <i class="text-monospace">{{ .version }}</i></p>
|
|
||||||
<p>Commit <i class="text-monospace">{{ .commit }}</i></p>
|
|
||||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License.</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="Account deletion" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="deleteModalTitle"></h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="deleteModalNotify">
|
|
||||||
<label class="form-check-label" for="deleteModalNotify" id="deleteModalNotifyLabel">Notify users of account deletion</label>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 unfocused" id="deleteModalReasonBox">
|
|
||||||
<label for="deleteModalReason" class="form-label">Reason for deletion</label>
|
|
||||||
<textarea class="form-control" id="deleteModalReason" rows="2"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-danger" id="deleteModalSend">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="newUserModal" role="dialog" aria-labelledby="Create new user" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Create a user</h5>
|
|
||||||
<button type="button" class="{{ if .bs5 }}btn-{{ end }}close" data-dismiss="modal" aria-label="Close">
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
{{ end }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="newUserEmail" class="form-label">Email address</label>
|
|
||||||
<input type="email" class="form-control" id="newUserEmail" aria-describedby="Email address">
|
|
||||||
</div>
|
|
||||||
{{ if .username }}
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="newUserName" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control" id="newUserName" aria-describedby="Username">
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="newUserPassword" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="newUserPassword" aria-describedby="Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="newUserCreate">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageContainer">
|
|
||||||
<ul class="nav nav-pills" style="margin-bottom: 2rem;">
|
|
||||||
<li class="nav-item">
|
|
||||||
<h2><a id="invitesTabButton" class="nl nav-link active">Invites</a></h2>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<h2><a id="accountsTabButton" class="nl nav-link">Accounts</a></h2>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<h2><a id="settingsTabButton" class="nl nav-link">Settings</a></h2>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="btn-group" role="group" id="headerButtons">
|
|
||||||
<button type="button" class="btn btn-danger unfocused" id="logoutButton">
|
|
||||||
Logout <i class="fa fa-sign-out"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="invitesTab">
|
|
||||||
<div class="card mb-3 tabGroup">
|
|
||||||
<div class="card-header">Current Invites</div>
|
|
||||||
<ul class="list-group list-group-flush" id="invites">
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="linkForm">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">Generate Invite</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form action="#" method="POST" id="inviteForm" class="container">
|
|
||||||
<div class="row align-items-start">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="days">Days</label>
|
|
||||||
<select class="form-control form-select" id="days" name="days">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="hours">Hours</label>
|
|
||||||
<select class="form-control form-select" id="hours" name="hours">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="minutes">Minutes</label>
|
|
||||||
<select class="form-control form-select" id="minutes" name="minutes">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="multiUseCount">
|
|
||||||
Multiple uses
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('multiUseCount').disabled = !this.checked; document.getElementById('noUseLimit').disabled = !this.checked" aria-label="Checkbox to allow choice of invite usage limit" name="multiple-uses" id="multiUseEnabled">
|
|
||||||
</div>
|
|
||||||
<input type="number" class="form-control" name="remaining-uses" id="multiUseCount">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group form-check" style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" name="no-limit" id="noUseLimit" onchange="document.getElementById('multiUseCount').disabled = this.checked; if (this.checked) { document.getElementById('noLimitWarning').style = 'display: block;' } else { document.getElementById('noLimitWarning').style = 'display: none;'; }">
|
|
||||||
<label class="form-check-label" for="noUseLimit">
|
|
||||||
No use limit
|
|
||||||
</label>
|
|
||||||
<div id="noLimitWarning" class="form-text" style="display: none;">Warning: Unlimited usage invites pose a risk if published online.</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 1rem;">
|
|
||||||
<label for="inviteProfile">Account creation profile</label>
|
|
||||||
<select class="form-control form-select" id="inviteProfile" name="profile">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{{ if .email_enabled }}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="send_to_address">Send invite to address</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input" type="checkbox" onchange="document.getElementById('send_to_address').disabled = !this.checked;" aria-label="Checkbox to allow input of email address" id="send_to_address_enabled">
|
|
||||||
</div>
|
|
||||||
<input type="email" class="form-control" placeholder="example@example.com" id="send_to_address" disabled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-group d-flex float-right">
|
|
||||||
<button type="submit" id="generateSubmit" class="btn btn-primary" style="margin-top: 1rem;">
|
|
||||||
Generate
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="accountsTab" class="unfocused">
|
|
||||||
<div class="card mb-3 tabGroup">
|
|
||||||
<div class="card-header d-flex" style="align-items: center;">
|
|
||||||
<div>Accounts</div>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<button type="button" class="btn btn-secondary" id="accountsTabAddUser">Add User</button>
|
|
||||||
<button type="button" class="btn btn-primary unfocused" id="accountsTabSetDefaults">Modify Settings</button>
|
|
||||||
<button type="button" class="btn btn-danger unfocused" id="accountsTabDelete"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body table-responsive">
|
|
||||||
<table class="table table-hover table-striped table-borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{{ if .bs5 }}
|
|
||||||
<th scope="col"><input class="form-check-input" type="checkbox" value="" id="selectAll"></th>
|
|
||||||
{{ else }}
|
|
||||||
<th scope="col"><input type="checkbox" value="" id="selectAll"></th>
|
|
||||||
{{ end }}
|
|
||||||
<th scope="col">Username</th>
|
|
||||||
<th scope="col">Email Address</th>
|
|
||||||
<th scope="col">Last Active</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="accountsList">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="settingsTab" class="unfocused mb-3 tabGroup card">
|
|
||||||
<div class="card-header d-flex" style="align-items: center;">
|
|
||||||
<div>Settings</div>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<button type="button" class="btn btn-primary" id="settingsSave">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="" id="settingsLeft">
|
|
||||||
<ul class="list-group list-group-flush" style="margin-bottom: 1rem;">
|
|
||||||
<p>Note: <sup class="text-danger">*</sup> Indicates required field, <sup class="text-danger">R</sup> Indicates changes require a restart.</p>
|
|
||||||
<button type="button" class="list-group-item list-group-item-action static" id="openAbout">
|
|
||||||
About <i class="fa fa-info-circle settingIcon"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="list-group-item list-group-item-action" id="profiles_button">
|
|
||||||
User Profiles <i class="fa fa-user settingIcon"></i>
|
|
||||||
</button>
|
|
||||||
{{ if .ombiEnabled }}
|
|
||||||
<button type="button" class="list-group-item list-group-item-action static" id="openOmbiDefaults" onclick="window.openOmbiDefaults()">
|
|
||||||
Ombi User Defaults <i class="fa fa-chain-broken settingIcon"></i>
|
|
||||||
</button>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
<div class="list-group list-group-flush" id="settingsSections">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="" id="settingsContent">
|
|
||||||
<div id="profiles" class="unfocused">
|
|
||||||
<div class="card card-body">
|
|
||||||
<p>Profiles are applied to users when they create an account. They include things like access rights and homescreen layout. You can create them here.</p>
|
|
||||||
<table class="table table-sm table-striped table-borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
<th scope="col">Default</th>
|
|
||||||
<th scope="col">From</th>
|
|
||||||
<th scope="col">Admin?</th>
|
|
||||||
<th scope="col">Libraries</th>
|
|
||||||
<th scope="col"><button class="btn btn-outline-primary" onclick="createProfile()">Create</button></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="profileList">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="contactBox">
|
|
||||||
<p>{{ .contactMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.bs5 = {{ .bs5 }};
|
|
||||||
window.availableProfiles = [];
|
|
||||||
{{ if .notifications }}
|
|
||||||
window.notifications_enabled = true;
|
|
||||||
{{ else }}
|
|
||||||
window.notifications_enabled = false;
|
|
||||||
{{ end }}
|
|
||||||
</script>
|
|
||||||
<script src="admin.js" type="module"></script>
|
|
||||||
<script src="invites.js" type="module"></script>
|
|
||||||
{{ if .ombiEnabled }}
|
|
||||||
<script src="ombi.js" type="module"></script>
|
|
||||||
{{ end }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,106 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
|
|
||||||
{{ template "header.html" . }}
|
|
||||||
<style>
|
|
||||||
.pageContainer {
|
|
||||||
margin: 5% 20% 5% 20%;
|
|
||||||
}
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.pageContainer {
|
|
||||||
margin: 2%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contactBox {
|
|
||||||
color: grey;
|
|
||||||
}
|
|
||||||
#container {
|
|
||||||
margin-top: 5%;
|
|
||||||
margin-bottom: 5%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<title>{{ .lang.pageTitle }}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="modal fade" id="successBox" tabindex="-1" role="dialog" aria-labelledby="successBox" aria-hidden="true" data-backdrop="static">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="successHeader">{{ .lang.successHeader }}</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="successBody">
|
|
||||||
<p>{{ .successMessage }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<a href="{{ .jfLink }}" class="btn btn-primary">{{ .lang.successContinueButton }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pageContainer">
|
|
||||||
<h1>
|
|
||||||
{{ .lang.createAccountHeader }}
|
|
||||||
</h1>
|
|
||||||
<p>{{ .helpMessage }}</p>
|
|
||||||
<p class="contactBox">{{ .contactMessage }}</p>
|
|
||||||
<div class="container" id="container">
|
|
||||||
<div class="row" id="cardContainer">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">{{ .lang.accountDetails }}</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form action="#" method="POST" id="accountForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="inputEmail">{{ .lang.emailAddress }}</label>
|
|
||||||
<input type="email" class="form-control" id="{{ if .username }}inputEmail{{ else }}inputUsername{{ end }}" name="{{ if .username }}email{{ else }}username{{ end }}" placeholder="{{ .lang.emailAddress }}" value="{{ .email }}" required>
|
|
||||||
</div>
|
|
||||||
{{ if .username }}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="inputUsername">{{ .lang.username }}</label>
|
|
||||||
<input type="username" class="form-control" id="inputUsername" name="username" placeholder="{{ .lang.username }}" required>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="inputPassword">{{ .lang.password }}</label>
|
|
||||||
<input type="password" class="form-control" id="inputPassword" name="password" placeholder="{{ .lang.password }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="inputPassword">{{ .lang.reEnterPassword }}</label>
|
|
||||||
<input type="password" class="form-control" id="reInputPassword" onkeyup="window.checkPassword()" placeholder="{{ .lang.password }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group" role="group" aria-label="Button & Error" id="errorBox" style="margin-top: 1rem;">
|
|
||||||
<button type="submit" class="btn btn-outline-primary" id="submitButton">
|
|
||||||
<span id="createAccount">{{ .lang.createAccountButton }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ if .validate }}
|
|
||||||
<div class="col-sm" id="requirementBox">
|
|
||||||
<div class="card mb-3 requirementBox">
|
|
||||||
<div class="card-header">{{ .lang.passwordRequirementsHeader }}</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
{{ range $key, $value := .requirements }}
|
|
||||||
<li id="{{ $key }}" min="{{ $value }}" class="list-group-item list-group-item-danger">
|
|
||||||
<div></div>
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.validationStrings = {{ .lang.validationStrings }};
|
|
||||||
</script>
|
|
||||||
{{ template "form-base" . }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
|||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
|
|
||||||
<meta name="msapplication-TileColor" content="#603cba">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
|
|
||||||
|
|
||||||
{{ if not .bs5 }}
|
|
||||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
|
||||||
{{ end }}
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
|
|
||||||
{{ if .bs5 }}
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-t6I8D5dJmMXjCsRLhSzCltuhNZg6P10kE0m0nAncLUjH6GeYLhRU1zfLoW3QNQDF" crossorigin="anonymous"></script>
|
|
||||||
{{ else }}
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
|
||||||
{{ end }}
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Invalid Code - jfa-go</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .cssFile }}">
|
|
||||||
{{ template "header.html" . }}
|
|
||||||
<style>
|
|
||||||
.messageBox {
|
|
||||||
margin: 20%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="messageBox">
|
|
||||||
<h1>Invalid Code.</h1>
|
|
||||||
<p>The above code is either incorrect, or has expired.</p>
|
|
||||||
<p>{{ .contactMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
# set +e
|
|
||||||
# npx tsc -p ts/
|
|
||||||
# set -e
|
|
||||||
npx esbuild ts/* --outdir=data/static --minify
|
|
6
go.mod
@ -19,6 +19,7 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // 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/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
github.com/gofrs/uuid v3.3.0+incompatible // indirect
|
github.com/gofrs/uuid v3.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.4.3
|
github.com/golang/protobuf v1.4.3
|
||||||
@ -45,10 +46,9 @@ require (
|
|||||||
github.com/ugorji/go v1.2.0 // indirect
|
github.com/ugorji/go v1.2.0 // indirect
|
||||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
|
||||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba // indirect
|
|
||||||
golang.org/x/text v0.3.4 // 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
|
google.golang.org/protobuf v1.25.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0
|
gopkg.in/ini.v1 v1.62.0
|
||||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
20
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-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 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
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.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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
|
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.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 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
|
||||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
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.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
|
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
|
||||||
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
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.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 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM=
|
||||||
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
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.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.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
|
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.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 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
|
||||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
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/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.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
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.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 h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||||
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
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 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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y=
|
||||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY=
|
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=
|
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-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 h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
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/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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-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 h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
|
||||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
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-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 h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0=
|
||||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=
|
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 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-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/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.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=
|
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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
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.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-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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
16
html/404.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
<title>404 - jfa-go</title>
|
||||||
|
</head>
|
||||||
|
<body class="section">
|
||||||
|
<div class="page-container">
|
||||||
|
<h1 class="heading">Page not found.</h1>
|
||||||
|
<p class="content">
|
||||||
|
{{ .contactMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
296
html/admin.html
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.URLBase = "{{ .urlBase }}";
|
||||||
|
window.notificationsEnabled = {{ .notifications }};
|
||||||
|
window.emailEnabled = {{ .email_enabled }};
|
||||||
|
window.ombiEnabled = {{ .ombiEnabled }};
|
||||||
|
window.usernamesEnabled = {{ .username }};
|
||||||
|
</script>
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
<title>Admin - jfa-go</title>
|
||||||
|
</head>
|
||||||
|
<body class="max-w-full overflow-x-hidden section">
|
||||||
|
<div id="modal-login" class="modal">
|
||||||
|
<form class="modal-content card" id="form-login" href="">
|
||||||
|
<span class="heading">Login</span>
|
||||||
|
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="login-user">
|
||||||
|
<input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="login-password">
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">Login</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="modal-add-user" class="modal">
|
||||||
|
<form class="modal-content card" id="form-add-user" href="">
|
||||||
|
<span class="heading">New User <span class="modal-close">×</span></span>
|
||||||
|
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="add-user-user">
|
||||||
|
<input type="email" class="field input ~neutral !high mt-half mb-1" placeholder="email address">
|
||||||
|
<input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="add-user-password">
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="modal-about" class="modal">
|
||||||
|
<div class="modal-content content card">
|
||||||
|
<span class="heading">About <span class="modal-close">×</span></span>
|
||||||
|
<img src="/banner.svg" class="mt-1" alt="jfa-go banner">
|
||||||
|
<p><i class="icon ri-github-fill"></i><a href="https://github.com/hrfee/jfa-go">jfa-go</a></p>
|
||||||
|
<p>Version <span class="code monospace">{{ .version }}</span></p>
|
||||||
|
<p>Commit <span class="code monospace">{{ .commit }}</span></p>
|
||||||
|
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License.</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-modify-user" class="modal">
|
||||||
|
<form class="modal-content card" id="form-modify-user" href="">
|
||||||
|
<span class="heading">Modify Settings for <span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||||
|
<p class="content">Apply settings from an existing profile, or source them directly from a user.</p>
|
||||||
|
<div class="flex-row mb-1">
|
||||||
|
<label class="flex-row-group mr-1">
|
||||||
|
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
|
||||||
|
<span class="button ~neutral !high supra full-width center">Profile</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex-row-group ml-1">
|
||||||
|
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
|
||||||
|
<span class="button ~neutral !normal supra full-width center">User</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="select ~neutral !normal mb-1">
|
||||||
|
<select id="modify-user-profiles">
|
||||||
|
<option>Friends</option>
|
||||||
|
<option>Family</option>
|
||||||
|
<option>Default</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="select ~neutral !normal mb-1 unfocused">
|
||||||
|
<select id="modify-user-users">
|
||||||
|
<option>Person</option>
|
||||||
|
<option>Other person</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label class="switch mb-1">
|
||||||
|
<input type="checkbox" id="modify-user-homescreen" checked>
|
||||||
|
<span>Apply homescreen layout</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">Apply</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="modal-delete-user" class="modal">
|
||||||
|
<form class="modal-content card" id="form-delete-user" href="">
|
||||||
|
<span class="heading">Delete <span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||||
|
<div class="content mt-half">
|
||||||
|
<label class="switch mb-1">
|
||||||
|
<input type="checkbox" id="delete-user-notify" checked>
|
||||||
|
<span>Send notification email</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="textarea-delete-user" class="textarea full-width ~neutral !normal mb-1" placeholder="Your account has been deleted."></textarea>
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~critical !normal full-width center supra submit">Delete</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="modal-restart" class="modal">
|
||||||
|
<div class="modal-content card ~critical !low">
|
||||||
|
<span class="heading">Restart needed <span class="modal-close">×</span></span>
|
||||||
|
<p class="content pb-1">A restart is needed to apply some settings you changed. Do it now or later?</p>
|
||||||
|
<div class="fr">
|
||||||
|
<span class="button ~info !normal" id="settings-apply-no-restart">Apply, restart later</span>
|
||||||
|
<span class="button ~critical !normal" id="settings-apply-restart">Apply & restart</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-refresh" class="modal">
|
||||||
|
<div class="modal-content card ~neutral !normal">
|
||||||
|
<span class="heading">Settings applied.</span>
|
||||||
|
<p class="content">Refresh the page in a few seconds.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-ombi-defaults" class="modal">
|
||||||
|
<form class="modal-content card" id="form-ombi-defaults" href="">
|
||||||
|
<span class="heading">Ombi user defaults <span class="modal-close">×</span></span>
|
||||||
|
<p class="content">Create an Ombi user and configure it, then select it here. It's settings/permissions will be stored and applied to new ombi users created by jfa-go.</p>
|
||||||
|
<div class="select ~neutral !normal mb-1">
|
||||||
|
<select>
|
||||||
|
<option>Person</option>
|
||||||
|
<option>Other person</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">Submit</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="modal-user-profiles" class="modal">
|
||||||
|
<div class="modal-content wide card">
|
||||||
|
<span class="heading">User profiles <span class="modal-close">×</span></span>
|
||||||
|
<p class="support lg">Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Default</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>Libraries</th>
|
||||||
|
<th><span class="button ~neutral !high" id="button-profile-create">Create</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="table-profiles">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-add-profile" class="modal">
|
||||||
|
<form class="modal-content card" id="form-add-profile" href="">
|
||||||
|
<span class="heading">Add profile <span class="modal-close">×</span></span>
|
||||||
|
<p class="content">Create a Jellyfin user and configure it. Select it here, and when this profile is applied to an invite, new users will be created with its settings.</p>
|
||||||
|
<label>
|
||||||
|
<span class="supra">Profile Name </span>
|
||||||
|
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="Name" id="add-profile-name">
|
||||||
|
<label>
|
||||||
|
<span class="supra">User</span>
|
||||||
|
<div class="select ~neutral !normal mt-half mb-1">
|
||||||
|
<select id="add-profile-user">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="switch mb-1">
|
||||||
|
<input type="checkbox" id="add-profile-homescreen" checked>
|
||||||
|
<span>Store homescreen layout</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="notification-box"></div>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="mb-1">
|
||||||
|
<header class="flex flex-wrap items-center justify-between">
|
||||||
|
<div class="text-neutral-700">
|
||||||
|
<span id="button-tab-invites" class="tab-button portal">Invites</span>
|
||||||
|
<span id="button-tab-accounts" class="tab-button portal">Accounts</span>
|
||||||
|
<span id="button-tab-settings" class="tab-button portal">Settings</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<div class="text-neutral-700">
|
||||||
|
<span class="button ~critical !normal mb-1 unfocused" id="logout-button">Logout</span>
|
||||||
|
<span id="button-theme" class="button ~neutral !normal mb-1">Theme</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-invites">
|
||||||
|
<div class="card ~neutral !low invites mb-1">
|
||||||
|
<span class="heading">Invites</span>
|
||||||
|
<div id="invites"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !low">
|
||||||
|
<span class="heading">Create</span>
|
||||||
|
<div class="row" id="create-inv">
|
||||||
|
<div class="card ~neutral !normal col">
|
||||||
|
<label class="label supra" for="create-days">Days</label>
|
||||||
|
<div class="select ~neutral !normal mb-1 mt-half">
|
||||||
|
<select id="create-days">
|
||||||
|
<option>0</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label class="label supra" for="create-hours">Hours</label>
|
||||||
|
<div class="select ~neutral !normal mb-1 mt-half">
|
||||||
|
<select id="create-hours">
|
||||||
|
<option>0</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label class="label supra" for="create-minutes">Minutes</label>
|
||||||
|
<div class="select ~neutral !normal mb-1 mt-half">
|
||||||
|
<select id="create-minutes">
|
||||||
|
<option>0</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !normal col">
|
||||||
|
<label class="label supra" for="create-uses">Number of uses</label>
|
||||||
|
<div class="flex-expand mb-1 mt-half">
|
||||||
|
<input type="number" min="0" id="create-uses" class="input ~neutral !normal mr-1" value=1>
|
||||||
|
<label for="create-inf-uses" class="button ~neutral !normal">
|
||||||
|
<span>∞</span>
|
||||||
|
<input type="checkbox" class="unfocused" id="create-inf-uses" aria-label="Set uses to infinite">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="support unfocused" id="create-inf-uses-warning"><span class="badge ~critical">Warning</span> invites with infinite uses can be used abusively.</p>
|
||||||
|
<label class="label supra">Profile</label>
|
||||||
|
<div class="select ~neutral !normal mb-1 mt-half">
|
||||||
|
<select id="create-profile">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="create-send-to-container">
|
||||||
|
<label class="label supra">Send to</label>
|
||||||
|
<div class="flex-expand mb-1 mt-half">
|
||||||
|
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
||||||
|
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
||||||
|
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="button ~urge !normal supra full-width center lg" id="create-submit">Create</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-accounts" class="unfocused">
|
||||||
|
<div class="card ~neutral !low accounts mb-1">
|
||||||
|
<span class="heading">Accounts</span>
|
||||||
|
<div class="fr">
|
||||||
|
<span class="button ~neutral !normal" id="accounts-add-user">Add User</span>
|
||||||
|
<span class="button ~urge !normal" id="accounts-modify-user">Modify Settings</span>
|
||||||
|
<span class="button ~critical !normal" id="accounts-delete-user">Delete User</span>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !normal accounts-header table-responsive mt-half">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email Address</th>
|
||||||
|
<th>Last Active</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="accounts-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tab-settings" class="unfocused">
|
||||||
|
<div class="card ~neutral !low settings overflow">
|
||||||
|
<span class="heading">Settings</span>
|
||||||
|
<div class="fr">
|
||||||
|
<span class="button ~neutral !normal unfocused" id="settings-save">Save</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="card ~neutral !normal col" id="settings-sidebar">
|
||||||
|
<aside class="aside sm ~info mb-half">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info">R</span> indicates changes require a restart.</aside>
|
||||||
|
<span class="button ~neutral !low settings-section-button mb-half" id="setting-about"><span class="flex">About <i class="ri-information-line ml-half"></i></span></span>
|
||||||
|
<span class="button ~neutral !low settings-section-button mb-half" id="setting-profiles"><span class="flex">User profiles <i class="ri-user-line ml-half"></i></span></span>
|
||||||
|
</div>
|
||||||
|
<div class="card ~neutral !normal col overflow" id="settings-panel"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="js/admin.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,10 +1,9 @@
|
|||||||
{{ define "form-base" }}
|
{{ define "form-base" }}
|
||||||
<script>
|
<script>
|
||||||
window.bs5 = {{ .bs5 }};
|
|
||||||
window.usernameEnabled = {{ .username }};
|
window.usernameEnabled = {{ .username }};
|
||||||
window.validationStrings = JSON.parse({{ .lang.validationStrings }});
|
window.validationStrings = JSON.parse({{ .lang.validationStrings }});
|
||||||
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}";
|
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}";
|
||||||
window.URLBase = "{{ .urlBase }}";
|
window.URLBase = "{{ .urlBase }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="form.js" type="module"></script>
|
<script src="js/form.js" type="module"></script>
|
||||||
{{ end }}
|
{{ end }}
|
67
html/form.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
<title>{{ .lang.pageTitle }}</title>
|
||||||
|
</head>
|
||||||
|
<body class="max-w-full overflow-x-hidden section">
|
||||||
|
<div id="modal-success" class="modal">
|
||||||
|
<div class="modal-content card">
|
||||||
|
<span class="heading mb-1">{{ .lang.successHeader }}</span>
|
||||||
|
<p class="content mb-1">{{ .successMessage }}</p>
|
||||||
|
<span class="button ~urge !normal full-width center supra submit" id="create-success-button">{{ .lang.successContinueButton }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="notification-box"></div>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="card ~neutral !low">
|
||||||
|
<div class="row baseline">
|
||||||
|
<span class="col heading">{{ .lang.createAccountHeader }}</span>
|
||||||
|
<span class="col subheading"> {{ .helpMessage }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<form class="card ~neutral !normal" id="form-create" href="">
|
||||||
|
<label class="label supra">
|
||||||
|
{{ .lang.username }}
|
||||||
|
<input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .lang.username }}" id="create-username" aria-label="{{ .lang.username }}">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="label supra" for="create-email">{{ .lang.emailAddress }}</label>
|
||||||
|
<input type="email" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.emailAddress }}" id="create-email" aria-label="{{ .lang.emailAddress }}" value="{{ .email }}">
|
||||||
|
|
||||||
|
<label class="label supra" for="create-password">{{ .lang.password }}</label>
|
||||||
|
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-password" aria-label="{{ .lang.password }}">
|
||||||
|
|
||||||
|
<label class="label supra" for="create-reenter-password">{{ .lang.reEnterPassword }}</label>
|
||||||
|
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-reenter-password" aria-label="{{ .lang.reEnterPassword }}">
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">{{ .lang.createAccountButton }}</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card ~neutral !normal">
|
||||||
|
<span class="label supra" for="inv-uses">{{ .lang.passwordRequirementsHeader }}</span>
|
||||||
|
<ul>
|
||||||
|
{{ range $key, $value := .requirements }}
|
||||||
|
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||||
|
<span class="badge lg ~positive requirement-valid"></span> <span class="content requirement-content"></span>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<aside class="col aside sm ~info">{{ .contactMessage }}</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
window.validationStrings = {{ .lang.validationStrings }};
|
||||||
|
</script>
|
||||||
|
{{ template "form-base" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
11
html/header.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
|
<meta name="msapplication-TileColor" content="#603cba">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
17
html/invalidCode.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
<title>Invalid Code - jfa-go</title>
|
||||||
|
</head>
|
||||||
|
<body class="section">
|
||||||
|
<div class="page-container">
|
||||||
|
<h1 class="heading">Invalid invite code.</h1>
|
||||||
|
<p class="content">The code above was either incorrect, or has expired.</p>
|
||||||
|
<p class="content">
|
||||||
|
{{ .contactMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="stylesheet" href="bs5-jf.css">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-t6I8D5dJmMXjCsRLhSzCltuhNZg6P10kE0m0nAncLUjH6GeYLhRU1zfLoW3QNQDF" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-t6I8D5dJmMXjCsRLhSzCltuhNZg6P10kE0m0nAncLUjH6GeYLhRU1zfLoW3QNQDF" crossorigin="anonymous"></script>
|
||||||
@ -369,6 +369,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="setup.js"></script>
|
<script src="js/setup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
BIN
images/create.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
images/demo.gif
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 2.4 MiB |
3
images/gengif.txt
Normal file
@ -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
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 71 KiB |
@ -1 +0,0 @@
|
|||||||
../data/static/banner.svg
|
|
@ -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
|
|
@ -1,8 +1,13 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
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):
|
def runcmd(cmd):
|
||||||
if os.name == "nt":
|
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]
|
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:
|
for f in html:
|
||||||
shutil.copy(str(f), str(output / f.name))
|
shutil.copy(str(f), str(output / f.name))
|
||||||
|
35
main.go
@ -48,11 +48,10 @@ type appContext struct {
|
|||||||
config *ini.File
|
config *ini.File
|
||||||
configPath string
|
configPath string
|
||||||
configBasePath string
|
configBasePath string
|
||||||
configBase map[string]interface{}
|
configBase settings
|
||||||
dataPath string
|
dataPath string
|
||||||
localPath string
|
localPath string
|
||||||
cssFile string
|
cssClass string
|
||||||
bsVersion int
|
|
||||||
jellyfinLogin bool
|
jellyfinLogin bool
|
||||||
users []User
|
users []User
|
||||||
invalidTokens []string
|
invalidTokens []string
|
||||||
@ -82,10 +81,10 @@ type Languages struct {
|
|||||||
|
|
||||||
func (app *appContext) loadHTML(router *gin.Engine) {
|
func (app *appContext) loadHTML(router *gin.Engine) {
|
||||||
customPath := app.config.Section("files").Key("html_templates").MustString("")
|
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)
|
htmlFiles, err := ioutil.ReadDir(templatePath)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
loadFiles := make([]string, len(htmlFiles))
|
loadFiles := make([]string, len(htmlFiles))
|
||||||
@ -362,14 +361,6 @@ func start(asDaemon, firstCall bool) {
|
|||||||
|
|
||||||
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
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.debug.Println("Loading storage")
|
||||||
|
|
||||||
app.storage.invite_path = app.config.Section("files").Key("invites").String()
|
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)
|
json.Unmarshal(configBase, &app.configBase)
|
||||||
|
|
||||||
themes := map[string]string{
|
themes := map[string]string{
|
||||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
|
"Jellyfin (Dark)": "dark-theme",
|
||||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion),
|
"Default (Light)": "light-theme",
|
||||||
"Custom CSS": "",
|
}
|
||||||
|
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 {
|
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)
|
secret, err := generateSecret(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatal(err)
|
app.err.Fatal(err)
|
||||||
@ -559,7 +551,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
setGinLogger(router, debugMode)
|
setGinLogger(router, debugMode)
|
||||||
|
|
||||||
router.Use(gin.Recovery())
|
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)
|
app.loadHTML(router)
|
||||||
router.NoRoute(app.NoRouteHandler)
|
router.NoRoute(app.NoRouteHandler)
|
||||||
if debugMode {
|
if debugMode {
|
||||||
@ -568,10 +560,13 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
router.GET("/", app.AdminPage)
|
router.GET("/", app.AdminPage)
|
||||||
|
router.GET("/accounts", app.AdminPage)
|
||||||
|
router.GET("/settings", app.AdminPage)
|
||||||
|
|
||||||
router.GET("/token/login", app.getTokenLogin)
|
router.GET("/token/login", app.getTokenLogin)
|
||||||
router.GET("/token/refresh", app.getTokenRefresh)
|
router.GET("/token/refresh", app.getTokenRefresh)
|
||||||
router.POST("/newUser", app.NewUser)
|
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)
|
router.GET("/invite/:invCode", app.InviteProxy)
|
||||||
if *SWAGGER {
|
if *SWAGGER {
|
||||||
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||||
|
29
models.go
@ -127,3 +127,32 @@ type errorListDTO map[string]map[string]string
|
|||||||
|
|
||||||
type configDTO map[string]interface{}
|
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"`
|
||||||
|
}
|
||||||
|
2774
package-lock.json
generated
21
package.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyfin-accounts",
|
"name": "jfa-go",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "This is only used for grabbing scss build dependencies, and isn't a real package.",
|
"description": "This is only used for grabbing scss build dependencies, and isn't a real package.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@ -8,24 +8,21 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/hrfee/jellyfin-accounts.git"
|
"url": "git+https://github.com/hrfee/jfa-go.git"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"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": {
|
"dependencies": {
|
||||||
"@types/jquery": "^3.5.3",
|
"a17t": "^0.4.0",
|
||||||
"autoprefixer": "^9.8.5",
|
|
||||||
"bootstrap": "^5.0.0-alpha3",
|
|
||||||
"bootstrap4": "npm:bootstrap@^4.5.0",
|
|
||||||
"clean-css-cli": "^4.3.0",
|
|
||||||
"esbuild": "^0.7.8",
|
"esbuild": "^0.7.8",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"mjml": "^4.6.3",
|
"mjml": "^4.8.0",
|
||||||
"postcss-cli": "^7.1.1",
|
"remixicon": "^2.5.0",
|
||||||
"typescript": "^4.0.3"
|
"typescript": "^4.0.3"
|
||||||
}
|
},
|
||||||
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
|
@ -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`.
|
|
||||||
|
|
126
scss/base.scss
@ -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;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
@import "../jf-pre.scss";
|
|
||||||
@import "../../node_modules/bootstrap4/scss/bootstrap";
|
|
||||||
@import "../jf-post.scss";
|
|
||||||
@import "../base.scss";
|
|
@ -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";
|
|
@ -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";
|
|
@ -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";
|
|
@ -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())}."
|
|
||||||
)
|
|
@ -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;
|
|
||||||
}
|
|
101
scss/jf-pre.scss
@ -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";
|
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
283
static/banner.svg
Normal file
After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 768 B |
196
ts/accounts.ts
@ -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;
|
|
||||||
|
|
||||||
(<HTMLInputElement>document.getElementById('selectAll')).onclick = function (): void {
|
|
||||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
|
|
||||||
for (let i = 0; i < checkboxes.length; i++) {
|
|
||||||
checkboxes[i].checked = (<HTMLInputElement>this).checked;
|
|
||||||
}
|
|
||||||
checkCheckboxes();
|
|
||||||
};
|
|
||||||
|
|
||||||
(<HTMLInputElement>document.getElementById('deleteModalNotify')).onclick = function (): void {
|
|
||||||
const textbox: HTMLElement = document.getElementById('deleteModalReasonBox');
|
|
||||||
if ((<HTMLInputElement>this).checked) {
|
|
||||||
Focus(textbox);
|
|
||||||
} else {
|
|
||||||
Unfocus(textbox);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(<HTMLButtonElement>document.getElementById('accountsTabDelete')).onclick = function (): void {
|
|
||||||
const deleteButton = this as HTMLButtonElement;
|
|
||||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
|
||||||
let selected: Array<string> = 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();
|
|
||||||
};
|
|
||||||
|
|
||||||
(<HTMLInputElement>document.getElementById('selectAll')).checked = false;
|
|
||||||
|
|
||||||
(<HTMLButtonElement>document.getElementById('accountsTabSetDefaults')).onclick = function (): void {
|
|
||||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
|
||||||
let userIDs: Array<string> = 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 += `
|
|
||||||
<option value="${window.availableProfiles[i]}" ${(i == 0) ? "selected" : ""}>${window.availableProfiles[i]}</option>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
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'));
|
|
||||||
(<HTMLSelectElement>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();
|
|
||||||
};
|
|
||||||
|
|
||||||
(<HTMLSelectElement>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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(<HTMLButtonElement>document.getElementById('newUserCreate')).onclick = function (): void {
|
|
||||||
const button = this as HTMLButtonElement;
|
|
||||||
const ogText = button.textContent;
|
|
||||||
button.innerHTML = `
|
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...
|
|
||||||
`;
|
|
||||||
const email: string = (<HTMLInputElement>document.getElementById('newUserEmail')).value;
|
|
||||||
var username: string = email;
|
|
||||||
if (document.getElementById('newUserName') != null) {
|
|
||||||
username = (<HTMLInputElement>document.getElementById('newUserName')).value;
|
|
||||||
}
|
|
||||||
const password: string = (<HTMLInputElement>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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
(<HTMLButtonElement>document.getElementById('accountsTabAddUser')).onclick = function (): void {
|
|
||||||
(<HTMLInputElement>document.getElementById('newUserEmail')).value = '';
|
|
||||||
(<HTMLInputElement>document.getElementById('newUserPassword')).value = '';
|
|
||||||
if (document.getElementById('newUserName') != null) {
|
|
||||||
(<HTMLInputElement>document.getElementById('newUserName')).value = '';
|
|
||||||
}
|
|
||||||
window.Modals.newUser.show();
|
|
||||||
};
|
|
267
ts/admin.ts
@ -1,124 +1,99 @@
|
|||||||
import { serializeForm, rmAttr, addAttr, _get, _post, _delete } from "./modules/common.js";
|
import { toggleTheme, loadTheme } from "./modules/theme.js";
|
||||||
import { Focus, Unfocus } from "./modules/admin.js";
|
import { Modal } from "./modules/modal.js";
|
||||||
import { toggleCSS } from "./modules/animation.js";
|
import { Tabs } from "./modules/tabs.js";
|
||||||
import { populateUsers, checkCheckboxes } from "./modules/accounts.js";
|
import { inviteList, createInvite } from "./modules/invites.js";
|
||||||
import { generateInvites, addOptions, checkDuration } from "./modules/invites.js";
|
import { accountsList } from "./modules/accounts.js";
|
||||||
import { showSetting, openSettings } from "./modules/settings.js";
|
import { settingsList } from "./modules/settings.js";
|
||||||
import { BS4 } from "./modules/bs4.js";
|
import { ProfileEditor } from "./modules/profiles.js";
|
||||||
import { BS5 } from "./modules/bs5.js";
|
import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
|
||||||
import "./accounts.js";
|
|
||||||
import "./settings.js";
|
|
||||||
|
|
||||||
interface aWindow extends Window {
|
loadTheme();
|
||||||
toClipboard(str: string): void;
|
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme;
|
||||||
}
|
|
||||||
|
|
||||||
declare var window: aWindow;
|
window.animationEvent = whichAnimationEvent();
|
||||||
|
|
||||||
interface TabSwitcher {
|
window.token = "";
|
||||||
els: Array<HTMLDivElement>;
|
|
||||||
tabButtons: Array<HTMLAnchorElement>;
|
|
||||||
focus: (el: number) => void;
|
|
||||||
invites: () => void;
|
|
||||||
accounts: () => void;
|
|
||||||
settings: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: TabSwitcher = {
|
window.availableProfiles = window.availableProfiles || [];
|
||||||
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.bsVersion = window.bs5 ? 5 : 4
|
// load modals
|
||||||
|
(() => {
|
||||||
|
window.modals = {} as Modals;
|
||||||
|
|
||||||
if (window.bs5) {
|
window.modals.login = new Modal(document.getElementById('modal-login'), true);
|
||||||
window.BS = new BS5;
|
|
||||||
} else {
|
|
||||||
window.BS = new BS4;
|
|
||||||
window.BS.Compat();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Modals = {} as BSModals;
|
window.modals.addUser = new Modal(document.getElementById('modal-add-user'));
|
||||||
|
|
||||||
window.Modals.login = window.BS.newModal('login');
|
window.modals.about = new Modal(document.getElementById('modal-about'));
|
||||||
window.Modals.userDefaults = window.BS.newModal('userDefaults');
|
(document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle;
|
||||||
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');
|
|
||||||
|
|
||||||
tabs.tabButtons[0].onclick = tabs.invites;
|
window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user'));
|
||||||
tabs.tabButtons[1].onclick = tabs.accounts;
|
|
||||||
tabs.tabButtons[2].onclick = tabs.settings;
|
|
||||||
|
|
||||||
tabs.invites();
|
window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user'));
|
||||||
|
|
||||||
// Predefined colors for the theme button.
|
window.modals.settingsRestart = new Modal(document.getElementById('modal-restart'));
|
||||||
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)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonColor != "custom") {
|
window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh'));
|
||||||
const switchButton = document.createElement('button') as HTMLButtonElement;
|
|
||||||
switchButton.classList.add('btn', 'btn-secondary');
|
|
||||||
switchButton.innerHTML = `
|
|
||||||
Theme
|
|
||||||
<i class="fa fa-circle circle" style="color: ${buttonColor}; margin-left: 0.4rem;" id="fakeButton"></i>
|
|
||||||
`;
|
|
||||||
switchButton.onclick = (): void => toggleCSS(document.getElementById('fakeButton'));
|
|
||||||
document.getElementById('headerButtons').appendChild(switchButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
var availableProfiles: Array<string>;
|
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 => {
|
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
||||||
const el = document.createElement('textarea') as HTMLTextAreaElement;
|
})();
|
||||||
el.value = str;
|
|
||||||
el.readOnly = true;
|
var inviteCreator = new createInvite();
|
||||||
el.style.position = "absolute";
|
var accounts = new accountsList();
|
||||||
el.style.left = "-9999px";
|
|
||||||
document.body.appendChild(el);
|
window.invites = new inviteList();
|
||||||
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
|
||||||
el.select();
|
var settings = new settingsList();
|
||||||
document.execCommand("copy");
|
|
||||||
document.body.removeChild(el);
|
var profiles = new ProfileEditor();
|
||||||
if (selected) {
|
|
||||||
document.getSelection().removeAllRanges();
|
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
|
||||||
document.getSelection().addRange(selected);
|
|
||||||
|
/*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();
|
const req = new XMLHttpRequest();
|
||||||
req.responseType = 'json';
|
req.responseType = 'json';
|
||||||
let url = window.URLBase;
|
let url = window.URLBase;
|
||||||
@ -135,77 +110,63 @@ function login(username: string, password: string, modal: boolean, button?: HTML
|
|||||||
req.onreadystatechange = function (): void {
|
req.onreadystatechange = function (): void {
|
||||||
if (this.readyState == 4) {
|
if (this.readyState == 4) {
|
||||||
if (this.status != 200) {
|
if (this.status != 200) {
|
||||||
let errorMsg = this.response["error"];
|
let errorMsg = "Connection error.";
|
||||||
|
if (this.response) {
|
||||||
|
errorMsg = this.response["error"];
|
||||||
|
}
|
||||||
if (!errorMsg) {
|
if (!errorMsg) {
|
||||||
errorMsg = "Unknown error";
|
errorMsg = "Unknown error";
|
||||||
}
|
}
|
||||||
if (modal) {
|
if (!refresh) {
|
||||||
button.disabled = false;
|
window.notifications.customError("loginError", errorMsg);
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
window.Modals.login.show();
|
window.modals.login.show();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = this.response;
|
const data = this.response;
|
||||||
window.token = data["token"];
|
window.token = data["token"];
|
||||||
generateInvites();
|
window.modals.login.close();
|
||||||
setInterval((): void => generateInvites(), 60 * 1000);
|
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
|
||||||
addOptions(30, document.getElementById('days') as HTMLSelectElement);
|
const currentTab = window.tabs.current;
|
||||||
addOptions(24, document.getElementById('hours') as HTMLSelectElement);
|
switch (currentTab) {
|
||||||
const minutes = document.getElementById('minutes') as HTMLSelectElement;
|
case "invites":
|
||||||
addOptions(59, minutes);
|
window.invites.reload();
|
||||||
minutes.value = "30";
|
break;
|
||||||
checkDuration();
|
case "accounts":
|
||||||
if (modal) {
|
accounts.reload();
|
||||||
window.Modals.login.hide();
|
break;
|
||||||
|
case "settings":
|
||||||
|
settings.reload();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Focus(document.getElementById('logoutButton'));
|
document.getElementById("logout-button").classList.remove("unfocused");
|
||||||
}
|
|
||||||
if (run) {
|
|
||||||
run(+this.status);
|
|
||||||
}
|
}
|
||||||
|
if (run) { run(+this.status); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
req.send();
|
req.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
(document.getElementById('loginForm') as HTMLFormElement).onsubmit = function (): boolean {
|
(document.getElementById('form-login') as HTMLFormElement).onsubmit = (event: SubmitEvent) => {
|
||||||
window.token = "";
|
event.preventDefault();
|
||||||
const details = serializeForm('loginForm');
|
const button = (event.target as HTMLElement).querySelector(".submit") as HTMLSpanElement;
|
||||||
const button = document.getElementById('loginSubmit') as HTMLButtonElement;
|
const username = (document.getElementById("login-user") as HTMLInputElement).value;
|
||||||
addAttr(button, "btn-primary");
|
const password = (document.getElementById("login-password") as HTMLInputElement).value;
|
||||||
rmAttr(button, "btn-danger");
|
if (!username || !password) {
|
||||||
button.disabled = true;
|
window.notifications.customError("loginError", "The username and/or password were left blank.");
|
||||||
button.innerHTML = `
|
return;
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
}
|
||||||
Loading...`;
|
toggleLoader(button);
|
||||||
login(details["username"], details["password"], true, button);
|
login(username, password, () => toggleLoader(button));
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
generateInvites(true);
|
login("", "");
|
||||||
|
|
||||||
login("", "", false, null, (status: number): void => {
|
(document.getElementById('logout-button') as HTMLButtonElement).onclick = () => _post("/logout", null, (req: XMLHttpRequest): boolean => {
|
||||||
if (!(status == 200 || status == 204)) {
|
if (req.readyState == 4 && req.status == 200) {
|
||||||
window.Modals.login.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(document.getElementById('logoutButton') as HTMLButtonElement).onclick = function (): void {
|
|
||||||
_post("/logout", null, function (): boolean {
|
|
||||||
if (this.readyState == 4 && this.status == 200) {
|
|
||||||
window.token = "";
|
window.token = "";
|
||||||
location.reload();
|
location.reload();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
221
ts/form.ts
@ -1,25 +1,29 @@
|
|||||||
import { serializeForm, _post, _get, _delete, addAttr, rmAttr } from "./modules/common.js";
|
import { Modal } from "./modules/modal.js";
|
||||||
import { BS5 } from "./modules/bs5.js";
|
import { _post, toggleLoader } from "./modules/common.js";
|
||||||
import { BS4 } from "./modules/bs4.js";
|
|
||||||
|
|
||||||
interface formWindow extends Window {
|
interface formWindow extends Window {
|
||||||
usernameEnabled: boolean;
|
|
||||||
validationStrings: pwValStrings;
|
validationStrings: pwValStrings;
|
||||||
checkPassword(): void;
|
|
||||||
invalidPassword: string;
|
invalidPassword: string;
|
||||||
|
modal: Modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare var window: formWindow;
|
|
||||||
|
|
||||||
interface pwValString {
|
interface pwValString {
|
||||||
singular: string;
|
singular: string;
|
||||||
plural: string;
|
plural: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface pwValStrings {
|
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 = {
|
var defaultPwValStrings: pwValStrings = {
|
||||||
length: {
|
length: {
|
||||||
singular: "Must have at least {n} character",
|
singular: "Must have at least {n} character",
|
||||||
@ -43,111 +47,132 @@ var defaultPwValStrings: pwValStrings = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSpinner = (ogText?: string): string => {
|
const form = document.getElementById("form-create") as HTMLFormElement;
|
||||||
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
|
const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement;
|
||||||
if (document.getElementById('createAccountSpinner')) {
|
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
submitButton.innerHTML = ogText ? ogText : `<span>Create Account</span>`;
|
let usernameField = document.getElementById("create-username") as HTMLInputElement;
|
||||||
submitButton.disabled = false;
|
const emailField = document.getElementById("create-email") as HTMLInputElement;
|
||||||
return "";
|
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 {
|
} else {
|
||||||
let ogText = submitButton.innerHTML;
|
rePasswordField.setCustomValidity("");
|
||||||
submitButton.innerHTML = `
|
submitButton.disabled = false;
|
||||||
<span id="createAccountSpinner" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Creating...
|
submitSpan.removeAttribute("disabled");
|
||||||
`;
|
|
||||||
return ogText;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
rePasswordField.addEventListener("keyup", checkPasswords);
|
||||||
|
passwordField.addEventListener("keyup", checkPasswords);
|
||||||
|
|
||||||
for (let key in window.validationStrings) {
|
interface respDTO {
|
||||||
if (window.validationStrings[key].singular == "" || !(window.validationStrings[key].plural.includes("{n}"))) {
|
[ type: string ]: boolean;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.BS = window.bs5 ? new BS5 : new BS4;
|
interface sendDTO {
|
||||||
var successBox: BSModal = window.BS.newModal('successBox');;
|
code: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
var code = window.location.href.split('/').pop();
|
const create = (event: SubmitEvent) => {
|
||||||
|
|
||||||
(document.getElementById('accountForm') as HTMLFormElement).addEventListener('submit', (event: any): boolean => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const el = document.getElementById('errorMessage');
|
toggleLoader(submitSpan);
|
||||||
if (el) {
|
let send: sendDTO = {
|
||||||
el.remove();
|
code: window.location.href.split('/').pop(),
|
||||||
}
|
username: usernameField.value,
|
||||||
const ogText = toggleSpinner();
|
email: emailField.value,
|
||||||
let send: Object = serializeForm('accountForm');
|
password: passwordField.value
|
||||||
send["code"] = code;
|
};
|
||||||
if (!window.usernameEnabled) {
|
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||||
send["email"] = send["username"];
|
if (req.readyState == 4) {
|
||||||
}
|
let vals = JSON.parse(req.response) as respDTO;
|
||||||
_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 += `
|
|
||||||
<button id="errorMessage" class="btn btn-outline-danger" disabled>${errorMessage}</button>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
let valid = true;
|
let valid = true;
|
||||||
for (let key in data) {
|
for (let type in vals) {
|
||||||
if (data.hasOwnProperty(key)) {
|
if (requirements[type]) { requirements[type].valid = vals[type]; }
|
||||||
const criterion = document.getElementById(key);
|
if (!vals[type]) { valid = false; }
|
||||||
if (criterion) {
|
}
|
||||||
if (data[key] === false) {
|
toggleLoader(submitSpan);
|
||||||
valid = false;
|
if (req.status == 200 && valid) {
|
||||||
addAttr(criterion, "list-group-item-danger");
|
window.modal.show();
|
||||||
rmAttr(criterion, "list-group-item-success");
|
|
||||||
} else {
|
} else {
|
||||||
addAttr(criterion, "list-group-item-success");
|
submitSpan.classList.add("~critical");
|
||||||
rmAttr(criterion, "list-group-item-danger");
|
submitSpan.classList.remove("~urge");
|
||||||
|
setTimeout(() => {
|
||||||
|
submitSpan.classList.add("~urge");
|
||||||
|
submitSpan.classList.remove("~critical");
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if (valid) {
|
|
||||||
successBox.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
window.checkPassword = (): void => {
|
form.onsubmit = create;
|
||||||
const entry = document.getElementById('inputPassword') as HTMLInputElement;
|
|
||||||
if (entry.value != "") {
|
class Requirement {
|
||||||
const reentry = document.getElementById('reInputPassword') as HTMLInputElement;
|
private _name: string;
|
||||||
const identical = (entry.value == reentry.value);
|
private _minCount: number;
|
||||||
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
|
private _content: HTMLSpanElement;
|
||||||
if (identical) {
|
private _valid: HTMLSpanElement;
|
||||||
reentry.setCustomValidity('');
|
private _li: HTMLLIElement;
|
||||||
rmAttr(submitButton, "btn-outline-danger");
|
|
||||||
addAttr(submitButton, "btn-outline-primary");
|
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 = `<i class="icon ri-check-line" title="valid"></i>`;
|
||||||
} else {
|
} else {
|
||||||
reentry.setCustomValidity(window.invalidPassword);
|
this._valid.classList.add("~critical");
|
||||||
addAttr(submitButton, "btn-outline-danger");
|
this._valid.classList.remove("~positive");
|
||||||
rmAttr(submitButton, "btn-outline-primary");
|
this._valid.innerHTML = `<i class="icon ri-close-line" title="invalid"></i>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<HTMLInputElement> = [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 = `
|
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
|
||||||
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<string> = ["days", "hours", "minutes"];
|
|
||||||
for (const i in nE) {
|
|
||||||
document.getElementById(nE[i]).addEventListener("change", checkDuration);
|
|
||||||
}
|
|
@ -1,163 +1,409 @@
|
|||||||
import { _get, _post, _delete, createEl } from "../modules/common.js";
|
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
|
||||||
import { Focus, Unfocus } from "../modules/admin.js";
|
|
||||||
|
|
||||||
interface aWindow extends Window {
|
interface User {
|
||||||
checkCheckboxes: () => void;
|
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 => {
|
get name(): string { return this._username.textContent; }
|
||||||
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
set name(value: string) { this._username.textContent = value; }
|
||||||
const deleteButton = document.getElementById('accountsTabDelete');
|
|
||||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
get admin(): boolean { return this._admin.classList.contains("chip"); }
|
||||||
let checked = checkboxes.length;
|
set admin(state: boolean) {
|
||||||
if (checked == 0) {
|
if (state) {
|
||||||
Unfocus(defaultsButton);
|
this._admin.classList.add("chip", "~info", "ml-1");
|
||||||
Unfocus(deleteButton);
|
this._admin.textContent = "Admin";
|
||||||
} else {
|
} else {
|
||||||
Focus(defaultsButton);
|
this._admin.classList.remove("chip", "~info", "ml-1");
|
||||||
Focus(deleteButton);
|
this._admin.textContent = ""
|
||||||
if (checked == 1) {
|
}
|
||||||
deleteButton.textContent = 'Delete User';
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
private _checkEvent = new CustomEvent("accountCheckEvent");
|
||||||
|
private _uncheckEvent = new CustomEvent("accountUncheckEvent");
|
||||||
|
|
||||||
|
constructor(user: User) {
|
||||||
|
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||||
|
this._row.innerHTML = `
|
||||||
|
<td><input type="checkbox" value=""></td>
|
||||||
|
<td><span class="accounts-username"></span> <span class="accounts-admin"></span></td>
|
||||||
|
<td><i class="icon ri-edit-line accounts-email-edit"></i><input type="email" class="input ~neutral !normal stealth-input stealth-input-hidden accounts-email" readonly></td>
|
||||||
|
<td class="accounts-last-active"></td>
|
||||||
|
`;
|
||||||
|
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 {
|
} else {
|
||||||
deleteButton.textContent = 'Delete Users';
|
this._updateEmail();
|
||||||
|
document.removeEventListener('click', outerClickListener);
|
||||||
}
|
}
|
||||||
|
toggleStealthInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateEmail = () => {
|
||||||
|
let oldEmail = this.email;
|
||||||
|
this.email = this._email.value;
|
||||||
|
let send = {};
|
||||||
|
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 {
|
||||||
|
this.email = oldEmail;
|
||||||
|
window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.checkCheckboxes = checkCheckboxes;
|
|
||||||
|
|
||||||
export function changeEmail(icon: HTMLElement, id: string): void {
|
|
||||||
const iconContent = icon.outerHTML;
|
|
||||||
icon.setAttribute('class', '');
|
export class accountsList {
|
||||||
const entry = icon.nextElementSibling as HTMLInputElement;
|
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||||
const ogEmail = entry.value;
|
|
||||||
entry.readOnly = false;
|
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||||
entry.classList.remove('form-control-plaintext');
|
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||||
entry.classList.add('form-control');
|
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||||
if (ogEmail == "") {
|
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
|
||||||
entry.placeholder = 'Address';
|
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;
|
||||||
}
|
}
|
||||||
const tick = createEl(`
|
this._selectAll.checked = state;
|
||||||
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
|
this._selectAll.indeterminate = false;
|
||||||
`);
|
state ? this._checkCount = Object.keys(this._users).length : 0;
|
||||||
tick.onclick = (): void => {
|
|
||||||
const newEmail = entry.value;
|
}
|
||||||
if (!validateEmail(newEmail) || newEmail == ogEmail) {
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
cross.remove();
|
}
|
||||||
const spinner = createEl(`
|
toggleLoader(button);
|
||||||
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
_post("/users", send, (req: XMLHttpRequest) => {
|
||||||
<span class="sr-only">Saving...</span>
|
if (req.readyState == 4) {
|
||||||
</div>
|
toggleLoader(button);
|
||||||
`);
|
if (req.status == 200) {
|
||||||
tick.replaceWith(spinner);
|
window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`);
|
||||||
let send = {};
|
}
|
||||||
send[id] = newEmail;
|
this.reload();
|
||||||
_post("/users/emails", send, function (): void {
|
window.modals.addUser.close();
|
||||||
if (this.readyState == 4) {
|
}
|
||||||
if (this.status == 200 || this.status == 204) {
|
});
|
||||||
entry.nextElementSibling.remove();
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
entry.value = ogEmail;
|
window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`);
|
||||||
}
|
}
|
||||||
|
this.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
icon.outerHTML = iconContent;
|
|
||||||
entry.readOnly = true;
|
|
||||||
entry.classList.remove('form-control');
|
|
||||||
entry.classList.add('form-control-plaintext');
|
|
||||||
entry.placeholder = '';
|
|
||||||
};
|
|
||||||
const cross = createEl(`
|
|
||||||
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
|
|
||||||
`);
|
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
window.modals.deleteUser.show();
|
||||||
|
}
|
||||||
|
|
||||||
export function populateUsers(): void {
|
modifyUsers = () => {
|
||||||
const acList = document.getElementById('accountsList');
|
const modalHeader = document.getElementById("header-modify-user");
|
||||||
acList.innerHTML = `
|
modalHeader.textContent = this._genCountString();
|
||||||
<div class="d-flex align-items-center">
|
let list = this._collectUsers();
|
||||||
<strong>Getting Users...</strong>
|
(() => {
|
||||||
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
|
let innerHTML = "";
|
||||||
</div>
|
for (const profile of window.availableProfiles) {
|
||||||
`;
|
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||||
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 = `
|
this._profileSelect.innerHTML = innerHTML;
|
||||||
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
|
})();
|
||||||
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
|
|
||||||
`;
|
|
||||||
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 `
|
|
||||||
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
|
|
||||||
<td nowrap="nowrap" class="align-middle">${username}${admin ? '<span style="margin-left: 1rem;" class="badge rounded-pill bg-info text-dark">Admin</span>' : ''}</td>
|
|
||||||
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
|
||||||
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
_get("/users", null, function (): void {
|
(() => {
|
||||||
if (this.readyState == 4 && this.status == 200) {
|
let innerHTML = "";
|
||||||
window.jfUsers = this.response['users'];
|
for (let id in this._users) {
|
||||||
for (const user of window.jfUsers) {
|
innerHTML += `<option value="${id}">${this._users[id].name}</option>`;
|
||||||
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'));
|
this._userSelect.innerHTML = innerHTML;
|
||||||
acList.replaceWith(accountsList);
|
})();
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function populateRadios(): void {
|
|
||||||
const radioList = document.getElementById('defaultUserRadios');
|
|
||||||
radioList.textContent = '';
|
constructor() {
|
||||||
let first = true;
|
this._users = {};
|
||||||
for (const i in window.jfUsers) {
|
this._selectAll.checked = false;
|
||||||
const user = window.jfUsers[i];
|
this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked };
|
||||||
const radio = document.createElement('div');
|
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
|
||||||
radio.classList.add('form-check');
|
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
|
||||||
let checked = '';
|
this._addUserButton.onclick = window.modals.addUser.toggle;
|
||||||
if (first) {
|
this._addUserForm.addEventListener("submit", this._addUser);
|
||||||
checked = 'checked';
|
|
||||||
first = false;
|
this._deleteNotify.onchange = () => {
|
||||||
|
if (this._deleteNotify.checked) {
|
||||||
|
this._deleteReason.classList.remove("unfocused");
|
||||||
|
} else {
|
||||||
|
this._deleteReason.classList.add("unfocused");
|
||||||
}
|
}
|
||||||
radio.innerHTML = `
|
};
|
||||||
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
|
this._modifySettings.onclick = this.modifyUsers;
|
||||||
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
|
this._modifySettings.classList.add("unfocused");
|
||||||
radioList.appendChild(radio);
|
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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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<string>): void {
|
|
||||||
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
|
|
||||||
button.disabled = true;
|
|
||||||
button.innerHTML =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -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<HTMLLinkElement> = 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");
|
|
||||||
}
|
|
||||||
};
|
|
@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
@ -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);
|
|
||||||
};
|
|
||||||
};
|
|
@ -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 addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||||
|
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
|
||||||
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
|
|
||||||
let req = new XMLHttpRequest();
|
let req = new XMLHttpRequest();
|
||||||
req.open("GET", window.URLBase + url, true);
|
req.open("GET", window.URLBase + url, true);
|
||||||
req.responseType = 'json';
|
req.responseType = 'json';
|
||||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
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));
|
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();
|
let req = new XMLHttpRequest();
|
||||||
req.open("POST", window.URLBase + url, true);
|
req.open("POST", window.URLBase + url, true);
|
||||||
if (response) {
|
if (response) {
|
||||||
@ -68,16 +75,131 @@ export const _post = (url: string, data: Object, onreadystatechange: () => void,
|
|||||||
}
|
}
|
||||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
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));
|
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();
|
let req = new XMLHttpRequest();
|
||||||
req.open("DELETE", window.URLBase + url, true);
|
req.open("DELETE", window.URLBase + url, true);
|
||||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
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));
|
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 = `<strong>Error:</strong> ${message}`;
|
||||||
|
const closeButton = document.createElement('span') as HTMLSpanElement;
|
||||||
|
closeButton.classList.add("button", "~critical", "!low", "ml-1");
|
||||||
|
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
|
||||||
|
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 = `<strong>${bold}</strong> ${message}`;
|
||||||
|
const closeButton = document.createElement('span') as HTMLSpanElement;
|
||||||
|
closeButton.classList.add("button", "~positive", "!low", "ml-1");
|
||||||
|
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
export class DOMInvite implements Invite {
|
||||||
setNotify(el: HTMLElement): void;
|
updateNotify = (checkbox: HTMLInputElement) => {
|
||||||
deleteInvite(code: string): void;
|
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 };
|
||||||
}
|
}
|
||||||
|
_post("/invites/notify", state, (req: XMLHttpRequest) => {
|
||||||
declare var window: aWindow;
|
if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) {
|
||||||
|
revertChanges();
|
||||||
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
|
|
||||||
|
|
||||||
function genUsedBy(usedBy: Array<Array<string>>): string {
|
|
||||||
let uB = "";
|
|
||||||
if (usedBy && usedBy.length != 0) {
|
|
||||||
uB = `
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
<li class="list-group-item py-1">Users created:</li>
|
|
||||||
`;
|
|
||||||
for (const i in usedBy) {
|
|
||||||
uB += `
|
|
||||||
<li class="list-group-item py-1 disabled">
|
|
||||||
<div class="d-flex float-left">${usedBy[i][0]}</div>
|
|
||||||
<div class="d-flex float-right">${usedBy[i][1]}</div>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
uB += `</ul>`
|
|
||||||
}
|
|
||||||
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 = `<a>None</a>`;
|
|
||||||
if (invite.empty) {
|
|
||||||
item.innerHTML = `
|
|
||||||
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
|
||||||
${innerHTML}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(item);
|
|
||||||
links.appendChild(container);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
link = window.location.href.split('#')[0] + "invite/" + invite.code;
|
|
||||||
innerHTML = `
|
|
||||||
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
|
||||||
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
|
|
||||||
<i class="fa fa-clipboard icon-button" onclick="window.toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
|
|
||||||
`;
|
|
||||||
if (invite.email) {
|
|
||||||
let email = invite.email;
|
|
||||||
if (!invite.email.includes("Failed to send to")) {
|
|
||||||
email = `Sent to ${email}`;
|
|
||||||
}
|
|
||||||
innerHTML += `
|
|
||||||
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
innerHTML += `
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
|
|
||||||
<div style="display: inline-block;">
|
|
||||||
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
|
|
||||||
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="window.rotateButton(this)"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
item.innerHTML = innerHTML;
|
|
||||||
container.appendChild(item);
|
|
||||||
|
|
||||||
let profiles = `
|
|
||||||
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
|
|
||||||
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="window.setProfile(this)">
|
|
||||||
<option value="NoProfile" selected>No Profile</option>
|
|
||||||
`;
|
|
||||||
for (const i in window.availableProfiles) {
|
|
||||||
let selected = "";
|
|
||||||
if (window.availableProfiles[i] == invite.profile) {
|
|
||||||
selected = "selected";
|
|
||||||
}
|
|
||||||
profiles += `<option value="${window.availableProfiles[i]}" ${selected}>${window.availableProfiles[i]}</option>`;
|
|
||||||
}
|
|
||||||
profiles += `</select>`;
|
|
||||||
|
|
||||||
let dateCreated: string;
|
|
||||||
if (invite.created) {
|
|
||||||
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let middle: string;
|
|
||||||
if (window.notifications_enabled) {
|
|
||||||
middle = `
|
|
||||||
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
Notify on:
|
|
||||||
<li class="list-group-item py-1 form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
|
|
||||||
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item py-1 form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
|
|
||||||
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div class="container row align-items-start card-body">
|
|
||||||
<div class="col">
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
<li class="input-group py-1">
|
|
||||||
${profiles}
|
|
||||||
</li>
|
|
||||||
${dateCreated}
|
|
||||||
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
${middle}
|
|
||||||
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
|
|
||||||
${right}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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]} `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
|
||||||
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
|
if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
|
||||||
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
|
this.remove();
|
||||||
if (remainingUses) {
|
const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code });
|
||||||
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
|
document.dispatchEvent(inviteDeletedEvent);
|
||||||
}
|
}
|
||||||
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
|
})
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete invite from DOM
|
private _remainingUses: string = "1";
|
||||||
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
|
get remainingUses(): string { return this._remainingUses; }
|
||||||
|
set remainingUses(remaining: string) {
|
||||||
// delete invite from jfa-go
|
this._remainingUses = remaining;
|
||||||
window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
|
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
||||||
if (this.readyState == 4) {
|
|
||||||
generateInvites();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
export function generateInvites(empty?: boolean): void {
|
private _email: string = "";
|
||||||
if (empty) {
|
get email(): string { return this._email };
|
||||||
document.getElementById('invites').textContent = '';
|
set email(address: string) {
|
||||||
addItem(emptyInvite());
|
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 = `<p class="content">None yet!</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_get("/invites", null, function (): void {
|
this._right.classList.remove("empty");
|
||||||
if (this.readyState == 4) {
|
let innerHTML = `
|
||||||
let data = this.response;
|
<table class="table inv-table">
|
||||||
window.availableProfiles = data['profiles'];
|
<thead>
|
||||||
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
|
<tr>
|
||||||
let innerHTML = "";
|
<th>Name</th>
|
||||||
for (let i = 0; i < window.availableProfiles.length; i++) {
|
<th>Date</th>
|
||||||
const profile = window.availableProfiles[i];
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
for (let user of uB) {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
|
<tr>
|
||||||
|
<td>${user[0]}</td>
|
||||||
|
<td>${user[1]}</td>
|
||||||
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
|
</tbody>
|
||||||
|
</table>
|
||||||
`;
|
`;
|
||||||
Profiles.innerHTML = innerHTML;
|
this._userTable.innerHTML = innerHTML;
|
||||||
if (data['invites'] == null || data['invites'].length == 0) {
|
|
||||||
document.getElementById('invites').textContent = '';
|
|
||||||
addItem(emptyInvite());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
let items = document.getElementById('invites').children;
|
|
||||||
for (const i in data['invites']) {
|
private _created: string;
|
||||||
let match = false;
|
get created(): string { return this._created; }
|
||||||
const inv = parseInvite(data['invites'][i]);
|
set created(created: string) {
|
||||||
for (const x in items) {
|
this._created = created;
|
||||||
if (items[x].id == inv.code) {
|
this._middle.querySelector("strong.inv-created").textContent = created;
|
||||||
match = true;
|
|
||||||
updateInvite(inv);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notifyExpiry: boolean = false;
|
||||||
|
get notifyExpiry(): boolean { return this._notifyExpiry }
|
||||||
|
set notifyExpiry(state: boolean) {
|
||||||
|
this._notifyExpiry = state;
|
||||||
|
(this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state;
|
||||||
}
|
}
|
||||||
if (!match) {
|
|
||||||
addItem(inv);
|
private _notifyCreation: boolean = false;
|
||||||
|
get notifyCreation(): boolean { return this._notifyCreation }
|
||||||
|
set notifyCreation(state: boolean) {
|
||||||
|
this._notifyCreation = state;
|
||||||
|
(this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _profile: string;
|
||||||
|
get profile(): string { return this._profile; }
|
||||||
|
set profile(profile: string) { this.loadProfiles(profile); }
|
||||||
|
loadProfiles = (selected?: string) => {
|
||||||
|
const select = this._left.querySelector("select") as HTMLSelectElement;
|
||||||
|
let noProfile = false;
|
||||||
|
if (selected === "") {
|
||||||
|
noProfile = true;
|
||||||
|
} else {
|
||||||
|
selected = selected || select.value;
|
||||||
}
|
}
|
||||||
// second pass to check for expired invites
|
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>No Profile</option>`;
|
||||||
items = document.getElementById('invites').children;
|
for (let profile of window.availableProfiles) {
|
||||||
for (let i = 0; i < items.length; i++) {
|
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addOptions = (length: number, el: HTMLSelectElement): void => {
|
private _container: HTMLDivElement;
|
||||||
for (let v = 0; v <= length; v++) {
|
|
||||||
const opt = document.createElement('option');
|
private _header: HTMLDivElement;
|
||||||
opt.textContent = ""+v;
|
private _codeArea: HTMLDivElement;
|
||||||
opt.value = ""+v;
|
private _infoArea: HTMLDivElement;
|
||||||
el.appendChild(opt);
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
el.value = "0";
|
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 = `
|
||||||
|
<a class="invite-link code monospace mr-1" href=""></a>
|
||||||
|
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function checkDuration(): void {
|
this._infoArea = document.createElement('div') as HTMLDivElement;
|
||||||
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
|
this._header.appendChild(this._infoArea);
|
||||||
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
|
this._infoArea.classList.add("inv-infoarea");
|
||||||
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
|
this._infoArea.innerHTML = `
|
||||||
submit.disabled = true;
|
<div class="tooltip left">
|
||||||
|
<span class="inv-email-chip"><i></i></span>
|
||||||
|
<span class="content sm"></span>
|
||||||
|
</div>
|
||||||
|
<span class="inv-expiry mr-1"></span>
|
||||||
|
<span class="button ~critical !normal inv-delete">Delete</span>
|
||||||
|
<label>
|
||||||
|
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||||
|
<input class="inv-toggle-details unfocused" type="checkbox">
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
|
||||||
|
(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 = `
|
||||||
|
<p class="supra mb-1 top">Profile</p>
|
||||||
|
<div class="select ~neutral !normal inv-profileselect inline-block">
|
||||||
|
<select>
|
||||||
|
<option value="noProfile" selected>No Profile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
if (window.notificationsEnabled) {
|
||||||
|
innerHTML += `
|
||||||
|
<p class="label supra">Notify on:</p>
|
||||||
|
<label class="switch block">
|
||||||
|
<input class="inv-notify-expiry" type="checkbox">
|
||||||
|
<span>On expiry</span>
|
||||||
|
</label>
|
||||||
|
<label class="switch block">
|
||||||
|
<input class="inv-notify-creation" type="checkbox">
|
||||||
|
<span>On user creation</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
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 = `
|
||||||
|
<p class="supra mb-1 top">Created <strong class="inv-created"></strong></p>
|
||||||
|
<p class="supra mb-1">Remaining uses <strong class="inv-remaining"></strong></p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this._right = document.createElement('div') as HTMLDivElement;
|
||||||
|
detailsInner.appendChild(this._right);
|
||||||
|
this._right.classList.add("card", "~neutral", "!low", "inv-created-users");
|
||||||
|
this._right.innerHTML = `<strong class="supra table-header">Created users</strong>`;
|
||||||
|
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 = `
|
||||||
|
<div class="inv inv-empty">
|
||||||
|
<div class="card ~neutral !normal inv-header flex-expand mt-half">
|
||||||
|
<div class="inv-codearea">
|
||||||
|
<span class="code monospace">None</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
submit.disabled = false;
|
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;
|
||||||
|
}
|
||||||
|
// 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<any>)) {
|
||||||
|
const invite = parseInvite(inv);
|
||||||
|
if (invite.code in this.invites) {
|
||||||
|
this.invites[invite.code].update(invite);
|
||||||
|
delete invitesOnDOM[invite.code];
|
||||||
|
} else {
|
||||||
|
this.add(invite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
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 = `<option value="noProfile">No Profile</option>`;
|
||||||
|
for (let profile of window.availableProfiles) {
|
||||||
|
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
42
ts/modules/modal.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
204
ts/modules/profiles.ts
Normal file
@ -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 = `
|
||||||
|
<td><b class="profile-name"></b> <span class="profile-admin"></span></td>
|
||||||
|
<td><input type="radio" name="profile-default"></td>
|
||||||
|
<td class="profile-from ellipsis"></td>
|
||||||
|
<td class="profile-libraries"></td>
|
||||||
|
<td><span class="button ~critical !normal">Delete</span></td>
|
||||||
|
`;
|
||||||
|
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 = `<tr><td class="empty">None</td></tr>`
|
||||||
|
} 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 += `<option value="${user['id']}">${user['name']}</option>`;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,164 +1,614 @@
|
|||||||
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
|
import { _get, _post, toggleLoader } from "../modules/common.js";
|
||||||
import { Focus, Unfocus } from "../modules/admin.js";
|
|
||||||
|
|
||||||
interface Profile {
|
interface settingsBoolEvent extends Event {
|
||||||
Admin: boolean;
|
detail: boolean;
|
||||||
LibraryAccess: string;
|
|
||||||
FromUser: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
|
interface Meta {
|
||||||
if (this.readyState == 4 && this.status == 200) {
|
name: string;
|
||||||
const profileList = document.getElementById('profileList');
|
description: string;
|
||||||
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") {
|
interface Setting {
|
||||||
const profile: Profile = {
|
name: string;
|
||||||
Admin: reqProfile["admin"],
|
description: string;
|
||||||
LibraryAccess: reqProfile["libraries"],
|
required: boolean;
|
||||||
FromUser: reqProfile["fromUser"]
|
requires_restart: boolean;
|
||||||
};
|
type: string;
|
||||||
profileList.innerHTML += `
|
value: string | boolean | number;
|
||||||
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
|
depends_true?: Setting;
|
||||||
<td nowrap="nowrap" class="align-middle"><input class="${window.bs5 ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == window.availableProfiles[0]) ? "checked" : ""}></td>
|
depends_false?: Setting;
|
||||||
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
|
|
||||||
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
|
asElement: () => HTMLElement;
|
||||||
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
|
update: (s: Setting) => void;
|
||||||
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(inputType: string, setting: Setting, section: string, name: string) {
|
||||||
|
this._container = document.createElement("div");
|
||||||
|
this._container.classList.add("setting");
|
||||||
|
this._container.innerHTML = `
|
||||||
|
<label class="label">
|
||||||
|
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||||
|
<div class="setting-tooltip tooltip right unfocused">
|
||||||
|
<i class="icon ri-information-line"></i>
|
||||||
|
<span class="content sm"></span>
|
||||||
|
</div>
|
||||||
|
<input type="${inputType}" class="input ~neutral !normal mt-half">
|
||||||
|
</label>
|
||||||
`;
|
`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox">
|
||||||
|
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||||
|
<div class="setting-tooltip tooltip right unfocused">
|
||||||
|
<i class="icon ri-information-line"></i>
|
||||||
|
<span class="content sm"></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
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 += `<option value="${option}">${option}</option>`;
|
||||||
|
}
|
||||||
|
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 = `
|
||||||
|
<label class="label">
|
||||||
|
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||||
|
<div class="setting-tooltip tooltip right unfocused">
|
||||||
|
<i class="icon ri-information-line"></i>
|
||||||
|
<span class="content sm"></span>
|
||||||
|
</div>
|
||||||
|
<div class="select ~neutral !normal mt-half">
|
||||||
|
<select class="settings-select"></select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<span class="heading">${s.meta.name}</span>
|
||||||
|
<p class="support lg">${s.meta.description}</p>
|
||||||
|
`;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
|
constructor() {
|
||||||
if (this.readyState == 4 && this.status == 200) {
|
this._sections = {};
|
||||||
settingsList.textContent = '';
|
this._buttons = {};
|
||||||
window.config = this.response;
|
document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused"));
|
||||||
for (const i in window.config["order"]) {
|
this._saveButton.onclick = this._save;
|
||||||
const section: string = window.config["order"][i]
|
document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; });
|
||||||
const sectionCollapse = document.createElement('div') as HTMLDivElement;
|
|
||||||
Unfocus(sectionCollapse);
|
|
||||||
sectionCollapse.id = section;
|
|
||||||
|
|
||||||
const title: string = window.config[section]["meta"]["name"];
|
if (window.ombiEnabled) {
|
||||||
const description: string = window.config[section]["meta"]["description"];
|
let ombi = new ombiDefaults();
|
||||||
const entryListID: string = `${section}_entryList`;
|
this._sidebar.appendChild(ombi.button());
|
||||||
// const footerID: string = `${section}_footer`;
|
|
||||||
|
|
||||||
sectionCollapse.innerHTML = `
|
|
||||||
<div class="card card-body">
|
|
||||||
<small class="text-muted">${description}</small>
|
|
||||||
<div class="${entryListID}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const x in config[section]["order"]) {
|
|
||||||
const entry: string = config[section]["order"][x];
|
|
||||||
if (entry == "meta") {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
let entryName: string = window.config[section][entry]["name"];
|
|
||||||
let required = false;
|
|
||||||
if (window.config[section][entry]["required"]) {
|
|
||||||
entryName += ` <sup class="text-danger">*</sup>`;
|
|
||||||
required = true;
|
|
||||||
}
|
|
||||||
if (window.config[section][entry]["requires_restart"]) {
|
|
||||||
entryName += ` <sup class="text-danger">R</sup>`;
|
|
||||||
}
|
|
||||||
if ("description" in window.config[section][entry]) {
|
|
||||||
entryName +=`
|
|
||||||
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${window.config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
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 = `
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
|
|
||||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
|
||||||
`;
|
|
||||||
(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 = `
|
|
||||||
<label for="${section}_${entry}">${entryName}</label>
|
|
||||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
|
|
||||||
`;
|
|
||||||
} else if (entryType == 'select') {
|
|
||||||
entryGroup.classList.add("form-group");
|
|
||||||
const entryOptions: Array<string> = window.config[section][entry]["options"];
|
|
||||||
let innerGroup = `
|
|
||||||
<label for="${section}_${entry}">${entryName}</label>
|
|
||||||
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
|
|
||||||
`;
|
|
||||||
for (const z in entryOptions) {
|
|
||||||
const entryOption = entryOptions[z];
|
|
||||||
let selected: boolean = (entryOption == entryValue);
|
|
||||||
innerGroup += `
|
|
||||||
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
innerGroup += `</select>`;
|
|
||||||
entryGroup.innerHTML = innerGroup;
|
|
||||||
}
|
|
||||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsList.innerHTML += `
|
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||||
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
|
if (req.readyState == 4) {
|
||||||
`;
|
if (req.status != 200) {
|
||||||
settingsContent.appendChild(sectionCollapse);
|
window.notifications.customError("settingsLoadError", "Failed to load settings.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (callback) {
|
let settings = req.response as Settings;
|
||||||
callback();
|
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 = `<span class="flex">Ombi user defaults <i class="ri-link-unlink-m ml-half"></i></span>`;
|
||||||
|
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 += `<option value="${user.id}">${user.name}</option>`;
|
||||||
|
}
|
||||||
|
this._select.innerHTML = innerHTML;
|
||||||
|
toggleLoader(this._button);
|
||||||
|
window.modals.ombiDefaults.show();
|
||||||
|
} else {
|
||||||
|
toggleLoader(this._button);
|
||||||
|
window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function showSetting(id: string, runBefore?: () => void): void {
|
|
||||||
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addAttr(document.getElementById(`${id}_button`), "active");
|
|
||||||
const section = document.getElementById(id);
|
|
||||||
if (runBefore) {
|
|
||||||
runBefore();
|
|
||||||
}
|
|
||||||
Focus(section);
|
|
||||||
if (screen.width <= 1100) {
|
|
||||||
// ugly
|
|
||||||
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
40
ts/modules/tabs.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export class Tabs implements Tabs {
|
||||||
|
private _current: string = "";
|
||||||
|
tabs: Array<Tab>;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
ts/modules/theme.ts
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
}
|
84
ts/ombi.ts
@ -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 =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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 = `
|
|
||||||
<input class="form-check-input" type="radio" name="ombiRadios" id="ombiDefault_${user['id']}" ${checked}>
|
|
||||||
<label class="form-check-label" for="ombiDefault_${user['id']}">${user['name']}</label>
|
|
||||||
`;
|
|
||||||
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 =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
205
ts/settings.ts
@ -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 =
|
|
||||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
|
||||||
'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 = `
|
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
|
||||||
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";
|
|
||||||
};
|
|
260
ts/setup.ts
Normal file
@ -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 =
|
||||||
|
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||||
|
'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 =`
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../data/static",
|
"outDir": "../js",
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"lib": ["dom", "es2017"],
|
"lib": ["dom", "es2017"],
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||||
|
@ -1,62 +1,91 @@
|
|||||||
declare interface ModalConstructor {
|
declare interface Modal {
|
||||||
(id: string, find?: boolean): BSModal;
|
modal: HTMLElement;
|
||||||
|
closeButton: HTMLSpanElement
|
||||||
|
show: () => void;
|
||||||
|
close: (event?: Event) => void;
|
||||||
|
toggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface BSModal {
|
interface ArrayConstructor {
|
||||||
el: HTMLDivElement;
|
from(arrayLike: any, mapFn?, thisArg?): Array<any>;
|
||||||
modal: any;
|
|
||||||
show: () => void;
|
|
||||||
hide: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Window {
|
declare interface Window {
|
||||||
getComputedStyle(element: HTMLElement, pseudoElt: HTMLElement): any;
|
|
||||||
bsVersion: number;
|
|
||||||
bs5: boolean;
|
|
||||||
BS: Bootstrap;
|
|
||||||
URLBase: string;
|
URLBase: string;
|
||||||
Modals: BSModals;
|
modals: Modals;
|
||||||
cssFile: string;
|
cssFile: string;
|
||||||
availableProfiles: Array<any>;
|
availableProfiles: string[];
|
||||||
jfUsers: Array<Object>;
|
jfUsers: Array<Object>;
|
||||||
notifications_enabled: boolean;
|
notificationsEnabled: boolean;
|
||||||
|
emailEnabled: boolean;
|
||||||
|
ombiEnabled: boolean;
|
||||||
|
usernameEnabled: boolean;
|
||||||
token: string;
|
token: string;
|
||||||
buttonWidth: number;
|
buttonWidth: number;
|
||||||
|
transitionEvent: string;
|
||||||
|
animationEvent: string;
|
||||||
|
tabs: Tabs;
|
||||||
|
invites: inviteList;
|
||||||
|
notifications: NotificationBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface tooltipTrigger {
|
declare interface NotificationBox {
|
||||||
(): void;
|
connectionError: () => void;
|
||||||
|
customError: (type: string, message: string) => void;
|
||||||
|
customPositive: (type: string, bold: string, message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Bootstrap {
|
declare interface Tabs {
|
||||||
newModal: ModalConstructor;
|
current: string;
|
||||||
triggerTooltips: tooltipTrigger;
|
tabs: Array<Tab>;
|
||||||
Compat?(): void;
|
addTab: (tabID: string, preFunc?: () => void, postFunc?: () => void) => void;
|
||||||
|
switch: (tabID: string, noRun?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface BSModals {
|
declare interface Tab {
|
||||||
login: BSModal;
|
tabID: string;
|
||||||
userDefaults: BSModal;
|
tabEl: HTMLDivElement;
|
||||||
users: BSModal;
|
buttonEl: HTMLSpanElement;
|
||||||
restart: BSModal;
|
preFunc?: () => void;
|
||||||
refresh: BSModal;
|
postFunc?: () => void;
|
||||||
about: BSModal;
|
}
|
||||||
delete: BSModal;
|
|
||||||
newUser: BSModal;
|
|
||||||
|
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 {
|
interface Invite {
|
||||||
code?: string;
|
code?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
empty: boolean;
|
|
||||||
remainingUses?: string;
|
remainingUses?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
usedBy?: Array<Array<string>>;
|
usedBy?: string[][];
|
||||||
created?: string;
|
created?: string;
|
||||||
notifyExpiry?: boolean;
|
notifyExpiry?: boolean;
|
||||||
notifyCreation?: boolean;
|
notifyCreation?: boolean;
|
||||||
profile?: string;
|
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 config: Object;
|
||||||
declare var modifiedConfig: Object;
|
declare var modifiedConfig: Object;
|
||||||
|
13
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) {
|
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()
|
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||||
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
||||||
"urlBase": app.URLBase,
|
"urlBase": app.URLBase,
|
||||||
"bs5": bs5,
|
"cssClass": app.cssClass,
|
||||||
"cssFile": app.cssFile,
|
|
||||||
"contactMessage": "",
|
"contactMessage": "",
|
||||||
"email_enabled": emailEnabled,
|
"email_enabled": emailEnabled,
|
||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
@ -42,7 +40,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
|
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
|
||||||
"urlBase": app.URLBase,
|
"urlBase": app.URLBase,
|
||||||
"cssFile": app.cssFile,
|
"cssClass": app.cssClass,
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
"helpMessage": app.config.Section("ui").Key("help_message").String(),
|
"helpMessage": app.config.Section("ui").Key("help_message").String(),
|
||||||
"successMessage": app.config.Section("ui").Key("success_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),
|
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||||
"requirements": app.validator.getCriteria(),
|
"requirements": app.validator.getCriteria(),
|
||||||
"email": email,
|
"email": email,
|
||||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
|
||||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||||
"lang": app.storage.lang.Form["strings"],
|
"lang": app.storage.lang.Form["strings"],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
"cssClass": app.cssClass,
|
||||||
"cssFile": app.cssFile,
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
"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) {
|
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
||||||
gcHTML(gc, 404, "404.html", gin.H{
|
gcHTML(gc, 404, "404.html", gin.H{
|
||||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
"cssClass": app.cssClass,
|
||||||
"cssFile": app.cssFile,
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|