mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
3 Commits
a3f5396211
...
563888c5e7
Author | SHA1 | Date | |
---|---|---|---|
563888c5e7 | |||
29fafba035 | |||
75d12b4a5c |
11
api.go
11
api.go
@ -667,6 +667,9 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
|
||||
gc.BindJSON(&req)
|
||||
name := req.Name
|
||||
if _, ok := app.storage.profiles[name]; ok {
|
||||
if app.storage.defaultProfile == name {
|
||||
app.storage.defaultProfile = ""
|
||||
}
|
||||
delete(app.storage.profiles, name)
|
||||
}
|
||||
app.storage.storeProfiles()
|
||||
@ -1097,15 +1100,12 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
s := resp.Sections["ui"].Settings["language"]
|
||||
s.Options = app.lang.langOptions
|
||||
s.Value = app.lang.langOptions[app.lang.chosenIndex]
|
||||
resp.Sections["ui"].Settings["language"] = s
|
||||
for sectName, section := range resp.Sections {
|
||||
for settingName, setting := range section.Settings {
|
||||
val := app.config.Section(sectName).Key(settingName)
|
||||
s := resp.Sections[sectName].Settings[settingName]
|
||||
switch setting.Type {
|
||||
case "text", "email", "select":
|
||||
case "text", "email", "select", "password":
|
||||
s.Value = val.MustString("")
|
||||
case "number":
|
||||
s.Value = val.MustInt(0)
|
||||
@ -1115,6 +1115,9 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
resp.Sections[sectName].Settings[settingName] = s
|
||||
}
|
||||
}
|
||||
s.Options = app.lang.langOptions
|
||||
s.Value = app.lang.langOptions[app.lang.chosenIndex]
|
||||
resp.Sections["ui"].Settings["language"] = s
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
|
@ -81,9 +81,8 @@
|
||||
"requires_restart": true,
|
||||
"type": "select",
|
||||
"options": [
|
||||
"Bootstrap (Light)",
|
||||
"Jellyfin (Dark)",
|
||||
"Custom CSS"
|
||||
"Default (Light)"
|
||||
],
|
||||
"value": "Jellyfin (Dark)",
|
||||
"description": "Default appearance for all users."
|
||||
@ -179,14 +178,6 @@
|
||||
"value": "Your account has been created. Click below to continue to Jellyfin.",
|
||||
"description": "Displayed when a user creates an account"
|
||||
},
|
||||
"bs5": {
|
||||
"name": "Use Bootstrap 5",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use the Bootstrap 5 Alpha. Looks better and removes the need for jQuery, so the page should load faster."
|
||||
},
|
||||
"url_base": {
|
||||
"name": "URL Base",
|
||||
"required": false,
|
||||
@ -685,14 +676,6 @@
|
||||
"value": "",
|
||||
"description": "Location of stored user profiles (encompasses template and configuration and displayprefs) (json)"
|
||||
},
|
||||
"custom_css": {
|
||||
"name": "Custom CSS",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of custom bootstrap CSS."
|
||||
},
|
||||
"html_templates": {
|
||||
"name": "Custom HTML Template Directory",
|
||||
"required": false,
|
||||
|
14
css/base.css
14
css/base.css
@ -68,6 +68,10 @@
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ml-half {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
@ -96,6 +100,10 @@
|
||||
align-items: top;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-expand {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -208,6 +216,12 @@ sup.\~critical, .text-critical {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-pad {
|
||||
padding: 0px 0px 0px 0px;
|
||||
}
|
||||
|
@ -40,12 +40,22 @@
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.modal-content.wide {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.modal-shown .modal-content {
|
||||
animation: modal-content-show 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.modal-content.wide {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.modal-content {
|
||||
.modal-content, .modal-content.wide {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="light-theme">
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
|
||||
@ -101,17 +101,17 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-restart" class="modal">
|
||||
<div class="modal-content card ~critical !normal">
|
||||
<div class="modal-content card ~critical !low">
|
||||
<span class="heading">Restart needed <span class="modal-close">×</span></span>
|
||||
<p class="content pb-1">A restart is needed to apply some settings you changed. Do it now or later?</p>
|
||||
<div class="fr">
|
||||
<span class="button ~info !normal">Apply, restart later</span>
|
||||
<span class="button ~critical !normal">Apply & restart</span>
|
||||
<span class="button ~info !normal" id="settings-apply-no-restart">Apply, restart later</span>
|
||||
<span class="button ~critical !normal" id="settings-apply-restart">Apply & restart</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-refresh" class="modal">
|
||||
<div class="modal-content card ~urge !normal">
|
||||
<div class="modal-content card ~neutral !normal">
|
||||
<span class="heading">Settings applied.</span>
|
||||
<p class="content">Refresh the page in a few seconds.</p>
|
||||
</div>
|
||||
@ -132,6 +132,51 @@
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-user-profiles" class="modal">
|
||||
<div class="modal-content wide card">
|
||||
<span class="heading">User profiles <span class="modal-close">×</span></span>
|
||||
<p class="support lg">Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Default</th>
|
||||
<th>From</th>
|
||||
<th>Libraries</th>
|
||||
<th><span class="button ~neutral !high" id="button-profile-create">Create</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-profiles">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-add-profile" class="modal">
|
||||
<form class="modal-content card" id="form-add-profile" href="">
|
||||
<span class="heading">Add profile <span class="modal-close">×</span></span>
|
||||
<p class="content">Create a Jellyfin user and configure it. Select it here, and when this profile is applied to an invite, new users will be created with its settings.</p>
|
||||
<label>
|
||||
<span class="supra">Profile Name </span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="Name" id="add-profile-name">
|
||||
<label>
|
||||
<span class="supra">User</span>
|
||||
<div class="select ~neutral !normal mt-half mb-1">
|
||||
<select id="add-profile-user">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="switch mb-1">
|
||||
<input type="checkbox" id="add-profile-homescreen" checked>
|
||||
<span>Store homescreen layout</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container max-w-screen-lg px-6 py-4 mx-auto lg:mx-auto md:py-8">
|
||||
<div class="mb-1">
|
||||
@ -230,17 +275,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="settingsTab" class="unfocused">
|
||||
<div class="card ~neutral !low settings">
|
||||
<div class="card ~neutral !low settings overflow">
|
||||
<span class="heading">Settings</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal" id="accounts-add-user">Save</span>
|
||||
<span class="button ~neutral !normal unfocused" id="settings-save">Save</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="card ~neutral !normal col" id="settings-sidebar">
|
||||
<aside class="aside sm ~info mb-half">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~critical">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-about">About</span>
|
||||
<aside class="aside sm ~info mb-half">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-about"><span class="flex">About <i class="ri-information-line ml-half"></i></span></span>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-profiles"><span class="flex">User profiles <i class="ri-user-line ml-half"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col" id="settings-panel"></div>
|
||||
<div class="card ~neutral !normal col overflow" id="settings-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="light-theme">
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
{{ template "header.html" . }}
|
||||
|
22
main.go
22
main.go
@ -51,8 +51,7 @@ type appContext struct {
|
||||
configBase settings
|
||||
dataPath string
|
||||
localPath string
|
||||
cssFile string
|
||||
bsVersion int
|
||||
cssClass string
|
||||
jellyfinLogin bool
|
||||
users []User
|
||||
invalidTokens []string
|
||||
@ -362,14 +361,6 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
||||
|
||||
if app.config.Section("ui").Key("bs5").MustBool(false) {
|
||||
app.cssFile = "bs5-jf.css"
|
||||
app.bsVersion = 5
|
||||
} else {
|
||||
app.cssFile = "bs4-jf.css"
|
||||
app.bsVersion = 4
|
||||
}
|
||||
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
app.storage.invite_path = app.config.Section("files").Key("invites").String()
|
||||
@ -420,14 +411,15 @@ func start(asDaemon, firstCall bool) {
|
||||
json.Unmarshal(configBase, &app.configBase)
|
||||
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion),
|
||||
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion),
|
||||
"Custom CSS": "",
|
||||
"Jellyfin (Dark)": "dark-theme",
|
||||
"Default (Light)": "light-theme",
|
||||
}
|
||||
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
||||
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
||||
}
|
||||
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
|
||||
app.cssFile = val
|
||||
app.cssClass = val
|
||||
}
|
||||
app.debug.Printf("Using css file \"%s\"", app.cssFile)
|
||||
secret, err := generateSecret(16)
|
||||
if err != nil {
|
||||
app.err.Fatal(err)
|
||||
|
@ -4,6 +4,7 @@ import { Tabs } from "./modules/tabs.js";
|
||||
import { inviteList, createInvite } from "./modules/invites.js";
|
||||
import { accountsList } from "./modules/accounts.js";
|
||||
import { settingsList } from "./modules/settings.js";
|
||||
import { ProfileEditor } from "./modules/profiles.js";
|
||||
import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
|
||||
|
||||
loadTheme();
|
||||
@ -36,6 +37,10 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
|
||||
window.modals.ombiDefaults = new Modal(document.getElementById('modal-ombi-defaults'));
|
||||
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close);
|
||||
|
||||
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));
|
||||
|
||||
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
@ -45,6 +50,8 @@ window.invites = new inviteList();
|
||||
|
||||
var settings = new settingsList();
|
||||
|
||||
var profiles = new ProfileEditor();
|
||||
|
||||
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
|
||||
|
||||
/*const modifySettingsSource = function () {
|
||||
|
204
ts/modules/profiles.ts
Normal file
204
ts/modules/profiles.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
|
||||
|
||||
interface Profile {
|
||||
admin: boolean;
|
||||
libraries: string;
|
||||
fromUser: string;
|
||||
}
|
||||
|
||||
class profile implements Profile {
|
||||
private _row: HTMLTableRowElement;
|
||||
private _name: HTMLElement;
|
||||
private _adminChip: HTMLSpanElement;
|
||||
private _libraries: HTMLTableDataCellElement;
|
||||
private _fromUser: HTMLTableDataCellElement;
|
||||
private _defaultRadio: HTMLInputElement;
|
||||
|
||||
get name(): string { return this._name.textContent; }
|
||||
set name(v: string) { this._name.textContent = v; }
|
||||
|
||||
get admin(): boolean { return this._adminChip.classList.contains("chip"); }
|
||||
set admin(state: boolean) {
|
||||
if (state) {
|
||||
this._adminChip.classList.add("chip", "~info", "ml-half");
|
||||
this._adminChip.textContent = "Admin";
|
||||
} else {
|
||||
this._adminChip.classList.remove("chip", "~info", "ml-half");
|
||||
this._adminChip.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
get libraries(): string { return this._libraries.textContent; }
|
||||
set libraries(v: string) { this._libraries.textContent = v; }
|
||||
|
||||
get fromUser(): string { return this._fromUser.textContent; }
|
||||
set fromUser(v: string) { this._fromUser.textContent = v; }
|
||||
|
||||
get default(): boolean { return this._defaultRadio.checked; }
|
||||
set default(v: boolean) { this._defaultRadio.checked = v; }
|
||||
|
||||
constructor(name: string, p: Profile) {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.innerHTML = `
|
||||
<td><b class="profile-name"></b> <span class="profile-admin"></span></td>
|
||||
<td><input type="radio" name="profile-default"></td>
|
||||
<td class="profile-from ellipsis"></td>
|
||||
<td class="profile-libraries"></td>
|
||||
<td><span class="button ~critical !normal">Delete</span></td>
|
||||
`;
|
||||
this._name = this._row.querySelector("b.profile-name");
|
||||
this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement;
|
||||
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
|
||||
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
|
||||
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
|
||||
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
|
||||
(this._row.querySelector("span.button") as HTMLSpanElement).onclick = this.delete;
|
||||
|
||||
this.update(name, p);
|
||||
}
|
||||
|
||||
update = (name: string, p: Profile) => {
|
||||
this.name = name;
|
||||
this.admin = p.admin;
|
||||
this.fromUser = p.fromUser;
|
||||
this.libraries = p.libraries;
|
||||
}
|
||||
|
||||
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
|
||||
|
||||
delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
this.remove();
|
||||
} else {
|
||||
window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||
}
|
||||
|
||||
interface profileResp {
|
||||
default_profile: string;
|
||||
profiles: { [name: string]: Profile };
|
||||
}
|
||||
|
||||
export class ProfileEditor {
|
||||
private _table = document.getElementById("table-profiles") as HTMLTableElement;
|
||||
private _createButton = document.getElementById("button-profile-create") as HTMLSpanElement;
|
||||
private _profiles: { [name: string]: profile } = {};
|
||||
private _default: string;
|
||||
|
||||
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
|
||||
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
|
||||
private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement;
|
||||
private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement;
|
||||
|
||||
get empty(): boolean { return (Object.keys(this._table.children).length == 0) }
|
||||
set empty(state: boolean) {
|
||||
if (state) {
|
||||
this._table.innerHTML = `<tr><td class="empty">None</td></tr>`
|
||||
} else if (this._table.querySelector("td.empty")) {
|
||||
this._table.textContent = ``;
|
||||
}
|
||||
}
|
||||
|
||||
get default(): string { return this._default; }
|
||||
set default(v: string) {
|
||||
this._default = v;
|
||||
if (v != "") { this._profiles[v].default = true; }
|
||||
for (let name in this._profiles) {
|
||||
if (name != v) { this._profiles[name].default = false; }
|
||||
}
|
||||
}
|
||||
|
||||
load = () => _get("/profiles", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
let resp = req.response as profileResp;
|
||||
if (Object.keys(resp.profiles).length == 0) {
|
||||
this.empty = true;
|
||||
} else {
|
||||
this.empty = false;
|
||||
for (let name in resp.profiles) {
|
||||
if (name in this._profiles) {
|
||||
this._profiles[name].update(name, resp.profiles[name]);
|
||||
} else {
|
||||
this._profiles[name] = new profile(name, resp.profiles[name]);
|
||||
this._table.appendChild(this._profiles[name].asElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
this.default = resp.default_profile;
|
||||
window.modals.profiles.show();
|
||||
} else {
|
||||
window.notifications.customError("profileEditor", "Failed to load profiles.");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
constructor() {
|
||||
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
|
||||
document.addEventListener("profiles-default", (event: CustomEvent) => {
|
||||
const prevDefault = this.default;
|
||||
const newDefault = event.detail;
|
||||
_post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
this.default = newDefault;
|
||||
} else {
|
||||
this.default = prevDefault;
|
||||
window.notifications.customError("profileDefault", "Failed to set default profile.");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
document.addEventListener("profiles-delete", (event: CustomEvent) => {
|
||||
delete this._profiles[event.detail];
|
||||
this.load();
|
||||
});
|
||||
|
||||
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
let innerHTML = ``;
|
||||
for (let user of req.response["users"]) {
|
||||
innerHTML += `<option value="${user['id']}">${user['name']}</option>`;
|
||||
}
|
||||
this._userSelect.innerHTML = innerHTML;
|
||||
this._storeHomescreen.checked = true;
|
||||
window.modals.profiles.close();
|
||||
window.modals.addProfile.show();
|
||||
} else {
|
||||
window.notifications.customError("loadUsers", "Failed to load users.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._createForm.onsubmit = (event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
const button = this._createForm.querySelector("span.submit") as HTMLSpanElement;
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"homescreen": this._storeHomescreen.checked,
|
||||
"id": this._userSelect.value,
|
||||
"name": this._profileName.value
|
||||
}
|
||||
_post("/profiles", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
window.modals.addProfile.close();
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
this.load();
|
||||
window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`);
|
||||
} else {
|
||||
window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`);
|
||||
}
|
||||
window.modals.profiles.show();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
}
|
||||
}
|
@ -1,15 +1,9 @@
|
||||
import { _get } from "../modules/common.js";
|
||||
import { _get, _post, toggleLoader } from "../modules/common.js";
|
||||
|
||||
interface settingsBoolEvent extends Event {
|
||||
detail: boolean;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
Admin: boolean;
|
||||
LibraryAccess: string;
|
||||
FromUser: string;
|
||||
}
|
||||
|
||||
interface Meta {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -64,15 +58,15 @@ class DOMInput {
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~critical");
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~critical");
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
constructor(inputType: string, setting: Setting, section: string) {
|
||||
constructor(inputType: string, setting: Setting, section: string, name: string) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.classList.add("setting");
|
||||
this._container.innerHTML = `
|
||||
@ -97,6 +91,12 @@ class DOMInput {
|
||||
this._input.disabled = (event.detail !== state);
|
||||
});
|
||||
}
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
this._input.onchange = onValueChange;
|
||||
this.update(setting);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ interface SText extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMText extends DOMInput implements SText {
|
||||
constructor(setting: Setting, section: string) { super("text", setting, section); }
|
||||
constructor(setting: Setting, section: string, name: string) { super("text", setting, section, name); }
|
||||
type: string = "text";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
@ -128,7 +128,7 @@ interface SPassword extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMPassword extends DOMInput implements SPassword {
|
||||
constructor(setting: Setting, section: string) { super("password", setting, section); }
|
||||
constructor(setting: Setting, section: string, name: string) { super("password", setting, section, name); }
|
||||
type: string = "password";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
@ -138,7 +138,7 @@ interface SEmail extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMEmail extends DOMInput implements SEmail {
|
||||
constructor(setting: Setting, section: string) { super("email", setting, section); }
|
||||
constructor(setting: Setting, section: string, name: string) { super("email", setting, section, name); }
|
||||
type: string = "email";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
@ -148,7 +148,7 @@ interface SNumber extends Setting {
|
||||
value: number;
|
||||
}
|
||||
class DOMNumber extends DOMInput implements SNumber {
|
||||
constructor(setting: Setting, section: string) { super("number", setting, section); }
|
||||
constructor(setting: Setting, section: string, name: string) { super("number", setting, section, name); }
|
||||
type: string = "number";
|
||||
get value(): number { return +this._input.value; }
|
||||
set value(v: number) { this._input.value = ""+v; }
|
||||
@ -193,10 +193,10 @@ class DOMBool implements SBool {
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~critical");
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~critical");
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
@ -220,10 +220,13 @@ class DOMBool implements SBool {
|
||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this._input.checked })
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
this._input.onchange = onValueChange;
|
||||
this._input.onchange = () => {
|
||||
onValueChange();
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
document.addEventListener(`settings-loaded`, onValueChange);
|
||||
|
||||
if (setting.depends_false || setting.depends_true) {
|
||||
@ -288,10 +291,10 @@ class DOMSelect implements SSelect {
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~critical");
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~critical");
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
@ -308,7 +311,7 @@ class DOMSelect implements SSelect {
|
||||
this._select.innerHTML = innerHTML;
|
||||
}
|
||||
|
||||
constructor(setting: SSelect, section: string) {
|
||||
constructor(setting: SSelect, section: string, name: string) {
|
||||
this._options = [];
|
||||
this._container = document.createElement("div");
|
||||
this._container.classList.add("setting");
|
||||
@ -336,6 +339,12 @@ class DOMSelect implements SSelect {
|
||||
this._input.disabled = (event.detail !== state);
|
||||
});
|
||||
}
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
this._select.onchange = onValueChange;
|
||||
this.update(setting);
|
||||
}
|
||||
update = (s: SSelect) => {
|
||||
@ -360,15 +369,18 @@ class sectionPanel {
|
||||
private _section: HTMLDivElement;
|
||||
private _settings: { [name: string]: Setting };
|
||||
private _sectionName: string;
|
||||
values: { [field: string]: string } = {};
|
||||
|
||||
constructor(s: Section, sectionName: string) {
|
||||
this._sectionName = sectionName;
|
||||
this._settings = {};
|
||||
this._section = document.createElement("div") as HTMLDivElement;
|
||||
this._section.classList.add("settings-section", "unfocused");
|
||||
this._section.innerHTML = `<p class="support lg mb-half">${s.meta.description}</p>`;
|
||||
this._section.innerHTML = `
|
||||
<span class="heading">${s.meta.name}</span>
|
||||
<p class="support lg">${s.meta.description}</p>
|
||||
`;
|
||||
this.update(s);
|
||||
|
||||
}
|
||||
update = (s: Section) => {
|
||||
for (let name of s.order) {
|
||||
@ -378,24 +390,30 @@ class sectionPanel {
|
||||
} else {
|
||||
switch (setting.type) {
|
||||
case "text":
|
||||
setting = new DOMText(setting, this._sectionName);
|
||||
setting = new DOMText(setting, this._sectionName, name);
|
||||
break;
|
||||
case "password":
|
||||
setting = new DOMPassword(setting, this._sectionName);
|
||||
setting = new DOMPassword(setting, this._sectionName, name);
|
||||
break;
|
||||
case "email":
|
||||
setting = new DOMEmail(setting, this._sectionName);
|
||||
setting = new DOMEmail(setting, this._sectionName, name);
|
||||
break;
|
||||
case "number":
|
||||
setting = new DOMNumber(setting, this._sectionName);
|
||||
setting = new DOMNumber(setting, this._sectionName, name);
|
||||
break;
|
||||
case "bool":
|
||||
setting = new DOMBool(setting as SBool, this._sectionName, name);
|
||||
break;
|
||||
case "select":
|
||||
setting = new DOMSelect(setting as SSelect, this._sectionName);
|
||||
setting = new DOMSelect(setting as SSelect, this._sectionName, name);
|
||||
break;
|
||||
}
|
||||
this.values[name] = ""+setting.value;
|
||||
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
|
||||
const oldValue = this.values[name];
|
||||
this.values[name] = ""+event.detail;
|
||||
document.dispatchEvent(new CustomEvent("settings-section-changed"));
|
||||
});
|
||||
this._section.appendChild(setting.asElement());
|
||||
this._settings[name] = setting;
|
||||
}
|
||||
@ -422,10 +440,15 @@ interface Settings {
|
||||
}
|
||||
|
||||
export class settingsList {
|
||||
private _saveButton = document.getElementById("settings-save") as HTMLSpanElement;
|
||||
private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement;
|
||||
private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement;
|
||||
|
||||
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
|
||||
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
|
||||
private _sections: { [name: string]: sectionPanel }
|
||||
private _buttons: { [name: string]: HTMLSpanElement }
|
||||
private _needsRestart: boolean = false;
|
||||
|
||||
addSection = (name: string, s: Section) => {
|
||||
const section = new sectionPanel(s, name);
|
||||
@ -442,7 +465,6 @@ export class settingsList {
|
||||
private _showPanel = (name: string) => {
|
||||
for (let n in this._sections) {
|
||||
if (n == name) {
|
||||
console.log("found", n);
|
||||
this._sections[name].visible = true;
|
||||
this._buttons[name].classList.add("selected");
|
||||
} else {
|
||||
@ -452,9 +474,53 @@ export class settingsList {
|
||||
}
|
||||
}
|
||||
|
||||
private _save = () => {
|
||||
let config = {};
|
||||
for (let name in this._sections) {
|
||||
config[name] = this._sections[name].values;
|
||||
}
|
||||
if (this._needsRestart) {
|
||||
this._saveRestart.onclick = () => {
|
||||
config["restart-program"] = true;
|
||||
this._send(config, () => {
|
||||
window.modals.settingsRestart.close();
|
||||
window.modals.settingsRefresh.show();
|
||||
});
|
||||
};
|
||||
this._saveNoRestart.onclick = () => {
|
||||
config["restart-program"] = false;
|
||||
this._send(config, window.modals.settingsRestart.close);
|
||||
}
|
||||
window.modals.settingsRestart.show();
|
||||
} else {
|
||||
this._send(config);
|
||||
}
|
||||
// console.log(config);
|
||||
}
|
||||
|
||||
private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("settingsSaved", "Success:", "settings were saved.");
|
||||
} else {
|
||||
window.notifications.customError("settingsSaved", "Couldn't save settings.");
|
||||
}
|
||||
this.reload();
|
||||
if (run) { run(); }
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this._sections = {};
|
||||
this._buttons = {};
|
||||
document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused"));
|
||||
this._saveButton.onclick = this._save;
|
||||
document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; });
|
||||
|
||||
if (window.ombiEnabled) {
|
||||
let ombi = new ombiDefaults();
|
||||
this._sidebar.appendChild(ombi.button());
|
||||
}
|
||||
}
|
||||
|
||||
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||
@ -471,8 +537,78 @@ export class settingsList {
|
||||
this.addSection(name, settings.sections[name]);
|
||||
}
|
||||
}
|
||||
this._showPanel(settings.order[0]);
|
||||
this._needsRestart = false;
|
||||
document.dispatchEvent(new CustomEvent("settings-loaded"));
|
||||
this._saveButton.classList.add("unfocused");
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
interface ombiUser {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
class ombiDefaults {
|
||||
private _form: HTMLFormElement;
|
||||
private _button: HTMLSpanElement;
|
||||
private _select: HTMLSelectElement;
|
||||
private _users: { [id: string]: string } = {};
|
||||
constructor() {
|
||||
this._button = document.createElement("span") as HTMLSpanElement;
|
||||
this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
|
||||
this._button.innerHTML = `<span class="flex">Ombi user defaults <i class="ri-link-unlink-m ml-half"></i></span>`;
|
||||
this._button.onclick = this.load;
|
||||
this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement;
|
||||
this._form.onsubmit = this.send;
|
||||
this._select = this._form.querySelector("select") as HTMLSelectElement;
|
||||
}
|
||||
button = (): HTMLSpanElement => { return this._button; }
|
||||
send = () => {
|
||||
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
|
||||
toggleLoader(button);
|
||||
let resp = {} as ombiUser;
|
||||
resp.id = this._select.value;
|
||||
resp.name = this._users[resp.id];
|
||||
_post("/ombi/defaults", resp, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults.");
|
||||
} else {
|
||||
window.notifications.customError("ombiDefaults", "Failed to store ombi defaults.");
|
||||
}
|
||||
window.modals.ombiDefaults.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
load = () => {
|
||||
toggleLoader(this._button);
|
||||
_get("/ombi/users", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 && "users" in req.response) {
|
||||
const users = req.response["users"] as ombiUser[];
|
||||
let innerHTML = "";
|
||||
for (let user of users) {
|
||||
this._users[user.id] = user.name;
|
||||
innerHTML += `<option value="${user.id}">${user.name}</option>`;
|
||||
}
|
||||
this._select.innerHTML = innerHTML;
|
||||
toggleLoader(this._button);
|
||||
window.modals.ombiDefaults.show();
|
||||
} else {
|
||||
toggleLoader(this._button);
|
||||
window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -5,8 +5,12 @@ export function toggleTheme() {
|
||||
}
|
||||
|
||||
export function loadTheme() {
|
||||
if (localStorage.getItem('theme') == "dark") {
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (theme == "dark") {
|
||||
document.documentElement.classList.add('dark-theme');
|
||||
document.documentElement.classList.remove('light-theme');
|
||||
} else if (theme == "light") {
|
||||
document.documentElement.classList.add('light-theme');
|
||||
document.documentElement.classList.remove('dark-theme');
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ declare interface Modals {
|
||||
settingsRefresh: Modal;
|
||||
ombiDefaults?: Modal;
|
||||
newAccountSuccess?: Modal;
|
||||
profiles: Modal;
|
||||
addProfile: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
7
views.go
7
views.go
@ -18,6 +18,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
||||
"urlBase": app.URLBase,
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
@ -39,6 +40,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
|
||||
"urlBase": app.URLBase,
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
"helpMessage": app.config.Section("ui").Key("help_message").String(),
|
||||
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
||||
@ -46,14 +48,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"validate": app.config.Section("password_validation").Key("enabled").MustBool(false),
|
||||
"requirements": app.validator.getCriteria(),
|
||||
"email": email,
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"lang": app.storage.lang.Form["strings"],
|
||||
})
|
||||
} else {
|
||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"cssFile": app.cssClass,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
@ -62,7 +63,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
||||
gcHTML(gc, 404, "404.html", gin.H{
|
||||
"bs5": app.config.Section("ui").Key("bs5").MustBool(false),
|
||||
"cssFile": app.cssFile,
|
||||
"cssFile": app.cssClass,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user