diff --git a/api.go b/api.go
index ffb7548..c444b8d 100644
--- a/api.go
+++ b/api.go
@@ -469,6 +469,71 @@ func (app *appContext) SetProfile(gc *gin.Context) {
gc.JSON(200, map[string]bool{"success": true})
}
+func (app *appContext) GetProfiles(gc *gin.Context) {
+ app.storage.loadProfiles()
+ app.debug.Println("Profiles requested")
+ out := map[string]map[string]interface{}{}
+ for name, p := range app.storage.profiles {
+ out[name] = map[string]interface{}{
+ "admin": p.Admin,
+ "libraries": p.LibraryAccess,
+ "fromUser": p.FromUser,
+ }
+ }
+ fmt.Println(out)
+ gc.JSON(200, out)
+}
+
+type newProfileReq struct {
+ Name string `json:"name"`
+ ID string `json:"id"`
+ Homescreen bool `json:"homescreen"`
+}
+
+func (app *appContext) CreateProfile(gc *gin.Context) {
+ fmt.Println("Profile creation requested")
+ var req newProfileReq
+ gc.BindJSON(&req)
+ user, status, err := app.jf.userById(req.ID, false)
+ if !(status == 200 || status == 204) || err != nil {
+ app.err.Printf("Failed to get user from Jellyfin: Code %d", status)
+ app.debug.Printf("Error: %s", err)
+ respond(500, "Couldn't get user", gc)
+ return
+ }
+ profile := Profile{
+ FromUser: user["Name"].(string),
+ Policy: user["Policy"].(map[string]interface{}),
+ }
+ app.debug.Printf("Creating profile from user \"%s\"", user["Name"].(string))
+ if req.Homescreen {
+ profile.Configuration = user["Configuration"].(map[string]interface{})
+ profile.Displayprefs, status, err = app.jf.getDisplayPreferences(req.ID)
+ if !(status == 200 || status == 204) || err != nil {
+ app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
+ app.debug.Printf("Error: %s", err)
+ respond(500, "Couldn't get displayprefs", gc)
+ return
+ }
+ }
+ app.storage.loadProfiles()
+ app.storage.profiles[req.Name] = profile
+ app.storage.storeProfiles()
+ app.storage.loadProfiles()
+ gc.JSON(200, map[string]bool{"success": true})
+}
+
+func (app *appContext) DeleteProfile(gc *gin.Context) {
+ req := map[string]string{}
+ gc.BindJSON(&req)
+ name := req["name"]
+ if _, ok := app.storage.profiles[name]; ok {
+ delete(app.storage.profiles, name)
+ }
+ app.storage.storeProfiles()
+ gc.JSON(200, map[string]bool{"success": true})
+}
+
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
current_time := time.Now()
@@ -726,12 +791,13 @@ func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
type defaultsReq struct {
From string `json:"from"`
+ Profile string `json:"profile"`
ApplyTo []string `json:"apply_to"`
ID string `json:"id"`
Homescreen bool `json:"homescreen"`
}
-func (app *appContext) SetDefaults(gc *gin.Context) {
+/*func (app *appContext) SetDefaults(gc *gin.Context) {
var req defaultsReq
gc.BindJSON(&req)
userID := req.ID
@@ -765,28 +831,31 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
app.debug.Println("DisplayPrefs template stored")
}
gc.JSON(200, map[string]bool{"success": true})
-}
+}*/
func (app *appContext) ApplySettings(gc *gin.Context) {
+ app.info.Println("User settings change requested")
var req defaultsReq
gc.BindJSON(&req)
- applyingFrom := "template"
+ applyingFrom := "profile"
var policy, configuration, displayprefs map[string]interface{}
- if req.From == "template" {
- if len(app.storage.policy) == 0 {
- respond(500, "No policy template available", gc)
+ if req.From == "profile" {
+ app.storage.loadProfiles()
+ if _, ok := app.storage.profiles[req.Profile]; !ok || len(app.storage.profiles[req.Profile].Policy) == 0 {
+ app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile)
+ respond(500, "Couldn't find profile", gc)
return
}
- app.storage.loadPolicy()
- policy = app.storage.policy
if req.Homescreen {
- if len(app.storage.configuration) == 0 || len(app.storage.displayprefs) == 0 {
+ if len(app.storage.profiles[req.Profile].Configuration) == 0 || len(app.storage.profiles[req.Profile].Displayprefs) == 0 {
+ app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile)
respond(500, "No homescreen template available", gc)
return
}
- configuration = app.storage.configuration
- displayprefs = app.storage.displayprefs
+ configuration = app.storage.profiles[req.Profile].Configuration
+ displayprefs = app.storage.profiles[req.Profile].Displayprefs
}
+ policy = app.storage.profiles[req.Profile].Policy
} else if req.From == "user" {
applyingFrom = "user"
user, status, err := app.jf.userById(req.ID, false)
diff --git a/data/templates/admin.html b/data/templates/admin.html
index f091ef2..b5df83c 100644
--- a/data/templates/admin.html
+++ b/data/templates/admin.html
@@ -119,12 +119,21 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/main.go b/main.go
index 0622ba6..da387ef 100644
--- a/main.go
+++ b/main.go
@@ -458,13 +458,16 @@ func start(asDaemon, firstCall bool) {
api.POST("/newUserAdmin", app.NewUserAdmin)
api.POST("/generateInvite", app.GenerateInvite)
api.GET("/getInvites", app.GetInvites)
+ api.GET("/getProfiles", app.GetProfiles)
api.POST("/setProfile", app.SetProfile)
+ api.POST("/createProfile", app.CreateProfile)
+ api.POST("/deleteProfile", app.DeleteProfile)
api.POST("/setNotify", app.SetNotify)
api.POST("/deleteInvite", app.DeleteInvite)
api.POST("/deleteUser", app.DeleteUser)
api.GET("/getUsers", app.GetUsers)
api.POST("/modifyEmails", app.ModifyEmails)
- api.POST("/setDefaults", app.SetDefaults)
+ // api.POST("/setDefaults", app.SetDefaults)
api.POST("/applySettings", app.ApplySettings)
api.GET("/getConfig", app.GetConfig)
api.POST("/modifyConfig", app.ModifyConfig)
diff --git a/storage.go b/storage.go
index c37d983..c6772a9 100644
--- a/storage.go
+++ b/storage.go
@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"io/ioutil"
+ "strconv"
"time"
)
@@ -17,9 +18,12 @@ type Storage struct {
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
- Policy map[string]interface{} `json:"policy"`
- Configuration map[string]interface{} `json:"configuration"`
- Displayprefs map[string]interface{} `json:"displayprefs"`
+ Admin bool `json:"admin,omitempty"`
+ LibraryAccess string `json:"libraries,omitempty"`
+ FromUser string `json:"fromUser,omitempty"`
+ Policy map[string]interface{} `json:"policy,omitempty"`
+ Configuration map[string]interface{} `json:"configuration,omitempty"`
+ Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
}
type Invite struct {
@@ -84,7 +88,34 @@ func (st *Storage) storeOmbiTemplate() error {
}
func (st *Storage) loadProfiles() error {
- return loadJSON(st.profiles_path, &st.profiles)
+ err := loadJSON(st.profiles_path, &st.profiles)
+ for name, profile := range st.profiles {
+ change := false
+ if profile.Policy["IsAdministrator"] != nil {
+ profile.Admin = profile.Policy["IsAdministrator"].(bool)
+ change = true
+ }
+ if profile.Policy["EnabledFolders"] != nil {
+ length := len(profile.Policy["EnabledFolders"].([]interface{}))
+ if length == 0 {
+ profile.LibraryAccess = "All"
+ } else {
+ profile.LibraryAccess = strconv.Itoa(length)
+ }
+ change = true
+ }
+ if profile.FromUser == "" {
+ profile.FromUser = "Unknown"
+ change = true
+ }
+ if change {
+ st.profiles[name] = profile
+ }
+ }
+ if err != nil {
+ panic(err)
+ }
+ return err
}
func (st *Storage) storeProfiles() error {
diff --git a/ts/accounts.ts b/ts/accounts.ts
index 77ba40a..4d171cb 100644
--- a/ts/accounts.ts
+++ b/ts/accounts.ts
@@ -247,23 +247,36 @@ function populateRadios(): void {
if (userIDs.length > 1) {
userString += "s";
}
+ populateProfiles(true);
+ const profileSelect = document.getElementById('profileSelect') as HTMLSelectElement;
+ profileSelect.textContent = '';
+ for (let i = 0; i < availableProfiles.length; i++) {
+ profileSelect.innerHTML += `
+
+ `;
+ }
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userString}`;
document.getElementById('userDefaultsDescription').textContent = `
- Create an account and configure it to your liking, then choose it from below to apply to your selected users.
+ Apply settings from an existing profile or source settings from a user.
`;
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
Focus(document.getElementById('defaultsSourceSection'));
- (
document.getElementById('defaultsSource')).value = 'userTemplate';
+ (document.getElementById('defaultsSource')).value = 'profile';
+ Focus(document.getElementById('profileSelectBox'));
Unfocus(document.getElementById('defaultUserRadios'));
+ Unfocus(document.getElementById('newProfileBox'));
document.getElementById('storeDefaults').onclick = (): void => storeDefaults(userIDs);
userDefaultsModal.show();
};
(document.getElementById('defaultsSource')).addEventListener('change', function (): void {
const radios = document.getElementById('defaultUserRadios');
- if (this.value == 'userTemplate') {
+ const profileBox = document.getElementById('profileSelectBox');
+ if (this.value == 'profile') {
Unfocus(radios);
+ Focus(profileBox);
} else {
+ Unfocus(profileBox);
Focus(radios);
}
});
diff --git a/ts/admin.ts b/ts/admin.ts
index 0102297..cc5cad5 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -206,25 +206,23 @@ function storeDefaults(users: string | Array): void {
'' +
'Loading...';
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
- const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
- let id = radio.id.replace("default_", "");
- let route = "/setDefaults";
- let data = {
- "from": "user",
- "id": id,
- "homescreen": false
- };
- if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'userTemplate') {
- data["from"] = "template";
+ let data = { "homescreen": false };
+ if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
+ data["from"] = "profile";
+ data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
+ } else {
+ const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
+ let id = radio.id.replace("default_", "");
+ data["from"] = "user";
+ data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
- route = "/applySettings";
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
- _post(route, data, function (): void {
+ _post("/applySettings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
diff --git a/ts/settings.ts b/ts/settings.ts
index 1de7c40..ebd991e 100644
--- a/ts/settings.ts
+++ b/ts/settings.ts
@@ -25,40 +25,6 @@ function sendConfig(restart?: boolean): void {
});
}
-(document.getElementById('openDefaultsWizard') as HTMLButtonElement).onclick = function (): void {
- const button = this as HTMLButtonElement;
- button.disabled = true;
- const ogHTML = button.innerHTML;
- button.innerHTML = `
-
- Loading...`;
- _get("/getUsers", null, function (): void {
- if (this.readyState == 4) {
- if (this.status == 200) {
- jfUsers = this.response['users'];
- populateRadios();
- button.disabled = false;
- button.innerHTML = ogHTML;
- const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
- submitButton.disabled = false;
- submitButton.textContent = 'Submit';
- addAttr(submitButton, "btn-primary");
- rmAttr(submitButton, "btn-danger");
- rmAttr(submitButton, "btn-success");
- document.getElementById('defaultsTitle').textContent = `New user defaults`;
- document.getElementById('userDefaultsDescription').textContent = `
- Create an account and configure it to your liking, then choose it from below to store the settings as a template for all new users.`;
- document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
- (document.getElementById('defaultsSource') as HTMLSelectElement).value = 'fromUser';
- document.getElementById('defaultsSourceSection').classList.add('unfocused');
- (document.getElementById('storeDefaults') as HTMLButtonElement).onclick = (): void => storeDefaults('all');
- Focus(document.getElementById('defaultUserRadios'));
- userDefaultsModal.show();
- }
- }
- });
-};
-
(document.getElementById('openAbout') as HTMLButtonElement).onclick = (): void => {
aboutModal.show();
};
@@ -165,8 +131,126 @@ const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, c
}
});
-function showSetting(id: string): void {
- const els = document.getElementById('settingsSections').querySelectorAll("button[type=button]") as NodeListOf;
+interface Profile {
+ Admin: boolean;
+ LibraryAccess: string;
+ FromUser: string;
+}
+
+(document.getElementById('profiles_button') as HTMLButtonElement).onclick = (): void => showSetting("profiles", populateProfiles);
+
+const populateProfiles = (noTable?: boolean): void => _get("/getProfiles", null, function (): void {
+ if (this.readyState == 4 && this.status == 200) {
+ const profileList = document.getElementById('profileList');
+ profileList.textContent = '';
+ availableProfiles = [];
+ for (let name in this.response) {
+ availableProfiles.push(name);
+ if (!noTable) {
+ const profile: Profile = {
+ Admin: this.response[name]["admin"],
+ LibraryAccess: this.response[name]["libraries"],
+ FromUser: this.response[name]["fromUser"]
+ };
+ profileList.innerHTML += `
+ ${name} |
+ ${profile.FromUser} |
+ ${profile.Admin ? "Yes" : "No"} |
+ ${profile.LibraryAccess} |
+ |
+ `;
+ }
+ }
+ }
+});
+
+const deleteProfile = (name: string): void => _post("/deleteProfile", { "name": name }, function (): void {
+ if (this.readyState == 4 && this.status == 200) {
+ populateProfiles();
+ }
+});
+
+const createProfile = (): void => _get("/getUsers", null, function (): void {
+ if (this.readyState == 4 && this.status == 200) {
+ jfUsers = this.response["users"];
+ populateRadios();
+ const submitButton = document.getElementById('storeDefaults') as HTMLButtonElement;
+ submitButton.disabled = false;
+ submitButton.textContent = 'Create';
+ addAttr(submitButton, "btn-primary");
+ rmAttr(submitButton, "btn-danger");
+ rmAttr(submitButton, "btn-success");
+ document.getElementById('defaultsTitle').textContent = `Create Profile`;
+ document.getElementById('userDefaultsDescription').textContent = `
+ Create an account and configure it to your liking, then choose it from below to store the settings as a profile. Profiles can be specified per invite, so that any new user on that invite will have the settings applied.`;
+ document.getElementById('storeHomescreenLabel').textContent = `Store homescreen layout`;
+ (document.getElementById('defaultsSource') as HTMLSelectElement).value = 'fromUser';
+ document.getElementById('defaultsSourceSection').classList.add('unfocused');
+ (document.getElementById('storeDefaults') as HTMLButtonElement).onclick = storeProfile;
+ Focus(document.getElementById('newProfileBox'));
+ (document.getElementById('newProfileName') as HTMLInputElement).value = '';
+ Focus(document.getElementById('defaultUserRadios'));
+ userDefaultsModal.show();
+ }
+});
+
+function storeProfile(): void {
+ this.disabled = true;
+ this.innerHTML =
+ '' +
+ 'Loading...';
+ const button = document.getElementById('storeDefaults') as HTMLButtonElement;
+ const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
+ const name = (document.getElementById('newProfileName') as HTMLInputElement).value;
+ let id = radio.id.replace("default_", "");
+ let data = {
+ "name": name,
+ "id": id,
+ "homescreen": false
+ }
+ if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
+ data["homescreen"] = true;
+ }
+ _post("/createProfile", data, function (): void {
+ if (this.readyState == 4) {
+ if (this.status == 200 || this.status == 204) {
+ button.textContent = "Success";
+ addAttr(button, "btn-success");
+ rmAttr(button, "btn-danger");
+ rmAttr(button, "btn-primary");
+ button.disabled = false;
+ setTimeout((): void => {
+ button.textContent = "Create";
+ addAttr(button, "btn-primary");
+ rmAttr(button, "btn-success");
+ button.disabled = false;
+ populateProfiles();
+ userDefaultsModal.hide();
+
+ }, 1000);
+ } else {
+ if ("error" in this.response) {
+ button.textContent = this.response["error"];
+ } else if (("policy" in this.response) || ("homescreen" in this.response)) {
+ button.textContent = "Failed (check console)";
+ } else {
+ button.textContent = "Failed";
+ }
+ addAttr(button, "btn-danger");
+ rmAttr(button, "btn-primary");
+ setTimeout((): void => {
+ button.textContent = "Create";
+ addAttr(button, "btn-primary");
+ rmAttr(button, "btn-danger");
+ button.disabled = false;
+ }, 1000);
+ }
+ }
+ });
+}
+
+function showSetting(id: string, runBefore?: () => void): void {
+ const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
@@ -179,6 +263,9 @@ function showSetting(id: string): void {
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
+ if (runBefore) {
+ runBefore();
+ }
Focus(section);
if (screen.width <= 1100) {
// ugly