1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 17:10:11 +00:00

Compare commits

..

No commits in common. "618cc32a17f94d7caca5fab927d0a12207c69ce7" and "cd67d3e7ab8a7384a3dfd7a85f947417953a2f75" have entirely different histories.

20 changed files with 60 additions and 117 deletions

Binary file not shown.

View File

@ -20,12 +20,12 @@ steps:
- apt install build-essential python3-pip curl software-properties-common sed upx -y - apt install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -) - (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt install nodejs - apt install nodejs
- curl -sL https://git.io/goreleaser > ../goreleaser - curl -sL https://git.io/goreleaser > goreleaser
- chmod +x ../goreleaser - chmod +x goreleaser
- ./scripts/version.sh ../goreleaser - ./scripts/version.sh ./goreleaser
- wget https://builds.hrfee.pw/upload.py -P ../ - wget https://builds.hrfee.pw/upload.py
- pip3 install requests - pip3 install requests
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true' - bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
trigger: trigger:
event: event:
- tag - tag
@ -40,6 +40,9 @@ steps:
volumes: volumes:
- name: ssh_key - name: ssh_key
path: /root/drone_rsa path: /root/drone_rsa
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
settings: settings:
host: host:
from_secret: ssh2_host from_secret: ssh2_host
@ -51,6 +54,8 @@ steps:
- /root/.ssh/docker-build:/root/drone_rsa - /root/.ssh/docker-build:/root/drone_rsa
key_path: /root/drone_rsa key_path: /root/drone_rsa
command_timeout: 50m command_timeout: 50m
envs:
- BUILDRONE_KEY
script: script:
- /mnt/buildx/jfa-go/build.sh stable - /mnt/buildx/jfa-go/build.sh stable
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py - wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py

View File

@ -16,7 +16,6 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
* Granular control over invites: Validity period as well as number of uses can be specified. * Granular control over invites: Validity period as well as number of uses can be specified.
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation. * Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
* Password validation: Ensure users choose a strong password. * Password validation: Ensure users choose a strong password.
* ⌛ User expiry: Specify a validity period, and new user's accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions. * 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason. * Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation. * 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
@ -27,7 +26,6 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider. * Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
* Enables the usage of jfa-go by multiple people * Enables the usage of jfa-go by multiple people
* 🌓 Customizable look * 🌓 Customizable look
* Edit emails with variables and markdown
* Specify contact and help messages to appear in emails and pages * Specify contact and help messages to appear in emails and pages
* Light and dark themes available * Light and dark themes available
@ -37,9 +35,8 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
</p> </p>
<p align="center"> <p align="center">
<img src="images/invites.png" width="31%" style="margin-left: 1.5%;" alt="Invites tab"></img> <img src="images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
<img src="images/accounts.png" width="31%" style="margin-right: 1.5%;" alt="Accounts tab"></img> <img src="images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
<img src="images/create.png" width="31%" style="margin-right: 1.5%;" alt="Accounts creation"></img>
</p> </p>
#### Install #### Install

9
api.go
View File

@ -1246,15 +1246,6 @@ func (app *appContext) GetConfig(gc *gin.Context) {
el := resp.Sections["email"].Settings["language"] el := resp.Sections["email"].Settings["language"]
el.Options = emailOptions el.Options = emailOptions
el.Value = app.config.Section("email").Key("language").MustString("en-us") el.Value = app.config.Section("email").Key("language").MustString("en-us")
if updater == "" {
delete(resp.Sections, "updates")
for i, v := range resp.Order {
if v == "updates" {
resp.Order = append(resp.Order[:i], resp.Order[i+1:]...)
break
}
}
}
for sectName, section := range resp.Sections { for sectName, section := range resp.Sections {
for settingName, setting := range section.Settings { for settingName, setting := range section.Settings {
val := app.config.Section(sectName).Key(settingName) val := app.config.Section(sectName).Key(settingName)

View File

@ -153,10 +153,6 @@ div.card:contains(section.banner.footer) {
margin: 0.5rem; margin: 0.5rem;
} }
.col.sm {
margin: .25rem;
}
.flex-col { .flex-col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -438,19 +434,18 @@ pre {
overflow-y: scroll; overflow-y: scroll;
} }
a:link:not(.lang-link):not(.\~urge) { a:link {
color: var(--color-urge-200); color: var(--color-urge-200);
} }
a:visited:not(.lang-link):not(.\~urge) { a:visited {
color: var(--color-urge-100); color: var(--color-urge-100);
} }
a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) { a:hover, a:active {
color: var(--color-urge-200); color: var(--color-urge-200);
} }
.search { .search {
max-width: 15rem; max-width: 15rem;
min-width: 10rem;
} }

View File

@ -305,12 +305,12 @@
<span class="heading">{{ .strings.create }}</span> <span class="heading">{{ .strings.create }}</span>
<div class="row" id="create-inv"> <div class="row" id="create-inv">
<div class="card ~neutral !normal col"> <div class="card ~neutral !normal col">
<div class="row mb-1"> <div class="flex-row mb-1">
<label class="col mr-1"> <label class="flex-row-group mr-1">
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked> <input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
<span class="button ~neutral !high supra full-width center">{{ .strings.inviteDuration }}</span> <span class="button ~neutral !high supra full-width center">{{ .strings.inviteDuration }}</span>
</label> </label>
<label class="col ml-1"> <label class="flex-row-group ml-1">
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry"> <input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
<span class="button ~neutral !normal supra full-width center">{{ .strings.userExpiry }}</span> <span class="button ~neutral !normal supra full-width center">{{ .strings.userExpiry }}</span>
</label> </label>
@ -396,17 +396,17 @@
</div> </div>
<div id="tab-accounts" class="unfocused"> <div id="tab-accounts" class="unfocused">
<div class="card ~neutral !low accounts mb-1"> <div class="card ~neutral !low accounts mb-1">
<div class="flex-expand row"> <div class="flex-expand">
<div class="row"> <div class="flex-row">
<span class="heading mr-1 col sm">{{ .strings.accounts }}</span> <span class="heading">{{ .strings.accounts }}</span>
<input type="search" class="col sm field ~neutral !normal input search ml-1 mr-1" id="accounts-search" placeholder="{{ .strings.search }}"> <input type="search" class="field ~neutral !normal input search ml-1" id="accounts-search" placeholder="{{ .strings.search }}">
</div> </div>
<div class="row"> <div>
<span class="col sm button ~neutral !normal center mb-half" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span> <span class="button ~neutral !normal mb-half" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
<span class="col sm button ~info !normal center mb-half" id="accounts-announce">{{ .strings.announce }}</span> <span class="button ~info !normal mb-half" id="accounts-announce">{{ .strings.announce }}</span>
<span class="col sm button ~urge !normal center mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span> <span class="button ~urge !normal mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="col sm button ~warning !normal center mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span> <span class="button ~warning !normal mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<span class="col sm button ~critical !normal center mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span> <span class="button ~critical !normal mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
</div> </div>
</div> </div>
<div class="card ~neutral !normal accounts-header table-responsive mt-half"> <div class="card ~neutral !normal accounts-header table-responsive mt-half">

View File

@ -82,20 +82,6 @@
<span class="mt-half">{{ .lang.General.pathToKeyFile }}</span> <span class="mt-half">{{ .lang.General.pathToKeyFile }}</span>
<input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_key"> <input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_key">
</label> </label>
<span class="heading">{{ .lang.Updates.title }}</span>
<p class="content" id="updates-description"></p>
<label class="row switch pb-1">
<input type="checkbox" id="updates-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
</label>
<label class="label">
<span>{{ .lang.Updates.updateChannel }}</span>
<div class="select ~neutral !normal mt-half mb-1">
<select id="updates-channel">
<option value="stable">{{ .lang.Updates.stable }}</option>
<option value="unstable">{{ .lang.Updates.unstable }}</option>
</select>
</div>
</label>
</div> </div>
<div class="col"> <div class="col">
<label class="label"> <label class="label">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -94,7 +94,6 @@ type setupLang struct {
StartPage langSection `json:"startPage"` StartPage langSection `json:"startPage"`
EndPage langSection `json:"endPage"` EndPage langSection `json:"endPage"`
General langSection `json:"general"` General langSection `json:"general"`
Updates langSection `json:"updates"`
Language langSection `json:"language"` Language langSection `json:"language"`
Login langSection `json:"login"` Login langSection `json:"login"`
JellyfinEmby langSection `json:"jellyfinEmby"` JellyfinEmby langSection `json:"jellyfinEmby"`

View File

@ -87,8 +87,7 @@
"updates": "Updates", "updates": "Updates",
"update": "Bijwerken", "update": "Bijwerken",
"download": "Download", "download": "Download",
"search": "Zoeken", "search": "Zoeken"
"advancedSettings": "Geavanceerde instellingen"
}, },
"notifications": { "notifications": {
"changedEmailAddress": "E-mailadres van {n} gewijzigd.", "changedEmailAddress": "E-mailadres van {n} gewijzigd.",

View File

@ -48,13 +48,6 @@
"pathToCertificate": "Path to certificate", "pathToCertificate": "Path to certificate",
"pathToKeyFile": "Path to key file" "pathToKeyFile": "Path to key file"
}, },
"updates": {
"title": "Updates",
"description": "Enable to be notified when new updates are available. jfa-go will check {n} every 30 minutes. No IPs or personally identifiable information are collected.",
"updateChannel": "Update Channel",
"stable": "Stable",
"unstable": "Unstable"
},
"login": { "login": {
"title": "Login", "title": "Login",
"description": "To access the admin page, you need to login with a method below:", "description": "To access the admin page, you need to login with a method below:",

64
main.go
View File

@ -152,8 +152,7 @@ func start(asDaemon, firstCall bool) {
set default config and data paths set default config and data paths
data: Contains invites.json, emails.json, user_profile.json, etc. data: Contains invites.json, emails.json, user_profile.json, etc.
config: config.ini. Usually in data, but can be changed via -config. config: config.ini. Usually in data, but can be changed via -config.
localFS: jfa-go's internal data. On internal builds, this is contained within the binary. localFS is jfa-go's internal data. On external builds, the directory is named "data" and placed next to the executable.
On external builds, the directory is named "data" and placed next to the executable.
*/ */
userConfigDir, _ := os.UserConfigDir() userConfigDir, _ := os.UserConfigDir()
app.dataPath = filepath.Join(userConfigDir, "jfa-go") app.dataPath = filepath.Join(userConfigDir, "jfa-go")
@ -165,7 +164,7 @@ func start(asDaemon, firstCall bool) {
} }
app.info = NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite) app.info = NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
app.err = NewLogger(os.Stdout, "[ERROR] ", log.Ltime, color.FgRed) app.err = NewLogger(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile, color.FgRed)
if firstCall { if firstCall {
DATA = flag.String("data", app.dataPath, "alternate path to data directory.") DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
@ -201,7 +200,8 @@ func start(asDaemon, firstCall bool) {
app.dataPath = *DATA app.dataPath = *DATA
} }
// Previously used for self-restarts but leaving them here as they might be useful. // env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
if v := os.Getenv("JFA_CONFIGPATH"); v != "" { if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
app.configPath = v app.configPath = v
} }
@ -254,7 +254,6 @@ func start(asDaemon, firstCall bool) {
app.debug = emptyLogger(false) app.debug = emptyLogger(false)
} }
// Starts listener to receive commands over a unix socket. Use with 'jfa-go start/stop'
if asDaemon { if asDaemon {
go func() { go func() {
socket := SOCK socket := SOCK
@ -385,7 +384,6 @@ func start(asDaemon, firstCall bool) {
} }
// Read config-base for settings on web.
app.configBasePath = "config-base.json" app.configBasePath = "config-base.json"
configBase, _ := fs.ReadFile(localFS, app.configBasePath) configBase, _ := fs.ReadFile(localFS, app.configBasePath)
json.Unmarshal(configBase, &app.configBase) json.Unmarshal(configBase, &app.configBase)
@ -394,7 +392,6 @@ func start(asDaemon, firstCall bool) {
"Jellyfin (Dark)": "dark-theme", "Jellyfin (Dark)": "dark-theme",
"Default (Light)": "light-theme", "Default (Light)": "light-theme",
} }
// For move from Bootstrap to a17t
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" { if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
app.config.Section("ui").Key("theme").SetValue("Default (Light)") app.config.Section("ui").Key("theme").SetValue("Default (Light)")
} }
@ -406,8 +403,18 @@ func start(asDaemon, firstCall bool) {
app.err.Fatal(err) app.err.Fatal(err)
} }
os.Setenv("JFA_SECRET", secret) os.Setenv("JFA_SECRET", secret)
app.jellyfinLogin = true
if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val {
app.jellyfinLogin = false
user := User{}
user.UserID = shortuuid.New()
user.Username = app.config.Section("ui").Key("username").String()
user.Password = app.config.Section("ui").Key("password").String()
app.users = append(app.users, user)
} else {
app.debug.Println("Using Jellyfin for authentication")
}
// Initialize jellyfin/emby connection
server := app.config.Section("jellyfin").Key("server").String() server := app.config.Section("jellyfin").Key("server").String()
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30)) cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
stringServerType := app.config.Section("jellyfin").Key("type").String() stringServerType := app.config.Section("jellyfin").Key("type").String()
@ -437,8 +444,7 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status) app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
} }
app.info.Printf("Authenticated with %s", server) app.info.Printf("Authenticated with %s", server)
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs. // from 10.7.0, jellyfin may hyphenate user IDs. This checks if the version is equal or higher.
This checks if the version is equal or higher. */
checkVersion := func(version string) int { checkVersion := func(version string) int {
numberStrings := strings.Split(version, ".") numberStrings := strings.Split(version, ".")
n := 0 n := 0
@ -492,40 +498,27 @@ func start(asDaemon, firstCall bool) {
} }
} }
// Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true
if jfLogin, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !jfLogin {
app.jellyfinLogin = false
user := User{}
user.UserID = shortuuid.New()
user.Username = app.config.Section("ui").Key("username").String()
user.Password = app.config.Section("ui").Key("password").String()
app.users = append(app.users, user)
} else {
app.debug.Println("Using Jellyfin for authentication")
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
}
// 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)
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
app.loadStrftime() app.loadStrftime()
var validatorConf ValidatorConf validatorConf := ValidatorConf{
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
"number": app.config.Section("password_validation").Key("number").MustInt(0),
"special": app.config.Section("password_validation").Key("special").MustInt(0),
}
if !app.config.Section("password_validation").Key("enabled").MustBool(false) { if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
validatorConf = ValidatorConf{} for key := range validatorConf {
} else { validatorConf[key] = 0
validatorConf = ValidatorConf{
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
"number": app.config.Section("password_validation").Key("number").MustInt(0),
"special": app.config.Section("password_validation").Key("special").MustInt(0),
} }
} }
app.validator.init(validatorConf) app.validator.init(validatorConf)
// Test mode for testing connection to Jellyfin, accessed with 'jfa-go test'
if TEST { if TEST {
test(app) test(app)
os.Exit(0) os.Exit(0)
@ -553,7 +546,6 @@ func start(asDaemon, firstCall bool) {
app.info.Fatalf("Failed to load language files: %+v\n", err) app.info.Fatalf("Failed to load language files: %+v\n", err)
} }
} }
cssHeader = app.loadCSSHeader() cssHeader = app.loadCSSHeader()
// workaround for potentially broken windows mime types // workaround for potentially broken windows mime types
mime.AddExtensionType(".js", "application/javascript") mime.AddExtensionType(".js", "application/javascript")

View File

@ -89,7 +89,6 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
if fname != "en-us.json" { if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings) patchLang(&english.Strings, &lang.Strings)
patchLang(&english.StartPage, &lang.StartPage) patchLang(&english.StartPage, &lang.StartPage)
patchLang(&english.Updates, &lang.Updates)
patchLang(&english.EndPage, &lang.EndPage) patchLang(&english.EndPage, &lang.EndPage)
patchLang(&english.Language, &lang.Language) patchLang(&english.Language, &lang.Language)
patchLang(&english.Login, &lang.Login) patchLang(&english.Login, &lang.Login)

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/hrfee/jfa-go/mediabrowser" "github.com/hrfee/jfa-go/mediabrowser"
@ -26,7 +25,6 @@ type Storage struct {
policy mediabrowser.Policy policy mediabrowser.Policy
configuration mediabrowser.Configuration configuration mediabrowser.Configuration
lang Lang lang Lang
invitesLock sync.Mutex
} }
type customEmails struct { type customEmails struct {
@ -404,14 +402,10 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
type Invites map[string]Invite type Invites map[string]Invite
func (st *Storage) loadInvites() error { func (st *Storage) loadInvites() error {
st.invitesLock.Lock()
defer st.invitesLock.Unlock()
return loadJSON(st.invite_path, &st.invites) return loadJSON(st.invite_path, &st.invites)
} }
func (st *Storage) storeInvites() error { func (st *Storage) storeInvites() error {
st.invitesLock.Lock()
defer st.invitesLock.Unlock()
return storeJSON(st.invite_path, st.invites) return storeJSON(st.invite_path, st.invites)
} }

View File

@ -56,7 +56,7 @@ export const loadLangSelector = (page: string) => _get("/lang/" + page, null, (r
const list = document.getElementById("lang-list") as HTMLDivElement; const list = document.getElementById("lang-list") as HTMLDivElement;
let innerHTML = ''; let innerHTML = '';
for (let code in req.response) { for (let code in req.response) {
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half lang-link">${req.response[code]}</a>`; innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half">${req.response[code]}</a>`;
} }
list.innerHTML = innerHTML; list.innerHTML = innerHTML;
} }

View File

@ -173,9 +173,7 @@ class Select {
if (depends) { if (depends) {
document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => { document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
let el = this._el as HTMLElement; let el = this._el as HTMLElement;
while (el.tagName != "LABEL") { if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
el = el.parentElement;
}
if (event.detail !== dependsTrue) { if (event.detail !== dependsTrue) {
el.classList.add("unfocused"); el.classList.add("unfocused");
} else { } else {
@ -204,7 +202,6 @@ 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("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>`)); html("email-description", window.lang.var("email", "description", `<a href="https://mailgun.com">Mailgun</a>`));
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a href="https://strftime.ninja/">strftime.ninja</a>`)); html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a href="https://strftime.ninja/">strftime.ninja</a>`));
html("updates-description", window.lang.var("updates", "description", `<a href="https://builds.hrfee.dev/view/hrfee/jfa-go">buildrone</a>`));
const settings = { const settings = {
"jellyfin": { "jellyfin": {
@ -215,10 +212,6 @@ const settings = {
"password": new Input(get("jellyfin-password")), "password": new Input(get("jellyfin-password")),
"substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings")) "substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings"))
}, },
"updates": {
"enabled": new Checkbox(get("updates-enabled"), "", false, "updates", "enabled"),
"channel": new Select(get("updates-channel"), "enabled", true, "updates")
},
"ui": { "ui": {
"host": new Input(get("ui-host")), "host": new Input(get("ui-host")),
"port": new Input(get("ui-port")), "port": new Input(get("ui-port")),

View File

@ -467,7 +467,7 @@ func (app *appContext) checkForUpdates() {
if status != 200 || err != nil { if status != 200 || err != nil {
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") { if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
app.err.Println("No new updates available.") app.err.Println("No new updates available.")
} else if status != -1 { // -1 means updates disabled, we don't need to log it. } else {
app.err.Printf("Failed to get latest tag (%d): %v", status, err) app.err.Printf("Failed to get latest tag (%d): %v", status, err)
} }
return return