mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
14 Commits
1f6bbc75ff
...
f063298bf7
Author | SHA1 | Date | |
---|---|---|---|
|
f063298bf7 | ||
|
bb1e454850 | ||
|
11770d90f1 | ||
|
8a415140b6 | ||
3dd83bffbf | |||
79987ffa22 | |||
764639bbba | |||
eb67116ee6 | |||
7baea9101e | |||
167fae9892 | |||
c7f5aa2e2b | |||
8c871bc5fa | |||
bf1e6230dc | |||
687edf2b0b |
14
api.go
14
api.go
@ -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)
|
||||||
|
@ -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": {
|
||||||
|
20
css/base.css
20
css/base.css
@ -39,6 +39,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
margin: calc(-1 * var(--spacing-4,1rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner.header {
|
||||||
|
margin-bottom: var(--spacing-4,1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner.footer {
|
||||||
|
margin-top: var(--spacing-4,1rem);
|
||||||
|
padding: var(--spacing-4,1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.card:contains(section.banner.footer) {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-button {
|
.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;
|
||||||
}
|
}
|
||||||
|
808
html/setup.html
808
html/setup.html
@ -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 & 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
40
lang.go
@ -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 {
|
||||||
|
@ -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": {
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
14
lang/common/de-de.json
Normal 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
14
lang/common/en-us.json
Normal 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
15
lang/common/fr-fr.json
Normal 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
14
lang/common/nl-nl.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
129
lang/setup/en-us.json
Normal 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
27
main.go
@ -329,6 +329,15 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common")
|
||||||
|
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form")
|
||||||
|
app.storage.lang.AdminPath = filepath.Join(app.localPath, "lang", "admin")
|
||||||
|
app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email")
|
||||||
|
err := app.storage.loadLang()
|
||||||
|
if err != nil {
|
||||||
|
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !firstRun {
|
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
110
setup.go
@ -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
|
||||||
|
}
|
||||||
|
67
storage.go
67
storage.go
@ -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)
|
||||||
|
@ -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');
|
||||||
|
709
ts/setup.ts
709
ts/setup.ts
@ -1,260 +1,479 @@
|
|||||||
// Lord forgive me for this mess, i'll fix it one day i swear
|
import { _get, _post, toggleLoader } from "./modules/common.js";
|
||||||
|
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
|
||||||
|
|
||||||
document.getElementById("page-1").scrollIntoView({
|
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");
|
||||||
|
Loading…
Reference in New Issue
Block a user