diff --git a/api.go b/api.go index 04bdcd5..ee19501 100644 --- a/api.go +++ b/api.go @@ -1191,21 +1191,21 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { for setting, value := range settings.(map[string]interface{}) { if section == "ui" && setting == "language-form" { for key, lang := range app.storage.lang.Form { - if lang.Meta.Name == value.(string) { + if lang.Meta.Name == value.(string) || value.(string) == key { tempConfig.Section("ui").Key("language-form").SetValue(key) break } } } else if section == "ui" && setting == "language-admin" { for key, lang := range app.storage.lang.Admin { - if lang.Meta.Name == value.(string) { + if lang.Meta.Name == value.(string) || value.(string) == key { tempConfig.Section("ui").Key("language-admin").SetValue(key) break } } } else if section == "email" && setting == "language" { for key, lang := range app.storage.lang.Email { - if lang.Meta.Name == value.(string) { + if lang.Meta.Name == value.(string) || value.(string) == key { tempConfig.Section("email").Key("language").SetValue(key) break } @@ -1288,6 +1288,14 @@ func (app *appContext) GetLanguages(gc *gin.Context) { for key, lang := range app.storage.lang.Admin { resp[key] = lang.Meta.Name } + } else if page == "setup" { + for key, lang := range app.storage.lang.Setup { + resp[key] = lang.Meta.Name + } + } else if page == "email" { + for key, lang := range app.storage.lang.Email { + resp[key] = lang.Meta.Name + } } if len(resp) == 0 { respond(500, "Couldn't get languages", gc) diff --git a/config/config-base.json b/config/config-base.json index a33dee3..c25d7b5 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -720,7 +720,7 @@ "required": false, "requires_restart": false, "type": "text", - "value": "Your account was deleted - Jellyfin", + "value": "", "description": "Subject of account deletion emails." }, "email_html": { diff --git a/css/base.css b/css/base.css index b27afb1..c37d2b1 100644 --- a/css/base.css +++ b/css/base.css @@ -39,6 +39,22 @@ } } +.banner { + margin: calc(-1 * var(--spacing-4,1rem)); +} + +.banner.header { + margin-bottom: var(--spacing-4,1rem); +} + +.banner.footer { + margin-top: var(--spacing-4,1rem); + padding: var(--spacing-4,1rem); +} + +div.card:contains(section.banner.footer) { + padding-bottom: 0px; +} .tab-button { font-size: 2rem; @@ -256,6 +272,10 @@ sup.\~critical, .text-critical { justify-content: center; } +.middle { + align-items: center; +} + .no-lp { padding-left: 0px; } diff --git a/html/setup.html b/html/setup.html index ecffe4e..7eb7144 100644 --- a/html/setup.html +++ b/html/setup.html @@ -1,374 +1,460 @@ - + - - - - - - - - Setup - jfa-go + + {{ template "header.html" . }} + {{ .lang.Strings.pageTitle }} - -
-
+ +
+ + + + + + + +
+
-
-
-
-
-
Welcome!
-

- You'll need to do a few things to start using jfa-go. Click below to get started, or quit and edit the config file manually. -

- Get Started + +
+
+ {{ .lang.StartPage.welcome }} +
+
+

{{ .lang.StartPage.pressStart }}

+
+ +
+
+ {{ .lang.Language.title }} +

+ + + + +
+
+ {{ .lang.General.title }} +
+
+ + +

{{ .lang.General.useHTTPSNotice }}

+ + +
+
+ + + +
+ +
+
+ {{ .lang.Login.title }} +

{{ .lang.Login.description }}

+
+ + + +
+
+ + + +
+ +
+
+ {{ .lang.JellyfinEmby.title }} +

{{ .lang.JellyfinEmby.description }}

+
+
+
-
-
-
Login
-

- To access the admin page, you'll need to login. Choose how below. -

    -
  • Authorize through Jellyfin: Checks credentials with Jellyfin, allowing you to share login details and grant multiple users access.
  • -
  • Username & Password: Set your own username and password manually.
  • -
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
- - -
-
- - - Your email address is only required if you want to recieve activity notifications. -
-
-

-
- Back - Next -
-
-
-
-
-
Jellyfin
-

- jfa-go needs admin access so that it can create users, as this is currently not permitted via API tokens. - You should create a separate account for it, checking 'Allow this user to manage the server'. You can disable everything else. Once done, enter the credentials here. -

- - -
-
- - -
-
- - -
-
- - -
-
- -
- Back - Next -
-
-
-
-
-
-
Email
-

jfa-go is capable of sending a PIN code when a user tries to reset their password on Jellyfin. One can also choose to send an invite code directly to an email address. This can be done through SMTP or through Mailgun's API. -

-
- - -
-
- - -
-
- - -
-
-
-
- - - Note: SSL/TLS usually uses port 465, whereas STARTTLS usually uses 587. -
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
-
- -
-
- -
-
- -
-
-
-
Notifications
-

Enabling notifications will allow you to choose (per-invite) to recieve emails when an invite expires, or when a new user is created. If you chose to use Manual auth instead of Jellyfin auth previously, make sure you provided an email address.

-
- - - -
-
-

-
- Back - Next -
-
-
-
-
-
Email
-

Just a few more things to get your emails looking great. -

- - -
-
- - -
-
- - -
-
- - -
-
- - -
-

-
- Back - Next -
-
-
-
-
-
Password Resets
-

- When a user tries to reset their password in jellyfin, it informs them that a file has been created, named "passwordreset*.json" where * is a number. jfa-go will then read this file, and send the PIN to the user's email. Try it now, and put the folder that it informs you it put the file in below. Also, if enter a custom email subject if you don't like the default one. -

-
- - -
-
-
- - -
-
- - -
-
-
- Back - Next -
-
-
-
-
-
Invite Emails
-

- Allows you to send an invite code directly to a specified email address. - Since you'll most likely being running this behind a reverse proxy, the program has no way of knowing the address it will be accessed from. This is needed for sending emails with links. Write your URL Base with the protocol and append '/invite', e.g: -

-

-
- - -
-
-
- - -
-
- - -
-
-
- Back - Next -
-
-
-
-
-
Password Validation
-

- Enabling this will display a set of password requirements on the create account page, such as minimum length, uppercase characters, special characters, etc. -

-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- Back - Next -
-
-
-
-
-
Help Messages
-

- Just a few little messages that will display in various places. Leave these alone if you want. -

-
- - -
-
- - -
-
- - -
-
- Back - Next -
-
-
-
-
-
Finished!
-

- Press the button below to submit your settings. The program will restart. Once it's done, refresh this page. -

- +

{{ .lang.JellyfinEmby.embyNotice }}

+ + + + +
+
+ + +
+
+ +
+
+ {{ .lang.Ombi.title }} +

{{ .lang.Ombi.description }}

+ + + + +
+
+ {{ .lang.Email.title }} +

+
+
+ + +

{{ .lang.Email.useEmailAsUsernameNotice }}

+ + + +
+ +
-
+
+
+

SMTP

+ + + + + +
+
+

Mailgun

+ + +
+
+
+ +
+ + + +
+ {{ .lang.PasswordValidation.title }} +

{{ .lang.PasswordValidation.description }}

+ + + + + + + +
+
+ {{ .lang.HelpMessages.title }} +

{{ .lang.HelpMessages.description }}

+ + + + + +
+
+
+ {{ .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" -}); - -const checkAuthRadio = () => { - if ((document.getElementById('manualAuthRadio') as HTMLInputElement).checked) { - document.getElementById('adminOnlyArea').style.display = 'none'; - document.getElementById('manualAuthArea').style.display = ''; - } else { - document.getElementById('manualAuthArea').style.display = 'none'; - document.getElementById('adminOnlyArea').style.display = ''; - } -}; - -for (let radio of ['manualAuthRadio', 'jfAuthRadio']) { - document.getElementById(radio).addEventListener('change', checkAuthRadio); -}; - -const checkEmailRadio = () => { - (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-5'; - (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-7'; - if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = ''; - document.getElementById('emailMailgunArea').style.display = 'none'; - (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true; - } else if ((document.getElementById('emailMailgunRadio') as HTMLInputElement).checked) { - document.getElementById('emailCommonArea').style.display = ''; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = ''; - (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true; - } else if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) { - document.getElementById('emailCommonArea').style.display = 'none'; - document.getElementById('emailSMTPArea').style.display = 'none'; - document.getElementById('emailMailgunArea').style.display = 'none'; - (document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-8'; - (document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-4'; - (document.getElementById('notificationsEnabled') as HTMLInputElement).checked = false; - } -}; - -for (let radio of ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']) { - document.getElementById(radio).addEventListener('change', checkEmailRadio); +interface sWindow extends Window { + messages: {}; } -const checkSSL = () => { - var label = document.getElementById('emailSSL_TLSLabel'); - if ((document.getElementById('emailSSL_TLS') as HTMLInputElement).checked) { - label.textContent = 'Use SSL/TLS'; - } else { - label.textContent = 'Use STARTTLS'; - } -}; -document.getElementById('emailSSL_TLS').addEventListener('change', checkSSL); +declare var window: sWindow; +window.URLBase = ""; -var pwrEnabled = document.getElementById('pwrEnabled') as HTMLInputElement; -const checkPwrEnabled = () => { - if (pwrEnabled.checked) { - document.getElementById('pwrArea').style.display = ''; - } else { - document.getElementById('pwrArea').style.display = 'none'; - } -}; -pwrEnabled.addEventListener('change', checkPwrEnabled); +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; }; -var invEnabled = document.getElementById("invEnabled") as HTMLInputElement; -const checkInvEnabled = () => { - if (invEnabled.checked) { - document.getElementById('invArea').style.display = ''; - } else { - document.getElementById('invArea').style.display = 'none'; - } -}; -invEnabled.addEventListener('change', checkInvEnabled); +interface boolEvent extends Event { + detail: boolean; +} -var valEnabled = document.getElementById("valEnabled") as HTMLInputElement; -const checkValEnabled = () => { - const valArea = document.getElementById("valArea"); - if (valEnabled.checked) { - valArea.style.display = ''; - } else { - valArea.style.display = 'none'; - } -}; -valEnabled.addEventListener('change', checkValEnabled); - -checkValEnabled(); -checkInvEnabled(); -checkSSL(); -checkAuthRadio(); -checkEmailRadio(); -checkPwrEnabled(); - -var jfValid = false -document.getElementById('jfTestButton').onclick = () => { - let testButton = document.getElementById('jfTestButton') as HTMLInputElement; - let nextButton = document.getElementById('jfNextButton') as HTMLAnchorElement; - let jfData = {}; - jfData['jfHost'] = (document.getElementById('jfHost') as HTMLInputElement).value; - jfData['jfUser'] = (document.getElementById('jfUser') as HTMLInputElement).value; - jfData['jfPassword'] = (document.getElementById('jfPassword') as HTMLInputElement).value; - let valid = true; - for (let val in jfData) { - if (jfData[val] == "") { - valid = false; - } - } - if (!valid) { - if (!testButton.classList.contains('btn-danger')) { - testButton.classList.add('btn-danger'); - testButton.textContent = 'Fill out fields above.'; - setTimeout(function() { - if (testButton.classList.contains('btn-danger')) { - testButton.classList.remove('btn-danger'); - testButton.textContent = 'Test'; - } - }, 2000); - } - } else { - testButton.disabled = true; - testButton.innerHTML = - '' + - 'Testing...'; - nextButton.classList.add('disabled'); - nextButton.setAttribute('aria-disabled', 'true'); - var req = new XMLHttpRequest(); - req.open("POST", "/jellyfin/test", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - testButton.disabled = false; - testButton.className = ''; - if (this.response['success'] == true) { - testButton.classList.add('btn', 'btn-success'); - testButton.textContent = 'Success'; - nextButton.classList.remove('disabled'); - nextButton.setAttribute('aria-disabled', 'false'); +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 { - testButton.classList.add('btn', 'btn-danger'); - testButton.textContent = 'Failed'; - }; - }; - }; - req.send(JSON.stringify(jfData)); + el.classList.remove("unfocused"); + } + }); + } + } +} + +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"); + } + }); + } + } +} + +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; + } + + 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"); + } + } + }); + } + } +} + +// 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); + } + 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"); + } + }); + } + } +} + +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"; + } + }); + } +} + +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") } }; -document.getElementById('submitButton').onclick = () => { - const submitButton = document.getElementById('submitButton') as HTMLInputElement; - submitButton.disabled = true; - submitButton.innerHTML =` - - Submitting... - `; +(() => { + 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(); +})(); + + +const restartButton = document.getElementById("restart") as HTMLSpanElement; +const serialize = () => { + toggleLoader(restartButton); 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; + 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; + } + } } - config['jellyfin']['username'] = (document.getElementById('jfUser') as HTMLInputElement).value; - config['jellyfin']['password'] = (document.getElementById('jfPassword') as HTMLInputElement).value; - // Page 4: Email (Page 5, 6, 7 are only used if this is enabled) - if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) { - config['password_resets']['enabled'] = 'false'; - config['invite_emails']['enabled'] = 'false'; - config['notifications']['enabled'] = 'false'; - } else { - if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) { - config['smtp']['encryption'] = (document.getElementById('emailSSL_TLS') as HTMLInputElement).checked ? "ssl_tls" : "starttls"; - config['email']['method'] = 'smtp'; - config['smtp']['server'] = (document.getElementById('emailSMTPServer') as HTMLInputElement).value; - config['smtp']['port'] = (document.getElementById('emailSMTPPort') as HTMLInputElement).value; - config['smtp']['password'] = (document.getElementById('emailSMTPPassword') as HTMLInputElement).value; - config['email']['address'] = (document.getElementById('emailSMTPAddress') as HTMLInputElement).value; - } else { - config['email']['method'] = 'mailgun'; - config['mailgun']['api_url'] = (document.getElementById('emailMailgunURL') as HTMLInputElement).value; - config['mailgun']['api_key'] = (document.getElementById('emailMailgunKey') as HTMLInputElement).value; - config['email']['address'] = (document.getElementById('emailMailgunAddress') as HTMLInputElement).value; - }; - config['notifications']['enabled'] = ""+(document.getElementById('notificationsEnabled') as HTMLInputElement).checked; - // Page 5: Email formatting - config['email']['from'] = (document.getElementById('emailSender') as HTMLInputElement).value; - config['email']['date_format'] = (document.getElementById('emailDateFormat') as HTMLInputElement).value; - config['email']['use_24h'] = ""+(document.getElementById('email24hTimeRadio') as HTMLInputElement).checked; - config['email']['message'] = (document.getElementById('emailMessage') as HTMLInputElement).value; - // Page 6: Password Resets - if (pwrEnabled.checked) { - config['password_resets']['enabled'] = 'true'; - config['password_resets']['watch_directory'] = (document.getElementById('pwrJfPath') as HTMLInputElement).value; - config['password_resets']['subject'] = (document.getElementById('pwrSubject') as HTMLInputElement).value; - } else { - config['password_resets']['enabled'] = 'false'; - }; - // Page 7: Invite Emails - if ((document.getElementById('invEnabled') as HTMLInputElement).checked) { - config['invite_emails']['enabled'] = 'true'; - config['invite_emails']['url_base'] = (document.getElementById('invURLBase') as HTMLInputElement).value; - config['invite_emails']['subject'] = (document.getElementById('invSubject') as HTMLInputElement).value; - } else { - config['invite_emails']['enabled'] = 'false'; - }; - }; - // Page 8: Password Validation - if ((document.getElementById('valEnabled') as HTMLInputElement).checked) { - config['password_validation']['enabled'] = 'true'; - config['password_validation']['min_length'] = (document.getElementById('valLength') as HTMLInputElement).value; - config['password_validation']['upper'] = (document.getElementById('valUpper') as HTMLInputElement).value; - config['password_validation']['lower'] = (document.getElementById('valLower') as HTMLInputElement).value; - config['password_validation']['number'] = (document.getElementById('valNumber') as HTMLInputElement).value; - config['password_validation']['special'] = (document.getElementById('valSpecial') as HTMLInputElement).value; - } else { - config['password_validation']['enabled'] = 'false'; - }; - // Page 9: Messages - config['ui']['contact_message'] = (document.getElementById('msgContact') as HTMLInputElement).value; - config['ui']['help_message'] = (document.getElementById('msgHelp') as HTMLInputElement).value; - config['ui']['success_message'] = (document.getElementById('msgSuccess') as HTMLInputElement).value; - // Send it config["restart-program"] = true; - let req = new XMLHttpRequest(); - req.open("POST", "/config", true); - req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - req.responseType = 'json'; - req.onreadystatechange = function() { - if (this.readyState == 4) { - submitButton.disabled = false; - submitButton.className = ''; - submitButton.classList.add('btn', 'btn-success'); - submitButton.textContent = 'Success'; - }; - }; - req.send(JSON.stringify(config)); + _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 { + mailgun.classList.add("unfocused"); + smtp.classList.add("unfocused"); + for (let el of relatedToEmail) { + el.classList.add("hidden"); + } + } +}; +settings["email"]["method"].onchange = emailMethodChange; +emailMethodChange(); + +(window as any).settings = settings; + +for (let section in settings) { + for (let setting in settings[section]) { + settings[section][setting].broadcast(); + } +} + +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 { + 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, () => {}); + }; +})(); + +loadLangSelector("setup");