1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
virusperfect
f063298bf7 Translated using Weblate (German)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.pw/projects/jfa-go/emails/de/
2021-01-27 23:11:35 +01:00
virusperfect
bb1e454850 translation from Weblate (German)
Currently translated at 100.0% (108 of 108 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.pw/projects/jfa-go/admin/de/
2021-01-27 23:11:35 +01:00
Cornichon420
11770d90f1 translation from Weblate (French)
Currently translated at 100.0% (108 of 108 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.hrfee.pw/projects/jfa-go/admin/fr/
2021-01-27 23:11:35 +01:00
hrfee
8a415140b6 Translated using Weblate (English)
Currently translated at 100.0% (32 of 32 strings)

Translation: jfa-go/Emails
Translate-URL: https://weblate.hrfee.pw/projects/jfa-go/emails/en/
2021-01-27 23:11:35 +01:00
3dd83bffbf
Merge branch 'new-setup'
Merge new setup wizard

This is much more up-to-date than the previous setup page, with a new
design and previously missing/new settings. Currently only available in
english (hopefully that changes soon).

also fixes conflict in _post.
2021-01-27 22:06:44 +00:00
79987ffa22
add refresh button that uses url base if given 2021-01-27 21:51:01 +00:00
764639bbba
add header template 2021-01-27 21:38:35 +00:00
eb67116ee6
replace og setup 2021-01-27 21:37:43 +00:00
7baea9101e
Add general settings, ombi
host, port, theme, tls are included in general. Page theme changes with
setting. Fixed checkbox support messages. Split some cards into columns.
2021-01-27 21:35:41 +00:00
167fae9892
add jellyfin connection test, submission
fully functional now, but still need to add some sections (ombi mainly).
2021-01-27 12:55:39 +00:00
c7f5aa2e2b
split into pages, hide email pages when disabled, add history navigation 2021-01-27 00:51:19 +00:00
8c871bc5fa
Add ts to link setting dependance
Also make store each setting as classes in a settings object, to make it
easier to serialize on submitting. Also, added
"substitute_jellyfin_strings", "no_username" and welcome_email.
2021-01-26 22:57:29 +00:00
bf1e6230dc
split some strings into common file; use lang file to setup page 2021-01-25 21:26:54 +00:00
687edf2b0b
Initial setup page content
Rewritten with a17t. Content right now is just a copy of the original
setup.html, but settings for new features will be added later.Currently
all cards are shown, only the current one will show in future.
2021-01-24 23:05:04 +00:00
21 changed files with 1375 additions and 659 deletions

14
api.go
View File

@ -1191,21 +1191,21 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
for setting, value := range settings.(map[string]interface{}) { for setting, value := range settings.(map[string]interface{}) {
if section == "ui" && setting == "language-form" { if section == "ui" && setting == "language-form" {
for key, lang := range app.storage.lang.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) tempConfig.Section("ui").Key("language-form").SetValue(key)
break break
} }
} }
} else if section == "ui" && setting == "language-admin" { } else if section == "ui" && setting == "language-admin" {
for key, lang := range app.storage.lang.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) tempConfig.Section("ui").Key("language-admin").SetValue(key)
break break
} }
} }
} else if section == "email" && setting == "language" { } else if section == "email" && setting == "language" {
for key, lang := range app.storage.lang.Email { 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) tempConfig.Section("email").Key("language").SetValue(key)
break break
} }
@ -1288,6 +1288,14 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
for key, lang := range app.storage.lang.Admin { for key, lang := range app.storage.lang.Admin {
resp[key] = lang.Meta.Name 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 { if len(resp) == 0 {
respond(500, "Couldn't get languages", gc) respond(500, "Couldn't get languages", gc)

View File

@ -720,7 +720,7 @@
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"type": "text", "type": "text",
"value": "Your account was deleted - Jellyfin", "value": "",
"description": "Subject of account deletion emails." "description": "Subject of account deletion emails."
}, },
"email_html": { "email_html": {

View File

@ -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 { .tab-button {
font-size: 2rem; font-size: 2rem;
@ -256,6 +272,10 @@ sup.\~critical, .text-critical {
justify-content: center; justify-content: center;
} }
.middle {
align-items: center;
}
.no-lp { .no-lp {
padding-left: 0px; padding-left: 0px;
} }

View File

@ -1,374 +1,460 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="light-theme">
<head> <head>
<meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="css/bundle.css">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> {{ template "header.html" . }}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <title>{{ .lang.Strings.pageTitle }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-t6I8D5dJmMXjCsRLhSzCltuhNZg6P10kE0m0nAncLUjH6GeYLhRU1zfLoW3QNQDF" crossorigin="anonymous"></script>
<style>
.card-body {
width: 100%;
height: 100%;
padding: 10%;
}
.slider {
width: 100%;
height: 100%;
display: flex;
overflow-x: hidden;
-webkit-overflow-scrolling: none;
scroll-snap-type: x mandatory;
}
.slider > div {
scroll-snap-align: start;
}
.slide {
width: 100%;
flex-shrink: 0;
height: 100%;
margin: 10%;
}
</style>
<title>Setup - jfa-go</title>
</head> </head>
<body> <body class="max-w-full overflow-x-hidden section">
<div class="pageContainer"> <div id="notification-box"></div>
<div class="container"> <span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button">
<i class="ri-global-line"></i>
<span class="ml-1 chev"></span>
</span>
<div class="dropdown-display">
<div class="card ~neutral !low" id="lang-list">
</div>
</div>
</span>
<div class="page-container" id="page-container">
<div class="card ~neutral !low mb-1">
<div class="row"> <div class="row">
<div class="col-sm"></div> <img class="banner header" src="banner.svg" alt="jfa-go" />
<div id="setupCarousel" class="col-md-auto slider"> </div>
<div class="slide card text-center" id="page-1"> <div class="row col flex center">
<div class="card-body"> <span class="heading welcome">{{ .lang.StartPage.welcome }}</span>
<h5 class="card-title">Welcome!</h5> </div>
<p class="card-text"> <div class="row col flex center">
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. <p class="content">{{ .lang.StartPage.pressStart }}</p>
</p> </div>
<a class="btn btn-primary nextButton" href="#page-2">Get Started</a> <section class="section ~neutral banner footer flex-expand middle">
<span class="support">{{ .lang.StartPage.httpsNotice }}</span>
<span class="button ~urge !normal next">{{ .lang.StartPage.start }}</span>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.Language.title }}</span>
<p class="content" id="language-description"></p>
<label class="label">
<span class="mt-half">{{ .lang.Language.defaultAdminLang }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="ui-language-admin">
</select>
</div>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Language.defaultFormLang }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="ui-language-form">
</select>
</div>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Language.defaultEmailLang }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="email-language">
</select>
</div>
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.General.title }}</span>
<div class="row">
<div class="col">
<label class="label">
<span class="mt-half">{{ .lang.General.listenAddress }}</span>
<input type="url" class="input ~neutral !normal mt-half mb-1" id="ui-host" value="0.0.0.0">
</label>
<label class="row switch">
<input type="checkbox" id="advanced-tls"><span>{{ .lang.General.useHTTPS }}</span>
</label>
<p class="support mb-1">{{ .lang.General.useHTTPSNotice }}</p>
<label class="label">
<span class="mt-half">{{ .lang.General.pathToCertificate }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_cert">
</label>
<label class="label">
<span class="mt-half">{{ .lang.General.pathToKeyFile }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_key">
</label>
</div>
<div class="col">
<label class="label">
<span class="mt-half">{{ .lang.Strings.port }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="ui-port" value="8056">
</label>
<label class="label">
<span class="mt-half">{{ .lang.General.httpsPort }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_port" value="8057">
</label>
<label class="label">
<span class="mt-half">{{ .lang.General.urlBase }} ({{ .lang.Strings.optional }})</span>
<input type="url" class="input ~neutral !normal mt-half" id="ui-url_base">
<p class="support mb-1">{{ .lang.General.urlBaseNotice }}</p>
</label>
<label class="label">
<span>{{ .lang.Strings.theme }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="ui-theme">
<option value="Jellyfin (Dark)">{{ .lang.General.darkTheme }}</option>
<option value="Default (Light)">{{ .lang.General.lightTheme }}</option>
</select>
</div> </div>
<div class="card-footer"> </label>
<small>Note: Make sure you are accessing this page through HTTPS, or on a private network.</small> </div>
</div>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.Login.title }}</span>
<p class="content">{{ .lang.Login.description }}</p>
<div class="pl-1">
<label class="row switch pb-1">
<input type="radio" name="ui-jellyfin_login" value="true" checked><span>{{ .lang.Login.authorizeWithJellyfin }}</span>
</label>
<label class="row switch pl-1 pb-1">
<input type="checkbox" id="ui-admin_only"><span>{{ .lang.Login.adminOnly }}</span>
</label>
<label class="row switch pb-1">
<input type="radio" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
</label>
</div>
<div id="login-manual">
<label class="label">
<span class="mt-half">{{ .lang.Strings.username }}</span>
<input type="text" id="ui-username" class="input ~neutral !normal mt-half mb-1" placeholder="{{ .lang.Strings.username }}">
</label>
<label class="label">
<span>{{ .lang.Strings.password }}</span>
<input type="password" id="ui-password" class="input ~neutral !normal mt-half mb-1" placeholder="{{ .lang.Strings.password }}">
</label>
<label class="label">
<span>{{ .lang.Strings.emailAddress }} ({{ .lang.Strings.optional }})</span>
<input type="email" id="ui-email" class="input ~neutral !normal mt-half" placeholder="email@address">
<span class="support mb-1">{{ .lang.Login.emailNotice }}</span>
</label>
</div>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.JellyfinEmby.title }}</span>
<p class="content">{{ .lang.JellyfinEmby.description }}</p>
<div class="row">
<div class="col">
<label class="label">
<span>{{ .lang.Strings.serverType }}</span>
<div class="select ~neutral !normal mt-half">
<select id="jellyfin-type">
<option value="jellyfin">Jellyfin</option>
<option value="emby">Emby</option>
</select>
</div> </div>
</div> <p class="support mb-1">{{ .lang.JellyfinEmby.embyNotice }}</p>
<div class="slide card" id="page-2"> </label>
<div class="card-body"> <label class="label">
<h5 class="card-title">Login</h5> <span class="mt-half">{{ .lang.JellyfinEmby.replaceJellyfin }} ({{ .lang.Strings.optional }})</span>
<p class="card-text"> <input type="text" class="input ~neutral !normal mt-half" id="jellyfin-substitute_jellyfin_strings">
To access the admin page, you'll need to login. Choose how below. <p class="support mb-1">{{ .lang.JellyfinEmby.replaceJellyfinNotice }}</p>
<ul> </label>
<li><b>Authorize through Jellyfin: </b>Checks credentials with Jellyfin, allowing you to share login details and grant multiple users access.</li> <label class="label">
<li><b>Username & Password: </b>Set your own username and password manually.</li> <span class="mt-half">{{ .lang.Strings.username }}</span>
</ul> <input type="text" id="jellyfin-username" class="input ~neutral !normal mt-half mb-1" placeholder="{{ .lang.Strings.username }}">
<div class="form-check" id="jfAuthFormGroup"> </label>
<input class="form-check-input" type="radio" name="auth" id="jfAuthRadio" value="jfAuth" checked> <label class="label">
<label class="form-check-label" for="jfAuthRadio"> <span>{{ .lang.Strings.password }}</span>
Authorize through Jellyfin <input type="password" id="jellyfin-password" class="input ~neutral !normal mt-half mb-1" placeholder="{{ .lang.Strings.password }}">
</label> </label>
</div> </div>
<div id="adminOnlyArea"> <div class="col">
<div class="form-check" style="margin-left: 1rem;"> <label class="label">
<input type="checkbox" class="form-check-input" id="jfAuthAdminOnly" checked> <span class="mt-half">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.internal }})</span>
<label for="jfAuthAdminOnly" class="form-check-label">Allow admin users only</label> <input type="url" class="input ~neutral !normal mt-half mb-1" id="jellyfin-server" placeholder="http://jellyf.in:80">
</div> </label>
</div> <label class="label">
<div class="form-check"> <span class="mt-half">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.external }})</span>
<input class="form-check-input" type="radio" name="auth" id="manualAuthRadio" value="manualAuth"> <input type="url" class="input ~neutral !normal mt-half" id="jellyfin-public_server" placeholder="https://jellyf.in">
<label class="form-check-label" for="manualAuthRadio"> <p class="support mb-1">{{ .lang.JellyfinEmby.addressExternalNotice }}</p>
Manual username &amp; password </label>
</label> </div>
</div> </div>
<div id="manualAuthArea"> <section class="section ~neutral banner footer flex-expand middle">
<div class="form-group"> <span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<label for="manualAuthUsername">Username</label> <div>
<input type="text" class="form-control" id="manualAuthUsername" placeholder="Username"> <span class="button ~urge !normal" id="jellyfin-test-connection">{{ .lang.JellyfinEmby.testConnection }}</span>
</div> <span class="button ~urge !normal next" disabled>{{ .lang.Strings.next }}</span>
<div class="form-group"> </div>
<label for="manualAuthPassword">Password</label> </section>
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password"> </div>
</div> <div class="card ~neutral !low mb-1 unfocused">
<div class="form-group"> <span class="heading">{{ .lang.Ombi.title }}</span>
<label for="manualAuthEmail">Email (Optional)</label> <p class="content">{{ .lang.Ombi.description }}</p>
<input type="email" class="form-control" id="manualAuthEmail" placeholder="example@example.com"> <label class="row switch pb-1">
<small class="form-text text-muted">Your email address is only required if you want to recieve activity notifications.</small> <input type="checkbox" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span>
</div> </label>
</div> <label class="label">
</p> <span class="mt-half">{{ .lang.Strings.serverAddress }}</span>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons"> <input type="url" class="input ~neutral !normal mt-half mb-1" id="ombi-server" placeholder="ombi.jellyf.in">
<a class="btn btn-secondary backButton" href="#page-1">Back</a> </label>
<a class="btn btn-primary nextButton" href="#page-3">Next</a> <label class="label">
</div> <span class="mt-half">{{ .lang.Strings.apiKey }}</span>
</div> <input type="text" class="input ~neutral !normal mt-half" id="ombi-api_key">
</div> <p class="support mb-1">{{ .lang.Ombi.apiKeyNotice }}</p>
<div class="slide card" id="page-3"> </label>
<div class="card-body"> <section class="section ~neutral banner footer flex-expand middle">
<h5 class="card-title">Jellyfin</h5> <span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<p class="card-text"> <div>
jfa-go needs admin access so that it can create users, as this is currently not permitted via API tokens. <span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
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. </div>
<div class="form-group"> </section>
<label for="jfHost">Host (For internal use)</label> </div>
<input type="url" class="form-control" id="jfHost" placeholder="http://jellyf.in:443" required> <div class="card ~neutral !low mb-1 unfocused">
</div> <span class="heading">{{ .lang.Email.title }}</span>
<div class="form-group"> <p class="content" id="email-description"></p>
<label for="jfPublicHost">Public Host (For access by users)</label> <div class="row">
<input type="url" class="form-control" id="jfPublicHost" placeholder="Leave blank to use the above address."> <div class="col">
</div> <label class="label">
<div class="form-group"> <span>{{ .lang.Email.method }}</span>
<label for="jfUser">Username</label> <div class="select ~neutral !normal mt-half mb-1">
<input type="text" class="form-control" id="jfUser" placeholder="Username" required> <select id="email-method">
</div> <option value="">{{ .lang.Strings.disabled }}</option>
<div class="form-group"> <option value="smtp">SMTP</option>
<label for="jfPassword">Password</label> <option value="mailgun">Mailgun</option>
<input type="password" class="form-control" id="jfPassword" placeholder="Password" required> </select>
</div>
<div style="margin-top: 1rem;">
<button class="btn btn-secondary" id="jfTestButton">Test</button>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-2">Back</a>
<a class="btn btn-primary nextButton disabled" id="jfNextButton" aria-disabled="true" href="#page-4">Next</a>
</div>
</div>
</div>
</div>
<div class="slide card" id="page-4">
<div class="card-body">
<h5 class="card-title">Email</h5>
<p class="card-text">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 <a href="https://www.mailgun.com/">Mailgun's</a> API.
<div class="form-group">
<div class="form-check" id="emailDisabled">
<input class="form-check-input" type="radio" name="email" id="emailDisabledRadio" value="emailDisabled">
<label class="form-check-label" for="emailDisabledRadio">
Disabled
</label>
</div>
<div class="form-check" id="emailSMTP">
<input class="form-check-input" type="radio" name="email" id="emailSMTPRadio" value="emailSMTP" checked>
<label class="form-check-label" for="emailSMTPRadio">
SMTP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="email" id="emailMailgunRadio" value="emailMailgun">
<label class="form-check-label" for="emailMailgunRadio">
Mailgun API
</label>
</div>
</div>
<div id="emailSMTPArea">
<div class="form-group form-check form-switch">
<input type="checkbox" class="form-check-input" id="emailSSL_TLS" checked>
<label for="emailSSL_TLS" class="form-check-label" id="emailSSL_TLSLabel">Use SSL/TLS</label>
<small class="form-text text-muted">Note: SSL/TLS usually uses port 465, whereas STARTTLS usually uses 587.</small>
</div>
<div class="form-group form-row">
<div class="col">
<input type="text" class="form-control" id="emailSMTPServer" placeholder="SMTP Server Address">
</div>
<div class="col">
<input type="number" class="form-control" id="emailSMTPPort" placeholder="Port">
</div>
</div>
<div class="form-group form-row">
<div class="col">
<input type="email" class="form-control" id="emailSMTPAddress" placeholder="jellyfin@jellyf.in">
</div>
<div class="col">
<input type="password" class="form-control" id="emailSMTPPassword" placeholder="Password">
</div>
</div>
</div>
<div id="emailMailgunArea">
<div class="form-group">
<input type="url" class="form-control" id="emailMailgunURL" placeholder="API URL">
</div>
<div class="form-group">
<input type="password" class="form-control" id="emailMailgunKey" placeholder="API Key">
</div>
<div class="form-group">
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
</div>
</div>
<div id="emailCommonArea">
<h5 class="card-title">Notifications</h5>
<p class="card-text">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.</p>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="notificationsEnabled">
<label for="notificationsEnabled" class="form-check-label">Enabled</label>
</div>
</div>
</p>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
<a class="btn btn-primary nextButton" id="emailNextButton" href="#page-5">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-5">
<div class="card-body">
<h5 class="card-title">Email</h5>
<p class="card-text">Just a few more things to get your emails looking great.
<div class="form-group">
<label for="emailSender">Sender: The name shown when a user receives an email.</label>
<input type="text" class="form-control" id="emailSender" value="Jellyfin">
</div>
<div class="form-group">
<label for="emailDateFormat">Date Format: Follows <a target="_blank" href="https://strftime.org/">strftime</a> format.</label>
<input type="text" class="form-control" id="emailDateFormat" value="%d/%m/%y">
</div>
<div class="form-group form-check">
<input class="form-check-input" type="radio" name="time" id="email24hTimeRadio" value="email24hTime" checked>
<label class="form-check-label" for="email24hTimeRadio">24h time</label>
</div>
<div class="form-group form-check">
<input class="form-check-input" type="radio" name="time" id="email12hTimeRadio" value="email12hTime">
<label class="form-check-label" for="email12hTimeRadio">12h time</label>
</div>
<div class="form-group">
<label for="emailMessage">Message: Short message displayed at the bottom of emails.</label>
<input type="text" class="form-control" id="emailMessage" value="Need help? Contact me.">
</div>
</p>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-4">Back</a>
<a class="btn btn-primary nextButton" href="#page-6">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-6">
<div class="card-body">
<h5 class="card-title">Password Resets</h5>
<p class="card-text">
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.
</p>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="pwrEnabled" value="enabled">
<label class="form-check-label" for="pwrEnabled">Enabled</label>
</div>
<div id="pwrArea">
<div class="form-group">
<label for="pwrJfPath">Path to Jellyfin</label>
<input type="text" class="form-control" id="pwrJfPath" placeholder="Folder">
</div>
<div class="form-group">
<label for="pwrSubject">Email Subject</label>
<input type="text" class="form-control" id="pwrSubject" value="Password Reset - Jellyfin">
</div>
</div>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-5">Back</a>
<a class="btn btn-primary nextButton" href="#page-7">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-7">
<div class="card-body">
<h5 class="card-title">Invite Emails</h5>
<p class="card-text">
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:
<ul>
<li>On the local network, you might use <a href="#">http://localhost:8056/invite</a></li>
<li>Exposed to the internet, you might use <a href="#">https://accounts.jellyf.in/invite</a></li>
</ul>
</p>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="invEnabled" value="enabled" checked>
<label class="form-check-label" for="invEnabled">Enabled</label>
</div>
<div id="invArea">
<div class="form-group">
<label for="invURLBase">URL Base</label>
<input type="url" class="form-control" id="invURLBase" placeholder="https://accounts.jellyf.in/invite">
</div>
<div class="form-group">
<label for="invSubject">Subject</label>
<input type="text" class="form-control" id="invSubject" value="Invite - Jellyfin">
</div>
</div>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-6">Back</a>
<a class="btn btn-primary nextButton" href="#page-8">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-8">
<div class="card-body">
<h5 class="card-title">Password Validation</h5>
<p class="card-text">
Enabling this will display a set of password requirements on the create account page, such as minimum length, uppercase characters, special characters, etc.
</p>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="valEnabled" value="enabled">
<label class="form-check-label" for="valEnabled">Enabled</label>
</div>
<div id="valArea">
<div class="form-group">
<label for="valLength">Minimum Length</label>
<input type="number" class="form-control" id="valLength" value="8">
</div>
<div class="form-group">
<label for="valUpper">Minimum number of uppercase characters</label>
<input type="number" class="form-control" id="valUpper" value="1">
</div>
<div class="form-group">
<label for="valLower">Minimum number of lowercase characters</label>
<input type="number" class="form-control" id="valLower" value="0">
</div>
<div class="form-group">
<label for="valNumber">Minimum number of numbers</label>
<input type="number" class="form-control" id="valNumber" value="0">
</div>
<div class="form-group">
<label for="valSpecial">Minimum number of special characters</label>
<input type="number" class="form-control" id="valSpecial" value="0">
</div>
</div>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" id="valBackButton" href="#page-7">Back</a>
<a class="btn btn-primary nextButton" href="#page-9">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-9">
<div class="card-body">
<h5 class="card-title">Help Messages</h5>
<p class="card-text">
Just a few little messages that will display in various places. Leave these alone if you want.
</p>
<div class="form-group">
<label for="msgContact">Contact message: Displays at bottom of all pages (except admin).</label>
<input id="msgContact" type="text" class="form-control" value="Need help? Contact me.">
</div>
<div class="form-group">
<label for="msgHelp">Help message: Displays when a user is creating an account.</label>
<input id="msgHelp" type="text" class="form-control" value="Enter your details to create an account.">
</div>
<div class="form-group">
<label for="msgSuccess">Success message: Displays when a user successfully creates an account, just above a button taking the user to Jellyfin.</label>
<input id="msgSuccess" type="text" class="form-control" value="Your account has been created. Click below to continue to Jellyfin.">
</div>
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
<a class="btn btn-secondary backButton" href="#page-8">Back</a>
<a class="btn btn-primary nextButton" href="#page-10">Next</a>
</div>
</div>
</div>
<div class="slide card" id="page-10">
<div class="card-body text-center">
<h5 class="card-title">Finished!</h5>
<p class="card-text">
Press the button below to submit your settings. The program will restart. Once it's done, refresh this page.
</p>
<button id="submitButton" class="btn btn-primary">Submit</button>
</div> </div>
</label>
<label class="row switch">
<input type="checkbox" id="email-no_username"><span>{{ .lang.Email.useEmailAsUsername }}</span>
</label>
<p class="support mb-1">{{ .lang.Email.useEmailAsUsernameNotice }}</p>
<label class="label">
<span class="mt-half">{{ .lang.Email.fromAddress }}</span>
<input type="email" class="input ~neutral !normal mt-half mb-1" id="email-address" placeholder="mail@jellyf.in">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Email.senderName }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="email-from" value="Jellyfin">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Email.dateFormat }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="email-date_format" value="%d/%m/%y">
<p class="support mb-1" id="email-dateformat-notice"></p>
</label>
<div>
<label class="row switch pb-1">
<input type="radio" name="email-24h" value="true" checked><span>{{ .lang.Email.time24h }}</span>
</label>
<label class="row switch pb-1">
<input type="radio" name="email-24h" value="false"><span>{{ .lang.Email.time12h }}</span>
</label>
</div> </div>
</div> </div>
<div class="col-sm"></div> <div class="col">
<div id="email-smtp">
<p class="subheading">SMTP</p>
<label class="label">
<span>{{ .lang.Email.encryption }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="smtp-encryption">
<option value="starttls">STARTTLS ({{ .lang.Strings.port }} 587)</option>
<option value="ssl_tls">SSL/TLS ({{ .lang.Strings.port }} 465)</option>
</select>
</div>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.serverAddress }}</span>
<input type="url" class="input ~neutral !normal mt-half mb-1" id="smtp-server" placeholder="smtp.jellyf.in">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.port }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="smtp-port" placeholder="587">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.username }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="smtp-username">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.password }}</span>
<input type="password" class="input ~neutral !normal mt-half mb-1" id="smtp-password">
</label>
</div>
<div id="email-mailgun">
<p class="subheading">Mailgun</p>
<label class="label">
<span class="mt-half">{{ .lang.Email.mailgunApiURL }}</span>
<input type="url" class="input ~neutral !normal mt-half mb-1" id="mailgun-api_url" placeholder="https://api.eu.mailgun.net/v3/mail.jellyf.in/messages">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.apiKey }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="mailgun-api_key">
</label>
</div>
</div>
</div>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused related-to-email">
<span class="heading">{{ .lang.Notifications.title }}</span>
<p class="content">{{ .lang.Notifications.description }}</p>
<label class="row switch pb-1">
<input type="checkbox" id="notifications-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<span class="heading">{{ .lang.WelcomeEmails.title }}</span>
<p class="content">{{ .lang.WelcomeEmails.description }}</p>
<label class="row switch pb-1">
<input type="checkbox" id="welcome_email-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="welcome_email-subject" placeholder="{{ .emailLang.WelcomeEmail.title }}">
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused related-to-email">
<span class="heading">{{ .lang.InviteEmails.title }}</span>
<p class="content">{{ .lang.InviteEmails.description }}</p>
<label class="row switch pb-1">
<input type="checkbox" id="invite_emails-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.URL }}</span>
<input type="url" class="input ~neutral !normal mt-half mb-1" id="invite_emails-url_base" placeholder="https://accounts.jellyf.in/invite">
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="invite_emails-subject" placeholder="{{ .emailLang.InviteEmail.title }}">
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused related-to-email">
<span class="heading">{{ .lang.PasswordResets.title }}</span>
<p class="content">{{ .lang.PasswordResets.description }}</p>
<label class="row switch pb-1">
<input type="checkbox" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordResets.pathToJellyfin }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="password_resets-watch_directory" placeholder="/config/jellyfin">
<p class="support mb-1">{{ .lang.PasswordResets.pathToJellyfinNotice }}</p>
</label>
<label class="label">
<span class="mt-half">{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}">
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.PasswordValidation.title }}</span>
<p class="content">{{ .lang.PasswordValidation.description }}</p>
<label class="row switch pb-1">
<input type="checkbox" id="password_validation-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordValidation.length }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="password_validation-min_length" value="8">
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordValidation.uppercase }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="password_validation-upper" value="1">
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordValidation.lowercase }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="password_validation-lower" value="0">
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordValidation.numbers }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="password_validation-number" value="0">
</label>
<label class="label">
<span class="mt-half">{{ .lang.PasswordValidation.special }}</span>
<input type="number" class="input ~neutral !normal mt-half mb-1" id="password_validation-special" value="0">
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<span class="heading">{{ .lang.HelpMessages.title }}</span>
<p class="content">{{ .lang.HelpMessages.description }}</p>
<label class="label">
<span class="mt-half">{{ .lang.HelpMessages.contactMessage }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="ui-contact_message">
<p class="support mb-1">{{ .lang.HelpMessages.contactMessageNotice }}</p>
</label>
<label class="label">
<span class="mt-half">{{ .lang.HelpMessages.helpMessage }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="ui-help_message">
<p class="support mb-1">{{ .lang.HelpMessages.helpMessageNotice }}</p>
</label>
<label class="label">
<span class="mt-half">{{ .lang.HelpMessages.successMessage }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="ui-success_message">
<p class="support mb-1">{{ .lang.HelpMessages.successMessageNotice }}</p>
</label>
<label class="label related-to-email">
<span class="mt-half">{{ .lang.HelpMessages.emailMessage }}</span>
<input type="text" class="input ~neutral !normal mt-half" id="email-message">
<p class="support mb-1">{{ .lang.HelpMessages.emailMessageNotice }}</p>
</label>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral !normal back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge !normal next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral !low mb-1 unfocused">
<div class="row col flex center">
<span class="heading">{{ .lang.EndPage.finished }}</span>
</div>
<div class="row col flex center">
<p class="content">{{ .lang.EndPage.restartMessage }}</p>
</div>
<div class="row col flex center">
<span class="button ~neutral !normal back mr-1">{{ .lang.Strings.back }}</span>
<span class="button ~urge !normal" id="restart">{{ .lang.Strings.submit }}</span>
<span class="button ~urge !normal unfocused" id="refresh">{{ .lang.EndPage.refreshPage }}</span>
</div> </div>
</div> </div>
</div> </div>
<script src="js/setup.js"></script> <script>
window.langFile = JSON.parse({{ .language }});
window.messages = JSON.parse({{ .messages }});
</script>
<script src="js/setup.js" type="module"></script>
</body> </body>
</html> </html>

40
lang.go
View File

@ -26,6 +26,13 @@ func (ls *adminLangs) getOptions(chosen string) (string, []string) {
return chosenLang, opts return chosenLang, opts
} }
type commonLangs map[string]commonLang
type commonLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
}
type adminLang struct { type adminLang struct {
Meta langMeta `json:"meta"` Meta langMeta `json:"meta"`
Strings langSection `json:"strings"` Strings langSection `json:"strings"`
@ -79,6 +86,39 @@ type emailLang struct {
WelcomeEmail langSection `json:"welcomeEmail"` 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 type langSection map[string]string
func (el langSection) format(field string, vals ...string) string { func (el langSection) format(field string, vals ...string) string {

View File

@ -6,7 +6,6 @@
"invites": "Invites", "invites": "Invites",
"accounts": "Konten", "accounts": "Konten",
"settings": "Einstellungen", "settings": "Einstellungen",
"theme": "Thema",
"inviteDays": "Tage", "inviteDays": "Tage",
"inviteHours": "Stunden", "inviteHours": "Stunden",
"inviteMinutes": "Minuten", "inviteMinutes": "Minuten",
@ -19,12 +18,8 @@
"create": "Erstellen", "create": "Erstellen",
"apply": "Anwenden", "apply": "Anwenden",
"delete": "Löschen", "delete": "Löschen",
"submit": "Absenden",
"name": "Name", "name": "Name",
"date": "Datum", "date": "Datum",
"username": "Benutzername",
"password": "Passwort",
"emailAddress": "E-Mail-Adresse",
"lastActiveTime": "Zuletzt aktiv", "lastActiveTime": "Zuletzt aktiv",
"from": "Von", "from": "Von",
"user": "Benutzer", "user": "Benutzer",
@ -33,8 +28,6 @@
"commitNoun": "Commit", "commitNoun": "Commit",
"newUser": "Neuer Benutzer", "newUser": "Neuer Benutzer",
"profile": "Profil", "profile": "Profil",
"success": "Erfolg",
"error": "Fehler",
"unknown": "Unbekannt", "unknown": "Unbekannt",
"modifySettings": "Einstellungen ändern", "modifySettings": "Einstellungen ändern",
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.", "modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
@ -69,7 +62,8 @@
"inviteExpiresInTime": "Läuft in {n} ab", "inviteExpiresInTime": "Läuft in {n} ab",
"notifyEvent": "Benachrichtigen bei:", "notifyEvent": "Benachrichtigen bei:",
"notifyInviteExpiry": "Bei Ablauf", "notifyInviteExpiry": "Bei Ablauf",
"notifyUserCreation": "Bei Benutzererstellung" "notifyUserCreation": "Bei Benutzererstellung",
"label": "Label"
}, },
"notifications": { "notifications": {
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.", "changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
@ -96,7 +90,9 @@
"errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.", "errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.",
"errorChangedEmailAddress": "E-Mail-Adresse von {n} konnte nicht geändert werden.", "errorChangedEmailAddress": "E-Mail-Adresse von {n} konnte nicht geändert werden.",
"errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)", "errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)",
"errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)" "errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)",
"errorUserCreated": "Fehler beim Erstellen des Benutzers {n}.",
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommens-E-Mail (überprüfe die Konsole/Logs)"
}, },
"quantityStrings": { "quantityStrings": {
"modifySettingsFor": { "modifySettingsFor": {

View File

@ -6,7 +6,6 @@
"invites": "Invites", "invites": "Invites",
"accounts": "Accounts", "accounts": "Accounts",
"settings": "Settings", "settings": "Settings",
"theme": "Theme",
"inviteDays": "Days", "inviteDays": "Days",
"inviteHours": "Hours", "inviteHours": "Hours",
"inviteMinutes": "Minutes", "inviteMinutes": "Minutes",
@ -19,12 +18,8 @@
"create": "Create", "create": "Create",
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"submit": "Submit",
"name": "Name", "name": "Name",
"date": "Date", "date": "Date",
"username": "Username",
"password": "Password",
"emailAddress": "Email Address",
"lastActiveTime": "Last Active", "lastActiveTime": "Last Active",
"from": "From", "from": "From",
"user": "User", "user": "User",
@ -33,8 +28,6 @@
"commitNoun": "Commit", "commitNoun": "Commit",
"newUser": "New User", "newUser": "New User",
"profile": "Profile", "profile": "Profile",
"success": "Success",
"error": "Error",
"unknown": "Unknown", "unknown": "Unknown",
"label": "Label", "label": "Label",
"modifySettings": "Modify Settings", "modifySettings": "Modify Settings",

View File

@ -7,7 +7,6 @@
"invites": "Invite", "invites": "Invite",
"accounts": "Comptes", "accounts": "Comptes",
"settings": "Reglages", "settings": "Reglages",
"theme": "Thème",
"inviteDays": "Jours", "inviteDays": "Jours",
"inviteHours": "Heures", "inviteHours": "Heures",
"inviteMinutes": "Minutes", "inviteMinutes": "Minutes",
@ -20,12 +19,8 @@
"create": "Créer", "create": "Créer",
"apply": "Appliquer", "apply": "Appliquer",
"delete": "Effacer", "delete": "Effacer",
"submit": "Soumettre",
"name": "Nom", "name": "Nom",
"date": "Date", "date": "Date",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"emailAddress": "Addresse Email",
"lastActiveTime": "Dernière activité", "lastActiveTime": "Dernière activité",
"from": "De", "from": "De",
"user": "Utilisateur", "user": "Utilisateur",
@ -34,8 +29,6 @@
"commitNoun": "Commettre", "commitNoun": "Commettre",
"newUser": "Nouvel utilisateur", "newUser": "Nouvel utilisateur",
"profile": "Profil", "profile": "Profil",
"success": "Succès",
"error": "Erreur",
"unknown": "Inconnu", "unknown": "Inconnu",
"modifySettings": "Modifier les paramètres", "modifySettings": "Modifier les paramètres",
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.", "modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
@ -48,7 +41,7 @@
"settingsApplyRestartNow": "Appliquer et redémarrer", "settingsApplyRestartNow": "Appliquer et redémarrer",
"settingsApplied": "Paramètres appliqués.", "settingsApplied": "Paramètres appliqués.",
"settingsRefreshPage": "Actualisez la page dans quelques secondes", "settingsRefreshPage": "Actualisez la page dans quelques secondes",
"settingsRequiredOrRestartMessage": "Remarque: {n} indique un champ obligatoire, {n} indique que les modifications nécessitent un redémarrage.", "settingsRequiredOrRestartMessage": "Remarque: {n} indique un champ obligatoire, {n} indique que les modifications nécessitent un redémarrage.",
"settingsSave": "Sauver", "settingsSave": "Sauver",
"ombiUserDefaults": "Paramètres par défaut de l'utilisateur Ombi", "ombiUserDefaults": "Paramètres par défaut de l'utilisateur Ombi",
"ombiUserDefaultsDescription": "Créez un utilisateur Ombi et configurez-le, puis sélectionnez-le ci-dessous. Ses paramètres/autorisations seront stockés et appliqués aux nouveaux utilisateurs Ombi créés par jfa-go", "ombiUserDefaultsDescription": "Créez un utilisateur Ombi et configurez-le, puis sélectionnez-le ci-dessous. Ses paramètres/autorisations seront stockés et appliqués aux nouveaux utilisateurs Ombi créés par jfa-go",

View File

@ -6,7 +6,6 @@
"invites": "Uitnodigingen", "invites": "Uitnodigingen",
"accounts": "Accounts", "accounts": "Accounts",
"settings": "Instellingen", "settings": "Instellingen",
"theme": "Thema",
"inviteDays": "Dagen", "inviteDays": "Dagen",
"inviteHours": "Uren", "inviteHours": "Uren",
"inviteMinutes": "Minuten", "inviteMinutes": "Minuten",
@ -19,12 +18,8 @@
"create": "Aanmaken", "create": "Aanmaken",
"apply": "Toepassen", "apply": "Toepassen",
"delete": "Verwijderen", "delete": "Verwijderen",
"submit": "Verstuur",
"name": "Naam", "name": "Naam",
"date": "Datum", "date": "Datum",
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"emailAddress": "E-mailadres",
"lastActiveTime": "Laatst actief", "lastActiveTime": "Laatst actief",
"from": "Van", "from": "Van",
"user": "Gebruiker", "user": "Gebruiker",
@ -33,8 +28,6 @@
"commitNoun": "Commit", "commitNoun": "Commit",
"newUser": "Nieuwe gebruiker", "newUser": "Nieuwe gebruiker",
"profile": "Profiel", "profile": "Profiel",
"success": "Success",
"error": "Fout",
"unknown": "Onbekend", "unknown": "Onbekend",
"modifySettings": "Instellingen aanpassen", "modifySettings": "Instellingen aanpassen",
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.", "modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",

14
lang/common/de-de.json Normal file
View File

@ -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"
}
}

14
lang/common/en-us.json Normal file
View File

@ -0,0 +1,14 @@
{
"meta": {
"name": "English (US)"
},
"strings": {
"username": "Username",
"password": "Password",
"emailAddress": "Email Address",
"submit": "Submit",
"success": "Success",
"error": "Error",
"theme": "Theme"
}
}

15
lang/common/fr-fr.json Normal file
View File

@ -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"
}
}

14
lang/common/nl-nl.json Normal file
View File

@ -0,0 +1,14 @@
{
"meta": {
"name": "Nederlands (NL)"
},
"strings": {
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"emailAddress": "E-mailadres",
"submit": "Verstuur",
"success": "Success",
"error": "Fout",
"theme": "Thema"
}
}

View File

@ -35,7 +35,14 @@
"hello": "Hallo", "hello": "Hallo",
"youHaveBeenInvited": "Du wurdest zu Jellyfin eingeladen.", "youHaveBeenInvited": "Du wurdest zu Jellyfin eingeladen.",
"toJoin": "Um beizutreten, folge dem untenstehenden Link.", "toJoin": "Um beizutreten, folge dem untenstehenden Link.",
"inviteExpiry": "Dieser Invite wird am {n}, um {n} ablaufen, was in {n} ist, also handle schnell.", "inviteExpiry": "Dieser Invite wird am {n}; um {n} ablaufen, was in {n} ist, also handle schnell.",
"linkButton": "Richte dein Konto ein" "linkButton": "Richte dein Konto ein"
},
"welcomeEmail": {
"title": "Wilkommen bei Jellyfin",
"welcome": "Willkommen bei Jellyfin!",
"youCanLoginWith": "Du kannst dich mit den mit den untenstehenden Zugangsdaten anmelden",
"jellyfinURL": "URL",
"username": "Benutzername"
} }
} }

View File

@ -35,7 +35,7 @@
"hello": "Hi", "hello": "Hi",
"youHaveBeenInvited": "You've been invited to Jellyfin.", "youHaveBeenInvited": "You've been invited to Jellyfin.",
"toJoin": "To join, follow the below link.", "toJoin": "To join, follow the below link.",
"inviteExpiry": "This invite will expire on {n}, at {n}, which is in {n}, so act quick.", "inviteExpiry": "This invite will expire on {n} at {n}, which is in {n}, so act quick.",
"linkButton": "Setup your account" "linkButton": "Setup your account"
}, },
"welcomeEmail": { "welcomeEmail": {

129
lang/setup/en-us.json Normal file
View File

@ -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 '<path to jellyfin>/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."
}
}

27
main.go
View File

@ -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 { if !firstRun {
app.host = app.config.Section("ui").Key("host").String() app.host = app.config.Section("ui").Key("host").String()
if app.config.Section("advanced").Key("tls").MustBool(false) { 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. // Since email depends on language, the email reload in loadConfig won't work first time.
app.email = NewEmailer(app) app.email = NewEmailer(app)
@ -559,6 +561,11 @@ func start(asDaemon, firstCall bool) {
} else { } else {
debugMode = false debugMode = false
address = "0.0.0.0:8056" 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") app.info.Println("Loading routes")
if debugMode { if debugMode {
@ -578,12 +585,12 @@ func start(asDaemon, firstCall bool) {
app.debug.Println("Loading pprof") app.debug.Println("Loading pprof")
pprof.Register(router) pprof.Register(router)
} }
router.GET("/lang/:page", app.GetLanguages)
if !firstRun { if !firstRun {
router.GET("/", app.AdminPage) router.GET("/", app.AdminPage)
router.GET("/accounts", app.AdminPage) router.GET("/accounts", app.AdminPage)
router.GET("/settings", app.AdminPage) router.GET("/settings", app.AdminPage)
router.GET("/lang/:page/:file", app.ServeLang) router.GET("/lang/:page/:file", app.ServeLang)
router.GET("/lang/:page", app.GetLanguages)
router.GET("/token/login", app.getTokenLogin) router.GET("/token/login", app.getTokenLogin)
router.GET("/token/refresh", app.getTokenRefresh) router.GET("/token/refresh", app.getTokenRefresh)
router.POST("/newUser", app.NewUser) router.POST("/newUser", app.NewUser)
@ -618,9 +625,7 @@ func start(asDaemon, firstCall bool) {
} }
app.info.Printf("Starting router @ %s", address) app.info.Printf("Starting router @ %s", address)
} else { } else {
router.GET("/", func(gc *gin.Context) { router.GET("/", app.ServeSetup)
gc.HTML(200, "setup.html", gin.H{})
})
router.POST("/jellyfin/test", app.TestJF) router.POST("/jellyfin/test", app.TestJF)
router.POST("/config", app.ModifyConfig) router.POST("/config", app.ModifyConfig)
app.info.Printf("Loading setup @ %s", address) app.info.Printf("Loading setup @ %s", address)

110
setup.go
View File

@ -1,21 +1,66 @@
package main package main
import ( import (
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/common" "github.com/hrfee/jfa-go/common"
"github.com/hrfee/jfa-go/mediabrowser" "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 { type testReq struct {
Host string `json:"jfHost"` ServerType string `json:"type"`
Username string `json:"jfUser"` Server string `json:"server"`
Password string `json:"jfPassword"` Username string `json:"username"`
Password string `json:"password"`
} }
func (app *appContext) TestJF(gc *gin.Context) { func (app *appContext) TestJF(gc *gin.Context) {
var req testReq var req testReq
gc.BindJSON(&req) 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) _, status, err := tempjf.Authenticate(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil { if !(status == 200 || status == 204) || err != nil {
app.info.Printf("Auth failed with code %d (%s)", status, err) 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}) 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
}

View File

@ -55,9 +55,17 @@ type Lang struct {
Form formLangs Form formLangs
EmailPath string EmailPath string
Email emailLangs Email emailLangs
CommonPath string
Common commonLangs
SetupPath string
Setup setupLangs
} }
func (st *Storage) loadLang() (err error) { func (st *Storage) loadLang() (err error) {
err = st.loadLangCommon()
if err != nil {
return
}
err = st.loadLangAdmin() err = st.loadLangAdmin()
if err != nil { if err != nil {
return return
@ -70,6 +78,20 @@ func (st *Storage) loadLang() (err error) {
return 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. // If a given language has missing values, fill it in with the english value.
func patchLang(english, other *langSection) { func patchLang(english, other *langSection) {
if *other == nil { 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 { func (st *Storage) loadLangAdmin() error {
st.lang.Admin = map[string]adminLang{} st.lang.Admin = map[string]adminLang{}
var english adminLang var english adminLang
@ -114,6 +179,7 @@ func (st *Storage) loadLangAdmin() error {
if err != nil { if err != nil {
return err return err
} }
st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" { if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings) patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications) patchLang(&english.Notifications, &lang.Notifications)
@ -164,6 +230,7 @@ func (st *Storage) loadLangForm() error {
if err != nil { if err != nil {
return err return err
} }
st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" { if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings) patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications) patchLang(&english.Notifications, &lang.Notifications)

View File

@ -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 addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => { export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
let req = new XMLHttpRequest(); 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.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');

View File

@ -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({ interface sWindow extends Window {
behavior: "auto", messages: {};
block: "center",
inline: "center"
});
const checkAuthRadio = () => {
if ((document.getElementById('manualAuthRadio') as HTMLInputElement).checked) {
document.getElementById('adminOnlyArea').style.display = 'none';
document.getElementById('manualAuthArea').style.display = '';
} else {
document.getElementById('manualAuthArea').style.display = 'none';
document.getElementById('adminOnlyArea').style.display = '';
}
};
for (let radio of ['manualAuthRadio', 'jfAuthRadio']) {
document.getElementById(radio).addEventListener('change', checkAuthRadio);
};
const checkEmailRadio = () => {
(document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-5';
(document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-7';
if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) {
document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = '';
document.getElementById('emailMailgunArea').style.display = 'none';
(document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true;
} else if ((document.getElementById('emailMailgunRadio') as HTMLInputElement).checked) {
document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = '';
(document.getElementById('notificationsEnabled') as HTMLInputElement).checked = true;
} else if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) {
document.getElementById('emailCommonArea').style.display = 'none';
document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = 'none';
(document.getElementById('emailNextButton') as HTMLAnchorElement).href = '#page-8';
(document.getElementById('valBackButton') as HTMLAnchorElement).href = '#page-4';
(document.getElementById('notificationsEnabled') as HTMLInputElement).checked = false;
}
};
for (let radio of ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']) {
document.getElementById(radio).addEventListener('change', checkEmailRadio);
} }
const checkSSL = () => { declare var window: sWindow;
var label = document.getElementById('emailSSL_TLSLabel'); window.URLBase = "";
if ((document.getElementById('emailSSL_TLS') as HTMLInputElement).checked) {
label.textContent = 'Use SSL/TLS';
} else {
label.textContent = 'Use STARTTLS';
}
};
document.getElementById('emailSSL_TLS').addEventListener('change', checkSSL);
var pwrEnabled = document.getElementById('pwrEnabled') as HTMLInputElement; const get = (id: string): HTMLElement => document.getElementById(id);
const checkPwrEnabled = () => { const text = (id: string, val: string) => { document.getElementById(id).textContent = val; };
if (pwrEnabled.checked) { const html = (id: string, val: string) => { document.getElementById(id).innerHTML = val; };
document.getElementById('pwrArea').style.display = '';
} else {
document.getElementById('pwrArea').style.display = 'none';
}
};
pwrEnabled.addEventListener('change', checkPwrEnabled);
var invEnabled = document.getElementById("invEnabled") as HTMLInputElement; interface boolEvent extends Event {
const checkInvEnabled = () => { detail: boolean;
if (invEnabled.checked) { }
document.getElementById('invArea').style.display = '';
} else {
document.getElementById('invArea').style.display = 'none';
}
};
invEnabled.addEventListener('change', checkInvEnabled);
var valEnabled = document.getElementById("valEnabled") as HTMLInputElement; class Input {
const checkValEnabled = () => { private _el: HTMLInputElement;
const valArea = document.getElementById("valArea"); get value(): string { return ""+this._el.value; }
if (valEnabled.checked) { set value(v: string) { this._el.value = v; }
valArea.style.display = ''; // Nothing depends on input, but we add an empty broadcast function so we can just loop over all settings to fix dependents on start.
} else { broadcast = () => {}
valArea.style.display = 'none'; constructor(el: HTMLElement, placeholder?: any, value?: any, depends?: string, dependsTrue?: boolean, section?: string) {
} this._el = el as HTMLInputElement;
}; if (placeholder) { this._el.placeholder = placeholder; }
valEnabled.addEventListener('change', checkValEnabled); if (value) { this.value = value; }
if (depends) {
checkValEnabled(); document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
checkInvEnabled(); let el = this._el as HTMLElement;
checkSSL(); if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
checkAuthRadio(); if (event.detail !== dependsTrue) {
checkEmailRadio(); el.classList.add("unfocused");
checkPwrEnabled();
var jfValid = false
document.getElementById('jfTestButton').onclick = () => {
let testButton = document.getElementById('jfTestButton') as HTMLInputElement;
let nextButton = document.getElementById('jfNextButton') as HTMLAnchorElement;
let jfData = {};
jfData['jfHost'] = (document.getElementById('jfHost') as HTMLInputElement).value;
jfData['jfUser'] = (document.getElementById('jfUser') as HTMLInputElement).value;
jfData['jfPassword'] = (document.getElementById('jfPassword') as HTMLInputElement).value;
let valid = true;
for (let val in jfData) {
if (jfData[val] == "") {
valid = false;
}
}
if (!valid) {
if (!testButton.classList.contains('btn-danger')) {
testButton.classList.add('btn-danger');
testButton.textContent = 'Fill out fields above.';
setTimeout(function() {
if (testButton.classList.contains('btn-danger')) {
testButton.classList.remove('btn-danger');
testButton.textContent = 'Test';
}
}, 2000);
}
} else {
testButton.disabled = true;
testButton.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Testing...';
nextButton.classList.add('disabled');
nextButton.setAttribute('aria-disabled', 'true');
var req = new XMLHttpRequest();
req.open("POST", "/jellyfin/test", true);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.responseType = 'json';
req.onreadystatechange = function() {
if (this.readyState == 4) {
testButton.disabled = false;
testButton.className = '';
if (this.response['success'] == true) {
testButton.classList.add('btn', 'btn-success');
testButton.textContent = 'Success';
nextButton.classList.remove('disabled');
nextButton.setAttribute('aria-disabled', 'false');
} else { } else {
testButton.classList.add('btn', 'btn-danger'); el.classList.remove("unfocused");
testButton.textContent = 'Failed'; }
}; });
}; }
}; }
req.send(JSON.stringify(jfData)); }
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<HTMLInputElement>;
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<HTMLInputElement>;
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", `<a href="https://weblate.hrfee.pw">Weblate</a>`));
html("email-description", window.lang.var("email", "description", `<a href="https://mailgun.com">Mailgun</a>`));
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; const checkTheme = () => {
submitButton.disabled = true; if (settings["ui"]["theme"].value.includes("Dark")) {
submitButton.innerHTML =` document.documentElement.classList.add("dark-theme");
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span> document.documentElement.classList.remove("light-theme");
Submitting... } 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 = {}; let config = {};
config['jellyfin'] = {}; for (let section in settings) {
config['ui'] = {}; config[section] = {};
config['password_validation'] = {}; for (let setting in settings[section]) {
config['email'] = {}; if (settings[section][setting].value) {
config['password_resets'] = {}; config[section][setting] = settings[section][setting].value;
config['invite_emails'] = {}; }
config['mailgun'] = {}; }
config['smtp'] = {};
config['notifications'] = {};
// Page 2: Auth
if ((document.getElementById('jfAuthRadio') as HTMLInputElement).checked) {
config['ui']['jellyfin_login'] = 'true';
config['ui']['admin_only'] = ""+(document.getElementById("jfAuthAdminOnly") as HTMLInputElement).checked;
} else {
config['ui']['username'] = (document.getElementById('manualAuthUsername') as HTMLInputElement).value;
config['ui']['password'] = (document.getElementById('manualAuthPassword') as HTMLInputElement).value;
config['ui']['email'] = (document.getElementById('manualAuthEmail') as HTMLInputElement).value;
};
// Page 3: Connect to jellyfin
config['jellyfin']['server'] = (document.getElementById('jfHost') as HTMLInputElement).value;
let publicAddress = (document.getElementById('jfPublicHost') as HTMLInputElement).value;
if (publicAddress != "") {
config['jellyfin']['public_server'] = publicAddress;
} }
config['jellyfin']['username'] = (document.getElementById('jfUser') as HTMLInputElement).value;
config['jellyfin']['password'] = (document.getElementById('jfPassword') as HTMLInputElement).value;
// Page 4: Email (Page 5, 6, 7 are only used if this is enabled)
if ((document.getElementById('emailDisabledRadio') as HTMLInputElement).checked) {
config['password_resets']['enabled'] = 'false';
config['invite_emails']['enabled'] = 'false';
config['notifications']['enabled'] = 'false';
} else {
if ((document.getElementById('emailSMTPRadio') as HTMLInputElement).checked) {
config['smtp']['encryption'] = (document.getElementById('emailSSL_TLS') as HTMLInputElement).checked ? "ssl_tls" : "starttls";
config['email']['method'] = 'smtp';
config['smtp']['server'] = (document.getElementById('emailSMTPServer') as HTMLInputElement).value;
config['smtp']['port'] = (document.getElementById('emailSMTPPort') as HTMLInputElement).value;
config['smtp']['password'] = (document.getElementById('emailSMTPPassword') as HTMLInputElement).value;
config['email']['address'] = (document.getElementById('emailSMTPAddress') as HTMLInputElement).value;
} else {
config['email']['method'] = 'mailgun';
config['mailgun']['api_url'] = (document.getElementById('emailMailgunURL') as HTMLInputElement).value;
config['mailgun']['api_key'] = (document.getElementById('emailMailgunKey') as HTMLInputElement).value;
config['email']['address'] = (document.getElementById('emailMailgunAddress') as HTMLInputElement).value;
};
config['notifications']['enabled'] = ""+(document.getElementById('notificationsEnabled') as HTMLInputElement).checked;
// Page 5: Email formatting
config['email']['from'] = (document.getElementById('emailSender') as HTMLInputElement).value;
config['email']['date_format'] = (document.getElementById('emailDateFormat') as HTMLInputElement).value;
config['email']['use_24h'] = ""+(document.getElementById('email24hTimeRadio') as HTMLInputElement).checked;
config['email']['message'] = (document.getElementById('emailMessage') as HTMLInputElement).value;
// Page 6: Password Resets
if (pwrEnabled.checked) {
config['password_resets']['enabled'] = 'true';
config['password_resets']['watch_directory'] = (document.getElementById('pwrJfPath') as HTMLInputElement).value;
config['password_resets']['subject'] = (document.getElementById('pwrSubject') as HTMLInputElement).value;
} else {
config['password_resets']['enabled'] = 'false';
};
// Page 7: Invite Emails
if ((document.getElementById('invEnabled') as HTMLInputElement).checked) {
config['invite_emails']['enabled'] = 'true';
config['invite_emails']['url_base'] = (document.getElementById('invURLBase') as HTMLInputElement).value;
config['invite_emails']['subject'] = (document.getElementById('invSubject') as HTMLInputElement).value;
} else {
config['invite_emails']['enabled'] = 'false';
};
};
// Page 8: Password Validation
if ((document.getElementById('valEnabled') as HTMLInputElement).checked) {
config['password_validation']['enabled'] = 'true';
config['password_validation']['min_length'] = (document.getElementById('valLength') as HTMLInputElement).value;
config['password_validation']['upper'] = (document.getElementById('valUpper') as HTMLInputElement).value;
config['password_validation']['lower'] = (document.getElementById('valLower') as HTMLInputElement).value;
config['password_validation']['number'] = (document.getElementById('valNumber') as HTMLInputElement).value;
config['password_validation']['special'] = (document.getElementById('valSpecial') as HTMLInputElement).value;
} else {
config['password_validation']['enabled'] = 'false';
};
// Page 9: Messages
config['ui']['contact_message'] = (document.getElementById('msgContact') as HTMLInputElement).value;
config['ui']['help_message'] = (document.getElementById('msgHelp') as HTMLInputElement).value;
config['ui']['success_message'] = (document.getElementById('msgSuccess') as HTMLInputElement).value;
// Send it
config["restart-program"] = true; config["restart-program"] = true;
let req = new XMLHttpRequest(); _post("/config", config, (req: XMLHttpRequest) => {
req.open("POST", "/config", true); if (req.readyState == 4) {
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); toggleLoader(restartButton);
req.responseType = 'json'; restartButton.parentElement.querySelector("span.back").classList.add("unfocused");
req.onreadystatechange = function() { restartButton.classList.add("unfocused");
if (this.readyState == 4) { const refresh = document.getElementById("refresh") as HTMLSpanElement;
submitButton.disabled = false; refresh.classList.remove("unfocused");
submitButton.className = ''; refresh.onclick = () => {
submitButton.classList.add('btn', 'btn-success'); let host = window.location.href.split("#")[0].split("?")[0] + settings["ui"]["url_base"].value;
submitButton.textContent = 'Success'; window.location.href = host;
}; };
}; }
req.send(JSON.stringify(config)); }, 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<HTMLDivElement>;
(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");