+
{{ .lang.Email.title }}
+
+
+
+
+ {{ .lang.Email.method }}
+
+
+ {{ .lang.Strings.disabled }}
+ SMTP
+ Mailgun
+
+
+
+ {{ .lang.Email.useEmailAsUsername }}
+
+
{{ .lang.Email.useEmailAsUsernameNotice }}
+
+ {{ .lang.Email.fromAddress }}
+
+
+
+ {{ .lang.Email.senderName }}
+
+
+
+ {{ .lang.Email.dateFormat }}
+
+
+
+
+
+ {{ .lang.Email.time24h }}
+
+
+ {{ .lang.Email.time12h }}
+
-
-
-
Help Messages
-
- Just a few little messages that will display in various places. Leave these alone if you want.
-
-
- Contact message: Displays at bottom of all pages (except admin).
-
-
-
- Help message: Displays when a user is creating an account.
-
-
-
- Success message: Displays when a user successfully creates an account, just above a button taking the user to Jellyfin.
-
-
-
-
+
+
+
-
-
-
Finished!
-
- Press the button below to submit your settings. The program will restart. Once it's done, refresh this page.
-
-
Submit
-
+
-
+
+
+
+
+
+
+
+
+
{{ .lang.HelpMessages.title }}
+
{{ .lang.HelpMessages.description }}
+
+ {{ .lang.HelpMessages.contactMessage }}
+
+ {{ .lang.HelpMessages.contactMessageNotice }}
+
+
+ {{ .lang.HelpMessages.helpMessage }}
+
+ {{ .lang.HelpMessages.helpMessageNotice }}
+
+
+ {{ .lang.HelpMessages.successMessage }}
+
+ {{ .lang.HelpMessages.successMessageNotice }}
+
+
+ {{ .lang.HelpMessages.emailMessage }}
+
+ {{ .lang.HelpMessages.emailMessageNotice }}
+
+
+
+
+
+ {{ .lang.EndPage.finished }}
+
+
+
{{ .lang.EndPage.restartMessage }}
+
+
+ {{ .lang.Strings.back }}
+ {{ .lang.Strings.submit }}
+ {{ .lang.EndPage.refreshPage }}
-
+
+
-
+
+
diff --git a/lang.go b/lang.go
index cc8940d..169ba8f 100644
--- a/lang.go
+++ b/lang.go
@@ -26,6 +26,13 @@ func (ls *adminLangs) getOptions(chosen string) (string, []string) {
return chosenLang, opts
}
+type commonLangs map[string]commonLang
+
+type commonLang struct {
+ Meta langMeta `json:"meta"`
+ Strings langSection `json:"strings"`
+}
+
type adminLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
@@ -79,6 +86,39 @@ type emailLang struct {
WelcomeEmail langSection `json:"welcomeEmail"`
}
+type setupLangs map[string]setupLang
+
+type setupLang struct {
+ Meta langMeta `json:"meta"`
+ Strings langSection `json:"strings"`
+ StartPage langSection `json:"startPage"`
+ EndPage langSection `json:"endPage"`
+ General langSection `json:"general"`
+ Language langSection `json:"language"`
+ Login langSection `json:"login"`
+ JellyfinEmby langSection `json:"jellyfinEmby"`
+ Ombi langSection `json:"ombi"`
+ Email langSection `json:"email"`
+ Notifications langSection `json:"notifications"`
+ WelcomeEmails langSection `json:"welcomeEmails"`
+ PasswordResets langSection `json:"passwordResets"`
+ InviteEmails langSection `json:"inviteEmails"`
+ PasswordValidation langSection `json:"passwordValidation"`
+ HelpMessages langSection `json:"helpMessages"`
+ JSON string
+}
+
+func (ls *setupLangs) getOptions(chosen string) (string, []string) {
+ opts := make([]string, len(*ls))
+ chosenLang := (*ls)[chosen].Meta.Name
+ i := 0
+ for _, lang := range *ls {
+ opts[i] = lang.Meta.Name
+ i++
+ }
+ return chosenLang, opts
+}
+
type langSection map[string]string
func (el langSection) format(field string, vals ...string) string {
diff --git a/lang/admin/de-de.json b/lang/admin/de-de.json
index 9456f99..156cdda 100644
--- a/lang/admin/de-de.json
+++ b/lang/admin/de-de.json
@@ -6,7 +6,6 @@
"invites": "Invites",
"accounts": "Konten",
"settings": "Einstellungen",
- "theme": "Thema",
"inviteDays": "Tage",
"inviteHours": "Stunden",
"inviteMinutes": "Minuten",
@@ -19,12 +18,8 @@
"create": "Erstellen",
"apply": "Anwenden",
"delete": "Löschen",
- "submit": "Absenden",
"name": "Name",
"date": "Datum",
- "username": "Benutzername",
- "password": "Passwort",
- "emailAddress": "E-Mail-Adresse",
"lastActiveTime": "Zuletzt aktiv",
"from": "Von",
"user": "Benutzer",
@@ -33,8 +28,6 @@
"commitNoun": "Commit",
"newUser": "Neuer Benutzer",
"profile": "Profil",
- "success": "Erfolg",
- "error": "Fehler",
"unknown": "Unbekannt",
"modifySettings": "Einstellungen ändern",
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 83b5dff..3fcdd6f 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -6,7 +6,6 @@
"invites": "Invites",
"accounts": "Accounts",
"settings": "Settings",
- "theme": "Theme",
"inviteDays": "Days",
"inviteHours": "Hours",
"inviteMinutes": "Minutes",
@@ -19,12 +18,8 @@
"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",
@@ -33,8 +28,6 @@
"commitNoun": "Commit",
"newUser": "New User",
"profile": "Profile",
- "success": "Success",
- "error": "Error",
"unknown": "Unknown",
"label": "Label",
"modifySettings": "Modify Settings",
diff --git a/lang/admin/fr-fr.json b/lang/admin/fr-fr.json
index 22d70fb..565e1a2 100644
--- a/lang/admin/fr-fr.json
+++ b/lang/admin/fr-fr.json
@@ -7,7 +7,6 @@
"invites": "Invite",
"accounts": "Comptes",
"settings": "Reglages",
- "theme": "Thème",
"inviteDays": "Jours",
"inviteHours": "Heures",
"inviteMinutes": "Minutes",
@@ -20,12 +19,8 @@
"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",
@@ -34,8 +29,6 @@
"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.",
diff --git a/lang/admin/nl-nl.json b/lang/admin/nl-nl.json
index 87ec5ec..6ca1ed3 100644
--- a/lang/admin/nl-nl.json
+++ b/lang/admin/nl-nl.json
@@ -6,7 +6,6 @@
"invites": "Uitnodigingen",
"accounts": "Accounts",
"settings": "Instellingen",
- "theme": "Thema",
"inviteDays": "Dagen",
"inviteHours": "Uren",
"inviteMinutes": "Minuten",
@@ -19,12 +18,8 @@
"create": "Aanmaken",
"apply": "Toepassen",
"delete": "Verwijderen",
- "submit": "Verstuur",
"name": "Naam",
"date": "Datum",
- "username": "Gebruikersnaam",
- "password": "Wachtwoord",
- "emailAddress": "E-mailadres",
"lastActiveTime": "Laatst actief",
"from": "Van",
"user": "Gebruiker",
@@ -33,8 +28,6 @@
"commitNoun": "Commit",
"newUser": "Nieuwe gebruiker",
"profile": "Profiel",
- "success": "Success",
- "error": "Fout",
"unknown": "Onbekend",
"modifySettings": "Instellingen aanpassen",
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",
diff --git a/lang/common/de-de.json b/lang/common/de-de.json
new file mode 100644
index 0000000..f14f982
--- /dev/null
+++ b/lang/common/de-de.json
@@ -0,0 +1,14 @@
+{
+ "meta": {
+ "name": "Deutsch (DE)"
+ },
+ "strings": {
+ "username": "Benutzername",
+ "password": "Passwort",
+ "emailAddress": "E-Mail-Adresse",
+ "submit": "Absenden",
+ "success": "Erfolg",
+ "error": "Fehler",
+ "theme": "Thema"
+ }
+}
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
new file mode 100644
index 0000000..21f0aa8
--- /dev/null
+++ b/lang/common/en-us.json
@@ -0,0 +1,14 @@
+{
+ "meta": {
+ "name": "English (US)"
+ },
+ "strings": {
+ "username": "Username",
+ "password": "Password",
+ "emailAddress": "Email Address",
+ "submit": "Submit",
+ "success": "Success",
+ "error": "Error",
+ "theme": "Theme"
+ }
+}
diff --git a/lang/common/fr-fr.json b/lang/common/fr-fr.json
new file mode 100644
index 0000000..7cc80b8
--- /dev/null
+++ b/lang/common/fr-fr.json
@@ -0,0 +1,15 @@
+{
+ "meta": {
+ "name": "Francais (FR)",
+ "author": "https://github.com/Killianbe"
+ },
+ "strings": {
+ "username": "Nom d'utilisateur",
+ "password": "Mot de passe",
+ "emailAddress": "Addresse Email",
+ "submit": "Soumettre",
+ "success": "Succès",
+ "error": "Erreur",
+ "theme": "Thème"
+ }
+}
diff --git a/lang/common/nl-nl.json b/lang/common/nl-nl.json
new file mode 100644
index 0000000..0d1e4d6
--- /dev/null
+++ b/lang/common/nl-nl.json
@@ -0,0 +1,14 @@
+{
+ "meta": {
+ "name": "Nederlands (NL)"
+ },
+ "strings": {
+ "username": "Gebruikersnaam",
+ "password": "Wachtwoord",
+ "emailAddress": "E-mailadres",
+ "submit": "Verstuur",
+ "success": "Success",
+ "error": "Fout",
+ "theme": "Thema"
+ }
+}
diff --git a/lang/setup/en-us.json b/lang/setup/en-us.json
new file mode 100644
index 0000000..a9dd4ff
--- /dev/null
+++ b/lang/setup/en-us.json
@@ -0,0 +1,129 @@
+{
+ "meta": {
+ "name": "English (US)"
+ },
+ "strings": {
+ "pageTitle": "Setup - jfa-go",
+ "next": "Next",
+ "back": "Back",
+ "optional": "Optional",
+ "serverType": "Server Type",
+ "disabled": "Disabled",
+ "enabled": "Enabled",
+ "port": "Port",
+ "message": "Message",
+ "serverAddress": "Server Address",
+ "emailSubject": "Email Subject",
+ "URL": "URL",
+ "apiKey": "API Key"
+ },
+ "startPage": {
+ "welcome": "Welcome!",
+ "pressStart": "You'll need to do a few things to set up jfa-go. Press start to get continue.",
+ "httpsNotice": "Make sure you're accessing this page via HTTPS or on a private network.",
+ "start": "Start"
+ },
+ "endPage": {
+ "finished": "Finished!",
+ "restartMessage": "There are more settings you can configure on the admin page. Click below to restart, then refresh the page.",
+ "refreshPage": "Refresh"
+ },
+ "language": {
+ "title": "Language",
+ "description": "Community translations are available for most parts of jfa-go. You can choose the default languages below, but users can still change it if they wish. If you want to help translate, sign up to {n} to start contributing!",
+ "defaultAdminLang": "Default admin language",
+ "defaultFormLang": "Default account creation language",
+ "defaultEmailLang": "Default email language"
+ },
+ "general": {
+ "title": "General",
+ "listenAddress": "Listen Address",
+ "urlBase": "URL Base",
+ "urlBaseNotice": "Only needed if using a reverse proxy on a subdomain (e.g 'jellyf.in/accounts').",
+ "lightTheme": "Light",
+ "darkTheme": "Dark",
+ "useHTTPS": "Use HTTPS",
+ "httpsPort": "HTTPS Port",
+ "useHTTPSNotice": "Only recommended if you aren't using a reverse proxy.",
+ "pathToCertificate": "Path to certificate",
+ "pathToKeyFile": "Path to key file"
+ },
+ "login": {
+ "title": "Login",
+ "description": "To access the admin page, you need to login with a method below:",
+ "authorizeWithJellyfin": "Authorize with Jellyfin/Emby: Login details are shared with Jellyfin, which allows for multiple users.",
+ "authorizeManual": "Username and Password: Manually set the username and password.",
+ "adminOnly": "Admin users only (recommended)",
+ "emailNotice": "Your email address can be used to receive notifications."
+ },
+ "jellyfinEmby": {
+ "title": "Jellyfin/Emby",
+ "description": "An admin account is needed because the API does not allow user creation using an API key. You should create a separate account and check 'Allow this user to manage the server'. You can disable everything else. Once done, enter the login details here.",
+ "embyNotice": "Emby support is limited and does not support password resets.",
+ "internal": "Internal",
+ "external": "External",
+ "replaceJellyfin": "Server name",
+ "replaceJellyfinNotice": "If given, this will replace any occurrence of 'Jellyfin' in the app.",
+ "addressExternalNotice": "Leave blank to use the same address.",
+ "testConnection": "Test Connection"
+ },
+ "ombi": {
+ "title": "Ombi",
+ "description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup if finished, go to Settings to set a default profile for new ombi users.",
+ "apiKeyNotice": "Find this in the first tab of Ombi settings."
+ },
+ "email": {
+ "title": "Email",
+ "description": "jfa-go can send password reset PINs and various notifications through email. You can connect to an SMTP server, or use the {n} API.",
+ "method": "Sending method",
+ "useEmailAsUsername": "Use email addresses as username",
+ "useEmailAsUsernameNotice": "If enabled, new users will login to Jellyfin/Emby with their email address instead of a username.",
+ "fromAddress": "From Address",
+ "senderName": "Sender Name",
+ "dateFormat": "Date Format",
+ "dateFormatNotice": "Date follows the strftime format. For more info, visit {n}.",
+ "time24h": "24h Time",
+ "time12h": "12h Time",
+ "encryption": "Encryption",
+ "mailgunApiURL": "API URL"
+ },
+ "notifications": {
+ "title": "Notifications",
+ "description": "If enabled, you can choose (per invite) to receive an email when an invite expires, or a user is created. If you didn't choose the Jellyfin login method, make sure you provided your email address."
+ },
+ "welcomeEmails": {
+ "title": "Welcome emails",
+ "description": "If enabled, an email will be sent to new users with the Jellyfin/Emby URL and their username."
+ },
+ "inviteEmails": {
+ "title": "Invite Emails",
+ "description": "If enabled, you can send invites directly to a user's email address. Because you might be using a reverse proxy, you need to provide the URL invites are accessed from. Write your URL Base, and append '/invite'."
+ },
+ "passwordResets": {
+ "title": "Password Resets",
+ "description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user.",
+ "pathToJellyfin": "Path to Jellyfin configuration directory",
+ "pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '
/passwordreset-*.json' will appear."
+ },
+ "passwordValidation": {
+ "title": "Password Validation",
+ "description": "If enabled, a set of password requirements will show on the account creation page, such as minimum length, uppercase/lowercase characters, etc.",
+ "length": "Length",
+ "uppercase": "Uppercase characters",
+ "lowercase": "Lowercase characters",
+ "numbers": "Numbers",
+ "special": "Special characters (%, *, etc.)"
+ },
+ "helpMessages": {
+ "title": "Help Messages",
+ "description": "These messages will display in the account creation page and in some emails.",
+ "contactMessage": "Contact Message",
+ "contactMessageNotice": "Displays at the bottom of all pages except admin.",
+ "helpMessage": "Help Message",
+ "helpMessageNotice": "Displays on the account creation page.",
+ "successMessage": "Success Message",
+ "successMessageNotice": "Displays when a user creates their account.",
+ "emailMessage": "Email Message",
+ "emailMessageNotice": "Displays at the bottom of emails."
+ }
+}
diff --git a/main.go b/main.go
index 4525f44..a305c0b 100644
--- a/main.go
+++ b/main.go
@@ -329,6 +329,15 @@ func start(asDaemon, firstCall bool) {
}()
}
+ app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common")
+ 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)
+ }
+
if !firstRun {
app.host = app.config.Section("ui").Key("host").String()
if app.config.Section("advanced").Key("tls").MustBool(false) {
@@ -516,13 +525,6 @@ 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)
- }
// Since email depends on language, the email reload in loadConfig won't work first time.
app.email = NewEmailer(app)
@@ -559,6 +561,11 @@ func start(asDaemon, firstCall bool) {
} else {
debugMode = false
address = "0.0.0.0:8056"
+ app.storage.lang.SetupPath = filepath.Join(app.localPath, "lang", "setup")
+ err := app.storage.loadLangSetup()
+ if err != nil {
+ app.info.Fatalf("Failed to load language files: %+v\n", err)
+ }
}
app.info.Println("Loading routes")
if debugMode {
@@ -578,12 +585,12 @@ func start(asDaemon, firstCall bool) {
app.debug.Println("Loading pprof")
pprof.Register(router)
}
+ router.GET("/lang/:page", app.GetLanguages)
if !firstRun {
router.GET("/", app.AdminPage)
router.GET("/accounts", app.AdminPage)
router.GET("/settings", app.AdminPage)
router.GET("/lang/:page/:file", app.ServeLang)
- router.GET("/lang/:page", app.GetLanguages)
router.GET("/token/login", app.getTokenLogin)
router.GET("/token/refresh", app.getTokenRefresh)
router.POST("/newUser", app.NewUser)
@@ -618,9 +625,7 @@ func start(asDaemon, firstCall bool) {
}
app.info.Printf("Starting router @ %s", address)
} else {
- router.GET("/", func(gc *gin.Context) {
- gc.HTML(200, "setup.html", gin.H{})
- })
+ router.GET("/", app.ServeSetup)
router.POST("/jellyfin/test", app.TestJF)
router.POST("/config", app.ModifyConfig)
app.info.Printf("Loading setup @ %s", address)
diff --git a/setup.go b/setup.go
index 4bb1ab3..083669e 100644
--- a/setup.go
+++ b/setup.go
@@ -1,21 +1,66 @@
package main
import (
+ "encoding/json"
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/common"
"github.com/hrfee/jfa-go/mediabrowser"
)
+func (app *appContext) ServeSetup(gc *gin.Context) {
+ lang := gc.Query("lang")
+ if lang == "" {
+ lang = "en-us"
+ } else if _, ok := app.storage.lang.Admin[lang]; !ok {
+ lang = "en-us"
+ }
+ emailLang := lang
+ if _, ok := app.storage.lang.Email[lang]; !ok {
+ emailLang = "en-us"
+ }
+
+ messages := map[string]map[string]string{
+ "ui": {
+ "contact_message": app.config.Section("ui").Key("contact_message").String(),
+ "help_message": app.config.Section("ui").Key("help_message").String(),
+ "success_message": app.config.Section("ui").Key("success_message").String(),
+ },
+ "email": {
+ "message": app.config.Section("email").Key("message").String(),
+ },
+ }
+ msg, err := json.Marshal(messages)
+ if err != nil {
+ respond(500, "Failed to fetch default values", gc)
+ return
+ }
+ gc.HTML(200, "setup.html", gin.H{
+ "lang": app.storage.lang.Setup[lang],
+ "emailLang": app.storage.lang.Email[emailLang],
+ "language": app.storage.lang.Setup[lang].JSON,
+ "messages": string(msg),
+ })
+}
+
type testReq struct {
- Host string `json:"jfHost"`
- Username string `json:"jfUser"`
- Password string `json:"jfPassword"`
+ ServerType string `json:"type"`
+ Server string `json:"server"`
+ Username string `json:"username"`
+ Password string `json:"password"`
}
func (app *appContext) TestJF(gc *gin.Context) {
var req testReq
gc.BindJSON(&req)
- tempjf, _ := mediabrowser.NewServer(mediabrowser.JellyfinServer, req.Host, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Host, true), 30)
+ serverType := mediabrowser.JellyfinServer
+ if req.ServerType == "emby" {
+ serverType = mediabrowser.EmbyServer
+ }
+ tempjf, _ := mediabrowser.NewServer(serverType, req.Server, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Server, true), 30)
_, status, err := tempjf.Authenticate(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
app.info.Printf("Auth failed with code %d (%s)", status, err)
@@ -24,3 +69,60 @@ func (app *appContext) TestJF(gc *gin.Context) {
}
gc.JSON(200, map[string]bool{"success": true})
}
+
+func (st *Storage) loadLangSetup() error {
+ st.lang.Setup = map[string]setupLang{}
+ var english setupLang
+ load := func(fname string) error {
+ index := strings.TrimSuffix(fname, filepath.Ext(fname))
+ lang := setupLang{}
+ f, err := ioutil.ReadFile(filepath.Join(st.lang.SetupPath, fname))
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(f, &lang)
+ if err != nil {
+ return err
+ }
+ st.lang.Common.patchCommon(index, &lang.Strings)
+ if fname != "en-us.json" {
+ patchLang(&english.Strings, &lang.Strings)
+ patchLang(&english.StartPage, &lang.StartPage)
+ patchLang(&english.EndPage, &lang.EndPage)
+ patchLang(&english.Language, &lang.Language)
+ patchLang(&english.Login, &lang.Login)
+ patchLang(&english.JellyfinEmby, &lang.JellyfinEmby)
+ patchLang(&english.Email, &lang.Email)
+ patchLang(&english.Notifications, &lang.Notifications)
+ patchLang(&english.PasswordResets, &lang.PasswordResets)
+ patchLang(&english.InviteEmails, &lang.InviteEmails)
+ patchLang(&english.PasswordValidation, &lang.PasswordValidation)
+ patchLang(&english.HelpMessages, &lang.HelpMessages)
+ }
+ stringSettings, err := json.Marshal(lang)
+ if err != nil {
+ return err
+ }
+ lang.JSON = string(stringSettings)
+ st.lang.Setup[index] = lang
+ return nil
+ }
+ err := load("en-us.json")
+ if err != nil {
+ return err
+ }
+ english = st.lang.Setup["en-us"]
+ files, err := ioutil.ReadDir(st.lang.SetupPath)
+ if err != nil {
+ return err
+ }
+ for _, f := range files {
+ if f.Name() != "en-us.json" {
+ err = load(f.Name())
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/storage.go b/storage.go
index 972b43c..3153417 100644
--- a/storage.go
+++ b/storage.go
@@ -55,9 +55,17 @@ type Lang struct {
Form formLangs
EmailPath string
Email emailLangs
+ CommonPath string
+ Common commonLangs
+ SetupPath string
+ Setup setupLangs
}
func (st *Storage) loadLang() (err error) {
+ err = st.loadLangCommon()
+ if err != nil {
+ return
+ }
err = st.loadLangAdmin()
if err != nil {
return
@@ -70,6 +78,20 @@ func (st *Storage) loadLang() (err error) {
return
}
+func (common *commonLangs) patchCommon(lang string, other *langSection) {
+ if *other == nil {
+ *other = langSection{}
+ }
+ if _, ok := (*common)[lang]; !ok {
+ lang = "en-us"
+ }
+ for n, ev := range (*common)[lang].Strings {
+ if v, ok := (*other)[n]; !ok || v == "" {
+ (*other)[n] = ev
+ }
+ }
+}
+
// If a given language has missing values, fill it in with the english value.
func patchLang(english, other *langSection) {
if *other == nil {
@@ -97,6 +119,49 @@ func patchQuantityStrings(english, other *map[string]quantityString) {
}
}
+func (st *Storage) loadLangCommon() error {
+ st.lang.Common = map[string]commonLang{}
+ var english commonLang
+ load := func(fname string) error {
+ index := strings.TrimSuffix(fname, filepath.Ext(fname))
+ lang := commonLang{}
+ f, err := ioutil.ReadFile(filepath.Join(st.lang.CommonPath, fname))
+ if err != nil {
+ return err
+ }
+ if substituteStrings != "" {
+ f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
+ }
+ err = json.Unmarshal(f, &lang)
+ if err != nil {
+ return err
+ }
+ if fname != "en-us.json" {
+ patchLang(&english.Strings, &lang.Strings)
+ }
+ st.lang.Common[index] = lang
+ return nil
+ }
+ err := load("en-us.json")
+ if err != nil {
+ return err
+ }
+ english = st.lang.Common["en-us"]
+ files, err := ioutil.ReadDir(st.lang.CommonPath)
+ if err != nil {
+ return err
+ }
+ for _, f := range files {
+ if f.Name() != "en-us.json" {
+ err = load(f.Name())
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
func (st *Storage) loadLangAdmin() error {
st.lang.Admin = map[string]adminLang{}
var english adminLang
@@ -114,6 +179,7 @@ func (st *Storage) loadLangAdmin() error {
if err != nil {
return err
}
+ st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications)
@@ -164,6 +230,7 @@ func (st *Storage) loadLangForm() error {
if err != nil {
return err
}
+ st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications)
diff --git a/ts/modules/common.ts b/ts/modules/common.ts
index b6dc008..9a07dfd 100644
--- a/ts/modules/common.ts
+++ b/ts/modules/common.ts
@@ -51,7 +51,8 @@ export const rmAttr = (el: HTMLElement, attr: string): void => {
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
let req = new XMLHttpRequest();
- req.open("GET", window.URLBase + url, true);
+ if (window.URLBase) { url = window.URLBase + url; }
+ req.open("GET", url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
diff --git a/ts/setup.ts b/ts/setup.ts
index daa4a17..b7b861b 100644
--- a/ts/setup.ts
+++ b/ts/setup.ts
@@ -1,260 +1,479 @@
-// Lord forgive me for this mess, i'll fix it one day i swear
+import { _get, _post, toggleLoader } from "./modules/common.js";
+import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
-document.getElementById("page-1").scrollIntoView({
- behavior: "auto",
- block: "center",
- inline: "center"
-});
+interface sWindow extends Window {
+ messages: {};
+}
-const checkAuthRadio = () => {
- if ((document.getElementById('manualAuthRadio') as HTMLInputElement).checked) {
- document.getElementById('adminOnlyArea').style.display = 'none';
- document.getElementById('manualAuthArea').style.display = '';
- } else {
- document.getElementById('manualAuthArea').style.display = 'none';
- document.getElementById('adminOnlyArea').style.display = '';
- }
-};
+declare var window: sWindow;
+window.URLBase = "";
-for (let radio of ['manualAuthRadio', 'jfAuthRadio']) {
- document.getElementById(radio).addEventListener('change', checkAuthRadio);
-};
+const get = (id: string): HTMLElement => document.getElementById(id);
+const text = (id: string, val: string) => { document.getElementById(id).textContent = val; };
+const html = (id: string, val: string) => { document.getElementById(id).innerHTML = val; };
+
+interface boolEvent extends Event {
+ detail: boolean;
+}
-const checkEmailRadio = () => {
- (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-5';
- (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-7';
- if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) {
- document.getElementById('emailCommonArea').style.display = '';
- document.getElementById('emailSMTPArea').style.display = '';
- document.getElementById('emailMailgunArea').style.display = 'none';
- (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true;
- } else if ((document.getElementById('emailMailgunRadio') as HTMLInputElement).checked) {
- document.getElementById('emailCommonArea').style.display = '';
- document.getElementById('emailSMTPArea').style.display = 'none';
- document.getElementById('emailMailgunArea').style.display = '';
- (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true;
- } else if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) {
- document.getElementById('emailCommonArea').style.display = 'none';
- document.getElementById('emailSMTPArea').style.display = 'none';
- document.getElementById('emailMailgunArea').style.display = 'none';
- (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-8';
- (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-4';
- (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = false;
+class Input {
+ private _el: HTMLInputElement;
+ get value(): string { return ""+this._el.value; }
+ set value(v: string) { this._el.value = v; }
+ // Nothing depends on input, but we add an empty broadcast function so we can just loop over all settings to fix dependents on start.
+ broadcast = () => {}
+ constructor(el: HTMLElement, placeholder?: any, value?: any, depends?: string, dependsTrue?: boolean, section?: string) {
+ this._el = el as HTMLInputElement;
+ if (placeholder) { this._el.placeholder = placeholder; }
+ if (value) { this.value = value; }
+ if (depends) {
+ document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
+ let el = this._el as HTMLElement;
+ if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
+ if (event.detail !== dependsTrue) {
+ el.classList.add("unfocused");
+ } else {
+ el.classList.remove("unfocused");
+ }
+ });
+ }
}
-};
+}
-for (let radio of ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']) {
- document.getElementById(radio).addEventListener('change', checkEmailRadio);
+class Checkbox {
+ private _el: HTMLInputElement;
+ get value(): string { return this._el.checked ? "true" : "false"; }
+ set value(v: string) { this._el.checked = (v == "true") ? true : false; }
+
+ private _section: string;
+ private _setting: string;
+ broadcast = () => {
+ if (this._section && this._setting) {
+ const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this._el.checked })
+ document.dispatchEvent(ev);
+ }
+ }
+ constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
+ this._el = el as HTMLInputElement;
+ if (section && setting) {
+ this._section = section;
+ this._setting = setting;
+ this._el.onchange = this.broadcast;
+ }
+ if (depends) {
+ document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
+ let el = this._el as HTMLElement;
+ if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
+ if (event.detail !== dependsTrue) {
+ el.classList.add("unfocused");
+ } else {
+ el.classList.remove("unfocused");
+ }
+ });
+ }
+ }
}
-const checkSSL = () => {
- var label = document.getElementById('emailSSL_TLSLabel');
- if ((document.getElementById('emailSSL_TLS') as HTMLInputElement).checked) {
- label.textContent = 'Use SSL/TLS';
- } else {
- label.textContent = 'Use STARTTLS';
+class BoolRadios {
+ private _els: NodeListOf;
+ get value(): string { return this._els[0].checked ? "true" : "false" }
+ set value(v: string) {
+ const bool = (v == "true") ? true : false;
+ this._els[0].checked = bool;
+ this._els[1].checked = !bool;
}
-};
-document.getElementById('emailSSL_TLS').addEventListener('change', checkSSL);
+
+ private _section: string;
+ private _setting: string;
+ broadcast = () => {
+ if (this._section && this._setting) {
+ const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this._els[0].checked })
+ document.dispatchEvent(ev);
+ }
+ }
+ constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
+ this._els = document.getElementsByName(name) as NodeListOf;
+ if (section && setting) {
+ this._section = section;
+ this._setting = setting;
+ this._els[0].onchange = this.broadcast;
+ this._els[1].onchange = this.broadcast;
+ }
+ if (depends) {
+ document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
+ if (event.detail !== dependsTrue) {
+ if (this._els[0].parentElement.tagName == "LABEL") {
+ this._els[0].parentElement.classList.add("unfocused");
+ }
+ if (this._els[1].parentElement.tagName == "LABEL") {
+ this._els[1].parentElement.classList.add("unfocused");
+ }
+ } else {
+ if (this._els[0].parentElement.tagName == "LABEL") {
+ this._els[0].parentElement.classList.remove("unfocused");
+ }
+ if (this._els[1].parentElement.tagName == "LABEL") {
+ this._els[1].parentElement.classList.remove("unfocused");
+ }
+ }
+ });
+ }
+ }
+}
-var pwrEnabled = document.getElementById('pwrEnabled') as HTMLInputElement;
-const checkPwrEnabled = () => {
- if (pwrEnabled.checked) {
- document.getElementById('pwrArea').style.display = '';
- } else {
- document.getElementById('pwrArea').style.display = 'none';
+// class Radios {
+// private _el: HTMLInputElement;
+// get value(): string { return this._el.value; }
+// set value(v: string) { this._el.value = v; }
+// constructor(name: string, depends?: string, dependsTrue?: boolean, section?: string) {
+// this._el = document.getElementsByName(name)[0] as HTMLInputElement;
+// if (depends) {
+// document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
+// let el = this._el as HTMLElement;
+// if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
+// if (event.detail !== dependsTrue) {
+// el.classList.add("unfocused");
+// } else {
+// el.classList.remove("unfocused");
+// }
+// });
+// }
+// }
+// }
+
+class Select {
+ private _el: HTMLSelectElement;
+ get value(): string { return this._el.value; }
+ set value(v: string) { this._el.value = v; }
+ add = (val: string, label: string) => {
+ const item = document.createElement("option") as HTMLOptionElement;
+ item.value = val;
+ item.textContent = label;
+ this._el.appendChild(item);
}
-};
-pwrEnabled.addEventListener('change', checkPwrEnabled);
+ set onchange(f: () => void) {
+ this._el.addEventListener("change", f);
+ }
+
+ private _section: string;
+ private _setting: string;
+ broadcast = () => {
+ if (this._section && this._setting) {
+ const ev = new CustomEvent(`settings-${this._section}-${this._setting}`, { "detail": this.value ? true : false })
+ document.dispatchEvent(ev);
+ }
+ }
+ constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
+ this._el = el as HTMLSelectElement;
+ if (section && setting) {
+ this._section = section;
+ this._setting = setting;
+ this._el.addEventListener("change", this.broadcast);
+ }
+ if (depends) {
+ document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
+ let el = this._el as HTMLElement;
+ if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
+ if (event.detail !== dependsTrue) {
+ el.classList.add("unfocused");
+ } else {
+ el.classList.remove("unfocused");
+ }
+ });
+ }
+ }
+}
-var invEnabled = document.getElementById("invEnabled") as HTMLInputElement;
-const checkInvEnabled = () => {
- if (invEnabled.checked) {
- document.getElementById('invArea').style.display = '';
- } else {
- document.getElementById('invArea').style.display = 'none';
+class LangSelect extends Select {
+ constructor(page: string, el: HTMLElement) {
+ super(el);
+ _get("/lang/" + page, null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4 && req.status == 200) {
+ for (let code in req.response) {
+ this.add(code, req.response[code]);
+ }
+ this.value = "en-us";
+ }
+ });
}
-};
-invEnabled.addEventListener('change', checkInvEnabled);
+}
-var valEnabled = document.getElementById("valEnabled") as HTMLInputElement;
-const checkValEnabled = () => {
- const valArea = document.getElementById("valArea");
- if (valEnabled.checked) {
- valArea.style.display = '';
- } else {
- valArea.style.display = 'none';
+window.lang = new lang(window.langFile as LangFile);
+html("language-description", window.lang.var("language", "description", `Weblate `));
+html("email-description", window.lang.var("email", "description", `Mailgun `));
+
+const settings = {
+ "jellyfin": {
+ "type": new Select(get("jellyfin-type")),
+ "server": new Input(get("jellyfin-server")),
+ "public_server": new Input(get("jellyfin-public_server")),
+ "username": new Input(get("jellyfin-username")),
+ "password": new Input(get("jellyfin-password")),
+ "substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings"))
+ },
+ "ui": {
+ "host": new Input(get("ui-host")),
+ "port": new Input(get("ui-port")),
+ "url_base": new Input(get("ui-url_base")),
+ "theme": new Select(get("ui-theme")),
+ "language-form": new LangSelect("form", get("ui-language-form")),
+ "language-admin": new LangSelect("admin", get("ui-language-admin")),
+ "jellyfin_login": new BoolRadios("ui-jellyfin_login", "", false, "ui", "jellyfin_login"),
+ "admin_only": new Checkbox(get("ui-admin_only"), "jellyfin_login", true, "ui"),
+ "username": new Input(get("ui-username"), "", "", "jellyfin_login", false, "ui"),
+ "password": new Input(get("ui-password"), "", "", "jellyfin_login", false, "ui"),
+ "email": new Input(get("ui-email"), "", "", "jellyfin_login", false, "ui"),
+ "contact_message": new Input(get("ui-contact_message"), window.messages["ui"]["contact_message"]),
+ "help_message": new Input(get("ui-help_message"), window.messages["ui"]["help_message"]),
+ "success_message": new Input(get("ui-success_message"), window.messages["ui"]["success_message"])
+ },
+ "password_validation": {
+ "enabled": new Checkbox(get("password_validation-enabled"), "", false, "password_validation", "enabled"),
+ "min_length": new Input(get("password_validation-min_length"), "", 8, "enabled", true, "password_validation"),
+ "upper": new Input(get("password_validation-upper"), "", 1, "enabled", true, "password_validation"),
+ "lower": new Input(get("password_validation-lower"), "", 0, "enabled", true, "password_validation"),
+ "number": new Input(get("password_validation-number"), "", 1, "enabled", true, "password_validation"),
+ "special": new Input(get("password_validation-special"), "", 0, "enabled", true, "password_validation")
+ },
+ "email": {
+ "language": new LangSelect("email", get("email-language")),
+ "no_username": new Checkbox(get("email-no_username"), "method", true, "email"),
+ "use_24h": new BoolRadios("email-24h", "method", true, "email"),
+ "date_format": new Input(get("email-date_format"), "", "%d/%m/%y", "method", true, "email"),
+ "message": new Input(get("email-message"), window.messages["email"]["message"], "", "method", true, "email"),
+ "method": new Select(get("email-method"), "", false, "email", "method"),
+ "address": new Input(get("email-address"), "jellyfin@jellyf.in", "", "method", true, "email"),
+ "from": new Input(get("email-from"), "", "Jellyfin", "method", true, "email")
+ },
+ "password_resets": {
+ "enabled": new Checkbox(get("password_resets-enabled"), "", false, "password_resets", "enabled"),
+ "watch_directory": new Input(get("password_resets-watch_directory"), "", "", "enabled", true, "password_resets"),
+ "subject": new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets")
+ },
+ "notifications": {
+ "enabled": new Checkbox(get("notifications-enabled"))
+ },
+ "welcome_email": {
+ "enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"),
+ "subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email")
+ },
+ "invite_emails": {
+ "enabled": new Checkbox(get("invite_emails-enabled"), "", false, "invite_emails", "enabled"),
+ "subject": new Input(get("invite_emails-subject"), "", "", "enabled", true, "invite_emails"),
+ "url_base": new Input(get("invite_emails-url_base"), "", "", "enabled", true, "invite_emails")
+ },
+ "mailgun": {
+ "api_url": new Input(get("mailgun-api_url")),
+ "api_key": new Input(get("mailgun-api_key"))
+ },
+ "smtp": {
+ "username": new Input(get("smtp-username")),
+ "encryption": new Select(get("smtp-encryption")),
+ "server": new Input(get("smtp-server")),
+ "port": new Input(get("smtp-port")),
+ "password": new Input(get("smtp-password"))
+ },
+ "ombi": {
+ "enabled": new Checkbox(get("ombi-enabled"), "", false, "ombi", "enabled"),
+ "server": new Input(get("ombi-server"), "", "", "enabled", true, "ombi"),
+ "api_key": new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi")
+ },
+ "advanced": {
+ "tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
+ "tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
+ "tls_cert": new Input(get("advanced-tls_cert"), "", "", "tls", true, "advanced"),
+ "tls_key": new Input(get("advanced-tls_key"), "", "", "tls", true, "advanced")
}
};
-valEnabled.addEventListener('change', checkValEnabled);
-checkValEnabled();
-checkInvEnabled();
-checkSSL();
-checkAuthRadio();
-checkEmailRadio();
-checkPwrEnabled();
+(() => {
+ const checkTheme = () => {
+ if (settings["ui"]["theme"].value.includes("Dark")) {
+ document.documentElement.classList.add("dark-theme");
+ document.documentElement.classList.remove("light-theme");
+ } else {
+ document.documentElement.classList.add("light-theme");
+ document.documentElement.classList.remove("dark-theme");
+ }
+ };
+ settings["ui"]["theme"].onchange = checkTheme;
+ checkTheme();
+})();
-var jfValid = false
-document.getElementById('jfTestButton').onclick = () => {
- let testButton = document.getElementById('jfTestButton') as HTMLInputElement;
- let nextButton = document.getElementById('jfNextButton') as HTMLAnchorElement;
- let jfData = {};
- jfData['jfHost'] = (document.getElementById('jfHost') as HTMLInputElement).value;
- jfData['jfUser'] = (document.getElementById('jfUser') as HTMLInputElement).value;
- jfData['jfPassword'] = (document.getElementById('jfPassword') as HTMLInputElement).value;
- let valid = true;
- for (let val in jfData) {
- if (jfData[val] == "") {
- valid = false;
+
+const restartButton = document.getElementById("restart") as HTMLSpanElement;
+const serialize = () => {
+ toggleLoader(restartButton);
+ let config = {};
+ for (let section in settings) {
+ config[section] = {};
+ for (let setting in settings[section]) {
+ if (settings[section][setting].value) {
+ config[section][setting] = settings[section][setting].value;
+ }
}
}
- if (!valid) {
- if (!testButton.classList.contains('btn-danger')) {
- testButton.classList.add('btn-danger');
- testButton.textContent = 'Fill out fields above.';
- setTimeout(function() {
- if (testButton.classList.contains('btn-danger')) {
- testButton.classList.remove('btn-danger');
- testButton.textContent = 'Test';
- }
- }, 2000);
+ config["restart-program"] = true;
+ _post("/config", config, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ toggleLoader(restartButton);
+ restartButton.parentElement.querySelector("span.back").classList.add("unfocused");
+ restartButton.classList.add("unfocused");
+ const refresh = document.getElementById("refresh") as HTMLSpanElement;
+ refresh.classList.remove("unfocused");
+ refresh.onclick = () => {
+ let host = window.location.href.split("#")[0].split("?")[0] + settings["ui"]["url_base"].value;
+ window.location.href = host;
+ };
+ }
+ }, true, () => {});
+}
+restartButton.onclick = serialize;
+
+const relatedToEmail = Array.from(document.getElementsByClassName("related-to-email"));
+const emailMethodChange = () => {
+ const val = settings["email"]["method"].value;
+ const smtp = document.getElementById("email-smtp");
+ const mailgun = document.getElementById("email-mailgun");
+ if (val == "smtp") {
+ smtp.classList.remove("unfocused");
+ mailgun.classList.add("unfocused");
+ for (let el of relatedToEmail) {
+ el.classList.remove("hidden");
+ }
+ } else if (val == "mailgun") {
+ mailgun.classList.remove("unfocused");
+ smtp.classList.add("unfocused");
+ for (let el of relatedToEmail) {
+ el.classList.remove("hidden");
}
} else {
- testButton.disabled = true;
- testButton.innerHTML =
- ' ' +
- 'Testing...';
- nextButton.classList.add('disabled');
- nextButton.setAttribute('aria-disabled', 'true');
- var req = new XMLHttpRequest();
- req.open("POST", "/jellyfin/test", true);
- req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
- req.responseType = 'json';
- req.onreadystatechange = function() {
- if (this.readyState == 4) {
- testButton.disabled = false;
- testButton.className = '';
- if (this.response['success'] == true) {
- testButton.classList.add('btn', 'btn-success');
- testButton.textContent = 'Success';
- nextButton.classList.remove('disabled');
- nextButton.setAttribute('aria-disabled', 'false');
- } else {
- testButton.classList.add('btn', 'btn-danger');
- testButton.textContent = 'Failed';
- };
- };
- };
- req.send(JSON.stringify(jfData));
+ mailgun.classList.add("unfocused");
+ smtp.classList.add("unfocused");
+ for (let el of relatedToEmail) {
+ el.classList.add("hidden");
+ }
}
};
+settings["email"]["method"].onchange = emailMethodChange;
+emailMethodChange();
-document.getElementById('submitButton').onclick = () => {
- const submitButton = document.getElementById('submitButton') as HTMLInputElement;
- submitButton.disabled = true;
- submitButton.innerHTML =`
-
- Submitting...
- `;
- let config = {};
- config['jellyfin'] = {};
- config['ui'] = {};
- config['password_validation'] = {};
- config['email'] = {};
- config['password_resets'] = {};
- config['invite_emails'] = {};
- config['mailgun'] = {};
- config['smtp'] = {};
- config['notifications'] = {};
- // Page 2: Auth
- if ((document.getElementById('jfAuthRadio') as HTMLInputElement).checked) {
- config['ui']['jellyfin_login'] = 'true';
- config['ui']['admin_only'] = ""+(document.getElementById("jfAuthAdminOnly") as HTMLInputElement).checked;
- } else {
- config['ui']['username'] = (document.getElementById('manualAuthUsername') as HTMLInputElement).value;
- config['ui']['password'] = (document.getElementById('manualAuthPassword') as HTMLInputElement).value;
- config['ui']['email'] = (document.getElementById('manualAuthEmail') as HTMLInputElement).value;
- };
- // Page 3: Connect to jellyfin
- config['jellyfin']['server'] = (document.getElementById('jfHost') as HTMLInputElement).value;
- let publicAddress = (document.getElementById('jfPublicHost') as HTMLInputElement).value;
- if (publicAddress != "") {
- config['jellyfin']['public_server'] = publicAddress;
+(window as any).settings = settings;
+
+for (let section in settings) {
+ for (let setting in settings[section]) {
+ settings[section][setting].broadcast();
}
- config['jellyfin']['username'] = (document.getElementById('jfUser') as HTMLInputElement).value;
- config['jellyfin']['password'] = (document.getElementById('jfPassword') as HTMLInputElement).value;
- // Page 4: Email (Page 5, 6, 7 are only used if this is enabled)
- if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) {
- config['password_resets']['enabled'] = 'false';
- config['invite_emails']['enabled'] = 'false';
- config['notifications']['enabled'] = 'false';
- } else {
- if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) {
- config['smtp']['encryption'] = (document.getElementById('emailSSL_TLS') as HTMLInputElement).checked ? "ssl_tls" : "starttls";
- config['email']['method'] = 'smtp';
- config['smtp']['server'] = (document.getElementById('emailSMTPServer') as HTMLInputElement).value;
- config['smtp']['port'] = (document.getElementById('emailSMTPPort') as HTMLInputElement).value;
- config['smtp']['password'] = (document.getElementById('emailSMTPPassword') as HTMLInputElement).value;
- config['email']['address'] = (document.getElementById('emailSMTPAddress') as HTMLInputElement).value;
- } else {
- config['email']['method'] = 'mailgun';
- config['mailgun']['api_url'] = (document.getElementById('emailMailgunURL') as HTMLInputElement).value;
- config['mailgun']['api_key'] = (document.getElementById('emailMailgunKey') as HTMLInputElement).value;
- config['email']['address'] = (document.getElementById('emailMailgunAddress') as HTMLInputElement).value;
- };
- config['notifications']['enabled'] = ""+(document.getElementById('notificationsEnabled') as HTMLInputElement).checked;
- // Page 5: Email formatting
- config['email']['from'] = (document.getElementById('emailSender') as HTMLInputElement).value;
- config['email']['date_format'] = (document.getElementById('emailDateFormat') as HTMLInputElement).value;
- config['email']['use_24h'] = ""+(document.getElementById('email24hTimeRadio') as HTMLInputElement).checked;
- config['email']['message'] = (document.getElementById('emailMessage') as HTMLInputElement).value;
- // Page 6: Password Resets
- if (pwrEnabled.checked) {
- config['password_resets']['enabled'] = 'true';
- config['password_resets']['watch_directory'] = (document.getElementById('pwrJfPath') as HTMLInputElement).value;
- config['password_resets']['subject'] = (document.getElementById('pwrSubject') as HTMLInputElement).value;
- } else {
- config['password_resets']['enabled'] = 'false';
- };
- // Page 7: Invite Emails
- if ((document.getElementById('invEnabled') as HTMLInputElement).checked) {
- config['invite_emails']['enabled'] = 'true';
- config['invite_emails']['url_base'] = (document.getElementById('invURLBase') as HTMLInputElement).value;
- config['invite_emails']['subject'] = (document.getElementById('invSubject') as HTMLInputElement).value;
+}
+
+const pageNames: string[][] = [];
+
+window.history.replaceState("welcome", "Setup - jfa-go");
+
+const changePage = (title: string, pageTitle: string) => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const lang = urlParams.get("lang");
+ let page = "/#" + title;
+ if (lang) { page += "?lang=" + lang; }
+ window.history.pushState(title || "welcome", pageTitle, page);
+};
+const cards = Array.from(document.getElementById("page-container").getElementsByClassName("card")) as Array;
+(window as any).cards = cards;
+window.onpopstate = (event: PopStateEvent) => {
+ if (event.state === "welcome") {
+ cards[0].classList.remove("unfocused");
+ for (let i = 1; i < cards.length; i++) { cards[i].classList.add("unfocused"); }
+ return;
+ }
+ for (let i = 0; i < cards.length; i++) {
+ if (event.state === pageNames[i][0]) {
+ cards[i].classList.remove("unfocused");
} else {
- config['invite_emails']['enabled'] = 'false';
- };
- };
- // Page 8: Password Validation
- if ((document.getElementById('valEnabled') as HTMLInputElement).checked) {
- config['password_validation']['enabled'] = 'true';
- config['password_validation']['min_length'] = (document.getElementById('valLength') as HTMLInputElement).value;
- config['password_validation']['upper'] = (document.getElementById('valUpper') as HTMLInputElement).value;
- config['password_validation']['lower'] = (document.getElementById('valLower') as HTMLInputElement).value;
- config['password_validation']['number'] = (document.getElementById('valNumber') as HTMLInputElement).value;
- config['password_validation']['special'] = (document.getElementById('valSpecial') as HTMLInputElement).value;
- } else {
- config['password_validation']['enabled'] = 'false';
- };
- // Page 9: Messages
- config['ui']['contact_message'] = (document.getElementById('msgContact') as HTMLInputElement).value;
- config['ui']['help_message'] = (document.getElementById('msgHelp') as HTMLInputElement).value;
- config['ui']['success_message'] = (document.getElementById('msgSuccess') as HTMLInputElement).value;
- // Send it
- config["restart-program"] = true;
- let req = new XMLHttpRequest();
- req.open("POST", "/config", true);
- req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
- req.responseType = 'json';
- req.onreadystatechange = function() {
- if (this.readyState == 4) {
- submitButton.disabled = false;
- submitButton.className = '';
- submitButton.classList.add('btn', 'btn-success');
- submitButton.textContent = 'Success';
+ cards[i].classList.add("unfocused");
+ }
+ }
+};
+
+(() => {
+ for (let i = 0; i < cards.length; i++) {
+ const card = cards[i];
+ const back = card.getElementsByClassName("back")[0] as HTMLSpanElement;
+ const next = card.getElementsByClassName("next")[0] as HTMLSpanElement;
+ console.log(cards[i]);
+ const titleEl = cards[i].querySelector("span.heading") as HTMLElement;
+ let title = titleEl.textContent.replace("/", "_").replace(" ", "-");
+ if (titleEl.classList.contains("welcome")) {
+ title = "";
+ }
+ let pageTitle = titleEl.textContent + " - jfa-go";
+ pageNames.push([title, pageTitle]);
+ if (back) { back.addEventListener("click", () => {
+ let found = false;
+ for (let ind = cards.length - 1; ind >= 0; ind--) {
+ cards[ind].classList.add("unfocused");
+ if (ind < i && !(cards[ind].classList.contains("hidden")) && !found) {
+ cards[ind].classList.remove("unfocused");
+ changePage(pageNames[ind][0], pageNames[ind][1]);
+ found = true;
+ }
+ }
+ window.scrollTo(0, 0);
+ }); }
+ if (next) { next.addEventListener("click", () => {
+ let found = false;
+ for (let ind = 0; ind < cards.length; ind++) {
+ cards[ind].classList.add("unfocused");
+ if (ind > i && !(cards[ind].classList.contains("hidden")) && !found) {
+ cards[ind].classList.remove("unfocused");
+ changePage(pageNames[ind][0], pageNames[ind][1]);
+ found = true;
+ }
+ }
+ window.scrollTo(0, 0);
+ }); }
+ }
+})();
+
+(() => {
+ const button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement;
+ const ogText = button.textContent;
+ const nextButton = button.parentElement.querySelector("span.next") as HTMLSpanElement;
+ button.onclick = () => {
+ toggleLoader(button);
+ let send = {
+ "type": settings["jellyfin"]["type"].value,
+ "server": settings["jellyfin"]["server"].value,
+ "username": settings["jellyfin"]["username"].value,
+ "password": settings["jellyfin"]["password"].value
};
+ _post("/jellyfin/test", send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ toggleLoader(button);
+ const success = req.response["success"] as boolean;
+ if (success) {
+ nextButton.removeAttribute("disabled");
+ button.textContent = window.lang.strings("success");
+ button.classList.add("~positive");
+ button.classList.remove("~urge");
+ setTimeout(() => {
+ button.textContent = ogText;
+ button.classList.add("~urge");
+ button.classList.remove("~positive");
+ }, 5000);
+ } else {
+ nextButton.setAttribute("disabled", "");
+ button.textContent = window.lang.strings("error");
+ button.classList.add("~critical");
+ button.classList.remove("~urge");
+ setTimeout(() => {
+ button.textContent = ogText;
+ button.classList.add("~urge");
+ button.classList.remove("~critical");
+ }, 5000);
+ }
+ }
+ }, true, () => {});
};
- req.send(JSON.stringify(config));
-};
+})();
+loadLangSelector("setup");