1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 09:00:11 +00:00

Compare commits

..

No commits in common. "1fc8873c0964d3a29b75f4a6d13859880a40f7d4" and "0cb1e7b24bf7c72b0a384d297639efb3ec3e153a" have entirely different histories.

18 changed files with 137 additions and 319 deletions

55
api.go
View File

@ -1085,25 +1085,17 @@ func (app *appContext) GetConfig(gc *gin.Context) {
app.info.Println("Config requested") app.info.Println("Config requested")
resp := app.configBase resp := app.configBase
// Load language options // Load language options
loadLangs := func(langs *map[string]map[string]interface{}, settingsKey string) (string, []string) { langOptions := make([]string, len(app.storage.lang.Form))
langOptions := make([]string, len(*langs)) chosenLang := app.config.Section("ui").Key("language").MustString("en-us")
chosenLang := app.config.Section("ui").Key("language-" + settingsKey).MustString("en-us") chosenLangName := app.storage.lang.Form[chosenLang]["meta"].(map[string]interface{})["name"].(string)
chosenLangName := (*langs)[chosenLang]["meta"].(map[string]interface{})["name"].(string) i := 0
i := 0 for _, lang := range app.storage.lang.Form {
for _, lang := range *langs { langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string)
langOptions[i] = lang["meta"].(map[string]interface{})["name"].(string) i++
i++
}
return chosenLangName, langOptions
} }
formChosen, formOptions := loadLangs(&app.storage.lang.Form, "form") l := resp.Sections["ui"].Settings["language"]
fl := resp.Sections["ui"].Settings["language-form"] l.Options = langOptions
fl.Options = formOptions l.Value = chosenLangName
fl.Value = formChosen
adminChosen, adminOptions := loadLangs(&app.storage.lang.Admin, "admin")
al := resp.Sections["ui"].Settings["language-admin"]
al.Options = adminOptions
al.Value = adminChosen
for sectName, section := range resp.Sections { for sectName, section := range resp.Sections {
for settingName, setting := range section.Settings { for settingName, setting := range section.Settings {
val := app.config.Section(sectName).Key(settingName) val := app.config.Section(sectName).Key(settingName)
@ -1119,12 +1111,11 @@ func (app *appContext) GetConfig(gc *gin.Context) {
resp.Sections[sectName].Settings[settingName] = s resp.Sections[sectName].Settings[settingName] = s
} }
} }
resp.Sections["ui"].Settings["language-form"] = fl resp.Sections["ui"].Settings["language"] = l
resp.Sections["ui"].Settings["language-admin"] = al
t := resp.Sections["jellyfin"].Settings["type"] t := resp.Sections["jellyfin"].Settings["type"]
opts := make([]string, len(serverTypes)) opts := make([]string, len(serverTypes))
i := 0 i = 0
for _, v := range serverTypes { for _, v := range serverTypes {
opts[i] = v opts[i] = v
i++ i++
@ -1155,17 +1146,10 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
tempConfig.NewSection(section) tempConfig.NewSection(section)
} }
for setting, value := range settings.(map[string]interface{}) { for setting, value := range settings.(map[string]interface{}) {
if section == "ui" && setting == "language-form" { if section == "ui" && setting == "language" {
for key, lang := range app.storage.lang.Form { for key, lang := range app.storage.lang.Form {
if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) { if lang["meta"].(map[string]interface{})["name"].(string) == value.(string) {
tempConfig.Section("ui").Key("language-form").SetValue(key) tempConfig.Section("ui").Key("language").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 break
} }
} }
@ -1237,16 +1221,9 @@ func (app *appContext) Logout(gc *gin.Context) {
// @Router /lang [get] // @Router /lang [get]
// @tags Other // @tags Other
func (app *appContext) GetLanguages(gc *gin.Context) { func (app *appContext) GetLanguages(gc *gin.Context) {
page := gc.Param("page")
resp := langDTO{} resp := langDTO{}
if page == "form" { for key, lang := range app.storage.lang.Form {
for key, lang := range app.storage.lang.Form { resp[key] = lang["meta"].(map[string]interface{})["name"].(string)
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 { if len(resp) == 0 {
respond(500, "Couldn't get languages", gc) respond(500, "Couldn't get languages", gc)

View File

@ -84,15 +84,8 @@ func (app *appContext) loadConfig() error {
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
oldFormLang := app.config.Section("ui").Key("language").MustString("") app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us")
if oldFormLang != "" { app.storage.lang.chosenFormLang = app.config.Section("ui").Key("language").MustString("en-us")
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")
return nil return nil
} }

View File

@ -84,7 +84,7 @@
"description": "Settings related to the UI and program functionality." "description": "Settings related to the UI and program functionality."
}, },
"settings": { "settings": {
"language-form": { "language": {
"name": "Default Form Language", "name": "Default Form Language",
"required": false, "required": false,
"requires_restart": true, "requires_restart": true,
@ -93,18 +93,7 @@
"en-us" "en-us"
], ],
"value": "en-US", "value": "en-US",
"description": "Default Account Form Language. Submit a PR on github if you'd like to translate." "description": "Default UI Language. Currently only implemented for account creation form. 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": { "theme": {
"name": "Default Look", "name": "Default Look",

View File

@ -9,7 +9,6 @@
window.emailEnabled = {{ .email_enabled }}; window.emailEnabled = {{ .email_enabled }};
window.ombiEnabled = {{ .ombiEnabled }}; window.ombiEnabled = {{ .ombiEnabled }};
window.usernamesEnabled = {{ .username }}; window.usernamesEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
</script> </script>
{{ template "header.html" . }} {{ template "header.html" . }}
<title>Admin - jfa-go</title> <title>Admin - jfa-go</title>
@ -167,16 +166,6 @@
</form> </form>
</div> </div>
<div id="notification-box"></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="page-container">
<div class="mb-1"> <div class="mb-1">
<header class="flex flex-wrap items-center justify-between"> <header class="flex flex-wrap items-center justify-between">
@ -275,7 +264,7 @@
</div> </div>
<div id="tab-settings" class="unfocused"> <div id="tab-settings" class="unfocused">
<div class="card ~neutral !low settings overflow"> <div class="card ~neutral !low settings overflow">
<span class="heading">{{ .strings.settings }}</span> <span class="heading">{{ .string.settings }}</span>
<div class="fr"> <div class="fr">
<span class="button ~neutral !normal unfocused" id="settings-save">{{ .strings.settingsSave }}</span> <span class="button ~neutral !normal unfocused" id="settings-save">{{ .strings.settingsSave }}</span>
</div> </div>

5
lang/admin/README.md Normal file
View File

@ -0,0 +1,5 @@
##### admin page translation
* [x] static page content
* [ ] Typescript:
* [x] accounts.ts

View File

@ -21,7 +21,6 @@
"delete": "Delete", "delete": "Delete",
"submit": "Submit", "submit": "Submit",
"name": "Name", "name": "Name",
"date": "Date",
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"emailAddress": "Email Address", "emailAddress": "Email Address",
@ -33,9 +32,6 @@
"commitNoun": "Commit", "commitNoun": "Commit",
"newUser": "New User", "newUser": "New User",
"profile": "Profile", "profile": "Profile",
"success": "Success",
"error": "Error",
"unknown": "Unknown",
"modifySettings": "Modify Settings", "modifySettings": "Modify Settings",
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
"applyHomescreenLayout": "Apply homescreen layout", "applyHomescreenLayout": "Apply homescreen layout",
@ -47,7 +43,7 @@
"settingsApplyRestartNow": "Apply & restart", "settingsApplyRestartNow": "Apply & restart",
"settingsApplied": "Settings applied.", "settingsApplied": "Settings applied.",
"settingsRefreshPage": "Refresh the page in a few seconds", "settingsRefreshPage": "Refresh the page in a few seconds",
"settingsRequiredOrRestartMessage": "Note: {n} indicates a required field, {n} indicates changes require a restart.", "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart.",
"settingsSave": "Save", "settingsSave": "Save",
"ombiUserDefaults": "Ombi user defaults", "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", "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",
@ -58,49 +54,11 @@
"addProfile": "Add Profile", "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.", "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", "addProfileNameOf": "Profile Name",
"addProfileStoreHomescreenLayout": "Store homescreen layout", "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": { "variableStrings": {
"changedEmailAddress": "Changed email address of {n}.", "settingsRequiredOrRestartMessage": "Note: {*} indicates a required field, {R} indicates changes require a restart."
"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": { "quantityStrings": {
"modifySettingsFor": { "modifySettingsFor": {
"singular": "Modify Settings for {n} user", "singular": "Modify Settings for {n} user",
@ -117,14 +75,6 @@
"deleteUser": { "deleteUser": {
"singular": "Delete User", "singular": "Delete User",
"plural": "Delete Users" "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."
} }
} }
} }

View File

@ -577,7 +577,7 @@ func start(asDaemon, firstCall bool) {
router.GET("/accounts", app.AdminPage) router.GET("/accounts", app.AdminPage)
router.GET("/settings", app.AdminPage) router.GET("/settings", app.AdminPage)
router.GET("/lang/:page", app.GetLanguages) router.GET("/lang", app.GetLanguages)
router.GET("/token/login", app.getTokenLogin) router.GET("/token/login", app.getTokenLogin)
router.GET("/token/refresh", app.getTokenRefresh) router.GET("/token/refresh", app.getTokenRefresh)
router.POST("/newUser", app.NewUser) router.POST("/newUser", app.NewUser)

View File

@ -25,7 +25,6 @@ type Lang struct {
chosenAdminLang string chosenAdminLang string
AdminPath string AdminPath string
Admin map[string]map[string]interface{} Admin map[string]map[string]interface{}
AdminJSON map[string]string
FormPath string FormPath string
Form map[string]map[string]interface{} Form map[string]map[string]interface{}
} }
@ -64,45 +63,40 @@ func (st *Storage) storeInvites() error {
} }
func (st *Storage) loadLang() error { func (st *Storage) loadLang() error {
loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) { loadData := func(path string) (map[string]map[string]interface{}, error) {
files, err := ioutil.ReadDir(path) files, err := ioutil.ReadDir(path)
outString := map[string]string{}
out := map[string]map[string]interface{}{} out := map[string]map[string]interface{}{}
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
for _, f := range files { for _, f := range files {
index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
var data map[string]interface{} var data map[string]interface{}
var file []byte
var err error
file, err = ioutil.ReadFile(filepath.Join(path, f.Name()))
if err != nil {
file = []byte("{}")
}
// Replace Jellyfin with emby on form
if substituteStrings != "" { if substituteStrings != "" {
fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings) var file []byte
file = []byte(fileString) var err error
} file, err = ioutil.ReadFile(filepath.Join(path, f.Name()))
err = json.Unmarshal(file, &data)
if err != nil {
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
return nil, nil, err
}
if stringJson {
stringJSON, err := json.Marshal(data)
if err != nil { if err != nil {
return nil, nil, err file = []byte("{}")
}
// Replace Jellyfin with emby on form
file = []byte(strings.ReplaceAll(string(file), "Jellyfin", substituteStrings))
err = json.Unmarshal(file, &data)
if err != nil {
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
return nil, err
}
} else {
err := loadJSON(filepath.Join(path, f.Name()), &data)
if err != nil {
return nil, err
} }
outString[index] = string(stringJSON)
} }
out[index] = data out[index] = data
} }
return outString, out, nil return out, nil
} }
_, form, err := loadData(st.lang.FormPath, false) form, err := loadData(st.lang.FormPath)
if err != nil { if err != nil {
return err return err
} }
@ -118,9 +112,8 @@ func (st *Storage) loadLang() error {
form[index] = lang form[index] = lang
} }
st.lang.Form = form st.lang.Form = form
adminJSON, admin, err := loadData(st.lang.AdminPath, true) admin, err := loadData(st.lang.AdminPath)
st.lang.Admin = admin st.lang.Admin = admin
st.lang.AdminJSON = adminJSON
return err return err
} }

View File

@ -1,25 +1,15 @@
import { toggleTheme, loadTheme } from "./modules/theme.js"; import { toggleTheme, loadTheme } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { Tabs } from "./modules/tabs.js"; import { Tabs } from "./modules/tabs.js";
import { inviteList, createInvite } from "./modules/invites.js"; import { inviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js"; import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js"; import { settingsList } from "./modules/settings.js";
import { ProfileEditor } from "./modules/profiles.js"; import { ProfileEditor } from "./modules/profiles.js";
import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
loadTheme(); loadTheme();
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; (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.animationEvent = whichAnimationEvent();
window.token = ""; window.token = "";
@ -120,12 +110,12 @@ function login(username: string, password: string, run?: (state?: number) => voi
req.onreadystatechange = function (): void { req.onreadystatechange = function (): void {
if (this.readyState == 4) { if (this.readyState == 4) {
if (this.status != 200) { if (this.status != 200) {
let errorMsg = window.lang.notif("errorConnection"); let errorMsg = "Connection error.";
if (this.response) { if (this.response) {
errorMsg = this.response["error"]; errorMsg = this.response["error"];
} }
if (!errorMsg) { if (!errorMsg) {
errorMsg = window.lang.notif("errorUnknown"); errorMsg = "Unknown error";
} }
if (!refresh) { if (!refresh) {
window.notifications.customError("loginError", errorMsg); window.notifications.customError("loginError", errorMsg);
@ -163,7 +153,7 @@ function login(username: string, password: string, run?: (state?: number) => voi
const username = (document.getElementById("login-user") as HTMLInputElement).value; const username = (document.getElementById("login-user") as HTMLInputElement).value;
const password = (document.getElementById("login-password") as HTMLInputElement).value; const password = (document.getElementById("login-password") as HTMLInputElement).value;
if (!username || !password) { if (!username || !password) {
window.notifications.customError("loginError", window.lang.notif("errorLoginBlank")); window.notifications.customError("loginError", "The username and/or password were left blank.");
return; return;
} }
toggleLoader(button); toggleLoader(button);

View File

@ -1,6 +1,5 @@
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { _get, _post, toggleLoader } from "./modules/common.js"; import { _get, _post, toggleLoader } from "./modules/common.js";
import { loadLangSelector } from "./modules/lang.js";
interface formWindow extends Window { interface formWindow extends Window {
validationStrings: pwValStrings; validationStrings: pwValStrings;
@ -22,7 +21,23 @@ interface pwValStrings {
[ type: string ]: pwValString; [ type: string ]: pwValString;
} }
loadLangSelector("form"); _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;
}
});
window.modal = new Modal(document.getElementById("modal-success")); window.modal = new Modal(document.getElementById("modal-success"));
declare var window: formWindow; declare var window: formWindow;

View File

@ -100,10 +100,10 @@ class user implements User {
_post("/users/emails", send, (req: XMLHttpRequest) => { _post("/users/emails", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200) { if (req.status == 200) {
window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`)); window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`);
} else { } else {
this.email = oldEmail; this.email = oldEmail;
window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`)); window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`);
} }
} }
}); });
@ -184,9 +184,11 @@ export class accountsList {
} }
this._modifySettings.classList.remove("unfocused"); this._modifySettings.classList.remove("unfocused");
this._deleteUser.classList.remove("unfocused"); this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", this._checkCount); (this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users";
} }
} }
private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; }
private _collectUsers = (): string[] => { private _collectUsers = (): string[] => {
let list: string[] = []; let list: string[] = [];
@ -206,7 +208,7 @@ export class accountsList {
}; };
for (let field in send) { for (let field in send) {
if (!send[field]) { if (!send[field]) {
window.notifications.customError("addUserBlankField", window.lang.notif("errorBlankFields")); window.notifications.customError("addUserBlankField", "Fields were left blank.");
return; return;
} }
} }
@ -215,7 +217,7 @@ export class accountsList {
if (req.readyState == 4) { if (req.readyState == 4) {
toggleLoader(button); toggleLoader(button);
if (req.status == 200) { if (req.status == 200) {
window.notifications.customSuccess("addUser", window.lang.var("notifications", "userCreated", `"${send['username']}"`)); window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`);
} }
this.reload(); this.reload();
window.modals.addUser.close(); window.modals.addUser.close();
@ -225,7 +227,7 @@ export class accountsList {
deleteUsers = () => { deleteUsers = () => {
const modalHeader = document.getElementById("header-delete-user"); const modalHeader = document.getElementById("header-delete-user");
modalHeader.textContent = window.lang.quantity("deleteNUsers", this._checkCount); modalHeader.textContent = this._genCountString();
let list = this._collectUsers(); let list = this._collectUsers();
const form = document.getElementById("form-delete-user") as HTMLFormElement; const form = document.getElementById("form-delete-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement; const button = form.querySelector("span.submit") as HTMLSpanElement;
@ -245,13 +247,13 @@ export class accountsList {
toggleLoader(button); toggleLoader(button);
window.modals.deleteUser.close(); window.modals.deleteUser.close();
if (req.status != 200 && req.status != 204) { if (req.status != 200 && req.status != 204) {
let errorMsg = window.lang.notif("errorFailureCheckLogs"); let errorMsg = "Failed (check console/logs).";
if (!("error" in req.response)) { if (!("error" in req.response)) {
errorMsg = window.lang.notif("errorPartialFailureCheckLogs"); errorMsg = "Partial failure (check console/logs).";
} }
window.notifications.customError("deleteUserError", errorMsg); window.notifications.customError("deleteUserError", errorMsg);
} else { } else {
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", this._checkCount)); window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`);
} }
this.reload(); this.reload();
} }
@ -262,7 +264,7 @@ export class accountsList {
modifyUsers = () => { modifyUsers = () => {
const modalHeader = document.getElementById("header-modify-user"); const modalHeader = document.getElementById("header-modify-user");
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._checkCount) modalHeader.textContent = this._genCountString();
let list = this._collectUsers(); let list = this._collectUsers();
(() => { (() => {
let innerHTML = ""; let innerHTML = "";
@ -308,18 +310,18 @@ export class accountsList {
const homescreen = Object.keys(response["homescreen"]).length; const homescreen = Object.keys(response["homescreen"]).length;
const policy = Object.keys(response["policy"]).length; const policy = Object.keys(response["policy"]).length;
if (homescreen != 0 && policy == 0) { if (homescreen != 0 && policy == 0) {
errorMsg = window.lang.notif("errorSettingsAppliedNoHomescreenLayout"); errorMsg = "Settings were applied, but applying homescreen layout may have failed.";
} else if (policy != 0 && homescreen == 0) { } else if (policy != 0 && homescreen == 0) {
errorMsg = window.lang.notif("errorHomescreenAppliedNoSettings"); errorMsg = "Homescreen layout was applied, but applying settings may have failed.";
} else if (policy != 0 && homescreen != 0) { } else if (policy != 0 && homescreen != 0) {
errorMsg = window.lang.notif("errorSettingsFailed"); errorMsg = "Application failed.";
} }
} else if ("error" in response) { } else if ("error" in response) {
errorMsg = response["error"]; errorMsg = response["error"];
} }
window.notifications.customError("modifySettingsError", errorMsg); window.notifications.customError("modifySettingsError", errorMsg);
} else if (req.status == 200 || req.status == 204) { } else if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("modifySettingsSuccess", window.lang.quantity("appliedSettings", this._checkCount)); window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`);
} }
this.reload(); this.reload();
window.modals.modifyUser.close(); window.modals.modifyUser.close();
@ -329,6 +331,8 @@ export class accountsList {
window.modals.modifyUser.show(); window.modals.modifyUser.show();
} }
constructor() { constructor() {
this._users = {}; this._users = {};
this._selectAll.checked = false; this._selectAll.checked = false;

View File

@ -60,7 +60,7 @@ export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHtt
window.notifications.connectionError(); window.notifications.connectionError();
return; return;
} else if (req.status == 401) { } else if (req.status == 401) {
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
} }
onreadystatechange(req); onreadystatechange(req);
}; };
@ -80,7 +80,7 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt
window.notifications.connectionError(); window.notifications.connectionError();
return; return;
} else if (req.status == 401) { } else if (req.status == 401) {
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
} }
onreadystatechange(req); onreadystatechange(req);
}; };
@ -97,7 +97,7 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML
window.notifications.connectionError(); window.notifications.connectionError();
return; return;
} else if (req.status == 401) { } else if (req.status == 401) {
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized")); window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
} }
onreadystatechange(req); onreadystatechange(req);
}; };
@ -131,7 +131,7 @@ export class notificationBox implements NotificationBox {
private _error = (message: string): HTMLElement => { private _error = (message: string): HTMLElement => {
const noti = document.createElement('aside'); const noti = document.createElement('aside');
noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error"); noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error");
noti.innerHTML = `<strong>${window.lang.strings("error")}:</strong> ${message}`; noti.innerHTML = `<strong>Error:</strong> ${message}`;
const closeButton = document.createElement('span') as HTMLSpanElement; const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "!low", "ml-1"); closeButton.classList.add("button", "~critical", "!low", "ml-1");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`; closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
@ -152,7 +152,7 @@ export class notificationBox implements NotificationBox {
return noti; return noti;
} }
connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); } connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); }
customError = (type: string, message: string) => { customError = (type: string, message: string) => {
this._errorTypes[type] = this._errorTypes[type] || false; this._errorTypes[type] = this._errorTypes[type] || false;
@ -179,8 +179,6 @@ export class notificationBox implements NotificationBox {
this._positiveTypes[type] = true; this._positiveTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); 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 = () => { export const whichAnimationEvent = () => {

View File

@ -92,7 +92,7 @@ export class DOMInvite implements Invite {
this._usedBy = uB; this._usedBy = uB;
if (uB.length == 0) { if (uB.length == 0) {
this._right.classList.add("empty"); this._right.classList.add("empty");
this._userTable.innerHTML = `<p class="content">${window.lang.strings("inviteNoUsersCreated")}</p>`; this._userTable.innerHTML = `<p class="content">None yet!</p>`;
return; return;
} }
this._right.classList.remove("empty"); this._right.classList.remove("empty");
@ -100,8 +100,8 @@ export class DOMInvite implements Invite {
<table class="table inv-table"> <table class="table inv-table">
<thead> <thead>
<tr> <tr>
<th>${window.lang.strings("name")}</th> <th>Name</th>
<th>${window.lang.strings("date")}</th> <th>Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -153,7 +153,7 @@ export class DOMInvite implements Invite {
} else { } else {
selected = selected || select.value; selected = selected || select.value;
} }
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>${window.lang.strings("inviteNoProfile")}</option>`; let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>No Profile</option>`;
for (let profile of window.availableProfiles) { for (let profile of window.availableProfiles) {
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`; 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.classList.add("inv-codearea");
this._codeArea.innerHTML = ` this._codeArea.innerHTML = `
<a class="invite-link code monospace mr-1" href=""></a> <a class="invite-link code monospace mr-1" href=""></a>
<span class="button ~info !normal" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span> <span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
`; `;
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement; const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
copyButton.onclick = () => { copyButton.onclick = () => {
@ -248,7 +248,7 @@ export class DOMInvite implements Invite {
<span class="content sm"></span> <span class="content sm"></span>
</div> </div>
<span class="inv-expiry mr-1"></span> <span class="inv-expiry mr-1"></span>
<span class="button ~critical !normal inv-delete">${window.lang.strings("delete")}</span> <span class="button ~critical !normal inv-delete">Delete</span>
<label> <label>
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i> <i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
<input class="inv-toggle-details unfocused" type="checkbox"> <input class="inv-toggle-details unfocused" type="checkbox">
@ -271,23 +271,23 @@ export class DOMInvite implements Invite {
detailsInner.appendChild(this._left); detailsInner.appendChild(this._left);
this._left.classList.add("inv-profilearea"); this._left.classList.add("inv-profilearea");
let innerHTML = ` let innerHTML = `
<p class="supra mb-1 top">${window.lang.strings("profile")}</p> <p class="supra mb-1 top">Profile</p>
<div class="select ~neutral !normal inv-profileselect inline-block"> <div class="select ~neutral !normal inv-profileselect inline-block">
<select> <select>
<option value="noProfile" selected>${window.lang.strings("inviteNoProfile")}</option> <option value="noProfile" selected>No Profile</option>
</select> </select>
</div> </div>
`; `;
if (window.notificationsEnabled) { if (window.notificationsEnabled) {
innerHTML += ` innerHTML += `
<p class="label supra">${window.lang.strings("notifyEvent")}</p> <p class="label supra">Notify on:</p>
<label class="switch block"> <label class="switch block">
<input class="inv-notify-expiry" type="checkbox"> <input class="inv-notify-expiry" type="checkbox">
<span>${window.lang.strings("notifyInviteExpiry")}</span> <span>On expiry</span>
</label> </label>
<label class="switch block"> <label class="switch block">
<input class="inv-notify-creation" type="checkbox"> <input class="inv-notify-creation" type="checkbox">
<span>${window.lang.strings("notifyUserCreation")}</span> <span>On user creation</span>
</label> </label>
`; `;
} }
@ -306,14 +306,14 @@ export class DOMInvite implements Invite {
detailsInner.appendChild(this._middle); detailsInner.appendChild(this._middle);
this._middle.classList.add("block"); this._middle.classList.add("block");
this._middle.innerHTML = ` this._middle.innerHTML = `
<p class="supra mb-1 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p> <p class="supra mb-1 top">Created <strong class="inv-created"></strong></p>
<p class="supra mb-1">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p> <p class="supra mb-1">Remaining uses <strong class="inv-remaining"></strong></p>
`; `;
this._right = document.createElement('div') as HTMLDivElement; this._right = document.createElement('div') as HTMLDivElement;
detailsInner.appendChild(this._right); detailsInner.appendChild(this._right);
this._right.classList.add("card", "~neutral", "!low", "inv-created-users"); this._right.classList.add("card", "~neutral", "!low", "inv-created-users");
this._right.innerHTML = `<strong class="supra table-header">${window.lang.strings("inviteUsersCreated")}</strong>`; this._right.innerHTML = `<strong class="supra table-header">Created users</strong>`;
this._userTable = document.createElement('div') as HTMLDivElement; this._userTable = document.createElement('div') as HTMLDivElement;
this._right.appendChild(this._userTable); this._right.appendChild(this._userTable);
@ -376,7 +376,7 @@ export class inviteList implements inviteList {
<div class="inv inv-empty"> <div class="inv inv-empty">
<div class="card ~neutral !normal inv-header flex-expand mt-half"> <div class="card ~neutral !normal inv-header flex-expand mt-half">
<div class="inv-codearea"> <div class="inv-codearea">
<span class="code monospace">${window.lang.strings("inviteNoInvites")}</span> <span class="code monospace">None</span>
</div> </div>
</div> </div>
</div> </div>
@ -441,10 +441,10 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
time += `${invite[fields[i]]}${fields[i][0]} `; time += `${invite[fields[i]]}${fields[i][0]} `;
} }
} }
parsed.expiresIn = window.lang.var("strings", "inviteExpiresInTime", time.slice(0, -1)); parsed.expiresIn = `Expires in ${time.slice(0, -1)}`;
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"]) parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
parsed.usedBy = invite["used-by"] as string[][] || []; parsed.usedBy = invite["used-by"] as string[][] || [];
parsed.created = invite["created"] as string || window.lang.strings("unknown"); parsed.created = invite["created"] as string || "Unknown";
parsed.profile = invite["profile"] as string || ""; parsed.profile = invite["profile"] as string || "";
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false; parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
parsed.notifyCreation = invite["notify-creation"] as boolean || false; parsed.notifyCreation = invite["notify-creation"] as boolean || false;
@ -566,7 +566,7 @@ export class createInvite {
} }
loadProfiles = () => { loadProfiles = () => {
let innerHTML = `<option value="noProfile">${window.lang.strings("inviteNoProfile")}</option>`; let innerHTML = `<option value="noProfile">No Profile</option>`;
for (let profile of window.availableProfiles) { for (let profile of window.availableProfiles) {
innerHTML += `<option value="${profile}">${profile}</option>`; innerHTML += `<option value="${profile}">${profile}</option>`;
} }

View File

@ -1,67 +0,0 @@
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;
}
});

View File

@ -44,7 +44,7 @@ class profile implements Profile {
<td><input type="radio" name="profile-default"></td> <td><input type="radio" name="profile-default"></td>
<td class="profile-from ellipsis"></td> <td class="profile-from ellipsis"></td>
<td class="profile-libraries"></td> <td class="profile-libraries"></td>
<td><span class="button ~critical !normal">${window.lang.strings("delete")}</span></td> <td><span class="button ~critical !normal">Delete</span></td>
`; `;
this._name = this._row.querySelector("b.profile-name"); this._name = this._row.querySelector("b.profile-name");
this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; 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) { if (req.status == 200 || req.status == 204) {
this.remove(); this.remove();
} else { } else {
window.notifications.customError("profileDelete", window.lang.var("notifications", "errorDeleteProfile", `"${this.name}"`)); window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`);
} }
} }
}) })
@ -98,7 +98,7 @@ export class ProfileEditor {
get empty(): boolean { return (Object.keys(this._table.children).length == 0) } get empty(): boolean { return (Object.keys(this._table.children).length == 0) }
set empty(state: boolean) { set empty(state: boolean) {
if (state) { if (state) {
this._table.innerHTML = `<tr><td class="empty">${window.lang.strings("inviteNoInvites")}</td></tr>` this._table.innerHTML = `<tr><td class="empty">None</td></tr>`
} else if (this._table.querySelector("td.empty")) { } else if (this._table.querySelector("td.empty")) {
this._table.textContent = ``; this._table.textContent = ``;
} }
@ -133,7 +133,7 @@ export class ProfileEditor {
this.default = resp.default_profile; this.default = resp.default_profile;
window.modals.profiles.show(); window.modals.profiles.show();
} else { } else {
window.notifications.customError("profileEditor", window.lang.notif("errorLoadProfiles")); window.notifications.customError("profileEditor", "Failed to load profiles.");
} }
} }
}) })
@ -149,7 +149,7 @@ export class ProfileEditor {
this.default = newDefault; this.default = newDefault;
} else { } else {
this.default = prevDefault; this.default = prevDefault;
window.notifications.customError("profileDefault", window.lang.notif("errorSetDefaultProfile")); window.notifications.customError("profileDefault", "Failed to set default profile.");
} }
} }
}); });
@ -171,7 +171,7 @@ export class ProfileEditor {
window.modals.profiles.close(); window.modals.profiles.close();
window.modals.addProfile.show(); window.modals.addProfile.show();
} else { } else {
window.notifications.customError("loadUsers", window.lang.notif("errorLoadUsers")); window.notifications.customError("loadUsers", "Failed to load users.");
} }
} }
}); });
@ -191,9 +191,9 @@ export class ProfileEditor {
window.modals.addProfile.close(); window.modals.addProfile.close();
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
this.load(); this.load();
window.notifications.customSuccess("createProfile", window.lang.var("notifications", "createProfile", `"${send['name']}"`)); window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`);
} else { } else {
window.notifications.customError("createProfile", window.lang.var("notifications", "errorCreateProfile", `"${send['name']}"`)); window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`);
} }
window.modals.profiles.show(); window.modals.profiles.show();
} }

View File

@ -345,17 +345,6 @@ class DOMSelect implements SSelect {
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
}; };
this._select.onchange = onValueChange; 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); this.update(setting);
} }
update = (s: SSelect) => { update = (s: SSelect) => {
@ -512,9 +501,9 @@ export class settingsList {
private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => { private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("settingsSaved", window.lang.notif("saveSettings")); window.notifications.customPositive("settingsSaved", "Success:", "settings were saved.");
} else { } else {
window.notifications.customError("settingsSaved", window.lang.notif("errorSaveSettings")); window.notifications.customError("settingsSaved", "Couldn't save settings.");
} }
this.reload(); this.reload();
if (run) { run(); } if (run) { run(); }
@ -537,7 +526,7 @@ export class settingsList {
reload = () => _get("/config", null, (req: XMLHttpRequest) => { reload = () => _get("/config", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200) { if (req.status != 200) {
window.notifications.customError("settingsLoadError", window.lang.notif("errorLoadSettings")); window.notifications.customError("settingsLoadError", "Failed to load settings.");
return; return;
} }
let settings = req.response as Settings; let settings = req.response as Settings;
@ -569,7 +558,7 @@ class ombiDefaults {
constructor() { constructor() {
this._button = document.createElement("span") as HTMLSpanElement; this._button = document.createElement("span") as HTMLSpanElement;
this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half"); this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
this._button.innerHTML = `<span class="flex">${window.lang.strings("ombiUserDefaults")} <i class="ri-link-unlink-m ml-half"></i></span>`; this._button.innerHTML = `<span class="flex">Ombi user defaults <i class="ri-link-unlink-m ml-half"></i></span>`;
this._button.onclick = this.load; this._button.onclick = this.load;
this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement; this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement;
this._form.onsubmit = this.send; this._form.onsubmit = this.send;
@ -586,9 +575,9 @@ class ombiDefaults {
if (req.readyState == 4) { if (req.readyState == 4) {
toggleLoader(button); toggleLoader(button);
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("ombiDefaults", window.lang.notif("setOmbiDefaults")); window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults.");
} else { } else {
window.notifications.customError("ombiDefaults", window.lang.notif("errorSetOmbiDefaults")); window.notifications.customError("ombiDefaults", "Failed to store ombi defaults.");
} }
window.modals.ombiDefaults.close(); window.modals.ombiDefaults.close();
} }
@ -611,9 +600,15 @@ class ombiDefaults {
window.modals.ombiDefaults.show(); window.modals.ombiDefaults.show();
} else { } else {
toggleLoader(this._button); toggleLoader(this._button);
window.notifications.customError("ombiLoadError", window.lang.notif("errorLoadOmbiUsers")) window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
} }
} }
}); });
} }
} }

View File

@ -27,24 +27,12 @@ declare interface Window {
tabs: Tabs; tabs: Tabs;
invites: inviteList; invites: inviteList;
notifications: NotificationBox; 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 { declare interface NotificationBox {
connectionError: () => void; connectionError: () => void;
customError: (type: string, message: string) => void; customError: (type: string, message: string) => void;
customPositive: (type: string, bold: string, message: string) => void; customPositive: (type: string, bold: string, message: string) => void;
customSuccess: (type: string, message: string) => void;
} }
declare interface Tabs { declare interface Tabs {

View File

@ -15,9 +15,9 @@ func gcHTML(gc *gin.Context, code int, file string, templ gin.H) {
func (app *appContext) AdminPage(gc *gin.Context) { func (app *appContext) AdminPage(gc *gin.Context) {
lang := gc.Query("lang") lang := gc.Query("lang")
if lang == "" { if lang == "" {
lang = app.storage.lang.chosenAdminLang lang = app.storage.lang.chosenFormLang
} else if _, ok := app.storage.lang.Form[lang]; !ok { } else if _, ok := app.storage.lang.Form[lang]; !ok {
lang = app.storage.lang.chosenAdminLang lang = app.storage.lang.chosenFormLang
} }
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
@ -34,7 +34,6 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"username": !app.config.Section("email").Key("no_username").MustBool(false), "username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang]["strings"], "strings": app.storage.lang.Admin[lang]["strings"],
"quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"], "quantityStrings": app.storage.lang.Admin[lang]["quantityStrings"],
"language": app.storage.lang.AdminJSON[lang],
}) })
} }