diff --git a/api-users.go b/api-users.go
index a4e6214..bbaa9d7 100644
--- a/api-users.go
+++ b/api-users.go
@@ -4,6 +4,7 @@ import (
"fmt"
"net/url"
"os"
+ "slices"
"strings"
"time"
@@ -949,19 +950,26 @@ func (app *appContext) SearchUsers(gc *gin.Context) {
}
var resp getUsersDTO
- userList, err := app.userCache.Gen(app, false)
+ userList, err := app.userCache.Gen(app, req.SortByField == USER_DEFAULT_SORT_FIELD)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
var filtered []*respUser
- if (req.SearchTerms != nil && len(req.SearchTerms) != 0) || (req.Queries != nil && len(req.Queries) != 0) {
+ if len(req.SearchTerms) != 0 || len(req.Queries) != 0 {
filtered = app.userCache.Filter(userList, req.SearchTerms, req.Queries)
} else {
- filtered = userList
+ filtered = slices.Clone(userList)
+ }
+
+ if req.SortByField == USER_DEFAULT_SORT_FIELD {
+ if req.Ascending != USER_DEFAULT_SORT_ASCENDING {
+ slices.Reverse(filtered)
+ }
+ } else {
+ app.userCache.Sort(filtered, req.SortByField, req.Ascending)
}
- app.userCache.Sort(filtered, req.SortByField, req.Ascending)
startIndex := (req.Page * req.Limit)
if startIndex < len(filtered) {
diff --git a/css/tooltip.css b/css/tooltip.css
index 85a289e..fc92a1d 100644
--- a/css/tooltip.css
+++ b/css/tooltip.css
@@ -27,6 +27,12 @@
right: 0;
}
+.tooltip.above .content {
+ bottom: 2.5rem;
+ left: 0;
+ right: 0;
+}
+
.tooltip.darker .content {
background-color: rgba(0, 0, 0, 0.8);
}
diff --git a/html/admin.html b/html/admin.html
index 2a9a8fc..23e70e9 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -726,7 +726,10 @@
-
+
{{ .strings.searchAllRecords }}
@@ -810,20 +813,23 @@
-
-
{{ .strings.noResultsFound }}
-
{{ .strings.keepSearchingDescription }}
+
+
{{ .strings.noResultsFound }}
+
{{ .strings.noResultsFoundLocally }}
-
-
-
-
+
+
+
+
@@ -843,7 +849,7 @@
-
+
{{ .strings.searchAllRecords }}
@@ -867,9 +873,9 @@
-
-
{{ .strings.noResultsFound }}
-
{{ .strings.keepSearchingDescription }}
+
+
{{ .strings.noResultsFound }}
+
{{ .strings.noResultsFoundLocally }}
-
-
-
+
+
+
+
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index ff3dec1..d420ee8 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -58,6 +58,7 @@
"disabled": "Disabled",
"sendPWR": "Send Password Reset",
"noResultsFound": "No Results Found",
+ "noResultsFoundLocally": "Only loaded records were searched. You can load more, or perform the search over all records on the server.",
"keepSearching": "Keep Searching",
"keepSearchingDescription": "Only the current loaded activities were searched. Click below if you wish to search all activities.",
"contactThrough": "Contact through:",
@@ -136,6 +137,7 @@
"filters": "Filters",
"clickToRemoveFilter": "Click to remove this filter.",
"clearSearch": "Clear search",
+ "searchAll": "Search all",
"searchAllRecords": "Search all records (on server)",
"actions": "Actions",
"searchOptions": "Search Options",
diff --git a/scripts/account-gen/main.go b/scripts/account-gen/main.go
index 47faa6f..c61928f 100644
--- a/scripts/account-gen/main.go
+++ b/scripts/account-gen/main.go
@@ -14,8 +14,9 @@ import (
)
var (
- names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab"}
- COUNT = 3000
+ names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab", "Graig", "Rhoda", "Tyler", "Quentin", "Melinda", "Zelma", "Jack", "Clifton", "Sherry", "Boyce", "Elma", "Jere", "Shelby", "Caitlin", "Bertie", "Mallory", "Thelma", "Charley", "Santo", "Merrill", "Royal", "Jefferson", "Ester", "Dee", "Susanna", "Adriana", "Alfonso", "Lillie", "Carmen", "Federico", "Ernie", "Kory", "Kimberly", "Donn", "Lilian", "Irvin", "Sherri", "Cordell", "Adrienne", "Edwin", "Serena", "Otis", "Latasha", "Johanna", "Clarence", "Noe", "Mindy", "Felix", "Audra"}
+ COUNT = 4000
+ DELAY = 1 * time.Millisecond
)
const (
@@ -101,11 +102,12 @@ func main() {
rand.Seed(time.Now().Unix())
for i := 0; i < COUNT; i++ {
- name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(100))
+ name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(500))
user, status, err := jf.NewUser(name, PASSWORD)
if (status != 200 && status != 201 && status != 204) || err != nil {
- log.Fatalf("Acc no %d: Failed to create user \"%s\" (%d): %+v\n", i, name, status, err)
+ log.Printf("Acc no %d: Failed to create user \"%s\" (%d): %+v\n", i, name, status, err)
+ continue
}
if rand.Intn(100) > 65 {
@@ -116,13 +118,17 @@ func main() {
user.Policy.IsDisabled = true
}
+ time.Sleep(DELAY / 4)
status, err = jf.SetPolicy(user.ID, user.Policy)
if (status != 200 && status != 201 && status != 204) || err != nil {
log.Fatalf("Acc no %d: Failed to set policy for user \"%s\" (%d): %+v\n", i, name, status, err)
}
if rand.Intn(100) > 20 {
+ time.Sleep(DELAY / 4)
jfTemp.Authenticate(name, PASSWORD)
}
+ log.Printf("Acc %d done\n", i)
+ time.Sleep(DELAY / 4)
}
}
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index a95b14f..46f50b3 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -9,6 +9,9 @@ import { PaginatedList } from "./list.js";
declare var window: GlobalWindow;
+const USER_DEFAULT_SORT_FIELD = "name";
+const USER_DEFAULT_SORT_ASCENDING = true;
+
const dateParser = require("any-date-parser");
interface User {
@@ -911,16 +914,14 @@ export class accountsList extends PaginatedList {
loadMoreButton: document.getElementById("accounts-load-more") as HTMLButtonElement,
loadAllButton: document.getElementById("accounts-load-all") as HTMLButtonElement,
refreshButton: document.getElementById("accounts-refresh") as HTMLButtonElement,
- keepSearchingDescription: document.getElementById("accounts-keep-searching-description"),
- keepSearchingButton: document.getElementById("accounts-keep-searching"),
filterArea: document.getElementById("accounts-filter-area"),
searchOptionsHeader: document.getElementById("accounts-search-options-header"),
searchBox: document.getElementById("accounts-search") as HTMLInputElement,
- notFoundPanel: document.getElementById("accounts-not-found"),
recordCounter: document.getElementById("accounts-record-counter"),
totalEndpoint: "/users/count",
getPageEndpoint: "/users",
- limit: 4,
+ itemsPerPage: 40,
+ maxItemsLoadedForSearch: 200,
newElementsFromPage: (resp: paginatedDTO) => {
for (let u of ((resp as UsersDTO).users || [])) {
if (u.id in this.users) {
@@ -962,7 +963,8 @@ export class accountsList extends PaginatedList {
this._search.ascending
);
},
- defaultSortField: "name",
+ defaultSortField: USER_DEFAULT_SORT_FIELD,
+ defaultSortAscending: USER_DEFAULT_SORT_ASCENDING,
pageLoadCallback: (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
// FIXME: Error message
@@ -975,7 +977,8 @@ export class accountsList extends PaginatedList {
filterArea: this._c.filterArea,
sortingByButton: this._sortingByButton,
searchOptionsHeader: this._c.searchOptionsHeader,
- notFoundPanel: this._c.notFoundPanel,
+ notFoundPanel: document.getElementById("accounts-not-found"),
+ notFoundLocallyText: document.getElementById("accounts-no-local-results"),
filterList: document.getElementById("accounts-filter-list"),
search: this._c.searchBox,
queries: queries(),
@@ -1188,7 +1191,7 @@ export class accountsList extends PaginatedList {
// Start off sorting by username (this._c.defaultSortField)
const defaultSort = () => {
document.dispatchEvent(new CustomEvent("header-click", { detail: this._c.defaultSortField }));
- this._columns[this._c.defaultSortField].ascending = true;
+ this._columns[this._c.defaultSortField].ascending = this._c.defaultSortAscending;
this._columns[this._c.defaultSortField].hideIcon();
this._sortingByButton.parentElement.classList.add("hidden");
this._search.showHideSearchOptionsHeader();
@@ -1209,15 +1212,17 @@ export class accountsList extends PaginatedList {
this._search.onSearchBoxChange();
} else {
this.setVisibility(this._search.ordering, true);
- this._c.notFoundPanel.classList.add("unfocused");
+ this._search.setNotFoundPanelVisibility(false);
+ this._search.setServerSearchButtonsDisabled(
+ event.detail == this._c.defaultSortField && this._columns[event.detail].ascending == this._c.defaultSortAscending
+ );
}
this._search.showHideSearchOptionsHeader();
});
defaultSort();
- this._search.showHideSearchOptionsHeader();
- this._search.generateFilterList();
+ this._search.showHideSearchOptionsHeader();
this.registerURLListener();
}
@@ -2168,7 +2173,7 @@ class Column {
// Returns the inner HTML to show in the "Sorting By" button.
get buttonContent() {
- return `` + window.lang.strings("sortingBy") + ": " + `` + this._headerContent;
+ return `` + window.lang.strings("sortingBy") + ": " + `` + this._headerContent;
}
get ascending() { return this._ascending; }
diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts
index 5780962..7f63017 100644
--- a/ts/modules/activity.ts
+++ b/ts/modules/activity.ts
@@ -6,6 +6,9 @@ import { PaginatedList } from "./list.js";
declare var window: GlobalWindow;
+const ACTIVITY_DEFAULT_SORT_FIELD = "time";
+const ACTIVITY_DEFAULT_SORT_ASCENDING = false;
+
export interface activity {
id: string;
type: string;
@@ -488,16 +491,14 @@ export class activityList extends PaginatedList {
loadMoreButton: document.getElementById("activity-load-more") as HTMLButtonElement,
loadAllButton: document.getElementById("activity-load-all") as HTMLButtonElement,
refreshButton: document.getElementById("activity-refresh") as HTMLButtonElement,
- keepSearchingDescription: document.getElementById("activity-keep-searching-description"),
- keepSearchingButton: document.getElementById("activity-keep-searching"),
filterArea: document.getElementById("activity-filter-area"),
searchOptionsHeader: document.getElementById("activity-search-options-header"),
searchBox: document.getElementById("activity-search") as HTMLInputElement,
- notFoundPanel: document.getElementById("activity-not-found"),
recordCounter: document.getElementById("activity-record-counter"),
totalEndpoint: "/activity/count",
getPageEndpoint: "/activity",
- limit: 10,
+ itemsPerPage: 20,
+ maxItemsLoadedForSearch: 200,
newElementsFromPage: (resp: paginatedDTO) => {
let ordering: string[] = this._search.ordering;
for (let act of ((resp as ActivitiesDTO).activities || [])) {
@@ -513,7 +514,8 @@ export class activityList extends PaginatedList {
this._search.setOrdering([], this._c.defaultSortField, this.ascending);
this._c.newElementsFromPage(resp);
},
- defaultSortField: "time",
+ defaultSortField: ACTIVITY_DEFAULT_SORT_FIELD,
+ defaultSortAscending: ACTIVITY_DEFAULT_SORT_ASCENDING,
pageLoadCallback: (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
if (req.status != 200) {
@@ -531,7 +533,8 @@ export class activityList extends PaginatedList {
// Exclude this: We only sort by date, and don't want to show a redundant header indicating so.
// sortingByButton: this._sortingByButton,
searchOptionsHeader: this._c.searchOptionsHeader,
- notFoundPanel: this._c.notFoundPanel,
+ notFoundPanel: document.getElementById("activity-not-found"),
+ notFoundLocallyText: document.getElementById("activity-no-local-results"),
search: this._c.searchBox,
clearSearchButtonSelector: ".activity-search-clear",
serverSearchButtonSelector: ".activity-search-server",
@@ -546,7 +549,7 @@ export class activityList extends PaginatedList {
this.initSearch(searchConfig);
- this.ascending = false;
+ this.ascending = this._c.defaultSortAscending;
this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending);
}
diff --git a/ts/modules/list.ts b/ts/modules/list.ts
index 0978317..414b550 100644
--- a/ts/modules/list.ts
+++ b/ts/modules/list.ts
@@ -76,19 +76,18 @@ export interface PaginatedListConfig {
loadMoreButton: HTMLButtonElement;
loadAllButton: HTMLButtonElement;
refreshButton: HTMLButtonElement;
- keepSearchingDescription: HTMLElement;
- keepSearchingButton: HTMLElement;
- notFoundPanel: HTMLElement;
filterArea: HTMLElement;
searchOptionsHeader: HTMLElement;
searchBox: HTMLInputElement;
recordCounter: HTMLElement;
totalEndpoint: string;
getPageEndpoint: string;
- limit: number;
+ itemsPerPage: number;
+ maxItemsLoadedForSearch: number;
newElementsFromPage: (resp: paginatedDTO) => void;
updateExistingElementsFromPage: (resp: paginatedDTO) => void;
defaultSortField: string;
+ defaultSortAscending: boolean;
pageLoadCallback?: (req: XMLHttpRequest) => void;
}
@@ -109,13 +108,13 @@ export abstract class PaginatedList {
if (v) {
this._c.loadAllButton.classList.add("unfocused");
this._c.loadMoreButton.textContent = window.lang.strings("noMoreResults");
+ this._search.setServerSearchButtonsDisabled(this._search.inServerSearch);
this._c.loadMoreButton.disabled = true;
} else {
this._c.loadMoreButton.textContent = window.lang.strings("loadMore");
this._c.loadMoreButton.disabled = false;
- if (this._search.inSearch) {
- this._c.loadAllButton.classList.remove("unfocused");
- }
+ this._search.setServerSearchButtonsDisabled(false);
+ this._c.loadAllButton.classList.remove("unfocused");
}
}
@@ -160,9 +159,15 @@ export abstract class PaginatedList {
// if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused");
// else this._c.loadAllButton.classList.add("unfocused");
-
+
+ if (this._search.sortField == this._c.defaultSortField && this._search.ascending == this._c.defaultSortAscending) {
+ this._search.setServerSearchButtonsDisabled(!this._search.inSearch)
+ } else {
+ this._search.setServerSearchButtonsDisabled(false)
+ }
+
// FIXME: Figure out why this makes sense and make it clearer.
- if ((visibleCount < this._c.limit && !this.lastPage) || loadAll) {
+ if ((visibleCount < this._c.itemsPerPage && this._counter.loaded < this._c.maxItemsLoadedForSearch && !this.lastPage) || loadAll) {
if (!newItems ||
this._previousPageSize != visibleCount ||
(visibleCount == 0 && !this.lastPage) ||
@@ -183,6 +188,7 @@ export abstract class PaginatedList {
if (previousServerSearch) previousServerSearch(params, newSearch);
};
searchConfig.clearServerSearch = () => {
+ console.log("Clearing server search");
this._page = 0;
this.reload();
}
@@ -195,6 +201,7 @@ export abstract class PaginatedList {
public abstract setVisibility: (elements: string[], visible: boolean) => void;
// Removes all elements, and reloads the first page.
+ // FIXME: Share more code between reload and loadMore, and go over the logic, it's messy.
public abstract reload: () => void;
protected _reload = (
callback?: (req: XMLHttpRequest) => void
@@ -206,7 +213,7 @@ export abstract class PaginatedList {
this._counter.getTotal(this._c.totalEndpoint);
// Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)).
- let limit = this._c.limit;
+ let limit = this._c.itemsPerPage;
if (this._page != 0) {
limit *= this._page+1;
}
@@ -216,7 +223,7 @@ export abstract class PaginatedList {
params.page = 0;
if (params.sortByField == "") {
params.sortByField = this._c.defaultSortField;
- params.ascending = true;
+ params.ascending = this._c.defaultSortAscending;
}
_post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => {
@@ -246,8 +253,7 @@ export abstract class PaginatedList {
} else {
this._counter.shown = this._counter.loaded;
this.setVisibility(this._search.ordering, true);
- this._c.loadAllButton.classList.add("unfocused");
- this._c.notFoundPanel.classList.add("unfocused");
+ // this._search.showHideNotFoundPanel(false);
}
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
if (callback) callback(req);
@@ -268,11 +274,11 @@ export abstract class PaginatedList {
this._page += 1;
let params = this._search.inServerSearch ? this._searchParams : this.defaultParams();
- params.limit = this._c.limit;
+ params.limit = this._c.itemsPerPage;
params.page = this._page;
if (params.sortByField == "") {
params.sortByField = this._c.defaultSortField;
- params.ascending = true;
+ params.ascending = this._c.defaultSortAscending;
}
_post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => {
@@ -304,7 +310,7 @@ export abstract class PaginatedList {
this._search.onSearchBoxChange(true, loadAll);
} else {
this.setVisibility(this._search.ordering, true);
- this._c.notFoundPanel.classList.add("unfocused");
+ this._search.setNotFoundPanelVisibility(false);
}
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
if (callback) callback(req);
diff --git a/ts/modules/search.ts b/ts/modules/search.ts
index e93b653..92f99ec 100644
--- a/ts/modules/search.ts
+++ b/ts/modules/search.ts
@@ -35,6 +35,7 @@ export interface SearchConfiguration {
sortingByButton?: HTMLButtonElement;
searchOptionsHeader: HTMLElement;
notFoundPanel: HTMLElement;
+ notFoundLocallyText: HTMLElement;
notFoundCallback?: (notFound: boolean) => void;
filterList: HTMLElement;
clearSearchButtonSelector: string;
@@ -91,6 +92,16 @@ export abstract class Query {
out.operator = this._operator;
return out;
}
+
+ get subject(): QueryType { return this._subject; }
+
+ getValueFromItem(item: SearchableItem): any {
+ return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(item), this.subject.getter).get.call(item);
+ }
+
+ compareItem(item: SearchableItem): boolean {
+ return this.compare(this.getValueFromItem(item));
+ }
}
export class BoolQuery extends Query {
@@ -278,30 +289,19 @@ export class Search {
private _inServerSearch: boolean = false;
get inServerSearch(): boolean { return this._inServerSearch; }
set inServerSearch(v: boolean) {
+ const previous = this._inServerSearch;
this._inServerSearch = v;
- if (!v) {
+ if (!v && previous != v) {
this._c.clearServerSearch();
}
}
private _serverSearchButtons: HTMLElement[];
- // Returns a list of identifiers (keys in items, values in ordering).
- search = (query: String): string[] => {
- this._c.filterArea.textContent = "";
-
+ static tokenizeSearch = (query: string): string[] => {
query = query.toLowerCase();
- let result: string[] = [...this._ordering];
- // If we're in a server search already, the results are already correct.
- if (this.inServerSearch) return result;
-
-
let words: string[] = [];
- let queries = [];
- let searchTerms = [];
-
-
let quoteSymbol = ``;
let queryStart = -1;
let lastQuote = -1;
@@ -335,19 +335,17 @@ export class Search {
}
}
}
+ return words;
+ }
- query = "";
- for (let word of words) {
+ parseTokens = (tokens: string[]): [string[], Query[]] => {
+ let queries: Query[] = [];
+ let searchTerms: string[] = [];
+
+ for (let word of tokens) {
// 1. Normal search text, no filters or anything
if (!word.includes(":")) {
searchTerms.push(word);
- let cachedResult = [...result];
- for (let id of cachedResult) {
- const u = this.items[id];
- if (!u.matchesSearch(word)) {
- result.splice(result.indexOf(id), 1);
- }
- }
continue;
}
// 2. A filter query of some sort.
@@ -369,21 +367,6 @@ export class Search {
}
this._c.search.oninput((null as Event));
};
-
- this._c.filterArea.appendChild(q.asElement());
-
- // So removing elements doesn't affect us
- let cachedResult = [...result];
- for (let id of cachedResult) {
- const u = this.items[id];
- const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u);
- // Remove from result if not matching query
- if (!q.compare(value)) {
- // console.log("not matching, result is", result);
- result.splice(result.indexOf(id), 1);
- } else {
- }
- }
}
} else if (queryFormat.string) {
q = new StringQuery(queryFormat, split[1]);
@@ -395,17 +378,6 @@ export class Search {
}
this._c.search.oninput((null as Event));
}
-
- this._c.filterArea.appendChild(q.asElement());
-
- let cachedResult = [...result];
- for (let id of cachedResult) {
- const u = this.items[id];
- const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u).toLowerCase();
- if (!q.compare(value)) {
- result.splice(result.indexOf(id), 1);
- }
- }
} else if (queryFormat.date) {
let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]);
if (!isDate) continue;
@@ -419,13 +391,61 @@ export class Search {
this._c.search.oninput((null as Event));
}
-
- this._c.filterArea.appendChild(q.asElement());
+ }
+
+ if (q != null) queries.push(q);
+ }
+ return [searchTerms, queries];
+ }
- let cachedResult = [...result];
+ // Returns a list of identifiers (used as keys in items, values in ordering).
+ search = (query: string): string[] => {
+ this._c.filterArea.textContent = "";
+
+ let result: string[] = [...this._ordering];
+ // If we're in a server search already, the results are already correct.
+ if (this.inServerSearch) return result;
+
+ const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query));
+
+ query = "";
+
+ for (let term of searchTerms) {
+ let cachedResult = [...result];
+ for (let id of cachedResult) {
+ const u = this.items[id];
+ if (!u.matchesSearch(term)) {
+ result.splice(result.indexOf(id), 1);
+ }
+ }
+ }
+ for (let q of queries) {
+ this._c.filterArea.appendChild(q.asElement());
+ let cachedResult = [...result];
+ if (q.subject.bool) {
for (let id of cachedResult) {
const u = this.items[id];
- const unixValue = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u);
+ // Remove from result if not matching query
+ if (!q.compareItem(u)) {
+ // console.log("not matching, result is", result);
+ result.splice(result.indexOf(id), 1);
+ }
+ }
+ } else if (q.subject.string) {
+ for (let id of cachedResult) {
+ const u = this.items[id];
+ // We want to compare case-insensitively, so we get value, lower-case it then compare,
+ // rather than doing both with compareItem.
+ const value = q.getValueFromItem(u).toLowerCase();
+ if (!q.compare(value)) {
+ result.splice(result.indexOf(id), 1);
+ }
+ }
+ } else if(q.subject.date) {
+ for (let id of cachedResult) {
+ const u = this.items[id];
+ // Getter here returns a unix timestamp rather than a date, so we can't use compareItem.
+ const unixValue = q.getValueFromItem(u);
if (unixValue == 0) {
result.splice(result.indexOf(id), 1);
continue;
@@ -437,12 +457,11 @@ export class Search {
}
}
}
-
- if (q != null) queries.push(q);
}
this._queries = queries;
this._searchTerms = searchTerms;
+
return result;
}
@@ -486,25 +505,37 @@ export class Search {
const results = this.search(query);
this._c.setVisibility(results, true);
this._c.onSearchCallback(results.length, newItems, loadAll);
- if (this.inServerSearch) {
- this._serverSearchButtons.forEach((v: HTMLElement) => {
- v.classList.add("@low");
- v.classList.remove("@high");
- });
- } else {
- this._serverSearchButtons.forEach((v: HTMLElement) => {
- v.classList.add("@high");
- v.classList.remove("@low");
- });
+ if (this.inSearch) {
+ if (this.inServerSearch) {
+ this._serverSearchButtons.forEach((v: HTMLElement) => {
+ v.classList.add("@low");
+ v.classList.remove("@high");
+ });
+ } else {
+ this._serverSearchButtons.forEach((v: HTMLElement) => {
+ v.classList.add("@high");
+ v.classList.remove("@low");
+ });
+ }
}
this.showHideSearchOptionsHeader();
- if (results.length == 0) {
+ this.setNotFoundPanelVisibility(results.length == 0);
+ if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0);
+ }
+
+ setNotFoundPanelVisibility = (visible: boolean) => {
+ if (this._inServerSearch || !this.inSearch) {
+ this._c.notFoundLocallyText.classList.add("unfocused");
+ } else if (this.inSearch) {
+ this._c.notFoundLocallyText.classList.remove("unfocused");
+ }
+ if (visible) {
+ console.log("showing not found panel");
this._c.notFoundPanel.classList.remove("unfocused");
} else {
+ console.log("hiding not found panel");
this._c.notFoundPanel.classList.add("unfocused");
-
}
- if (this._c.notFoundCallback) this._c.notFoundCallback(results.length == 0);
}
fillInFilter = (name: string, value: string, offset?: number) => {
@@ -610,6 +641,10 @@ export class Search {
return req;
}
+ setServerSearchButtonsDisabled = (disabled: boolean) => {
+ this._serverSearchButtons.forEach((v: HTMLButtonElement) => v.disabled = disabled);
+ }
+
constructor(c: SearchConfiguration) {
// FIXME: Remove!
if (c.search.id.includes("accounts")) {
diff --git a/usercache.go b/usercache.go
index 9b2e050..7864667 100644
--- a/usercache.go
+++ b/usercache.go
@@ -11,8 +11,12 @@ import (
const (
// FIXME: Follow mediabrowser, or make tuneable, or both
- WEB_USER_CACHE_SYNC = 30 * time.Second
- USER_DEFAULT_SORT_FIELD = "name"
+ // After cache is this old, re-sync, but do it in the background and return the old cache.
+ WEB_USER_CACHE_SYNC = 30 * time.Second
+ // After cache is this old, re-sync and wait for it and return the new cache.
+ WEB_USER_CACHE_WAIT_FOR_SYNC = 5 * time.Minute
+ USER_DEFAULT_SORT_FIELD = "name"
+ USER_DEFAULT_SORT_ASCENDING = true
)
type UserCache struct {
@@ -20,30 +24,67 @@ type UserCache struct {
Ref []*respUser
Sorted bool
LastSync time.Time
- Lock sync.Mutex
+ SyncLock sync.Mutex
+ Syncing bool
+ SortLock sync.Mutex
+ Sorting bool
}
+// FIXME: If shouldSync, sync in background and return old version. If shouldWaitForSync, wait for sync and return new one.
+// FIXME: If locked, just wait for unlock and return someone elses work.
func (c *UserCache) gen(app *appContext) error {
- // FIXME: I don't like this.
- if !time.Now().After(c.LastSync.Add(WEB_USER_CACHE_SYNC)) {
+ shouldWaitForSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_WAIT_FOR_SYNC)) || c.Ref == nil || len(c.Ref) == 0
+ shouldSync := time.Now().After(c.LastSync.Add(WEB_USER_CACHE_SYNC))
+
+ if !shouldSync {
return nil
}
- c.Lock.Lock()
- users, err := app.jf.GetUsers(false)
- if err != nil {
+
+ syncStatus := make(chan error)
+
+ go func(status chan error, c *UserCache) {
+ c.SyncLock.Lock()
+ alreadySyncing := c.Syncing
+ // We're either already syncing or will be
+ c.Syncing = true
+ c.SyncLock.Unlock()
+ if !alreadySyncing {
+ users, err := app.jf.GetUsers(false)
+ if err != nil {
+ c.SyncLock.Lock()
+ c.Syncing = false
+ c.SyncLock.Unlock()
+ status <- err
+ return
+ }
+ cache := make([]respUser, len(users))
+ for i, jfUser := range users {
+ cache[i] = app.userSummary(jfUser)
+ }
+ ref := make([]*respUser, len(cache))
+ for i := range cache {
+ ref[i] = &(cache[i])
+ }
+ c.Cache = cache
+ c.Ref = ref
+ c.Sorted = false
+ c.LastSync = time.Now()
+
+ c.SyncLock.Lock()
+ c.Syncing = false
+ c.SyncLock.Unlock()
+ } else {
+ for c.Syncing {
+ continue
+ }
+ }
+ status <- nil
+ }(syncStatus, c)
+
+ if shouldWaitForSync {
+ err := <-syncStatus
return err
}
- c.Cache = make([]respUser, len(users))
- for i, jfUser := range users {
- c.Cache[i] = app.userSummary(jfUser)
- }
- c.Ref = make([]*respUser, len(c.Cache))
- for i := range c.Cache {
- c.Ref[i] = &(c.Cache[i])
- }
- c.Sorted = false
- c.LastSync = time.Now()
- c.Lock.Unlock()
return nil
}
@@ -52,11 +93,21 @@ func (c *UserCache) Gen(app *appContext, sorted bool) ([]*respUser, error) {
return nil, err
}
if sorted && !c.Sorted {
- c.Lock.Lock()
- // FIXME: Check we want ascending!
- c.Sort(c.Ref, USER_DEFAULT_SORT_FIELD, true)
- c.Sorted = true
- c.Lock.Unlock()
+ c.SortLock.Lock()
+ alreadySorting := c.Sorting
+ c.Sorting = true
+ c.SortLock.Unlock()
+ if !alreadySorting {
+ c.Sort(c.Ref, USER_DEFAULT_SORT_FIELD, USER_DEFAULT_SORT_ASCENDING)
+ c.Sorted = true
+ c.SortLock.Lock()
+ c.Sorting = false
+ c.SortLock.Unlock()
+ } else {
+ for c.Sorting {
+ continue
+ }
+ }
}
return c.Ref, nil
}