1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-28 03:50:10 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
7c808b56f7
api: adjust a couple of URIs
adjusted some things, likke changing /newUser to /user/invite.
2024-08-21 20:35:08 +01:00
2057823b7a
setup: flex-ify, light/dark, keep page position on reload
got rid of a bunch of m[l/r/x/y]-x tailwind classes and used more
flex-[row/col] gap-2's. UI should be more consistent in general, and
with the admin UI.

The page you were on is actually read from the URL on reload, however
does not keep settings (implemented just for ease of UI editing,
really).

`missing-colors.js` preprocessor script now applies dark prefixes for
<section>s, but like with cards, does not apply a default ~neutral to
those without, so that <section class=""> looks different to <section
class="~neutral">.

Light/dark selector added to setup too, and the actual mode given to the
browser through CSS `color-scheme` is correct, meaning things like textareas, checkboxes and
controls are now colored according to the theme.
2024-08-21 18:31:54 +01:00
17 changed files with 634 additions and 523 deletions

View File

@ -86,7 +86,7 @@ func activitySourceToString(v ActivitySource) string {
return "anon" return "anon"
} }
// @Summary Get the requested set of activities, Paginated, filtered and sorted. // @Summary Get the requested set of activities, Paginated, filtered and sorted. Is a POST because of some issues I was having, ideally should be a GET.
// @Produce json // @Produce json
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters" // @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
// @Success 200 {object} GetActivitiesRespDTO // @Success 200 {object} GetActivitiesRespDTO

View File

@ -11,7 +11,6 @@ import (
lm "github.com/hrfee/jfa-go/logmessages" lm "github.com/hrfee/jfa-go/logmessages"
"github.com/itchyny/timefmt-go" "github.com/itchyny/timefmt-go"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
) )
const ( const (
@ -332,22 +331,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
} }
invites = append(invites, invite) invites = append(invites, invite)
} }
fullProfileList := app.storage.GetProfiles()
profiles := make([]string, len(fullProfileList))
if len(profiles) != 0 {
defaultProfile := app.storage.GetDefaultProfile()
profiles[0] = defaultProfile.Name
i := 1
if len(fullProfileList) > 1 {
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
profiles[i] = p.Name
i++
return nil
})
}
}
resp := getInvitesDTO{ resp := getInvitesDTO{
Profiles: profiles,
Invites: invites, Invites: invites,
} }
gc.JSON(200, resp) gc.JSON(200, resp)
@ -360,7 +344,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
// @Failure 500 {object} stringResponse // @Failure 500 {object} stringResponse
// @Router /invites/profile [post] // @Router /invites/profile [post]
// @Security Bearer // @Security Bearer
// @tags Profiles & Settings // @tags Invites
func (app *appContext) SetProfile(gc *gin.Context) { func (app *appContext) SetProfile(gc *gin.Context) {
var req inviteProfileDTO var req inviteProfileDTO
gc.BindJSON(&req) gc.BindJSON(&req)

View File

@ -9,7 +9,34 @@ import (
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
// @Summary Get a list of profiles // @Summary Get the names of all available profile.
// @Produce json
// @Success 200 {object} getProfileNamesDTO
// @Router /profiles/names [get]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) GetProfileNames(gc *gin.Context) {
fullProfileList := app.storage.GetProfiles()
profiles := make([]string, len(fullProfileList))
if len(profiles) != 0 {
defaultProfile := app.storage.GetDefaultProfile()
profiles[0] = defaultProfile.Name
i := 1
if len(fullProfileList) > 1 {
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
profiles[i] = p.Name
i++
return nil
})
}
}
resp := getProfileNamesDTO{
Profiles: profiles,
}
gc.JSON(200, resp)
}
// @Summary Get all available profiles, indexed by their names.
// @Produce json // @Produce json
// @Success 200 {object} getProfilesDTO // @Success 200 {object} getProfilesDTO
// @Router /profiles [get] // @Router /profiles [get]

View File

@ -20,7 +20,7 @@ import (
// @Produce json // @Produce json
// @Param newUserDTO body newUserDTO true "New user request object" // @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200 // @Success 200
// @Router /users [post] // @Router /user [post]
// @Security Bearer // @Security Bearer
// @tags Users // @tags Users
func (app *appContext) NewUserFromAdmin(gc *gin.Context) { func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
@ -68,7 +68,7 @@ func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
// @Param newUserDTO body newUserDTO true "New user request object" // @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200 {object} PasswordValidation // @Success 200 {object} PasswordValidation
// @Failure 400 {object} PasswordValidation // @Failure 400 {object} PasswordValidation
// @Router /newUser [post] // @Router /user/invite [post]
// @tags Users // @tags Users
func (app *appContext) NewUserFromInvite(gc *gin.Context) { func (app *appContext) NewUserFromInvite(gc *gin.Context) {
/* /*
@ -709,7 +709,7 @@ func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Save an announcement as a template for use or editing later. // @Summary Gets the names of each available announcement template.
// @Produce json // @Produce json
// @Success 200 {object} getAnnouncementsDTO // @Success 200 {object} getAnnouncementsDTO
// @Router /users/announce [get] // @Router /users/announce [get]

View File

@ -18,6 +18,8 @@
--bg-light: #fff; --bg-light: #fff;
--bg-dark: #101010; --bg-dark: #101010;
color-scheme: light;
} }
.light { .light {
@ -26,6 +28,7 @@
.dark { .dark {
--settings-section-button-filter: 80%; --settings-section-button-filter: 80%;
color-scheme: dark !important;
} }
.dark body { .dark body {
@ -456,3 +459,14 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
margin-bottom: -0.5rem; margin-bottom: -0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
section.section:not(.\~neutral) {
background-color: inherit;
}
@layer components {
.switch input {
@apply mr-1;
}
}

View File

@ -7,7 +7,9 @@
</head> </head>
<body class="max-w-full overflow-x-hidden section"> <body class="max-w-full overflow-x-hidden section">
<div id="notification-box"></div> <div id="notification-box"></div>
<div class="top-2 left-2 absolute"> <div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
<div class="flex flex-row gap-2">
<span class="dropdown" tabindex="0" id="lang-dropdown"> <span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button"> <span class="button ~urge dropdown-button">
<i class="ri-global-line"></i> <i class="ri-global-line"></i>
@ -19,79 +21,83 @@
</div> </div>
</span> </span>
</div> </div>
<div class="page-container m-2 lg:my-20 lg:mx-64" id="page-container"> <span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
<div class="card ~neutral @low mb-2">
<div class="row">
<img class="banner header" src="banner.svg" alt="jfa-go" />
</div> </div>
<div class="row col flex center"> <div class="card sectioned ~neutral @low flex flex-col gap-4 justify-between items-center">
<img class="w-[105%] max-w-none" src="banner.svg" alt="jfa-go" />
<span class="heading welcome">{{ .lang.StartPage.welcome }}</span> <span class="heading welcome">{{ .lang.StartPage.welcome }}</span>
</div> <p class="content text-center">{{ .lang.StartPage.pressStart }}</p>
<div class="row col flex center"> <section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<p class="content my-2">{{ .lang.StartPage.pressStart }}</p>
</div>
<section class="section ~neutral banner footer flex flex-row justify-between middle">
<span class="support">{{ .lang.StartPage.httpsNotice }}</span> <span class="support">{{ .lang.StartPage.httpsNotice }}</span>
<span class="button ~urge @low next">{{ .lang.StartPage.start }}</span> <span class="button ~urge @low next">{{ .lang.StartPage.start }}</span>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.Language.title }}</span> <span class="heading">{{ .lang.Language.title }}</span>
<p class="content my-2" id="language-description"></p> <p class="content" id="language-description"></p>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Language.defaultAdminLang }}</span> <span>{{ .lang.Language.defaultAdminLang }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="ui-language-admin"> <select id="ui-language-admin">
</select> </select>
</div> </div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Language.defaultFormLang }}</span> <span>{{ .lang.Language.defaultFormLang }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="ui-language-form"> <select id="ui-language-form">
</select> </select>
</div> </div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Language.defaultEmailLang }}</span> <span>{{ .lang.Language.defaultEmailLang }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="email-language"> <select id="email-language">
</select> </select>
</div> </div>
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.General.title }}</span> <span class="heading">{{ .lang.General.title }}</span>
<div class="row"> <div class="flex flex-row gap-2 justify-between">
<div class="col"> <div class="flex flex-col gap-2">
<label class="label"> <div class="flex flex-row gap-2 justify-between">
<span class="mt-4">{{ .lang.General.listenAddress }}</span> <label class="label flex flex-col gap-2 grow">
<input type="url" class="input ~neutral @low mt-4 mb-2" id="ui-host" value="0.0.0.0"> <span>{{ .lang.General.listenAddress }}</span>
<input type="url" class="input ~neutral @low" id="ui-host" value="0.0.0.0">
</label> </label>
<label class="row switch"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="advanced-tls"><span>{{ .lang.General.useHTTPS }}</span> <span>{{ .lang.Strings.port }}</span>
<input type="number" class="input ~neutral @low" id="ui-port" value="8056">
</label> </label>
<p class="support mb-2 mt-1">{{ .lang.General.useHTTPSNotice }}</p> </div>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.General.pathToCertificate }}</span> <div class="switch"><input type="checkbox" id="advanced-tls"><span>{{ .lang.General.useHTTPS }}</span></div>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_cert"> <p class="support">{{ .lang.General.useHTTPSNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.General.pathToKeyFile }}</span> <span>{{ .lang.General.pathToCertificate }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_key"> <input type="text" class="input ~neutral @low" id="advanced-tls_cert">
</label>
<label class="label flex flex-col gap-2">
<span>{{ .lang.General.pathToKeyFile }}</span>
<input type="text" class="input ~neutral @low" id="advanced-tls_key">
</label> </label>
<span class="heading">{{ .lang.Updates.title }}</span> <span class="heading">{{ .lang.Updates.title }}</span>
<p class="content my-2" id="updates-description"></p> <p class="content" id="updates-description"></p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="updates-enabled" checked><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="updates-enabled" checked><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span>{{ .lang.Updates.updateChannel }}</span> <span>{{ .lang.Updates.updateChannel }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="updates-channel"> <select id="updates-channel">
<option value="stable">{{ .lang.Updates.stable }}</option> <option value="stable">{{ .lang.Updates.stable }}</option>
<option value="unstable">{{ .lang.Updates.unstable }}</option> <option value="unstable">{{ .lang.Updates.unstable }}</option>
@ -99,28 +105,24 @@
</div> </div>
</label> </label>
</div> </div>
<div class="col"> <div class="flex flex-col gap-2">
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.port }}</span> <span>{{ .lang.General.httpsPort }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="ui-port" value="8056"> <input type="number" class="input ~neutral @low" id="advanced-tls_port" value="8057">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.General.httpsPort }}</span> <span>{{ .lang.General.urlBase }} ({{ .lang.Strings.optional }})</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_port" value="8057"> <input type="text" class="input ~neutral @low" id="ui-url_base" placeholder="/mysubfolder">
<p class="support">{{ .lang.General.urlBaseNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.General.urlBase }} ({{ .lang.Strings.optional }})</span> <span>{{ .lang.General.externalURL }}</span>
<input type="text" class="input ~neutral @low mt-4" id="ui-url_base" placeholder="/mysubfolder"> <input type="text" class="input ~neutral @low" id="ui-jfa_url" placeholder="https://jellyf.in/mysubfolder">
<p class="support mb-2 mt-1">{{ .lang.General.urlBaseNotice }}</p> <p class="support">{{ .lang.General.externalURLNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.General.externalURL }}</span>
<input type="text" class="input ~neutral @low mt-4" id="ui-jfa_url" placeholder="https://jellyf.in/mysubfolder">
<p class="support mb-2 mt-1">{{ .lang.General.externalURLNotice }}</p>
</label>
<label class="label">
<span>{{ .lang.Strings.theme }}</span> <span>{{ .lang.Strings.theme }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="ui-theme"> <select id="ui-theme">
<option value="Jellyfin (Dark)">{{ .lang.General.darkTheme }}</option> <option value="Jellyfin (Dark)">{{ .lang.General.darkTheme }}</option>
<option value="Default (Light)">{{ .lang.General.lightTheme }}</option> <option value="Default (Light)">{{ .lang.General.lightTheme }}</option>
@ -128,208 +130,221 @@
</div> </div>
</label> </label>
<span class="heading">{{ .lang.Proxy.title }}</span> <span class="heading">{{ .lang.Proxy.title }}</span>
<p class="content my-2" id="proxy-description">{{ .lang.Proxy.description }}</p> <p class="content" id="proxy-description">{{ .lang.Proxy.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="advanced-proxy"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="advanced-proxy"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span>{{ .lang.Proxy.protocol }}</span> <span>{{ .lang.Proxy.protocol }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="advanced-proxy_protocol"> <select id="advanced-proxy_protocol">
<option value="http">HTTP</option> <option value="http">HTTP</option>
<option value="socks">SOCKS5</option> <option value="socks">SOCKS5</option>
</select> </select>
</div> </div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Proxy.address }}</span> <span>{{ .lang.Proxy.address }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-proxy_address"> <input type="text" class="input ~neutral @low" id="advanced-proxy_address">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.username }}</span> <span>{{ .lang.Strings.username }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-proxy_user"> <input type="text" class="input ~neutral @low" id="advanced-proxy_user">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.password }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-proxy_password">
</label>
</div>
</div>
<section class="section ~neutral banner footer flex flex-row justify-between middle">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</section>
</div>
<div class="card ~neutral @low mb-2 unfocused">
<span class="heading">{{ .lang.Login.title }}</span>
<p class="content my-2">{{ .lang.Login.description }}</p>
<div class="pl-4">
<label class="row switch pb-4">
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="true" checked><span>{{ .lang.Login.authorizeWithJellyfin }}</span>
</label>
<label class="row switch pl-4 pb-4">
<input type="checkbox" class="mr-2" id="ui-admin_only" checked><span>{{ .lang.Login.adminOnly }}</span>
</label>
<label class="row switch pl-4 pb-2">
<input type="checkbox" class="mr-2" id="ui-allow_all"><span>{{ .lang.Login.allowAll }}</span>
</label>
<p class="support pb-4 pl-4 mt-1" id="description-ui-allow_all">{{ .lang.Login.allowAllDescription }}</p>
<label class="row switch pb-4">
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
</label>
<p class="support pb-4 pl-4 mt-1">{{ .lang.Login.authorizeManualUserPageNotice }}</p>
</div>
<div id="login-manual">
<label class="label">
<span class="mt-4">{{ .lang.Strings.username }}</span>
<input type="text" id="ui-username" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.username }}">
</label>
<label class="label">
<span>{{ .lang.Strings.password }}</span> <span>{{ .lang.Strings.password }}</span>
<input type="password" id="ui-password" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.password }}"> <input type="text" class="input ~neutral @low" id="advanced-proxy_password">
</label>
<label class="label">
<span>{{ .lang.Strings.emailAddress }} ({{ .lang.Strings.optional }})</span>
<input type="email" id="ui-email" class="input ~neutral @low mt-4" placeholder="email@address">
<span class="support mb-2 mt-1">{{ .lang.Login.emailNotice }}</span>
</label> </label>
</div> </div>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </div>
</section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.Login.title }}</span>
<p class="content">{{ .lang.Login.description }}</p>
<div class="flex flex-col gap-2">
<label class="label flex flex-col gap-2">
<div class="switch"><input type="radio" name="ui-jellyfin_login" value="true" checked><span>{{ .lang.Login.authorizeWithJellyfin }}</span></div>
</label>
<div class="pl-4 flex flex-col gap-2">
<label class="label flex flex-col gap-2">
<div class="switch"><input type="checkbox" id="ui-admin_only" checked><span>{{ .lang.Login.adminOnly }}</span></div>
</label>
<label class="label flex flex-col gap-2">
<div class="switch"><input type="checkbox" id="ui-allow_all"><span>{{ .lang.Login.allowAll }}</span></div>
<p class="support" id="description-ui-allow_all">{{ .lang.Login.allowAllDescription }}</p>
</label>
</div>
</div>
<div class="flex flex-col gap-2">
<label class="label flex flex-col gap-2">
<div class="switch"><input type="radio" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span></div>
</label>
<p class="support">{{ .lang.Login.authorizeManualUserPageNotice }}</p>
</div>
<div class ="flex flex-col gap-2" id="login-manual">
<label class="label flex flex-col gap-2">
<span>{{ .lang.Strings.username }}</span>
<input type="text" id="ui-username" class="input ~neutral @low" placeholder="{{ .lang.Strings.username }}">
</label>
<label class="label flex flex-col gap-2">
<span>{{ .lang.Strings.password }}</span>
<input type="password" id="ui-password" class="input ~neutral @low" placeholder="{{ .lang.Strings.password }}">
</label>
<label class="label flex flex-col gap-2">
<span>{{ .lang.Strings.emailAddress }} ({{ .lang.Strings.optional }})</span>
<input type="email" id="ui-email" class="input ~neutral @low" placeholder="email@address">
<span class="support">{{ .lang.Login.emailNotice }}</span>
</label>
</div>
</section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</section>
</div>
<div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.JellyfinEmby.title }}</span> <span class="heading">{{ .lang.JellyfinEmby.title }}</span>
<p class="content my-2">{{ .lang.JellyfinEmby.description }}</p> <p class="content">{{ .lang.JellyfinEmby.description }}</p>
<div class="row"> <div class="flex flex-row gap-2 justify-between">
<div class="col"> <div class="flex flex-col gap-2 grow">
<label class="label"> <label class="label flex flex-col gap-2">
<span>{{ .lang.Strings.serverType }}</span> <span>{{ .lang.Strings.serverType }}</span>
<div class="select ~neutral @low mt-4"> <div class="select ~neutral @low">
<select id="jellyfin-type"> <select id="jellyfin-type">
<option value="jellyfin">Jellyfin</option> <option value="jellyfin">Jellyfin</option>
<option value="emby">Emby</option> <option value="emby">Emby</option>
</select> </select>
</div> </div>
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.embyNotice }}</p> <p class="support">{{ .lang.JellyfinEmby.embyNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.JellyfinEmby.replaceJellyfin }} ({{ .lang.Strings.optional }})</span> <span>{{ .lang.JellyfinEmby.replaceJellyfin }} ({{ .lang.Strings.optional }})</span>
<input type="text" class="input ~neutral @low mt-4" id="jellyfin-substitute_jellyfin_strings"> <input type="text" class="input ~neutral @low" id="jellyfin-substitute_jellyfin_strings">
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.replaceJellyfinNotice }}</p> <p class="support">{{ .lang.JellyfinEmby.replaceJellyfinNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.username }}</span> <span>{{ .lang.Strings.username }}</span>
<input type="text" id="jellyfin-username" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.username }}"> <input type="text" id="jellyfin-username" class="input ~neutral @low" placeholder="{{ .lang.Strings.username }}">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span>{{ .lang.Strings.password }}</span> <span>{{ .lang.Strings.password }}</span>
<input type="password" id="jellyfin-password" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.password }}"> <input type="password" id="jellyfin-password" class="input ~neutral @low" placeholder="{{ .lang.Strings.password }}">
</label> </label>
</div> </div>
<div class="col"> <div class="flex flex-col gap-2 grow ">
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.internal }})</span> <span>{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.internal }})</span>
<input type="url" class="input ~neutral @low mt-4 mb-2" id="jellyfin-server" placeholder="http://jellyf.in:80"> <input type="url" class="input ~neutral @low" id="jellyfin-server" placeholder="http://jellyf.in:80">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.external }})</span> <span>{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.external }})</span>
<input type="url" class="input ~neutral @low mt-4" id="jellyfin-public_server" placeholder="https://jellyf.in"> <input type="url" class="input ~neutral @low" id="jellyfin-public_server" placeholder="https://jellyf.in">
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.addressExternalNotice }}</p> <p class="support">{{ .lang.JellyfinEmby.addressExternalNotice }}</p>
</label> </label>
</div> </div>
</div> </div>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div class="flex flex-row gap-2">
<span class="button ~urge @low" id="jellyfin-test-connection">{{ .lang.JellyfinEmby.testConnection }}</span> <span class="button ~urge @low" id="jellyfin-test-connection">{{ .lang.JellyfinEmby.testConnection }}</span>
<span class="button ~urge @low next" disabled>{{ .lang.Strings.next }}</span> <span class="button ~urge @low next" disabled>{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.Ombi.title }}</span> <span class="heading">{{ .lang.Ombi.title }}</span>
<p class="content my-2">{{ .lang.Ombi.description }}</p> <p class="content">{{ .lang.Ombi.description }}</p>
<aside class="aside ~warning my-2" id="ombi-stability-warning">{{ .lang.Ombi.stabilityWarning }}</aside> <aside class="aside ~warning" id="ombi-stability-warning">{{ .lang.Ombi.stabilityWarning }}</aside>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span> <span>{{ .lang.Strings.serverAddress }}</span>
<input type="url" class="input ~neutral @low mt-4 mb-2" id="ombi-server" placeholder="ombi.jellyf.in"> <input type="url" class="input ~neutral @low" id="ombi-server" placeholder="ombi.jellyf.in">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.apiKey }}</span> <span>{{ .lang.Strings.apiKey }}</span>
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key"> <input type="text" class="input ~neutral @low" id="ombi-api_key">
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p> <p class="support">{{ .lang.Ombi.apiKeyNotice }}</p>
</label> </label>
<span class="heading">{{ .lang.Jellyseerr.title }}</span> <span class="heading">{{ .lang.Jellyseerr.title }}</span>
<p class="content my-2">{{ .lang.Jellyseerr.description }}</p> <p class="content">{{ .lang.Jellyseerr.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="jellyseerr-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="jellyseerr-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span> <span>{{ .lang.Strings.serverAddress }}</span>
<input type="url" class="input ~neutral @low mt-4 mb-2" id="jellyseerr-server" placeholder="https://jellyseerr.jellyf.in:5055"> <input type="url" class="input ~neutral @low" id="jellyseerr-server" placeholder="https://jellyseerr.jellyf.in:5055">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.apiKey }}</span> <span>{{ .lang.Strings.apiKey }}</span>
<input type="text" class="input ~neutral @low mt-4" id="jellyseerr-api_key"> <input type="text" class="input ~neutral @low" id="jellyseerr-api_key">
</label> </label>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="jellyseerr-import_existing"><span>{{ .lang.Jellyseerr.importExisting }}</span> <div class="switch"><input type="checkbox" id="jellyseerr-import_existing" checked><span>{{ .lang.Jellyseerr.importExisting }}</span></div>
<p class="support mb-2 mt-1">{{ .lang.Jellyseerr.importExistingDescription }}</p> <p class="support">{{ .lang.Jellyseerr.importExistingDescription }}</p>
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.UserPage.title }}</span> <span class="heading">{{ .lang.UserPage.title }}</span>
<p class="content my-2">{{ .lang.UserPage.description }}</p> <p class="content">{{ .lang.UserPage.description }}</p>
<p class="content my-2">{{ .lang.UserPage.customizeMessages }}</p> <p class="content">{{ .lang.UserPage.customizeMessages }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="userpage-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="userpage-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
<p class="support">{{ .lang.UserPage.requiredSettings }}</p>
</label> </label>
<p class="support mb-1 mt-1">{{ .lang.UserPage.requiredSettings }}</p> </section>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> <section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.Messages.title }}</span> <span class="heading">{{ .lang.Messages.title }}</span>
<p class="content my-2" id="messages-description"></p> <p class="content" id="messages-description"></p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="messages-enabled" checked><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="messages-enabled" checked><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Email.dateFormat }}</span> <span>{{ .lang.Email.dateFormat }}</span>
<input type="text" class="input ~neutral @low mt-4" id="email-date_format" value="%d/%m/%y"> <input type="text" class="input ~neutral @low" id="email-date_format" value="%d/%m/%y">
<p class="support mb-2 mt-1" id="email-dateformat-notice"></p> <p class="support" id="email-dateformat-notice"></p>
</label> </label>
<div> <div class="flex flex-col gap-2">
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="radio" class="mr-2" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span> <div class="switch"><input type="radio" class="mr-2" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span></div>
</label> </label>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="radio" class="mr-2" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span> <div class="switch"><input type="radio" class="mr-2" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span></div>
</label> </label>
</div> </div>
<div id="email-sect"> <div id="email-sect" class="flex flex-row gap-2 justify-between">
<div class="flex flex-col gap-2">
<span class="heading">{{ .lang.Email.title }}</span> <span class="heading">{{ .lang.Email.title }}</span>
<p class="content my-2" id="email-description"></p> <p class="content" id="email-description"></p>
<div class="row"> <label class="label flex flex-col gap-2">
<div class="col">
<label class="label">
<span>{{ .lang.Email.method }}</span> <span>{{ .lang.Email.method }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="email-method"> <select id="email-method">
<option value="">{{ .lang.Strings.disabled }}</option> <option value="">{{ .lang.Strings.disabled }}</option>
<option value="smtp">SMTP</option> <option value="smtp">SMTP</option>
@ -337,218 +352,225 @@
</select> </select>
</div> </div>
</label> </label>
<label class="row switch"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="email-no_username"><span>{{ .lang.Email.useEmailAsUsername }}</span> <div class="switch"><input type="checkbox" id="email-no_username"><span>{{ .lang.Email.useEmailAsUsername }}</span></div>
<p class="support mb-2 mt-1">{{ .lang.Email.useEmailAsUsernameNotice }}</p> <p class="support">{{ .lang.Email.useEmailAsUsernameNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Email.fromAddress }}</span> <span>{{ .lang.Email.fromAddress }}</span>
<input type="email" class="input ~neutral @low mt-4 mb-2" id="email-address" placeholder="mail@jellyf.in"> <input type="email" class="input ~neutral @low" id="email-address" placeholder="mail@jellyf.in">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Email.senderName }}</span> <span>{{ .lang.Email.senderName }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="email-from" value="Jellyfin"> <input type="text" class="input ~neutral @low" id="email-from" value="Jellyfin">
</label> </label>
</div> </div>
<div class="col"> <div id="email-smtp" class="flex flex-col gap-2 min-w-[40%]">
<div id="email-smtp"> <p class="text-2xl font-semibold">SMTP</p>
<p class="text-2xl font-semibold mb-2">SMTP</p> <label class="label flex flex-col gap-2">
<label class="label">
<span>{{ .lang.Email.encryption }}</span> <span>{{ .lang.Email.encryption }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="smtp-encryption"> <select id="smtp-encryption">
<option value="starttls">STARTTLS ({{ .lang.Strings.port }} 587)</option> <option value="starttls">STARTTLS ({{ .lang.Strings.port }} 587)</option>
<option value="ssl_tls">SSL/TLS ({{ .lang.Strings.port }} 465)</option> <option value="ssl_tls">SSL/TLS ({{ .lang.Strings.port }} 465)</option>
</select> </select>
</div> </div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span> <span>{{ .lang.Strings.serverAddress }}</span>
<input type="url" class="input ~neutral @low mt-4 mb-2" id="smtp-server" placeholder="smtp.jellyf.in"> <input type="url" class="input ~neutral @low" id="smtp-server" placeholder="smtp.jellyf.in">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.port }}</span> <span>{{ .lang.Strings.port }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="smtp-port" placeholder="587"> <input type="number" class="input ~neutral @low" id="smtp-port" placeholder="587">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.username }}</span> <span>{{ .lang.Strings.username }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="smtp-username"> <input type="text" class="input ~neutral @low" id="smtp-username">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.password }}</span> <span>{{ .lang.Strings.password }}</span>
<input type="password" class="input ~neutral @low mt-4 mb-2" id="smtp-password"> <input type="password" class="input ~neutral @low" id="smtp-password">
</label> </label>
</div> </div>
<div id="email-mailgun"> <div id="email-mailgun" class="flex flex-col gap-2 min-w-[40%]">
<p class="text-2xl font-semibold mb-2">Mailgun</p> <p class="text-2xl font-semibold">Mailgun</p>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Email.mailgunApiURL }}</span> <span>{{ .lang.Email.mailgunApiURL }}</span>
<input type="url" class="input ~neutral @low mt-4 mb-2" id="mailgun-api_url" placeholder="https://api.eu.mailgun.net/v3/mail.jellyf.in/messages"> <input type="url" class="input ~neutral @low" id="mailgun-api_url" placeholder="https://api.eu.mailgun.net/v3/mail.jellyf.in/messages">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.apiKey }}</span> <span>{{ .lang.Strings.apiKey }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="mailgun-api_key"> <input type="text" class="input ~neutral @low" id="mailgun-api_key">
</label> </label>
</div> </div>
</div> </div>
</div> </section>
</div> <section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<section class="section ~neutral banner footer flex flex-row justify-between middle">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused related-to-email"> <div class="card sectioned ~neutral @low unfocused related-to-email">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.Notifications.title }}</span> <span class="heading">{{ .lang.Notifications.title }}</span>
<p class="content my-2">{{ .lang.Notifications.description }}</p> <p class="content">{{ .lang.Notifications.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="notifications-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="notifications-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<span class="heading">{{ .lang.WelcomeEmails.title }}</span> <span class="heading">{{ .lang.WelcomeEmails.title }}</span>
<p class="content my-2">{{ .lang.WelcomeEmails.description }}</p> <p class="content">{{ .lang.WelcomeEmails.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="welcome_email-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="welcome_email-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span> <span>{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="welcome_email-subject" placeholder="{{ .emailLang.WelcomeEmail.title }}"> <input type="text" class="input ~neutral @low" id="welcome_email-subject" placeholder="{{ .emailLang.WelcomeEmail.title }}">
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused related-to-email"> <div class="card sectioned ~neutral @low unfocused related-to-email">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.InviteEmails.title }}</span> <span class="heading">{{ .lang.InviteEmails.title }}</span>
<p class="content my-2">{{ .lang.InviteEmails.description }}</p> <p class="content">{{ .lang.InviteEmails.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="invite_emails-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="invite_emails-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span> <span>{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="invite_emails-subject" placeholder="{{ .emailLang.InviteEmail.title }}"> <input type="text" class="input ~neutral @low" id="invite_emails-subject" placeholder="{{ .emailLang.InviteEmail.title }}">
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div id="password-resets" class="card ~neutral @low mb-2 unfocused related-to-email"> <div id="password-resets" class="card sectioned ~neutral @low unfocused related-to-email">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.PasswordResets.title }}</span> <span class="heading">{{ .lang.PasswordResets.title }}</span>
<p class="content my-2">{{ .lang.PasswordResets.description }}</p> <p class="content">{{ .lang.PasswordResets.description }}</p>
<p class="content my-2" id="password_resets-more-info">{{ .lang.PasswordResets.moreInfo }}</p> <p class="content" id="password_resets-more-info">{{ .lang.PasswordResets.moreInfo }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordResets.pathToJellyfin }}</span> <span>{{ .lang.PasswordResets.pathToJellyfin }}</span>
<input type="text" class="input ~neutral @low mt-4" id="password_resets-watch_directory" placeholder="/config/jellyfin"> <input type="text" class="input ~neutral @low" id="password_resets-watch_directory" placeholder="/config/jellyfin">
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.pathToJellyfinNotice }}</p> <p class="support">{{ .lang.PasswordResets.pathToJellyfinNotice }}</p>
</label> </label>
<label class="switch"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span> <div class="switch"><input type="checkbox" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span></div>
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }} {{ .lang.PasswordResets.resetLinksRequiredForUserPage }}</p> <p class="support">{{ .lang.PasswordResets.resetLinksNotice }} {{ .lang.PasswordResets.resetLinksRequiredForUserPage }}</p>
</label> </label>
<label class="switch"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span> <div class="switch"><input type="checkbox" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span></div>
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.setPasswordNotice }}</p> <p class="support">{{ .lang.PasswordResets.setPasswordNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<p class="mt-4">{{ .lang.PasswordResets.resetLinksLanguage }}</p> <span>{{ .lang.PasswordResets.resetLinksLanguage }}</span>
<div class="select ~neutral @low mt-4 mb-2"> <div class="select ~neutral @low">
<select id="password_resets-language"> <select id="password_resets-language">
</select> </select>
</div> </div>
</label> </label>
<label class="row label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span> <span>{{ .lang.Strings.emailSubject }}</span>
<input type="text" class="input ~neutral @low mt-4 mb-2" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}"> <input type="text" class="input ~neutral @low" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}">
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.PasswordValidation.title }}</span> <span class="heading">{{ .lang.PasswordValidation.title }}</span>
<p class="content my-2">{{ .lang.PasswordValidation.description }}</p> <p class="content">{{ .lang.PasswordValidation.description }}</p>
<label class="row switch pb-4"> <label class="label flex flex-col gap-2">
<input type="checkbox" class="mr-2" id="password_validation-enabled" checked><span>{{ .lang.Strings.enabled }}</span> <div class="switch"><input type="checkbox" id="password_validation-enabled" checked><span>{{ .lang.Strings.enabled }}</span></div>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordValidation.length }}</span> <span>{{ .lang.PasswordValidation.length }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-min_length" value="8"> <input type="number" class="input ~neutral @low" id="password_validation-min_length" value="8">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordValidation.uppercase }}</span> <span>{{ .lang.PasswordValidation.uppercase }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-upper" value="1"> <input type="number" class="input ~neutral @low" id="password_validation-upper" value="1">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordValidation.lowercase }}</span> <span>{{ .lang.PasswordValidation.lowercase }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-lower" value="0"> <input type="number" class="input ~neutral @low" id="password_validation-lower" value="0">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordValidation.numbers }}</span> <span>{{ .lang.PasswordValidation.numbers }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-number" value="0"> <input type="number" class="input ~neutral @low" id="password_validation-number" value="0">
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.PasswordValidation.special }}</span> <span>{{ .lang.PasswordValidation.special }}</span>
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-special" value="0"> <input type="number" class="input ~neutral @low" id="password_validation-special" value="0">
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<section class="section flex flex-col gap-2 justify-between">
<span class="heading">{{ .lang.HelpMessages.title }}</span> <span class="heading">{{ .lang.HelpMessages.title }}</span>
<p class="content my-2">{{ .lang.HelpMessages.description }}</p> <p class="content">{{ .lang.HelpMessages.description }}</p>
<label class="label"> <p class="content">{{ .lang.HelpMessages.markdownMessageNotice }}</p>
<span class="mt-4">{{ .lang.HelpMessages.contactMessage }}</span> <label class="label flex flex-col gap-2">
<input type="text" class="input ~neutral @low mt-4" id="ui-contact_message"> <span>{{ .lang.HelpMessages.contactMessage }}</span>
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.contactMessageNotice }}</p> <input type="text" class="input ~neutral @low" id="ui-contact_message">
<p class="support">{{ .lang.HelpMessages.contactMessageNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.HelpMessages.helpMessage }}</span> <span>{{ .lang.HelpMessages.helpMessage }}</span>
<input type="text" class="input ~neutral @low mt-4" id="ui-help_message"> <input type="text" class="input ~neutral @low" id="ui-help_message">
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.helpMessageNotice }}</p> <p class="support">{{ .lang.HelpMessages.helpMessageNotice }}</p>
</label> </label>
<label class="label"> <label class="label flex flex-col gap-2">
<span class="mt-4">{{ .lang.HelpMessages.successMessage }}</span> <span>{{ .lang.HelpMessages.successMessage }}</span>
<input type="text" class="input ~neutral @low mt-4" id="ui-success_message"> <input type="text" class="input ~neutral @low" id="ui-success_message">
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.successMessageNotice }}</p> <p class="support">{{ .lang.HelpMessages.successMessageNotice }}</p>
</label> </label>
<label class="label related-to-email"> <label class="label related-to-email">
<span class="mt-4">{{ .lang.HelpMessages.emailMessage }}</span> <span>{{ .lang.HelpMessages.emailMessage }}</span>
<input type="text" class="input ~neutral @low mt-4" id="email-message"> <input type="text" class="input ~neutral @low" id="email-message">
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.emailMessageNotice }}</p> <p class="support">{{ .lang.HelpMessages.emailMessageNotice }}</p>
</label> </label>
<section class="section ~neutral banner footer flex flex-row justify-between middle"> </section>
<section class="section w-full ~neutral footer flex flex-row justify-between items-center">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div> <div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span> <span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card sectioned ~neutral @low unfocused">
<div class="row col flex center"> <section class="section flex flex-col gap-2 justify-center items-center">
<span class="heading">{{ .lang.EndPage.finished }}</span> <span class="heading">{{ .lang.EndPage.finished }}</span>
</div> <p class="content">{{ .lang.EndPage.restartMessage }}</p>
<div class="row col flex center"> </section>
<p class="content my-2">{{ .lang.EndPage.restartMessage }}</p> <section class="section w-full ~neutral footer flex flex-row justify-center items-center gap-2">
</div> <span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div class="row col flex center">
<span class="button ~neutral @low back mr-4">{{ .lang.Strings.back }}</span>
<span class="button ~urge @low" id="restart">{{ .lang.Strings.submit }}</span> <span class="button ~urge @low" id="restart">{{ .lang.Strings.submit }}</span>
<span class="button ~urge @low unfocused" id="refresh">{{ .lang.EndPage.refreshPage }}</span> <span class="button ~urge @low unfocused" id="refresh">{{ .lang.EndPage.refreshPage }}</span>
</div> </div>

View File

@ -102,6 +102,7 @@
"jellyseerr": { "jellyseerr": {
"title": "Jellyseerr", "title": "Jellyseerr",
"description": "Jellyseerr is an alternative to Ombi, and integrates with jfa-go slightly better. Again, after setup is finished, go to Settings to create a profile and add a template for new Jellyseerr accounts.", "description": "Jellyseerr is an alternative to Ombi, and integrates with jfa-go slightly better. Again, after setup is finished, go to Settings to create a profile and add a template for new Jellyseerr accounts.",
"importExisting": "Import existing users",
"importExistingDescription": "If enabled, your existing users will have contact details and preferences from jfa-go synchronized." "importExistingDescription": "If enabled, your existing users will have contact details and preferences from jfa-go synchronized."
}, },
"messages": { "messages": {
@ -164,6 +165,7 @@
"helpMessages": { "helpMessages": {
"title": "Help Messages", "title": "Help Messages",
"description": "These messages will display in the account creation page and in some emails.", "description": "These messages will display in the account creation page and in some emails.",
"markdownMessageNotice": "Contents of some emails, pages and messages can be customized further with markdown in Settings.",
"contactMessage": "Contact Message", "contactMessage": "Contact Message",
"contactMessageNotice": "Displays at the bottom of all pages except admin.", "contactMessageNotice": "Displays at the bottom of all pages except admin.",
"helpMessage": "Help Message", "helpMessage": "Help Message",

View File

@ -88,6 +88,10 @@ type getProfilesDTO struct {
DefaultProfile string `json:"default_profile"` DefaultProfile string `json:"default_profile"`
} }
type getProfileNamesDTO struct {
Profiles []string `json:"profiles"` // List of profiles (name only)
}
type profileChangeDTO struct { type profileChangeDTO struct {
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
} }
@ -123,7 +127,6 @@ type inviteDTO struct {
} }
type getInvitesDTO struct { type getInvitesDTO struct {
Profiles []string `json:"profiles"` // List of profiles (name only)
Invites []inviteDTO `json:"invites"` // List of invites Invites []inviteDTO `json:"invites"` // List of invites
} }

View File

@ -135,7 +135,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/lang/:page/:file", app.ServeLang) router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin) router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh) router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/newUser", app.NewUserFromInvite) router.POST(p+"/user/invite", app.NewUserFromInvite)
router.Use(static.Serve(p+"/invite/", app.webFS)) router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy) router.GET(p+"/invite/:invCode", app.InviteProxy)
if app.config.Section("captcha").Key("enabled").MustBool(false) { if app.config.Section("captcha").Key("enabled").MustBool(false) {
@ -182,7 +182,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.POST(p+"/logout", app.Logout) router.POST(p+"/logout", app.Logout)
api.DELETE(p+"/users", app.DeleteUsers) api.DELETE(p+"/users", app.DeleteUsers)
api.GET(p+"/users", app.GetUsers) api.GET(p+"/users", app.GetUsers)
api.POST(p+"/users", app.NewUserFromAdmin) api.POST(p+"/user", app.NewUserFromAdmin)
api.POST(p+"/users/extend", app.ExtendExpiry) api.POST(p+"/users/extend", app.ExtendExpiry)
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry) api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
api.POST(p+"/users/enable", app.EnableDisableUsers) api.POST(p+"/users/enable", app.EnableDisableUsers)
@ -191,6 +191,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.DELETE(p+"/invites", app.DeleteInvite) api.DELETE(p+"/invites", app.DeleteInvite)
api.POST(p+"/invites/profile", app.SetProfile) api.POST(p+"/invites/profile", app.SetProfile)
api.GET(p+"/profiles", app.GetProfiles) api.GET(p+"/profiles", app.GetProfiles)
api.GET(p+"/profiles/names", app.GetProfileNames)
api.POST(p+"/profiles/default", app.SetDefaultProfile) api.POST(p+"/profiles/default", app.SetDefaultProfile)
api.POST(p+"/profiles", app.CreateProfile) api.POST(p+"/profiles", app.CreateProfile)
api.DELETE(p+"/profiles", app.DeleteProfile) api.DELETE(p+"/profiles", app.DeleteProfile)

View File

@ -33,7 +33,7 @@ function fixHTML(infile, outfile) {
} }
} }
let doc = new parser.load(f); let doc = new parser.load(f);
for (let item of ["badge", "chip", "shield", "input", "table", "button", "portal", "select", "aside", "card", "field", "textarea"]) { for (let item of ["badge", "chip", "shield", "input", "table", "button", "portal", "select", "aside", "card", "field", "textarea", "section"]) {
let items = doc("."+item); let items = doc("."+item);
items.each((i, elem) => { items.each((i, elem) => {
let hasColor = false; let hasColor = false;
@ -50,8 +50,8 @@ function fixHTML(infile, outfile) {
} }
if (!hasColor) { if (!hasColor) {
if (!hasDark(doc(elem))) { if (!hasDark(doc(elem))) {
// card without ~neutral look different than with. // card (and sections in sectioned cards) without ~neutral look different than with.
if (item != "card") doc(elem).addClass("~neutral"); if (item != "card" && item != "section") doc(elem).addClass("~neutral");
doc(elem).addClass("dark:~d_neutral"); doc(elem).addClass("dark:~d_neutral");
} }
} }

View File

@ -6,7 +6,7 @@ import { inviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js"; import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js"; import { settingsList } from "./modules/settings.js";
import { activityList } from "./modules/activity.js"; import { activityList } from "./modules/activity.js";
import { ProfileEditor } from "./modules/profiles.js"; import { ProfileEditor, reloadProfileNames } from "./modules/profiles.js";
import { _get, _post, notificationBox, whichAnimationEvent, bindManualDropdowns } from "./modules/common.js"; import { _get, _post, notificationBox, whichAnimationEvent, bindManualDropdowns } from "./modules/common.js";
import { Updater } from "./modules/update.js"; import { Updater } from "./modules/update.js";
import { Login } from "./modules/login.js"; import { Login } from "./modules/login.js";
@ -187,7 +187,7 @@ login.onLogin = () => {
console.log("Logged in."); console.log("Logged in.");
window.updater = new Updater(); window.updater = new Updater();
// FIXME: Decide whether to autoload activity or not // FIXME: Decide whether to autoload activity or not
window.invites.reload() reloadProfileNames();
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000); setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current; const currentTab = window.tabs.current;
switch (currentTab) { switch (currentTab) {

View File

@ -289,7 +289,7 @@ const create = (event: SubmitEvent) => {
send.captcha_text = captcha.input.value; send.captcha_text = captcha.input.value;
} }
} }
_post("/newUser", send, (req: XMLHttpRequest) => { _post("/user/invite", send, (req: XMLHttpRequest) => {
if (req.readyState != 4) return; if (req.readyState != 4) return;
removeLoader(submitSpan); removeLoader(submitSpan);
let vals = req.response as ValidatorRespDTO; let vals = req.response as ValidatorRespDTO;

View File

@ -1128,7 +1128,7 @@ export class accountsList {
} }
} }
toggleLoader(button); toggleLoader(button);
_post("/users", send, (req: XMLHttpRequest) => { _post("/user", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
toggleLoader(button); toggleLoader(button);
if (req.status == 200 || (req.response["user"] as boolean)) { if (req.status == 200 || (req.response["user"] as boolean)) {

View File

@ -1,5 +1,6 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js"; import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js"; import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { reloadProfileNames } from "../modules/profiles.js";
class DOMInvite implements Invite { class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => { updateNotify = (checkbox: HTMLInputElement) => {
@ -431,7 +432,6 @@ export class inviteList implements inviteList {
private _list: HTMLDivElement; private _list: HTMLDivElement;
private _empty: boolean; private _empty: boolean;
// since invite reload sends profiles, this event it broadcast so the createInvite object can load them. // since invite reload sends profiles, this event it broadcast so the createInvite object can load them.
private _profileLoadEvent = new CustomEvent("profileLoadEvent");
invites: { [code: string]: DOMInvite }; invites: { [code: string]: DOMInvite };
@ -502,13 +502,9 @@ export class inviteList implements inviteList {
this._list.appendChild(domInv.asElement()); this._list.appendChild(domInv.asElement());
} }
reload = (callback?: () => void) => _get("/invites", null, (req: XMLHttpRequest) => { reload = (callback?: () => void) => reloadProfileNames(() => _get("/invites", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
let data = req.response; let data = req.response;
if (req.status == 200) {
window.availableProfiles = data["profiles"];
document.dispatchEvent(this._profileLoadEvent);
}
if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) { if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
this.empty = true; this.empty = true;
return; return;
@ -534,7 +530,7 @@ export class inviteList implements inviteList {
if (callback) callback(); if (callback) callback();
} }
}) }));
} }
export const inviteURLEvent = (id: string) => { return new CustomEvent(inviteList._inviteURLEvent, {"detail": id}) }; export const inviteURLEvent = (id: string) => { return new CustomEvent(inviteList._inviteURLEvent, {"detail": id}) };

View File

@ -1,5 +1,13 @@
import { _get, _post, _delete, toggleLoader } from "../modules/common.js"; import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
export const profileLoadEvent = new CustomEvent("profileLoadEvent");
export const reloadProfileNames = (then?: () => void) => _get("/profiles/names", null, (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
window.availableProfiles = req.response["profiles"];
document.dispatchEvent(profileLoadEvent);
if (then) then();
});
interface Profile { interface Profile {
admin: boolean; admin: boolean;
libraries: string; libraries: string;

View File

@ -1,6 +1,7 @@
export class ThemeManager { export class ThemeManager {
private _themeButton: HTMLElement = null; private _themeButton: HTMLElement = null;
private _metaTag: HTMLMetaElement;
private _beforeTransition = () => { private _beforeTransition = () => {
const doc = document.documentElement; const doc = document.documentElement;
@ -45,6 +46,7 @@ export class ThemeManager {
} }
constructor(button?: HTMLElement) { constructor(button?: HTMLElement) {
this._metaTag = document.querySelector("meta[name=color-scheme]") as HTMLMetaElement;
const theme = localStorage.getItem("theme"); const theme = localStorage.getItem("theme");
if (theme == "dark") { if (theme == "dark") {
this._enable(true); this._enable(true);
@ -54,18 +56,22 @@ export class ThemeManager {
this._enable(true); this._enable(true);
} }
if (arguments.length == 1) if (button)
this.bindButton(button); this.bindButton(button);
} }
private _toggle = () => { private _toggle = () => {
let metaValue = "light dark";
this._beforeTransition(); this._beforeTransition();
if (!document.documentElement.classList.contains('dark')) { if (!document.documentElement.classList.contains('dark')) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
metaValue = "dark light";
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? "dark" : "light"); localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? "dark" : "light");
// this._metaTag.setAttribute("content", metaValue);
}; };
private _enable = (dark: boolean) => { private _enable = (dark: boolean) => {
@ -80,6 +86,8 @@ export class ThemeManager {
document.documentElement.classList.remove(opposite); document.documentElement.classList.remove(opposite);
} }
document.documentElement.classList.add(mode); document.documentElement.classList.add(mode);
// this._metaTag.setAttribute("content", `${mode} ${opposite}`);
}; };
enable = (dark: boolean) => { enable = (dark: boolean) => {

View File

@ -1,5 +1,6 @@
import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js"; import { _get, _post, toggleLoader, notificationBox } from "./modules/common.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { ThemeManager } from "./modules/theme.js";
interface sWindow extends Window { interface sWindow extends Window {
messages: {}; messages: {};
@ -8,6 +9,7 @@ interface sWindow extends Window {
declare var window: sWindow; declare var window: sWindow;
window.URLBase = ""; window.URLBase = "";
const theme = new ThemeManager(document.getElementById("button-theme"));
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
@ -68,7 +70,16 @@ class Checkbox {
constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) { constructor(el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
this._el = el as HTMLInputElement; this._el = el as HTMLInputElement;
this._hideEl = this._el as HTMLElement; this._hideEl = this._el as HTMLElement;
if (this._hideEl.parentElement.tagName == "LABEL") { this._hideEl = this._hideEl.parentElement; } if (this._hideEl.parentElement.tagName == "LABEL") {
this._hideEl = this._hideEl.parentElement;
} else if (this._hideEl.parentElement.classList.contains("switch")) {
if (this._hideEl.parentElement.parentElement.tagName == "LABEL") {
this._hideEl = this._hideEl.parentElement.parentElement;
} else {
this._hideEl = this._hideEl.parentElement;
}
}
if (section && setting) { if (section && setting) {
this._section = section; this._section = section;
this._setting = setting; this._setting = setting;
@ -86,11 +97,11 @@ class Checkbox {
}); });
} }
if (this._el.hasAttribute("checked")) { /* if (this._el.hasAttribute("checked")) {
this._el.checked = true; this._el.checked = true;
} else { } else {
this._el.checked = false; this._el.checked = false;
} } */
this.broadcast(); this.broadcast();
} }
} }
@ -217,7 +228,7 @@ class LangSelect extends Select {
} }
this.value = "en-us"; this.value = "en-us";
} }
}); }, true);
} }
} }
@ -448,23 +459,32 @@ settings["email"]["method"].onchange = emailMethodChange;
settings["messages"]["enabled"].onchange = emailMethodChange; settings["messages"]["enabled"].onchange = emailMethodChange;
emailMethodChange(); emailMethodChange();
const getParentCard = (el: HTMLElement): HTMLDivElement => {
let pEl = el.parentElement;
while (pEl.tagName != "html") {
if (pEl.classList.contains("card")) return pEl as HTMLDivElement;
pEl = pEl.parentElement;
}
return pEl as HTMLDivElement;
};
const jellyfinLoginAccessChange = () => { const jellyfinLoginAccessChange = () => {
const adminOnly = settings["ui"]["admin_only"].value == "true"; const adminOnly = settings["ui"]["admin_only"].value == "true";
const allowAll = settings["ui"]["allow_all"].value == "true"; const allowAll = settings["ui"]["allow_all"].value == "true";
const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement; const adminOnlyEl = document.getElementById("ui-admin_only") as HTMLInputElement;
const allowAllEls = [document.getElementById("ui-allow_all"), document.getElementById("description-ui-allow_all")]; const allowAllEl = document.getElementById("ui-allow_all") as HTMLInputElement;
const nextButton = adminOnlyEl.parentElement.parentElement.parentElement.querySelector("span.next") as HTMLSpanElement; const nextButton = getParentCard(adminOnlyEl).querySelector("span.next") as HTMLSpanElement;
if (adminOnly && !allowAll) { if (adminOnly && !allowAll) {
(allowAllEls[0] as HTMLInputElement).disabled = true; allowAllEl.disabled = true;
adminOnlyEl.disabled = false; adminOnlyEl.disabled = false;
nextButton.removeAttribute("disabled"); nextButton.removeAttribute("disabled");
} else if (!adminOnly && allowAll) { } else if (!adminOnly && allowAll) {
adminOnlyEl.disabled = true; adminOnlyEl.disabled = true;
(allowAllEls[0] as HTMLInputElement).disabled = false; allowAllEl.disabled = false;
nextButton.removeAttribute("disabled"); nextButton.removeAttribute("disabled");
} else { } else {
adminOnlyEl.disabled = false; adminOnlyEl.disabled = false;
(allowAllEls[0] as HTMLInputElement).disabled = false; allowAllEl.disabled = false;
nextButton.setAttribute("disabled", "true") nextButton.setAttribute("disabled", "true")
} }
}; };
@ -495,18 +515,17 @@ for (let section in settings) {
const pageNames: string[][] = []; const pageNames: string[][] = [];
window.history.replaceState("welcome", "Setup - jfa-go"); (() => {
const pushState = window.history.pushState;
window.history.pushState = function (data: any, __: string, _: string | URL) {
pushState.apply(window.history, arguments);
let ev = { state: data as string } as PopStateEvent;
window.onpopstate(ev);
};
})();
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) => { window.onpopstate = (event: PopStateEvent) => {
console.log("CALLLLLLLL", event);
if (event.state === "welcome") { if (event.state === "welcome") {
cards[0].classList.remove("unfocused"); cards[0].classList.remove("unfocused");
for (let i = 1; i < cards.length; i++) { cards[i].classList.add("unfocused"); } for (let i = 1; i < cards.length; i++) { cards[i].classList.add("unfocused"); }
@ -521,6 +540,35 @@ window.onpopstate = (event: PopStateEvent) => {
} }
}; };
const cards = Array.from(document.getElementsByClassName("page-container")[0].querySelectorAll(".card.sectioned")) as Array<HTMLDivElement>;
(window as any).cards = cards;
const changePageToIndex = (title: string, pageTitle: string, i: number) => {
cards[i].classList.remove("unfocused");
const urlParams = new URLSearchParams(window.location.search);
const lang = urlParams.get("lang");
let page = "/#" + title;
if (lang) { page += "?lang=" + lang; }
console.log("pushing", title, pageTitle, page);
window.history.pushState(title || "welcome", pageTitle, page);
};
const changePage = (title: string, pageTitle: string) => {
let found = false;
for (let i = 0; i < cards.length; i++) {
if (!found && pageNames[i][0] == title && !(cards[i].classList.contains("hidden"))) {
found = true;
changePageToIndex(title, pageTitle, i);
} else {
cards[i].classList.add("unfocused");
}
}
if (!found) {
changePageToIndex(title, pageTitle, 0);
}
window.scrollTo(0, 0);
};
(() => { (() => {
for (let i = 0; i < cards.length; i++) { for (let i = 0; i < cards.length; i++) {
const card = cards[i]; const card = cards[i];
@ -534,36 +582,34 @@ window.onpopstate = (event: PopStateEvent) => {
let pageTitle = titleEl.textContent + " - jfa-go"; let pageTitle = titleEl.textContent + " - jfa-go";
pageNames.push([title, pageTitle]); pageNames.push([title, pageTitle]);
if (back) { back.addEventListener("click", () => { if (back) { back.addEventListener("click", () => {
let found = false;
for (let ind = cards.length - 1; ind >= 0; ind--) { for (let ind = cards.length - 1; ind >= 0; ind--) {
cards[ind].classList.add("unfocused"); if (ind < i && !(cards[ind].classList.contains("hidden"))) {
if (ind < i && !(cards[ind].classList.contains("hidden")) && !found) {
cards[ind].classList.remove("unfocused");
changePage(pageNames[ind][0], pageNames[ind][1]); changePage(pageNames[ind][0], pageNames[ind][1]);
found = true; break;
} }
} }
window.scrollTo(0, 0);
}); } }); }
if (next) { if (next) {
const func = () => { const func = () => {
if (next.hasAttribute("disabled")) return; if (next.hasAttribute("disabled")) return;
let found = false;
for (let ind = 0; ind < cards.length; ind++) { for (let ind = 0; ind < cards.length; ind++) {
cards[ind].classList.add("unfocused"); if (ind > i && !(cards[ind].classList.contains("hidden"))) {
if (ind > i && !(cards[ind].classList.contains("hidden")) && !found) {
cards[ind].classList.remove("unfocused");
changePage(pageNames[ind][0], pageNames[ind][1]); changePage(pageNames[ind][0], pageNames[ind][1]);
found = true; break;
} }
} }
window.scrollTo(0, 0);
}; };
next.addEventListener("click", func) next.addEventListener("click", func)
} }
} }
})(); })();
(() => {
let initialLocation = window.location.hash.replace("#", "") || "welcome";
changePage(initialLocation, "Setup - jfa-go");
})();
// window.history.replaceState("welcome", "Setup - jfa-go",);
(() => { (() => {
const button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement; const button = document.getElementById("jellyfin-test-connection") as HTMLSpanElement;
const ogText = button.textContent; const ogText = button.textContent;