mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
24 Commits
a102199d5a
...
ca0889aaab
Author | SHA1 | Date | |
---|---|---|---|
ca0889aaab | |||
772e12d11c | |||
7d04487b18 | |||
0b482116bb | |||
a579bcd463 | |||
ab7017ff12 | |||
3d5bea003a | |||
bc99dc34ee | |||
965c449f1c | |||
4679c6f355 | |||
a3351f4da8 | |||
422f13202b | |||
c470e40737 | |||
0f92ce2166 | |||
b1becb9ef5 | |||
3c1599b6b7 | |||
3e53b742f4 | |||
5401593279 | |||
0710e05479 | |||
1707d011a2 | |||
5e8d7944bd | |||
2d2727f7e8 | |||
c72282613d | |||
4ac62a107c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,3 +9,6 @@ docs/*
|
||||
lang/langtostruct.py
|
||||
config-payload.json
|
||||
!docs/go.mod
|
||||
server.key
|
||||
server.pem
|
||||
server.crt
|
||||
|
@ -1,5 +1,5 @@
|
||||
#### Translation
|
||||
Currently only the account creation form can be translated. Strings are defined in `lang/form/<country-code>.json` (country code as in `en-us`, `fr-fr`, e.g). You can see the existing ones [here](https://github.com/hrfee/jfa-go/tree/main/lang/form).
|
||||
Currently the admin page, account creation form and emails can be translated. Strings are defined in `lang/<admin/form/email>/<country-code>.json` (country code as in `en-us`, `fr-fr`, e.g). You can see the existing ones [here](https://github.com/hrfee/jfa-go/tree/main/lang).
|
||||
Make sure to define `name` in the `meta` section, and you can optionally add an `author` value there as well. If you can, make a pull request with your new file. If not, email me or create an issue.
|
||||
|
||||
#### Code
|
||||
|
72
api.go
72
api.go
@ -455,7 +455,7 @@ func (app *appContext) DeleteUser(gc *gin.Context) {
|
||||
app.err.Printf("%s: Failed to send to %s", userID, address)
|
||||
app.debug.Printf("%s: Error: %s", userID, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to %s", userID, address)
|
||||
app.info.Printf("%s: Sent deletion email to %s", userID, address)
|
||||
}
|
||||
}(userID, req.Reason, addr.(string))
|
||||
}
|
||||
@ -1085,17 +1085,36 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := app.configBase
|
||||
// Load language options
|
||||
langOptions := make([]string, len(app.storage.lang.Form))
|
||||
chosenLang := app.config.Section("ui").Key("language").MustString("en-us")
|
||||
chosenLangName := app.storage.lang.Form[chosenLang]["meta"].(map[string]interface{})["name"].(string)
|
||||
loadLangs := func(langs *map[string]map[string]interface{}, settingsKey string) (string, []string) {
|
||||
langOptions := make([]string, len(*langs))
|
||||
chosenLang := app.config.Section("ui").Key("language" + settingsKey).MustString("en-us")
|
||||
chosenLangName := (*langs)[chosenLang]["meta"].(map[string]interface{})["name"].(string)
|
||||
i := 0
|
||||
for _, lang := range *langs {
|
||||
langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string)
|
||||
i++
|
||||
}
|
||||
return chosenLangName, langOptions
|
||||
}
|
||||
formChosen, formOptions := loadLangs(&app.storage.lang.Form, "-form")
|
||||
fl := resp.Sections["ui"].Settings["language-form"]
|
||||
fl.Options = formOptions
|
||||
fl.Value = formChosen
|
||||
adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "-admin")
|
||||
al := resp.Sections["ui"].Settings["language-admin"]
|
||||
al.Options = adminOptions
|
||||
al.Value = adminChosen
|
||||
emailOptions := make([]string, len(app.storage.lang.Email))
|
||||
chosenLang := app.config.Section("email").Key("language").MustString("en-us")
|
||||
emailChosen := app.storage.lang.Email.get(chosenLang, "meta", "name")
|
||||
i := 0
|
||||
for _, lang := range app.storage.lang.Form {
|
||||
langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string)
|
||||
for langName := range app.storage.lang.Email {
|
||||
emailOptions[i] = app.storage.lang.Email.get(langName, "meta", "name")
|
||||
i++
|
||||
}
|
||||
l := resp.Sections["ui"].Settings["language"]
|
||||
l.Options = langOptions
|
||||
l.Value = chosenLangName
|
||||
el := resp.Sections["email"].Settings["language"]
|
||||
el.Options = emailOptions
|
||||
el.Value = emailChosen
|
||||
for sectName, section := range resp.Sections {
|
||||
for settingName, setting := range section.Settings {
|
||||
val := app.config.Section(sectName).Key(settingName)
|
||||
@ -1111,7 +1130,9 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
resp.Sections[sectName].Settings[settingName] = s
|
||||
}
|
||||
}
|
||||
resp.Sections["ui"].Settings["language"] = l
|
||||
resp.Sections["ui"].Settings["language-form"] = fl
|
||||
resp.Sections["ui"].Settings["language-admin"] = al
|
||||
resp.Sections["email"].Settings["language"] = el
|
||||
|
||||
t := resp.Sections["jellyfin"].Settings["type"]
|
||||
opts := make([]string, len(serverTypes))
|
||||
@ -1146,10 +1167,24 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
tempConfig.NewSection(section)
|
||||
}
|
||||
for setting, value := range settings.(map[string]interface{}) {
|
||||
if section == "ui" && setting == "language" {
|
||||
if section == "ui" && setting == "language-form" {
|
||||
for key, lang := range app.storage.lang.Form {
|
||||
if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) {
|
||||
tempConfig.Section("ui").Key("language").SetValue(key)
|
||||
tempConfig.Section("ui").Key("language-form").SetValue(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if section == "ui" && setting == "language-admin" {
|
||||
for key, lang := range app.storage.lang.Admin {
|
||||
if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) {
|
||||
tempConfig.Section("ui").Key("language-admin").SetValue(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if section == "email" && setting == "language" {
|
||||
for key := range app.storage.lang.Email {
|
||||
if app.storage.lang.Email.get(key, "meta", "name") == value.(string) {
|
||||
tempConfig.Section("email").Key("language").SetValue(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -1160,7 +1195,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if value.(string) != app.config.Section(section).Key(setting).MustString("") {
|
||||
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
||||
}
|
||||
}
|
||||
@ -1221,9 +1256,16 @@ func (app *appContext) Logout(gc *gin.Context) {
|
||||
// @Router /lang [get]
|
||||
// @tags Other
|
||||
func (app *appContext) GetLanguages(gc *gin.Context) {
|
||||
page := gc.Param("page")
|
||||
resp := langDTO{}
|
||||
for key, lang := range app.storage.lang.Form {
|
||||
resp[key] = lang["meta"].(map[string]interface{})["name"].(string)
|
||||
if page == "form" {
|
||||
for key, lang := range app.storage.lang.Form {
|
||||
resp[key] = lang["meta"].(map[string]interface{})["name"].(string)
|
||||
}
|
||||
} else if page == "admin" {
|
||||
for key, lang := range app.storage.lang.Admin {
|
||||
resp[key] = lang["meta"].(map[string]interface{})["name"].(string)
|
||||
}
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
respond(500, "Couldn't get languages", gc)
|
||||
|
17
config.go
17
config.go
@ -52,7 +52,7 @@ func (app *appContext) loadConfig() error {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template"} {
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
@ -80,11 +80,20 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT))
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
||||
|
||||
app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us")
|
||||
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
||||
if oldFormLang != "" {
|
||||
app.storage.lang.chosenFormLang = oldFormLang
|
||||
}
|
||||
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
||||
if newFormLang != "" {
|
||||
app.storage.lang.chosenFormLang = newFormLang
|
||||
}
|
||||
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -84,7 +84,7 @@
|
||||
"description": "Settings related to the UI and program functionality."
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"language-form": {
|
||||
"name": "Default Form Language",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
@ -93,7 +93,18 @@
|
||||
"en-us"
|
||||
],
|
||||
"value": "en-US",
|
||||
"description": "Default UI Language. Currently only implemented for account creation form. Submit a PR on github if you'd like to translate."
|
||||
"description": "Default Account Form Language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"language-admin": {
|
||||
"name": "Default Admin Language",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"en-us"
|
||||
],
|
||||
"value": "en-US",
|
||||
"description": "Default Admin page Language. Settings has not been translated. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Default Look",
|
||||
@ -208,6 +219,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Advanced",
|
||||
"description": "Advanced settings."
|
||||
},
|
||||
"settings": {
|
||||
"tls": {
|
||||
"name": "TLS/HTTP2",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable TLS, and by extension HTTP2. This enables server push, where required files are pushed to the web browser before they request them, allowing quicker page loads."
|
||||
},
|
||||
"tls_port": {
|
||||
"name": "TLS Port",
|
||||
"depends_true": "tls",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "number",
|
||||
"value": 8057,
|
||||
"description": "Port to run TLS server on"
|
||||
},
|
||||
"tls_cert": {
|
||||
"name": "Path to TLS Certificate",
|
||||
"depends_true": "tls",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to .crt file. See jfa-go wiki for more info."
|
||||
},
|
||||
"tls_key": {
|
||||
"name": "Path to TLS Key file",
|
||||
"depends_true": "tls",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to .key file. See jfa-go wiki for more info."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_validation": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@ -266,6 +321,18 @@
|
||||
"description": "General email settings. Ignore if not using email features."
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"name": "Email Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "select",
|
||||
"options": [
|
||||
"en-us"
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default email language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
|
66
email.go
66
email.go
@ -73,6 +73,8 @@ func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error {
|
||||
// Emailer contains the email sender, email content, and methods to construct message content.
|
||||
type Emailer struct {
|
||||
fromAddr, fromName string
|
||||
lang *EmailLang
|
||||
cLang string
|
||||
sender emailClient
|
||||
}
|
||||
|
||||
@ -108,6 +110,8 @@ func NewEmailer(app *appContext) *Emailer {
|
||||
emailer := &Emailer{
|
||||
fromAddr: app.config.Section("email").Key("address").String(),
|
||||
fromName: app.config.Section("email").Key("from").String(),
|
||||
lang: &(app.storage.lang.Email),
|
||||
cLang: app.storage.lang.chosenEmailLang,
|
||||
}
|
||||
method := app.config.Section("email").Key("method").String()
|
||||
if method == "smtp" {
|
||||
@ -153,8 +157,9 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) {
|
||||
lang := emailer.cLang
|
||||
email := &Email{
|
||||
subject: app.config.Section("invite_emails").Key("subject").String(),
|
||||
subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.get(lang, "inviteEmail", "title")),
|
||||
}
|
||||
expiry := invite.ValidTill
|
||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
@ -170,11 +175,13 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"expiry_date": d,
|
||||
"expiry_time": t,
|
||||
"expires_in": expiresIn,
|
||||
"invite_link": inviteLink,
|
||||
"message": message,
|
||||
"hello": emailer.lang.get(lang, "inviteEmail", "hello"),
|
||||
"youHaveBeenInvited": emailer.lang.get(lang, "inviteEmail", "youHaveBeenInvited"),
|
||||
"toJoin": emailer.lang.get(lang, "inviteEmail", "toJoin"),
|
||||
"inviteExpiry": emailer.lang.format(lang, "inviteEmail", "inviteExpiry", d, t, expiresIn),
|
||||
"linkButton": emailer.lang.get(lang, "inviteEmail", "linkButton"),
|
||||
"invite_link": inviteLink,
|
||||
"message": message,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -189,8 +196,9 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) {
|
||||
lang := emailer.cLang
|
||||
email := &Email{
|
||||
subject: "Notice: Invite expired",
|
||||
subject: emailer.lang.get(lang, "inviteExpiry", "title"),
|
||||
}
|
||||
expiry := app.formatDatetime(invite.ValidTill)
|
||||
for _, key := range []string{"html", "text"} {
|
||||
@ -201,8 +209,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"code": code,
|
||||
"expiry": expiry,
|
||||
"inviteExpired": emailer.lang.get(lang, "inviteExpiry", "inviteExpired"),
|
||||
"expiredAt": emailer.lang.format(lang, "inviteExpiry", "expiredAt", "\""+code+"\"", expiry),
|
||||
"notificationNotice": emailer.lang.get(lang, "inviteExpiry", "notificationNotice"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -217,8 +226,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) {
|
||||
lang := emailer.cLang
|
||||
email := &Email{
|
||||
subject: "Notice: User created",
|
||||
subject: emailer.lang.get(lang, "userCreated", "title"),
|
||||
}
|
||||
created := app.formatDatetime(invite.Created)
|
||||
var tplAddress string
|
||||
@ -235,10 +245,14 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"code": code,
|
||||
"username": username,
|
||||
"address": tplAddress,
|
||||
"time": created,
|
||||
"aUserWasCreated": emailer.lang.format(lang, "userCreated", "aUserWasCreated", "\""+code+"\""),
|
||||
"name": emailer.lang.get(lang, "userCreated", "name"),
|
||||
"address": emailer.lang.get(lang, "userCreated", "emailAddress"),
|
||||
"time": emailer.lang.get(lang, "userCreated", "time"),
|
||||
"nameVal": username,
|
||||
"addressVal": tplAddress,
|
||||
"timeVal": created,
|
||||
"notificationNotice": emailer.lang.get(lang, "userCreated", "notificationNotice"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -253,8 +267,9 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) {
|
||||
lang := emailer.cLang
|
||||
email := &Email{
|
||||
subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"),
|
||||
subject: emailer.lang.get(lang, "passwordReset", "title"),
|
||||
}
|
||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
@ -266,12 +281,14 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"username": pwr.Username,
|
||||
"expiry_date": d,
|
||||
"expiry_time": t,
|
||||
"expires_in": expiresIn,
|
||||
"pin": pwr.Pin,
|
||||
"message": message,
|
||||
"helloUser": emailer.lang.format(lang, "passwordReset", "helloUser", pwr.Username),
|
||||
"someoneHasRequestedReset": emailer.lang.get(lang, "passwordReset", "someoneHasRequestedReset"),
|
||||
"ifItWasYou": emailer.lang.get(lang, "passwordReset", "ifItWasYou"),
|
||||
"codeExpiry": emailer.lang.format(lang, "passwordReset", "codeExpiry", d, t, expiresIn),
|
||||
"ifItWasNotYou": emailer.lang.get(lang, "passwordReset", "ifItWasNotYou"),
|
||||
"pin": emailer.lang.get(lang, "passwordReset", "pin"),
|
||||
"pinVal": pwr.Pin,
|
||||
"message": message,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -286,8 +303,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) {
|
||||
lang := emailer.cLang
|
||||
email := &Email{
|
||||
subject: app.config.Section("deletion").Key("subject").MustString("Your account was deleted - Jellyfin"),
|
||||
subject: emailer.lang.get(lang, "userDeleted", "title"),
|
||||
}
|
||||
for _, key := range []string{"html", "text"} {
|
||||
fpath := app.config.Section("deletion").Key("email_" + key).String()
|
||||
@ -297,7 +315,9 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, map[string]string{
|
||||
"reason": reason,
|
||||
"yourAccountWasDeleted": emailer.lang.get(lang, "userDeleted", "yourAccountWasDeleted"),
|
||||
"reason": emailer.lang.get(lang, "userDeleted", "reason"),
|
||||
"reasonVal": reason,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
179
html/admin.html
179
html/admin.html
@ -8,7 +8,8 @@
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernamesEnabled = {{ .username }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
</script>
|
||||
{{ template "header.html" . }}
|
||||
<title>Admin - jfa-go</title>
|
||||
@ -16,206 +17,204 @@
|
||||
<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">
|
||||
<span class="heading">{{ .strings.login }}</span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="login-user">
|
||||
<input type="password" class="field input ~neutral !high mb-1" placeholder="{{ .strings.password }}" id="login-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Login</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.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">
|
||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}">
|
||||
<input type="password" class="field input ~neutral !high mb-1" placeholder="{{ .strings.password }}" id="add-user-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.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>
|
||||
<span class="heading">{{ .strings.aboutProgram }} <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>{{ .strings.version }} <span class="code monospace">{{ .version }}</span></p>
|
||||
<p>{{ .strings.commitNoun }} <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>
|
||||
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.modifySettingsDescription }}</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>
|
||||
<span class="button ~neutral !high supra full-width center">{{ .strings.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>
|
||||
<span class="button ~neutral !normal supra full-width center">{{ .strings.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>
|
||||
<select id="modify-user-profiles"></select>
|
||||
</div>
|
||||
<div class="select ~neutral !normal mb-1 unfocused">
|
||||
<select id="modify-user-users">
|
||||
<option>Person</option>
|
||||
<option>Other person</option>
|
||||
</select>
|
||||
<select id="modify-user-users"></select>
|
||||
</div>
|
||||
<label class="switch mb-1">
|
||||
<input type="checkbox" id="modify-user-homescreen" checked>
|
||||
<span>Apply homescreen layout</span>
|
||||
<span>{{ .strings.applyHomescreenLayout }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Apply</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.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>
|
||||
<span class="heading"><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>
|
||||
<span>{{ .strings.sendDeleteNotificationEmail }}</span>
|
||||
</label>
|
||||
<textarea id="textarea-delete-user" class="textarea full-width ~neutral !normal mb-1" placeholder="Your account has been deleted."></textarea>
|
||||
<textarea id="textarea-delete-user" class="textarea full-width ~neutral !normal mb-1" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~critical !normal full-width center supra submit">Delete</span>
|
||||
<span class="button ~critical !normal full-width center supra submit">{{ .strings.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>
|
||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||
<p class="content pb-1">{{ .strings.settingsRestartRequiredDescription }}</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>
|
||||
<span class="button ~info !normal mb-half" id="settings-apply-no-restart">{{ .strings.settingsApplyRestartLater }}</span>
|
||||
<span class="button ~critical !normal" id="settings-apply-restart">{{ .strings.settingsApplyRestartNow }}</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>
|
||||
<span class="heading">{{ .strings.settingsApplied }}</span>
|
||||
<p class="content">{{ .strings.settingsRefreshPage }}</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>
|
||||
<span class="heading">{{ .strings.ombiUserDefaults }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.ombiUserDefaultsDescription }}</p>
|
||||
<div class="select ~neutral !normal mb-1">
|
||||
<select>
|
||||
<option>Person</option>
|
||||
<option>Other person</option>
|
||||
</select>
|
||||
<select></select>
|
||||
</div>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Submit</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.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>
|
||||
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">×</span></span>
|
||||
<p class="support lg">{{ .strings.userProfilesDescription }}</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>
|
||||
<th>{{ .strings.name }}</th>
|
||||
<th>{{ .strings.userProfilesIsDefault }}</th>
|
||||
<th>{{ .strings.from }}</th>
|
||||
<th>{{ .strings.userProfilesLibraries }}</th>
|
||||
<th><span class="button ~neutral !high" id="button-profile-create">{{ .strings.create }}</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-profiles">
|
||||
</tbody>
|
||||
<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>
|
||||
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.addProfileDescription }}</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">
|
||||
<span class="supra">{{ .strings.addProfileNameOf }} </span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.name }}" id="add-profile-name">
|
||||
<label>
|
||||
<span class="supra">User</span>
|
||||
<span class="supra">{{ .strings.user }}</span>
|
||||
<div class="select ~neutral !normal mt-half mb-1">
|
||||
<select id="add-profile-user">
|
||||
</select>
|
||||
<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>
|
||||
<span>{{ .strings.addProfileStoreHomescreenLayout }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.create }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-1 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral !low" id="lang-list">
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<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>
|
||||
<span id="button-tab-invites" class="tab-button portal">{{ .strings.invites }}</span>
|
||||
<span id="button-tab-accounts" class="tab-button portal">{{ .strings.accounts }}</span>
|
||||
<span id="button-tab-settings" class="tab-button portal">{{ .strings.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>
|
||||
<span class="button ~critical !normal mb-1 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
<span id="button-theme" class="button ~neutral !normal mb-1">{{ .strings.theme }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-invites">
|
||||
<div class="card ~neutral !low invites mb-1">
|
||||
<span class="heading">Invites</span>
|
||||
<span class="heading">{{ .strings.invites }}</span>
|
||||
<div id="invites"></div>
|
||||
</div>
|
||||
<div class="card ~neutral !low">
|
||||
<span class="heading">Create</span>
|
||||
<span class="heading">{{ .strings.create }}</span>
|
||||
<div class="row" id="create-inv">
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-days">Days</label>
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</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>
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</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>
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
@ -223,7 +222,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-uses">Number of uses</label>
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</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">
|
||||
@ -231,14 +230,14 @@
|
||||
<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>
|
||||
<p class="support unfocused" id="create-inf-uses-warning"><span class="badge ~critical">{{ .strings.warning }}</span> {{ .strings.inviteInfiniteUsesWarning }}</p>
|
||||
<label class="label supra">{{ .strings.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>
|
||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</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">
|
||||
@ -246,27 +245,27 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<span class="button ~urge !normal supra full-width center lg" id="create-submit">Create</span>
|
||||
<span class="button ~urge !normal supra full-width center lg" id="create-submit">{{ .strings.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>
|
||||
<span class="heading">{{ .strings.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>
|
||||
<span class="button ~neutral !normal" id="accounts-add-user">{{ .quantityStrings.addUser.singular }}</span>
|
||||
<span class="button ~urge !normal" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="button ~critical !normal" id="accounts-delete-user">{{ .quantityStrings.deleteUser.singular }}</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>
|
||||
<th>{{ .strings.username }}</th>
|
||||
<th>{{ .strings.emailAddress }}</th>
|
||||
<th>{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accounts-list"></tbody>
|
||||
@ -276,15 +275,15 @@
|
||||
</div>
|
||||
<div id="tab-settings" class="unfocused">
|
||||
<div class="card ~neutral !low settings overflow">
|
||||
<span class="heading">Settings</span>
|
||||
<span class="heading">{{ .strings.settings }}</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal unfocused" id="settings-save">Save</span>
|
||||
<span class="button ~neutral !normal unfocused" id="settings-save">{{ .strings.settingsSave }}</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>
|
||||
<aside class="aside sm ~info mb-half" id="settings-message">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">{{ .strings.aboutProgram }} <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">{{ .strings.userProfiles }} <i class="ri-user-line ml-half"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col overflow" id="settings-panel"></div>
|
||||
</div>
|
||||
|
130
lang/admin/en-us.json
Normal file
130
lang/admin/en-us.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invites",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"theme": "Theme",
|
||||
"inviteDays": "Days",
|
||||
"inviteHours": "Hours",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Number of uses",
|
||||
"warning": "Warning",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"create": "Create",
|
||||
"apply": "Apply",
|
||||
"delete": "Delete",
|
||||
"submit": "Submit",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"emailAddress": "Email Address",
|
||||
"lastActiveTime": "Last Active",
|
||||
"from": "From",
|
||||
"user": "User",
|
||||
"aboutProgram": "About",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "New User",
|
||||
"profile": "Profile",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"unknown": "Unknown",
|
||||
"modifySettings": "Modify Settings",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"sendDeleteNotificationEmail": "Send notification email",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
"settingsRestartRequired": "Restart needed",
|
||||
"settingsRestartRequiredDescription": "A restart is necessary to apply some settings you changed. Restart now or later?",
|
||||
"settingsApplyRestartLater": "Apply, restart later",
|
||||
"settingsApplyRestartNow": "Apply & restart",
|
||||
"settingsApplied": "Settings applied.",
|
||||
"settingsRefreshPage": "Refresh the page in a few seconds",
|
||||
"settingsRequiredOrRestartMessage": "Note: {n} indicates a required field, {n} indicates changes require a restart.",
|
||||
"settingsSave": "Save",
|
||||
"ombiUserDefaults": "Ombi user defaults",
|
||||
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go",
|
||||
"userProfiles": "User Profiles",
|
||||
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.",
|
||||
"userProfilesIsDefault": "Default",
|
||||
"userProfilesLibraries": "Libraries",
|
||||
"addProfile": "Add Profile",
|
||||
"addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.",
|
||||
"addProfileNameOf": "Profile Name",
|
||||
"addProfileStoreHomescreenLayout": "Store homescreen layout",
|
||||
|
||||
"inviteNoUsersCreated": "None yet!",
|
||||
"inviteUsersCreated": "Created users",
|
||||
"inviteNoProfile": "No Profile",
|
||||
"copy": "Copy",
|
||||
"inviteDateCreated": "Created",
|
||||
"inviteRemainingUses": "Remaining uses",
|
||||
"inviteNoInvites": "None",
|
||||
"inviteExpiresInTime": "Expires in {n}",
|
||||
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
"userCreated": "User {n} created.",
|
||||
"createProfile": "Created profile {n}.",
|
||||
"saveSettings": "Settings were saved",
|
||||
"setOmbiDefaults": "Stored ombi defaults.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||
"errorSettingsFailed": "Application failed.",
|
||||
"errorLoginBlank": "The username and/or password were left blank.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"errorBlankFields": "Fields were left blank",
|
||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||
"errorLoadProfiles": "Failed to load profiles.",
|
||||
"errorCreateProfile": "Failed to create profile {n}",
|
||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||
"errorLoadUsers": "Failed to load users.",
|
||||
"errorSaveSettings": "Couldn't save settings.",
|
||||
"errorLoadSettings": "Failed to load settings.",
|
||||
"errorSetOmbiDefaults": "Failed to store ombi defaults.",
|
||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||
"errorChangedEmailAddress": "Couldn't change email address of {n}.",
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)"
|
||||
},
|
||||
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modify Settings for {n} user",
|
||||
"plural": "Modify Settings for {n} users"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Delete {n} user",
|
||||
"plural": "Delete {n} users"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Add user",
|
||||
"plural": "Add users"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Delete User",
|
||||
"plural": "Delete Users"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Deleted {n} user.",
|
||||
"plural": "Deleted {n} users."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Applied settings to {n} user.",
|
||||
"plural": "Applied settings to {n} users."
|
||||
}
|
||||
}
|
||||
}
|
130
lang/admin/fr-fr.json
Normal file
130
lang/admin/fr-fr.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Francais (FR)",
|
||||
"author": "https://github.com/Killianbe"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invite",
|
||||
"accounts": "Comptes",
|
||||
"settings": "Reglages",
|
||||
"theme": "Thème",
|
||||
"inviteDays": "Jours",
|
||||
"inviteHours": "Heures",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Nombre d'utilisateur",
|
||||
"warning": "Attention",
|
||||
"inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement",
|
||||
"inviteSendToEmail": "Envoyer à",
|
||||
"login": "S'identifier",
|
||||
"logout": "Se déconecter",
|
||||
"create": "Créer",
|
||||
"apply": "Appliquer",
|
||||
"delete": "Effacer",
|
||||
"submit": "Soumettre",
|
||||
"name": "Nom",
|
||||
"date": "Date",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"emailAddress": "Addresse Email",
|
||||
"lastActiveTime": "Dernière activité",
|
||||
"from": "De",
|
||||
"user": "Utilisateur",
|
||||
"aboutProgram": "A propos",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commettre",
|
||||
"newUser": "Nouvel utilisateur",
|
||||
"profile": "Profil",
|
||||
"success": "Succès",
|
||||
"error": "Erreur",
|
||||
"unknown": "Inconnu",
|
||||
"modifySettings": "Modifier les paramètres",
|
||||
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
|
||||
"applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil",
|
||||
"sendDeleteNotificationEmail": "Envoyer un e-mail de notification ",
|
||||
"sendDeleteNotifiationExample": "Votre compte a été supprimé. ",
|
||||
"settingsRestartRequired": "Redémarrage nécessaire ",
|
||||
"settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard?",
|
||||
"settingsApplyRestartLater": "Appliquer, redémarrer plus tard ",
|
||||
"settingsApplyRestartNow": "Appliquer et redémarrer ",
|
||||
"settingsApplied": "Paramètres appliqués.",
|
||||
"settingsRefreshPage": "Actualisez la page dans quelques secondes ",
|
||||
"settingsRequiredOrRestartMessage": "Remarque: {n} indique un champ obligatoire, {n} indique que les modifications nécessitent un redémarrage. ",
|
||||
"settingsSave": "Sauver",
|
||||
"ombiUserDefaults": "Paramètres par défaut de l'utilisateur Ombi",
|
||||
"ombiUserDefaultsDescription": "Créez un utilisateur Ombi et configurez-le, puis sélectionnez-le ci-dessous. Ses paramètres / autorisations seront stockés et appliqués aux nouveaux utilisateurs Ombi créés par jfa-go ",
|
||||
"userProfiles": "Profils d'utilisateurs",
|
||||
"userProfilesDescription": "Les profils sont appliqués aux utilisateurs lorsqu'ils créent un compte. Un profil inclut les droits d'accès à la bibliothèque et la disposition de l'écran d'accueil. ",
|
||||
"userProfilesIsDefault": "Défaut",
|
||||
"userProfilesLibraries": "Bibliothèques",
|
||||
"addProfile": "Ajouter un profil",
|
||||
"addProfileDescription": "Créez un utilisateur Jellyfin et configurez-le, puis sélectionnez-le ci-dessous. Lorsque ce profil est appliqué à une invitation, de nouveaux utilisateurs seront créés avec les paramètres. ",
|
||||
"addProfileNameOf": "Nom de profil",
|
||||
"addProfileStoreHomescreenLayout": "Enregistrer la disposition de l'écran d'accueil",
|
||||
|
||||
"inviteNoUsersCreated": "Aucun pour l'instant!",
|
||||
"inviteUsersCreated": "Utilisateurs créer",
|
||||
"inviteNoProfile": "Aucun profil",
|
||||
"copy": "Copier",
|
||||
"inviteDateCreated": "Créer",
|
||||
"inviteRemainingUses": "Utilisations restantes",
|
||||
"inviteNoInvites": "Aucune",
|
||||
"inviteExpiresInTime": "Expires dans {n}",
|
||||
|
||||
"notifyEvent": "Notifier sur:",
|
||||
"notifyInviteExpiry": "À l'expiration",
|
||||
"notifyUserCreation": "à la création de l'utilisateur"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
"userCreated": "L'utilisateur {n} a été créé.",
|
||||
"createProfile": "Profil créé {n}.",
|
||||
"saveSettings": "Les paramètres ont été enregistrés",
|
||||
"setOmbiDefaults": "Valeurs par défaut de Ombi.",
|
||||
"errorConnection": "Impossible de se connecter à jfa-go.",
|
||||
"error401Unauthorized": "Non autorisé. Essayez d'actualiser la page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Les paramètres ont été appliqués, mais l'application de la disposition de l'écran d'accueil a peut-être échoué.",
|
||||
"errorHomescreenAppliedNoSettings": "La disposition de l'écran d'accueil a été appliquée, mais l'application des paramètres a peut-être échoué.",
|
||||
"errorSettingsFailed": "L'application a échoué.",
|
||||
"errorLoginBlank": "Le nom d'utilisateur et / ou le mot de passe sont vides",
|
||||
"errorUnknown": "Erreur inconnue.",
|
||||
"errorBlankFields": "Les champs sont vides",
|
||||
"errorDeleteProfile": "Échec de la suppression du profil {n}",
|
||||
"errorLoadProfiles": "Échec du chargement des profils.",
|
||||
"errorCreateProfile": "Échec de la création du profil {n}",
|
||||
"errorSetDefaultProfile": "Échec de la définition du profil par défaut",
|
||||
"errorLoadUsers": "Échec du chargement des utilisateurs.",
|
||||
"errorSaveSettings": "Impossible d'enregistrer les paramètres.",
|
||||
"errorLoadSettings": "Échec du chargement des paramètres.",
|
||||
"errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.",
|
||||
"errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.",
|
||||
"errorChangedEmailAddress": "Impossible de modifier l'adresse e-mail de {n}.",
|
||||
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
|
||||
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modifier les paramètres pour {n} utilisateur",
|
||||
"plural": "Modifier les paramètres pour {n} utilisateurs"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Supprimer {n} utilisateur",
|
||||
"plural": "Supprimer {n} utilisateurs"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Ajouter un utilisateur",
|
||||
"plural": "Ajouter des utilisateurs"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Supprimer l'utilisateur",
|
||||
"plural": "Supprimer les utilisateurs"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Supprimer {n} utilisateur.",
|
||||
"plural": "Supprimer {n} utilisateurs."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Appliquer le paramètre {n} utilisteur.",
|
||||
"plural": "Appliquer les paramètres {n} utilisteurs."
|
||||
}
|
||||
}
|
||||
}
|
41
lang/email/en-us.json
Normal file
41
lang/email/en-us.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"userCreated": {
|
||||
"title": "Notice: User created",
|
||||
"aUserWasCreated": "A user was created using code {n}.",
|
||||
"name": "Name",
|
||||
"emailAddress": "Address",
|
||||
"time": "Time",
|
||||
"notificationNotice": "Note: Notification emails can be toggled on the admin dashboard."
|
||||
},
|
||||
"inviteExpiry": {
|
||||
"title": "Notice: Invite expired",
|
||||
"inviteExpired": "Invite expired.",
|
||||
"expiredAt": "Code {n} expired at {n}.",
|
||||
"notificationNotice": "Note: Notification emails can be toggled on the admin dashboard."
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Password reset requested - Jellyfin",
|
||||
"helloUser": "Hi {n},",
|
||||
"someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.",
|
||||
"ifItWasYou": "If this was you, enter the pin below into the prompt.",
|
||||
"codeExpiry": "The code will expire on {n}, at {n} UTC, which is in {n}.",
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this email.",
|
||||
"pin": "PIN"
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Your account was deleted - Jellyfin",
|
||||
"yourAccountWasDeleted": "Your Jellyfin account was deleted.",
|
||||
"reason": "Reason"
|
||||
},
|
||||
"inviteEmail": {
|
||||
"title": "Invite - Jellyfin",
|
||||
"hello": "Hi",
|
||||
"youHaveBeenInvited": "You've been invited to Jellyfin.",
|
||||
"toJoin": "To join, follow the below link.",
|
||||
"inviteExpiry": "This invite will expire on {n}, at {n}, which is in {n}, so act quick.",
|
||||
"linkButton": "Setup your account"
|
||||
}
|
||||
}
|
42
lang/email/fr-fr.json
Normal file
42
lang/email/fr-fr.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Francais (FR)",
|
||||
"author": "https://github.com/Cornichon420"
|
||||
},
|
||||
"userCreated": {
|
||||
"title": "Notification : Utilisateur créé",
|
||||
"aUserWasCreated": "Un utilisateur a été créé avec ce code {n}",
|
||||
"name": "Nom",
|
||||
"emailAddress": "Adresse",
|
||||
"time": "Date",
|
||||
"notificationNotice": ""
|
||||
},
|
||||
"inviteExpiry": {
|
||||
"title": "Notification : Invitation expirée",
|
||||
"inviteExpired": "Invitation expirée.",
|
||||
"expiredAt": "Le code {n} a expiré à {n}.",
|
||||
"notificationNotice": ""
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Réinitialisation de mot du passe demandée - Jellyfin",
|
||||
"helloUser": "Salut {n},",
|
||||
"someoneHasRequestedReset": "Quelqu'un vient de demander une réinitialisation du mot de passe via Jellyfin.",
|
||||
"ifItWasYou": "Si c'était bien toi, renseigne le code PIN en dessous.",
|
||||
"codeExpiry": "Ce code expirera le {n}, à {n} UTC, soit dans {n}.",
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
|
||||
"pin": "PIN"
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Ton compte a été désactivé - Jellyfin",
|
||||
"yourAccountWasDeleted": "Ton compte Jellyfin a été supprimé.",
|
||||
"reason": "Motif"
|
||||
},
|
||||
"inviteEmail": {
|
||||
"title": "Invitation - Jellyfin",
|
||||
"hello": "Salut",
|
||||
"youHaveBeenInvited": "Tu as été invité à rejoindre Jellyfin.",
|
||||
"toJoin": "Pour continuer, suis le lien en dessous.",
|
||||
"inviteExpiry": "L'invitation expirera le {n}, à {n}, soit dans {n}, alors fais vite !",
|
||||
"linkButton": "Lien"
|
||||
}
|
||||
}
|
@ -20,26 +20,25 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>User Created</h3>
|
||||
<p>A user was created using code {{ .code }}.</p>
|
||||
<p>{{ .aUserWasCreated }}</p>
|
||||
</mj-text>
|
||||
<mj-table mj-class="text" container-background-color="#242424">
|
||||
<tr style="text-align: left;">
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Time</th>
|
||||
<th>{{ .name }}</th>
|
||||
<th>{{ .address }}</th>
|
||||
<th>{{ .time }}</th>
|
||||
</tr>
|
||||
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
|
||||
<th>{{ .username }}</th>
|
||||
<th>{{ .address }}</th>
|
||||
<th>{{ .time }}</th>
|
||||
<th>{{ .nameVal }}</th>
|
||||
<th>{{ .addressVal }}</th>
|
||||
<th>{{ .timeVal }}</th>
|
||||
</mj-table>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
Notification emails can be toggled on the admin dashboard.
|
||||
{{ .notificationNotice }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
@ -1,7 +1,7 @@
|
||||
A user was created using code {{ .code }}.
|
||||
{{ .aUserWasCreated }}
|
||||
|
||||
Name: {{ .username }}
|
||||
Address: {{ .address }}
|
||||
Time: {{ .time }}
|
||||
{{ .name }}: {{ .nameVal }}
|
||||
{{ .address }}: {{ .addressVal }}
|
||||
{{ .time }}: {{ .timeVal }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
{{ .notificationNotice }}
|
||||
|
@ -20,8 +20,8 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>Your account was deleted.</h3>
|
||||
<p>Reason: <i>{{ .reason }}</i></p>
|
||||
<h3>{{ .yourAccountWasDeleted }}</h3>
|
||||
<p>{{ .reason }}: <i>{{ .reasonVal }}</i></p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
@ -1,4 +1,4 @@
|
||||
Your Jellyfin account was deleted.
|
||||
Reason: {{ .reason }}
|
||||
{{ .yourAccountWasDeleted }}
|
||||
{{ .reason }}: {{ .reasonVal }}
|
||||
|
||||
{{ .message }}
|
||||
|
@ -20,13 +20,13 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi {{ .username }},</p>
|
||||
<p> Someone has recently requested a password reset on Jellyfin.</p>
|
||||
<p>If this was you, enter the below pin into the prompt.</p>
|
||||
<p>The code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}.</p>
|
||||
<p>If this wasn't you, please ignore this email.</p>
|
||||
<p>{{ .helloUser }}</p>
|
||||
<p>{{ .someoneHasRequestedReset }}</p>
|
||||
<p>{{ .ifItWasYou }}</p>
|
||||
<p>{{ .codeExpiry }}</p>
|
||||
<p>{{ .ifItWasNotYou }}</p>
|
||||
</mj-text>
|
||||
<mj-button mj-class="blue bold">{{ .pin }}</mj-button>
|
||||
<mj-button mj-class="blue bold">{{ .pinVal }}</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
|
@ -1,10 +1,10 @@
|
||||
Hi {{ .username }},
|
||||
{{ .helloUser }}
|
||||
|
||||
Someone has recently requests a password reset on Jellyfin.
|
||||
If this was you, enter the below pin into the prompt.
|
||||
This code will expire on {{ .expiry_date }}, at {{ .expiry_time }} UTC, which is in {{ .expires_in }}.
|
||||
If this wasn't you, please ignore this email.
|
||||
{{ .someoneHasRequestedReset }}
|
||||
{{ .ifItWasYou }}
|
||||
{{ .codeExpiry }}
|
||||
{{ .ifItWasNotYou }}
|
||||
|
||||
PIN: {{ .pin }}
|
||||
{{ .pin }}: {{ .pinVal }}
|
||||
|
||||
{{ .message }}
|
||||
|
@ -20,15 +20,15 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>Invite Expired.</h3>
|
||||
<p>Code {{ .code }} expired at {{ .expiry }}.</p>
|
||||
<h3>{{ .inviteExpired }}</h3>
|
||||
<p>{{ .expiredAt }}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
Notification emails can be toggled on the admin dashboard.
|
||||
{{ .notificationNotice }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
@ -1,5 +1,5 @@
|
||||
Invite expired.
|
||||
{{ .inviteExpired }}
|
||||
|
||||
Code {{ .code }} expired at {{ .expiry }}.
|
||||
{{ .expiredAt }}
|
||||
|
||||
Note: Notification emails can be toggled on the admin dashboard.
|
||||
{{ .notificationNotice }}
|
||||
|
@ -20,12 +20,12 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<p>Hi,</p>
|
||||
<h3>You've been invited to Jellyfin.</h3>
|
||||
<p>To join, click the button below.</p>
|
||||
<p>This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick.</p>
|
||||
<p>{{ .hello }},</p>
|
||||
<h3>{{ .youHaveBeenInvited }}</h3>
|
||||
<p>{{ .toJoin }}</p>
|
||||
<p>{{ .inviteExpiry }}</p>
|
||||
</mj-text>
|
||||
<mj-button mj-class="blue bold" href="{{ .invite_link }}">Setup your account</mj-button>
|
||||
<mj-button mj-class="blue bold" href="{{ .invite_link }}">{{ .linkButton }}</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
|
@ -1,7 +1,7 @@
|
||||
Hi,
|
||||
You've been invited to Jellyfin.
|
||||
To join, follow the below link.
|
||||
This invite will expire on {{ .expiry_date }}, at {{ .expiry_time }}, which is in {{ .expires_in }}, so act quick.
|
||||
{{ .hello }},
|
||||
{{ .youHaveBeenInvited }}
|
||||
{{ .toJoin }}
|
||||
{{ .inviteExpiry }}
|
||||
|
||||
{{ .invite_link }}
|
||||
|
||||
|
25
main.go
25
main.go
@ -331,7 +331,12 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
if !firstRun {
|
||||
app.host = app.config.Section("ui").Key("host").String()
|
||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||
app.info.Println("Using TLS/HTTP2")
|
||||
app.port = app.config.Section("advanced").Key("tls_port").MustInt(8057)
|
||||
} else {
|
||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||
}
|
||||
|
||||
if *HOST != app.host && *HOST != "" {
|
||||
app.host = *HOST
|
||||
@ -350,7 +355,6 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||
|
||||
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
||||
@ -370,7 +374,6 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
|
||||
if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) {
|
||||
app.info.Println("Migrating user template files to new profile format")
|
||||
app.storage.migrateToProfile()
|
||||
@ -514,6 +517,8 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
}
|
||||
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form")
|
||||
app.storage.lang.AdminPath = filepath.Join(app.localPath, "lang", "admin")
|
||||
app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email")
|
||||
err = app.storage.loadLang()
|
||||
if err != nil {
|
||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||
@ -576,7 +581,7 @@ func start(asDaemon, firstCall bool) {
|
||||
router.GET("/accounts", app.AdminPage)
|
||||
router.GET("/settings", app.AdminPage)
|
||||
|
||||
router.GET("/lang", app.GetLanguages)
|
||||
router.GET("/lang/:page", app.GetLanguages)
|
||||
router.GET("/token/login", app.getTokenLogin)
|
||||
router.GET("/token/refresh", app.getTokenRefresh)
|
||||
router.POST("/newUser", app.NewUser)
|
||||
@ -624,8 +629,16 @@ func start(asDaemon, firstCall bool) {
|
||||
Handler: router,
|
||||
}
|
||||
go func() {
|
||||
if err := SRV.ListenAndServe(); err != nil {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||
cert := app.config.Section("advanced").Key("tls_cert").MustString("")
|
||||
key := app.config.Section("advanced").Key("tls_key").MustString("")
|
||||
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
}
|
||||
} else {
|
||||
if err := SRV.ListenAndServe(); err != nil {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
app.quit = make(chan os.Signal)
|
||||
|
99
storage.go
99
storage.go
@ -20,10 +20,28 @@ type Storage struct {
|
||||
lang Lang
|
||||
}
|
||||
|
||||
type EmailLang map[string]map[string]map[string]interface{} // Map of lang codes to email name to fields
|
||||
|
||||
func (el *EmailLang) format(lang, email, field string, vals ...string) string {
|
||||
text := (*el)[lang][email][field].(string)
|
||||
for _, val := range vals {
|
||||
text = strings.Replace(text, "{n}", val, 1)
|
||||
}
|
||||
return text
|
||||
}
|
||||
func (el *EmailLang) get(lang, email, field string) string { return (*el)[lang][email][field].(string) }
|
||||
|
||||
type Lang struct {
|
||||
chosenFormLang string
|
||||
FormPath string
|
||||
Form map[string]map[string]interface{}
|
||||
chosenFormLang string
|
||||
chosenAdminLang string
|
||||
chosenEmailLang string
|
||||
AdminPath string
|
||||
Admin map[string]map[string]interface{}
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
Form map[string]map[string]interface{}
|
||||
EmailPath string
|
||||
Email EmailLang
|
||||
}
|
||||
|
||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||
@ -60,46 +78,75 @@ func (st *Storage) storeInvites() error {
|
||||
}
|
||||
|
||||
func (st *Storage) loadLang() error {
|
||||
formFiles, err := ioutil.ReadDir(st.lang.FormPath)
|
||||
st.lang.Form = map[string]map[string]interface{}{}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range formFiles {
|
||||
index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
|
||||
var data map[string]interface{}
|
||||
if substituteStrings != "" {
|
||||
loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
outString := map[string]string{}
|
||||
out := map[string]map[string]interface{}{}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, f := range files {
|
||||
index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
|
||||
var data map[string]interface{}
|
||||
var file []byte
|
||||
var err error
|
||||
file, err = ioutil.ReadFile(filepath.Join(st.lang.FormPath, f.Name()))
|
||||
file, err = ioutil.ReadFile(filepath.Join(path, f.Name()))
|
||||
if err != nil {
|
||||
file = []byte("{}")
|
||||
}
|
||||
// Replace Jellyfin with emby on form
|
||||
file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings))
|
||||
// Replace Jellyfin with something if necessary
|
||||
if substituteStrings != "" {
|
||||
fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)
|
||||
file = []byte(fileString)
|
||||
}
|
||||
err = json.Unmarshal(file, &data)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to read \"%s\": %s", st.lang.FormPath, err)
|
||||
return err
|
||||
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
err := loadJSON(filepath.Join(st.lang.FormPath, f.Name()), &data)
|
||||
if err != nil {
|
||||
return err
|
||||
if stringJson {
|
||||
stringJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
outString[index] = string(stringJSON)
|
||||
}
|
||||
}
|
||||
out[index] = data
|
||||
|
||||
strings := data["strings"].(map[string]interface{})
|
||||
}
|
||||
return outString, out, nil
|
||||
}
|
||||
_, form, err := loadData(st.lang.FormPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, lang := range form {
|
||||
strings := lang["strings"].(map[string]interface{})
|
||||
validationStrings := strings["validationStrings"].(map[string]interface{})
|
||||
vS, err := json.Marshal(validationStrings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
strings["validationStrings"] = string(vS)
|
||||
data["strings"] = strings
|
||||
st.lang.Form[index] = data
|
||||
lang["strings"] = strings
|
||||
form[index] = lang
|
||||
}
|
||||
return nil
|
||||
st.lang.Form = form
|
||||
adminJSON, admin, err := loadData(st.lang.AdminPath, true)
|
||||
st.lang.Admin = admin
|
||||
st.lang.AdminJSON = adminJSON
|
||||
|
||||
_, emails, err := loadData(st.lang.EmailPath, false)
|
||||
fixedEmails := map[string]map[string]map[string]interface{}{}
|
||||
for lang, e := range emails {
|
||||
f := map[string]map[string]interface{}{}
|
||||
for field, vals := range e {
|
||||
f[field] = vals.(map[string]interface{})
|
||||
}
|
||||
fixedEmails[lang] = f
|
||||
}
|
||||
st.lang.Email = fixedEmails
|
||||
return err
|
||||
}
|
||||
|
||||
func (st *Storage) loadEmails() error {
|
||||
|
18
ts/admin.ts
18
ts/admin.ts
@ -1,15 +1,25 @@
|
||||
import { toggleTheme, loadTheme } from "./modules/theme.js";
|
||||
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { Tabs } from "./modules/tabs.js";
|
||||
import { inviteList, createInvite } from "./modules/invites.js";
|
||||
import { accountsList } from "./modules/accounts.js";
|
||||
import { settingsList } from "./modules/settings.js";
|
||||
import { ProfileEditor } from "./modules/profiles.js";
|
||||
import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
|
||||
import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
|
||||
|
||||
loadTheme();
|
||||
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme;
|
||||
|
||||
window.lang = new lang(window.langFile as LangFile);
|
||||
loadLangSelector("admin");
|
||||
// _get(`/lang/admin/${window.language}.json`, null, (req: XMLHttpRequest) => {
|
||||
// if (req.readyState == 4 && req.status == 200) {
|
||||
// langLoaded = true;
|
||||
// window.lang = new lang(req.response as LangFile);
|
||||
// }
|
||||
// });
|
||||
|
||||
window.animationEvent = whichAnimationEvent();
|
||||
|
||||
window.token = "";
|
||||
@ -110,12 +120,12 @@ function login(username: string, password: string, run?: (state?: number) => voi
|
||||
req.onreadystatechange = function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status != 200) {
|
||||
let errorMsg = "Connection error.";
|
||||
let errorMsg = window.lang.notif("errorConnection");
|
||||
if (this.response) {
|
||||
errorMsg = this.response["error"];
|
||||
}
|
||||
if (!errorMsg) {
|
||||
errorMsg = "Unknown error";
|
||||
errorMsg = window.lang.notif("errorUnknown");
|
||||
}
|
||||
if (!refresh) {
|
||||
window.notifications.customError("loginError", errorMsg);
|
||||
@ -153,7 +163,7 @@ function login(username: string, password: string, run?: (state?: number) => voi
|
||||
const username = (document.getElementById("login-user") as HTMLInputElement).value;
|
||||
const password = (document.getElementById("login-password") as HTMLInputElement).value;
|
||||
if (!username || !password) {
|
||||
window.notifications.customError("loginError", "The username and/or password were left blank.");
|
||||
window.notifications.customError("loginError", window.lang.notif("errorLoginBlank"));
|
||||
return;
|
||||
}
|
||||
toggleLoader(button);
|
||||
|
19
ts/form.ts
19
ts/form.ts
@ -1,5 +1,6 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { _get, _post, toggleLoader } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
validationStrings: pwValStrings;
|
||||
@ -22,23 +23,7 @@ interface pwValStrings {
|
||||
[ type: string ]: pwValString;
|
||||
}
|
||||
|
||||
_get("/lang", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
document.getElementById("lang-dropdown").remove();
|
||||
return;
|
||||
}
|
||||
const list = document.getElementById("lang-list") as HTMLDivElement;
|
||||
let innerHTML = '';
|
||||
for (let code in req.response) {
|
||||
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half">${req.response[code]}</a>`;
|
||||
}
|
||||
list.innerHTML = innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
loadLangSelector("form");
|
||||
|
||||
window.modal = new Modal(document.getElementById("modal-success"));
|
||||
declare var window: formWindow;
|
||||
|
@ -100,10 +100,10 @@ class user implements User {
|
||||
_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}".`);
|
||||
window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`));
|
||||
} else {
|
||||
this.email = oldEmail;
|
||||
window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`);
|
||||
window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -184,11 +184,9 @@ export class accountsList {
|
||||
}
|
||||
this._modifySettings.classList.remove("unfocused");
|
||||
this._deleteUser.classList.remove("unfocused");
|
||||
(this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users";
|
||||
this._deleteUser.textContent = window.lang.quantity("deleteUser", this._checkCount);
|
||||
}
|
||||
}
|
||||
|
||||
private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; }
|
||||
|
||||
private _collectUsers = (): string[] => {
|
||||
let list: string[] = [];
|
||||
@ -208,7 +206,7 @@ export class accountsList {
|
||||
};
|
||||
for (let field in send) {
|
||||
if (!send[field]) {
|
||||
window.notifications.customError("addUserBlankField", "Fields were left blank.");
|
||||
window.notifications.customError("addUserBlankField", window.lang.notif("errorBlankFields"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -217,7 +215,7 @@ export class accountsList {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200) {
|
||||
window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`);
|
||||
window.notifications.customSuccess("addUser", window.lang.var("notifications", "userCreated", `"${send['username']}"`));
|
||||
}
|
||||
this.reload();
|
||||
window.modals.addUser.close();
|
||||
@ -227,7 +225,7 @@ export class accountsList {
|
||||
|
||||
deleteUsers = () => {
|
||||
const modalHeader = document.getElementById("header-delete-user");
|
||||
modalHeader.textContent = this._genCountString();
|
||||
modalHeader.textContent = window.lang.quantity("deleteNUsers", this._checkCount);
|
||||
let list = this._collectUsers();
|
||||
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
@ -247,13 +245,13 @@ export class accountsList {
|
||||
toggleLoader(button);
|
||||
window.modals.deleteUser.close();
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
let errorMsg = "Failed (check console/logs).";
|
||||
let errorMsg = window.lang.notif("errorFailureCheckLogs");
|
||||
if (!("error" in req.response)) {
|
||||
errorMsg = "Partial failure (check console/logs).";
|
||||
errorMsg = window.lang.notif("errorPartialFailureCheckLogs");
|
||||
}
|
||||
window.notifications.customError("deleteUserError", errorMsg);
|
||||
} else {
|
||||
window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`);
|
||||
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", this._checkCount));
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
@ -264,7 +262,7 @@ export class accountsList {
|
||||
|
||||
modifyUsers = () => {
|
||||
const modalHeader = document.getElementById("header-modify-user");
|
||||
modalHeader.textContent = this._genCountString();
|
||||
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._checkCount)
|
||||
let list = this._collectUsers();
|
||||
(() => {
|
||||
let innerHTML = "";
|
||||
@ -310,18 +308,18 @@ export class accountsList {
|
||||
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.";
|
||||
errorMsg = window.lang.notif("errorSettingsAppliedNoHomescreenLayout");
|
||||
} else if (policy != 0 && homescreen == 0) {
|
||||
errorMsg = "Homescreen layout was applied, but applying settings may have failed.";
|
||||
errorMsg = window.lang.notif("errorHomescreenAppliedNoSettings");
|
||||
} else if (policy != 0 && homescreen != 0) {
|
||||
errorMsg = "Application failed.";
|
||||
errorMsg = window.lang.notif("errorSettingsFailed");
|
||||
}
|
||||
} 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()}.`);
|
||||
window.notifications.customSuccess("modifySettingsSuccess", window.lang.quantity("appliedSettings", this._checkCount));
|
||||
}
|
||||
this.reload();
|
||||
window.modals.modifyUser.close();
|
||||
@ -331,8 +329,6 @@ export class accountsList {
|
||||
window.modals.modifyUser.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor() {
|
||||
this._users = {};
|
||||
this._selectAll.checked = false;
|
||||
|
@ -60,7 +60,7 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
@ -80,7 +80,7 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
@ -97,7 +97,7 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
@ -131,7 +131,7 @@ export class notificationBox implements NotificationBox {
|
||||
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}`;
|
||||
noti.innerHTML = `<strong>${window.lang.strings("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>`;
|
||||
@ -152,7 +152,7 @@ export class notificationBox implements NotificationBox {
|
||||
return noti;
|
||||
}
|
||||
|
||||
connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); }
|
||||
connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); }
|
||||
|
||||
customError = (type: string, message: string) => {
|
||||
this._errorTypes[type] = this._errorTypes[type] || false;
|
||||
@ -179,6 +179,8 @@ export class notificationBox implements NotificationBox {
|
||||
this._positiveTypes[type] = true;
|
||||
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
|
||||
}
|
||||
|
||||
customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message)
|
||||
}
|
||||
|
||||
export const whichAnimationEvent = () => {
|
||||
|
@ -92,7 +92,7 @@ export class DOMInvite implements Invite {
|
||||
this._usedBy = uB;
|
||||
if (uB.length == 0) {
|
||||
this._right.classList.add("empty");
|
||||
this._userTable.innerHTML = `<p class="content">None yet!</p>`;
|
||||
this._userTable.innerHTML = `<p class="content">${window.lang.strings("inviteNoUsersCreated")}</p>`;
|
||||
return;
|
||||
}
|
||||
this._right.classList.remove("empty");
|
||||
@ -100,8 +100,8 @@ export class DOMInvite implements Invite {
|
||||
<table class="table inv-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
<th>${window.lang.strings("name")}</th>
|
||||
<th>${window.lang.strings("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -153,7 +153,7 @@ export class DOMInvite implements Invite {
|
||||
} else {
|
||||
selected = selected || select.value;
|
||||
}
|
||||
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>No Profile</option>`;
|
||||
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>${window.lang.strings("inviteNoProfile")}</option>`;
|
||||
for (let profile of window.availableProfiles) {
|
||||
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
|
||||
}
|
||||
@ -221,7 +221,7 @@ export class DOMInvite implements Invite {
|
||||
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>
|
||||
<span class="button ~info !normal" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span>
|
||||
`;
|
||||
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
|
||||
copyButton.onclick = () => {
|
||||
@ -248,7 +248,7 @@ export class DOMInvite implements Invite {
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
<span class="inv-expiry mr-1"></span>
|
||||
<span class="button ~critical !normal inv-delete">Delete</span>
|
||||
<span class="button ~critical !normal inv-delete">${window.lang.strings("delete")}</span>
|
||||
<label>
|
||||
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||
<input class="inv-toggle-details unfocused" type="checkbox">
|
||||
@ -271,23 +271,23 @@ export class DOMInvite implements Invite {
|
||||
detailsInner.appendChild(this._left);
|
||||
this._left.classList.add("inv-profilearea");
|
||||
let innerHTML = `
|
||||
<p class="supra mb-1 top">Profile</p>
|
||||
<p class="supra mb-1 top">${window.lang.strings("profile")}</p>
|
||||
<div class="select ~neutral !normal inv-profileselect inline-block">
|
||||
<select>
|
||||
<option value="noProfile" selected>No Profile</option>
|
||||
<option value="noProfile" selected>${window.lang.strings("inviteNoProfile")}</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
if (window.notificationsEnabled) {
|
||||
innerHTML += `
|
||||
<p class="label supra">Notify on:</p>
|
||||
<p class="label supra">${window.lang.strings("notifyEvent")}</p>
|
||||
<label class="switch block">
|
||||
<input class="inv-notify-expiry" type="checkbox">
|
||||
<span>On expiry</span>
|
||||
<span>${window.lang.strings("notifyInviteExpiry")}</span>
|
||||
</label>
|
||||
<label class="switch block">
|
||||
<input class="inv-notify-creation" type="checkbox">
|
||||
<span>On user creation</span>
|
||||
<span>${window.lang.strings("notifyUserCreation")}</span>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
@ -306,14 +306,14 @@ export class DOMInvite implements Invite {
|
||||
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>
|
||||
<p class="supra mb-1 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
||||
<p class="supra mb-1">${window.lang.strings("inviteRemainingUses")} <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._right.innerHTML = `<strong class="supra table-header">${window.lang.strings("inviteUsersCreated")}</strong>`;
|
||||
this._userTable = document.createElement('div') as HTMLDivElement;
|
||||
this._right.appendChild(this._userTable);
|
||||
|
||||
@ -376,7 +376,7 @@ export class inviteList implements inviteList {
|
||||
<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>
|
||||
<span class="code monospace">${window.lang.strings("inviteNoInvites")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -441,10 +441,10 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
|
||||
time += `${invite[fields[i]]}${fields[i][0]} `;
|
||||
}
|
||||
}
|
||||
parsed.expiresIn = `Expires in ${time.slice(0, -1)}`;
|
||||
parsed.expiresIn = window.lang.var("strings", "inviteExpiresInTime", 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.created = invite["created"] as string || window.lang.strings("unknown");
|
||||
parsed.profile = invite["profile"] as string || "";
|
||||
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
|
||||
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
|
||||
@ -566,7 +566,7 @@ export class createInvite {
|
||||
}
|
||||
|
||||
loadProfiles = () => {
|
||||
let innerHTML = `<option value="noProfile">No Profile</option>`;
|
||||
let innerHTML = `<option value="noProfile">${window.lang.strings("inviteNoProfile")}</option>`;
|
||||
for (let profile of window.availableProfiles) {
|
||||
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||
}
|
||||
|
67
ts/modules/lang.ts
Normal file
67
ts/modules/lang.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { _get } from "../modules/common.js";
|
||||
|
||||
interface Meta {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface quantityString {
|
||||
singular: string;
|
||||
plural: string;
|
||||
}
|
||||
|
||||
export interface LangFile {
|
||||
meta: Meta;
|
||||
strings: { [key: string]: string };
|
||||
notifications: { [key: string]: string };
|
||||
quantityStrings: { [key: string]: quantityString };
|
||||
}
|
||||
|
||||
export class lang implements Lang {
|
||||
private _lang: LangFile;
|
||||
constructor(lang: LangFile) {
|
||||
this._lang = lang;
|
||||
}
|
||||
|
||||
get = (sect: string, key: string): string => {
|
||||
if (sect == "quantityStrings" || sect == "meta") { return ""; }
|
||||
return this._lang[sect][key];
|
||||
}
|
||||
|
||||
strings = (key: string): string => this.get("strings", key)
|
||||
notif = (key: string): string => this.get("notifications", key)
|
||||
|
||||
var = (sect: string, key: string, ...subs: string[]): string => {
|
||||
if (sect == "quantityStrings" || sect == "meta") { return ""; }
|
||||
let str = this._lang[sect][key];
|
||||
for (let sub of subs) {
|
||||
str = str.replace("{n}", sub);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
quantity = (key: string, number: number): string => {
|
||||
if (number == 1) {
|
||||
return this._lang.quantityStrings[key].singular.replace("{n}", ""+number)
|
||||
}
|
||||
return this._lang.quantityStrings[key].plural.replace("{n}", ""+number);
|
||||
}
|
||||
}
|
||||
|
||||
export const loadLangSelector = (page: string) => _get("/lang/" + page, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
document.getElementById("lang-dropdown").remove();
|
||||
return;
|
||||
}
|
||||
const list = document.getElementById("lang-list") as HTMLDivElement;
|
||||
let innerHTML = '';
|
||||
for (let code in req.response) {
|
||||
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half">${req.response[code]}</a>`;
|
||||
}
|
||||
list.innerHTML = innerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
@ -44,7 +44,7 @@ class profile implements Profile {
|
||||
<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>
|
||||
<td><span class="button ~critical !normal">${window.lang.strings("delete")}</span></td>
|
||||
`;
|
||||
this._name = this._row.querySelector("b.profile-name");
|
||||
this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement;
|
||||
@ -71,7 +71,7 @@ class profile implements Profile {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
this.remove();
|
||||
} else {
|
||||
window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`);
|
||||
window.notifications.customError("profileDelete", window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`));
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -98,7 +98,7 @@ export class ProfileEditor {
|
||||
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>`
|
||||
this._table.innerHTML = `<tr><td class="empty">${window.lang.strings("inviteNoInvites")}</td></tr>`
|
||||
} else if (this._table.querySelector("td.empty")) {
|
||||
this._table.textContent = ``;
|
||||
}
|
||||
@ -133,7 +133,7 @@ export class ProfileEditor {
|
||||
this.default = resp.default_profile;
|
||||
window.modals.profiles.show();
|
||||
} else {
|
||||
window.notifications.customError("profileEditor", "Failed to load profiles.");
|
||||
window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles"));
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -149,7 +149,7 @@ export class ProfileEditor {
|
||||
this.default = newDefault;
|
||||
} else {
|
||||
this.default = prevDefault;
|
||||
window.notifications.customError("profileDefault", "Failed to set default profile.");
|
||||
window.notifications.customError("profileDefault", window.lang.notif("errorSetDefaultProfile"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -171,7 +171,7 @@ export class ProfileEditor {
|
||||
window.modals.profiles.close();
|
||||
window.modals.addProfile.show();
|
||||
} else {
|
||||
window.notifications.customError("loadUsers", "Failed to load users.");
|
||||
window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -191,9 +191,9 @@ export class ProfileEditor {
|
||||
window.modals.addProfile.close();
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
this.load();
|
||||
window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`);
|
||||
window.notifications.customSuccess("createProfile", window.lang.var("notifications", "createProfile", `"${send['name']}"`));
|
||||
} else {
|
||||
window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`);
|
||||
window.notifications.customError("createProfile", window.lang.var("notifications", "errorCreateProfile", `"${send['name']}"`));
|
||||
}
|
||||
window.modals.profiles.show();
|
||||
}
|
||||
|
@ -345,6 +345,17 @@ class DOMSelect implements SSelect {
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
this._select.onchange = onValueChange;
|
||||
|
||||
const message = document.getElementById("settings-message") as HTMLElement;
|
||||
message.innerHTML = window.lang.var("strings",
|
||||
"settingsRequiredOrRestartMessage",
|
||||
`<span class="badge ~critical">*</span>`,
|
||||
`<span class="badge ~info">R</span>`
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
this.update(setting);
|
||||
}
|
||||
update = (s: SSelect) => {
|
||||
@ -501,9 +512,9 @@ export class settingsList {
|
||||
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.");
|
||||
window.notifications.customSuccess("settingsSaved", window.lang.notif("saveSettings"));
|
||||
} else {
|
||||
window.notifications.customError("settingsSaved", "Couldn't save settings.");
|
||||
window.notifications.customError("settingsSaved", window.lang.notif("errorSaveSettings"));
|
||||
}
|
||||
this.reload();
|
||||
if (run) { run(); }
|
||||
@ -526,7 +537,7 @@ export class settingsList {
|
||||
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("settingsLoadError", "Failed to load settings.");
|
||||
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings"));
|
||||
return;
|
||||
}
|
||||
let settings = req.response as Settings;
|
||||
@ -558,7 +569,7 @@ class ombiDefaults {
|
||||
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.innerHTML = `<span class="flex">${window.lang.strings("ombiUserDefaults")} <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;
|
||||
@ -575,9 +586,9 @@ class ombiDefaults {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults.");
|
||||
window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiDefaults"));
|
||||
} else {
|
||||
window.notifications.customError("ombiDefaults", "Failed to store ombi defaults.");
|
||||
window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiDefaults"));
|
||||
}
|
||||
window.modals.ombiDefaults.close();
|
||||
}
|
||||
@ -600,15 +611,9 @@ class ombiDefaults {
|
||||
window.modals.ombiDefaults.show();
|
||||
} else {
|
||||
toggleLoader(this._button);
|
||||
window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
|
||||
window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers"))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -27,12 +27,24 @@ declare interface Window {
|
||||
tabs: Tabs;
|
||||
invites: inviteList;
|
||||
notifications: NotificationBox;
|
||||
language: string;
|
||||
lang: Lang;
|
||||
langFile: {};
|
||||
}
|
||||
|
||||
declare interface Lang {
|
||||
get: (sect: string, key: string) => string;
|
||||
strings: (key: string) => string;
|
||||
notif: (key: string) => string;
|
||||
var: (sect: string, key: string, ...subs: string[]) => string;
|
||||
quantity: (key: string, number: number) => string;
|
||||
}
|
||||
|
||||
declare interface NotificationBox {
|
||||
connectionError: () => void;
|
||||
customError: (type: string, message: string) => void;
|
||||
customPositive: (type: string, bold: string, message: string) => void;
|
||||
customSuccess: (type: string, message: string) => void;
|
||||
}
|
||||
|
||||
declare interface Tabs {
|
||||
|
35
views.go
35
views.go
@ -13,19 +13,36 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) {
|
||||
}
|
||||
|
||||
func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
lang := gc.Query("lang")
|
||||
if lang == "" {
|
||||
lang = app.storage.lang.chosenAdminLang
|
||||
} else if _, ok := app.storage.lang.Form[lang]; !ok {
|
||||
lang = app.storage.lang.chosenAdminLang
|
||||
}
|
||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
if pusher := gc.Writer.Pusher(); pusher != nil {
|
||||
toPush := []string{"/js/admin.js", "/js/theme.js", "/js/lang.js", "/js/modal.js", "/js/tabs.js", "/js/invites.js", "/js/accounts.js", "/js/settings.js", "/js/profiles.js", "/js/common.js"}
|
||||
for _, f := range toPush {
|
||||
if err := pusher.Push(f, nil); err != nil {
|
||||
app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
||||
"urlBase": app.URLBase,
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": VERSION,
|
||||
"commit": COMMIT,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"urlBase": app.URLBase,
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": VERSION,
|
||||
"commit": COMMIT,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"strings": app.storage.lang.Admin[lang]["strings"],
|
||||
"quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"],
|
||||
"language": app.storage.lang.AdminJSON[lang],
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user