diff --git a/api-users.go b/api-users.go index 60934b1..38149f0 100644 --- a/api-users.go +++ b/api-users.go @@ -902,13 +902,13 @@ func (app *appContext) userSummary(jfUser mediabrowser.User) respUser { // @tags Activity func (app *appContext) GetUserCount(gc *gin.Context) { resp := PageCountDTO{} - err := app.userCache.Gen(app) + userList, err := app.userCache.Gen(app) if err != nil { app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } - resp.Count = uint64(len(app.userCache.Cache)) + resp.Count = uint64(len(userList)) gc.JSON(200, resp) } @@ -923,13 +923,14 @@ func (app *appContext) GetUsers(gc *gin.Context) { var resp getUsersDTO // We're sending all users, so this is always true resp.LastPage = true - err := app.userCache.Gen(app) + var err error + resp.UserList, err = app.userCache.Gen(app) if err != nil { app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } - resp.UserList = app.userCache.Cache + app.debug.Printf("sending usercache of length %d", len(resp.UserList)) gc.JSON(200, resp) } @@ -951,13 +952,13 @@ func (app *appContext) SearchUsers(gc *gin.Context) { var resp getUsersDTO // We're sending all users, so this is always true resp.LastPage = true - err := app.userCache.Gen(app) + var err error + resp.UserList, err = app.userCache.Gen(app) if err != nil { app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } - resp.UserList = app.userCache.Cache gc.JSON(200, resp) } diff --git a/ts/modules/search.ts b/ts/modules/search.ts index fa8d1e1..a0a446a 100644 --- a/ts/modules/search.ts +++ b/ts/modules/search.ts @@ -2,6 +2,23 @@ const dateParser = require("any-date-parser"); declare var window: GlobalWindow; +export enum QueryOperator { + Greater = ">", + Lower = "<", + Equal = "=" +} + +export function QueryOperatorToDateText(op: QueryOperator): string { + switch (op) { + case QueryOperator.Greater: + return window.lang.strings("after"); + case QueryOperator.Lower: + return window.lang.strings("before"); + default: + return ""; + } +} + export interface QueryType { name: string; description?: string; @@ -28,6 +45,172 @@ export interface SearchConfiguration { loadMore?: () => void; } +export abstract class Query { + protected _subject: QueryType; + protected _operator: QueryOperator; + protected _card: HTMLElement; + + constructor(subject: QueryType, operator: QueryOperator) { + this._subject = subject; + this._operator = operator; + this._card = document.createElement("span"); + this._card.ariaLabel = window.lang.strings("clickToRemoveFilter"); + } + + set onclick(v: () => void) { + this._card.addEventListener("click", v); + } + + asElement(): HTMLElement { return this._card; } +} + + +export class BoolQuery extends Query { + protected _value: boolean; + constructor(subject: QueryType, value: boolean) { + super(subject, QueryOperator.Equal); + this._value = value; + this._card.classList.add("button", "~" + (this._value ? "positive" : "critical"), "@high", "center", "mx-2", "h-full"); + this._card.innerHTML = ` + ${subject.name} + + `; + } + + public static paramsFromString(valueString: string): [boolean, boolean] { + let isBool = false; + let boolState = false; + if (valueString == "true" || valueString == "yes" || valueString == "t" || valueString == "y") { + isBool = true; + boolState = true; + } else if (valueString == "false" || valueString == "no" || valueString == "f" || valueString == "n") { + isBool = true; + boolState = false; + } + return [boolState, isBool] + } + + get value(): boolean { return this._value; } + + // Ripped from old code. Why it's like this, I don't know + public compare(subjectBool: boolean): boolean { + return ((subjectBool && this._value) || (!subjectBool && !this._value)) + } +} + +export class StringQuery extends Query { + protected _value: string; + constructor(subject: QueryType, value: string) { + super(subject, QueryOperator.Equal); + this._value = value; + this._card.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full"); + this._card.innerHTML = ` + ${subject.name}: "${this._value}" + `; + } + + get value(): string { return this._value; } +} + +export interface DateAttempt { + year?: number; + month?: number; + day?: number; + hour?: number; + minute?: number +} + +export interface ParsedDate { + attempt: DateAttempt; + date: Date; + text: string; +}; + +const dateGetters: Map number> = (() => { + let m = new Map number>(); + m.set("year", Date.prototype.getFullYear); + m.set("month", Date.prototype.getMonth); + m.set("day", Date.prototype.getDate); + m.set("hour", Date.prototype.getHours); + m.set("minute", Date.prototype.getMinutes); + return m; +})(); +const dateSetters: Map void> = (() => { + let m = new Map void>(); + m.set("year", Date.prototype.setFullYear); + m.set("month", Date.prototype.setMonth); + m.set("day", Date.prototype.setDate); + m.set("hour", Date.prototype.setHours); + m.set("minute", Date.prototype.setMinutes); + return m; +})(); + +export class DateQuery extends Query { + protected _value: ParsedDate; + + constructor(subject: QueryType, operator: QueryOperator, value: ParsedDate) { + super(subject, operator); + this._value = value; + console.log("op:", operator, "date:", value); + this._card.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); + let dateText = QueryOperatorToDateText(operator); + this._card.innerHTML = ` + ${subject.name}: ${dateText != "" ? dateText+" " : ""}${value.text} + `; + } + public static paramsFromString(valueString: string): [ParsedDate, QueryOperator, boolean] { + // FIXME: Validate this! + let op = QueryOperator.Equal; + if ((Object.values(QueryOperator) as string[]).includes(valueString.charAt(0))) { + op = valueString.charAt(0) as QueryOperator; + // Trim the operator from the string + valueString = valueString.substring(1); + } + + let out: ParsedDate = { + text: valueString, + // Used just to tell use what fields the user passed. + attempt: dateParser.attempt(valueString), + // note Date.fromString is also provided by dateParser. + date: (Date as any).fromString(valueString) as Date + }; + // Month in Date objects is 0-based, so make our parsed date that way too + if ("month" in out.attempt) out.attempt.month -= 1; + let isValid = true; + if ("invalid" in (out.date as any)) { isValid = false; }; + + return [out, op, isValid]; + } + + get value(): ParsedDate { return this._value; } + + public compare(subjectDate: Date): boolean { + // We want to compare only the fields given in this._value, + // so we copy subjectDate and apply on those fields from this._value. + const temp = new Date(subjectDate.valueOf()); + for (let [field] of dateGetters) { + if (field in this._value.attempt) { + dateSetters.get(field).call( + temp, + dateGetters.get(field).call(this._value.date) + ); + } + } + + if (this._operator == QueryOperator.Equal) { + return subjectDate.getTime() == temp.getTime(); + } else if (this._operator == QueryOperator.Lower) { + return subjectDate < temp; + } + return subjectDate > temp; + } +} + + +// FIXME: Continue taking stuff from search function, making XQuery classes! + + + export interface SearchableItem { matchesSearch: (query: string) => boolean; } @@ -99,33 +282,20 @@ export class Search { const queryFormat = this._c.queries[split[0]]; - if (queryFormat.bool) { - let isBool = false; - let boolState = false; - if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") { - isBool = true; - boolState = true; - } else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") { - isBool = true; - boolState = false; - } - if (isBool) { - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name} - - `; + let formattedQuery = [] - filterCard.addEventListener("click", () => { + if (queryFormat.bool) { + let [boolState, isBool] = BoolQuery.paramsFromString(split[1]); + if (isBool) { + let q = new BoolQuery(queryFormat, boolState); + q.onclick = () => { for (let quote of [`"`, `'`, ``]) { this._c.search.value = this._c.search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); } this._c.search.oninput((null as Event)); - }) + }; - this._c.filterArea.appendChild(filterCard); + this._c.filterArea.appendChild(q.asElement()); // console.log("is bool, state", boolState); // So removing elements doesn't affect us @@ -135,7 +305,7 @@ export class Search { const value = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(u), queryFormat.getter).get.call(u); // console.log("got", queryFormat.getter + ":", value); // Remove from result if not matching query - if (!((value && boolState) || (!value && !boolState))) { + if (!q.compare(value)) { // console.log("not matching, result is", result); result.splice(result.indexOf(id), 1); } @@ -144,22 +314,17 @@ export class Search { } } if (queryFormat.string) { - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name}: "${split[1]}" - `; + const q = new StringQuery(queryFormat, split[1]); - filterCard.addEventListener("click", () => { + q.onclick = () => { for (let quote of [`"`, `'`, ``]) { let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); this._c.search.value = this._c.search.value.replace(regex, ""); } this._c.search.oninput((null as Event)); - }) + } - this._c.filterArea.appendChild(filterCard); + this._c.filterArea.appendChild(q.asElement()); let cachedResult = [...result]; for (let id of cachedResult) { @@ -172,39 +337,20 @@ export class Search { continue; } if (queryFormat.date) { - // -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0 - let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2)); - let unmodifiedValue = split[1]; - if (compareType != 2) { - split[1] = split[1].substring(1); - } - if (compareType == 2) compareType = 0; - - let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]); - // Month in Date objects is 0-based, so make our parsed date that way too - if ("month" in attempt) attempt.month -= 1; - - let date: Date = (Date as any).fromString(split[1]) as Date; - console.log("Read", attempt, "and", date); - if ("invalid" in (date as any)) continue; - - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name}: ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]} - `; + let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]); + if (!isDate) continue; + const q = new DateQuery(queryFormat, op, parsedDate); - filterCard.addEventListener("click", () => { + q.onclick = () => { for (let quote of [`"`, `'`, ``]) { - let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig"); + let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); this._c.search.value = this._c.search.value.replace(regex, ""); } this._c.search.oninput((null as Event)); - }) + } - this._c.filterArea.appendChild(filterCard); + this._c.filterArea.appendChild(q.asElement()); let cachedResult = [...result]; for (let id of cachedResult) { @@ -215,33 +361,8 @@ export class Search { continue; } let value = new Date(unixValue*1000); - - const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]]; - // When doing > or <