From 08c350d50b8ef68dda96f160ebbf0da522a05fcf Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 25 Nov 2025 14:43:56 +0000 Subject: [PATCH] settings: fix search with groups works now, as in searching for a group's name works, and seeing matches within groups works. --- scripts/ini/go.mod | 14 +- scripts/ini/go.sum | 9 +- scripts/ini/main.go | 43 ------ ts/modules/settings.ts | 331 +++++++++++++++++++++++++++++------------ 4 files changed, 254 insertions(+), 143 deletions(-) diff --git a/scripts/ini/go.mod b/scripts/ini/go.mod index 412489d..63b80b8 100644 --- a/scripts/ini/go.mod +++ b/scripts/ini/go.mod @@ -2,15 +2,21 @@ module github.com/hrfee/jfa-go/scripts/ini replace github.com/hrfee/jfa-go/common => ../../common +replace github.com/hrfee/jfa-go/logmessages => ../../logmessages + go 1.22.4 require ( - github.com/fatih/color v1.18.0 // indirect - github.com/hrfee/jfa-go/common v0.0.0-20240824141650-fcdd4e451882 // indirect + github.com/fatih/color v1.18.0 + github.com/hrfee/jfa-go/common v0.0.0-00010101000000-000000000000 + gopkg.in/ini.v1 v1.67.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/stretchr/testify v1.11.1 // indirect golang.org/x/sys v0.25.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/scripts/ini/go.sum b/scripts/ini/go.sum index 624d5d9..495a954 100644 --- a/scripts/ini/go.sum +++ b/scripts/ini/go.sum @@ -1,16 +1,21 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a h1:qbXZgCqb9eaPSJfLEXczQD2lxTv6jb6silMPIWW9j6o= -github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a/go.mod h1:c5HKkLayo0GrEUDlJwT12b67BL9cdPjP271Xlv/KDRQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/scripts/ini/main.go b/scripts/ini/main.go index 3221462..9e68358 100644 --- a/scripts/ini/main.go +++ b/scripts/ini/main.go @@ -7,7 +7,6 @@ import ( "os" "strings" - "github.com/fatih/color" "github.com/hrfee/jfa-go/common" "gopkg.in/ini.v1" "gopkg.in/yaml.v3" @@ -27,48 +26,6 @@ func generateIni(yamlPath string, iniPath string) { if err != nil { panic(err) } - // Validate that all groups/sections are listed in the root order, if it exists - if len(configBase.Order) > 0 { - // Expand order - var traverseGroup func(groupName string) []string - traverseGroup = func(groupName string) []string { - out := []string{} - for _, group := range configBase.Groups { - if group.Group == groupName { - for _, groupMember := range group.Members { - if groupMember.Group != "" { - out = append(out, traverseGroup(groupMember.Group)...) - } else if groupMember.Section != "" { - out = append(out, groupMember.Section) - } - } - break - } - } - return out - } - listedSects := map[string]bool{} - for _, member := range configBase.Order { - if member.Group != "" { - for _, sect := range traverseGroup(member.Group) { - listedSects[sect] = true - } - } else if member.Section != "" { - listedSects[member.Section] = true - } - } - - missingSections := false - for _, section := range configBase.Sections { - if _, ok := listedSects[section.Section]; !ok { - if !missingSections { - color.Red("WARNING: Root order specified but the following sections were not listed, directly or indirectly:") - missingSections = true - } - color.Red("\t%s", section.Section) - } - } - } conf := ini.Empty() diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index a4ab749..9162226 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -479,6 +479,168 @@ interface Group { members: Member[]; } +class groupButton { + private _el: HTMLElement; + private _button: HTMLElement; + private _dropdown: HTMLElement; + private _icon: HTMLElement; + private _check: HTMLInputElement; + private _group: Group; + private _indent: number; + private _parentSidebar: HTMLElement; + + asElement = () => { return this._el; }; + + remove = () => { this._el.remove(); }; + + update = (g: Group) => { + this._group = g; + this.group = g.group; + this.name = g.name; + this.description = g.description; + }; + + append(item: HTMLElement|groupButton) { + if (item instanceof groupButton) { + this._dropdown.appendChild(item.asElement()); + } else { + this._dropdown.appendChild(item); + } + } + + get name(): string { return this._group.name; } + set name(v: string) { + this._group.name = v; + this._button.querySelector(".group-button-name").textContent = v; + } + + get group(): string { return this._group.group; } + set group(v: string) { + this._group.group = v; + this._el.setAttribute("data-group", v); + this._button.setAttribute("data-group", v); + this._check.setAttribute("data-group", v); + this._dropdown.setAttribute("data-group", v); + } + + get description(): string { return this._group.description; } + set description(v: string) { this._group.description = v; } + + get indent(): number { return this._indent; } + set indent(v: number) { + this._dropdown.classList.remove("ml-" + ((this._indent+1)*2)); + this._indent = v; + this._dropdown.classList.add("ml-" + ((this._indent+1)*2)); + } + + get hidden(): boolean { return this._el.classList.contains("unfocused"); } + set hidden(v: boolean) { + if (v) this._el.classList.add("unfocused"); + else this._el.classList.remove("unfocused"); + } + + get open(): boolean { return this._check.checked; } + set open(v: boolean) { + this.openCloseWithAnimation(v); + } + + openCloseWithAnimation(v: boolean) { + this._check.checked = v; + // When groups are nested, the outer group's scrollHeight will obviously change when an + // inner group is opened/closed. Instead of traversing the tree and adjusting the maxHeight property + // each open/close, just set the maxHeight to 9999px once the animation is completed. + // On close, quickly set maxHeight back to ~scrollHeight, then animate to 0. + if (this._check.checked) { + this._icon.classList.add("rotated"); + // Hide the scrollbar while we animate + this._parentSidebar.style.overflowY = "hidden"; + this._dropdown.classList.remove("unfocused"); + const fullHeight = () => { + this._dropdown.removeEventListener("transitionend", fullHeight); + this._dropdown.style.maxHeight = "9999px"; + // Return the scrollbar (or whatever, just don't hide it) + this._parentSidebar.style.overflowY = ""; + }; + this._dropdown.addEventListener("transitionend", fullHeight); + this._dropdown.style.maxHeight = (1.2*this._dropdown.scrollHeight)+"px"; + this._dropdown.style.opacity = "100%"; + } else { + this._icon.classList.remove("rotated"); + const mainTransitionEnd = () => { + this._dropdown.removeEventListener("transitionend", mainTransitionEnd); + this._dropdown.classList.add("unfocused"); + // Return the scrollbar (or whatever, just don't hide it) + this._parentSidebar.style.overflowY = ""; + }; + const mainTransitionStart = () => { + this._dropdown.removeEventListener("transitionend", mainTransitionStart) + this._dropdown.style.transitionDuration = ""; + this._dropdown.addEventListener("transitionend", mainTransitionEnd); + this._dropdown.style.maxHeight = "0"; + this._dropdown.style.opacity = "0"; + } + // Hide the scrollbar while we animate + this._parentSidebar.style.overflowY = "hidden"; + // Disabling transitions then going from 9999 - scrollHeight doesn't work in firefox to me, + // so instead just make the transition duration really short. + this._dropdown.style.transitionDuration = "1ms"; + this._dropdown.addEventListener("transitionend", mainTransitionStart); + this._dropdown.style.maxHeight = (1.2*this._dropdown.scrollHeight)+"px"; + } + } + + openCloseWithoutAnimation(v: boolean) { + this._check.checked = v; + if (this._check.checked) { + this._icon.classList.add("rotated"); + this._dropdown.style.maxHeight = "9999px"; + this._dropdown.style.opacity = "100%"; + this._dropdown.classList.remove("unfocused"); + } else { + this._icon.classList.remove("rotated"); + this._dropdown.style.maxHeight = "0"; + this._dropdown.style.opacity = "0"; + this._dropdown.classList.add("unfocused"); + } + } + + // Takes sidebar as we need to disable scrolling on it when animation starts. + constructor(parentSidebar: HTMLElement) { + this._parentSidebar = parentSidebar; + + this._el = document.createElement("div"); + this._el.classList.add("flex", "flex-col", "gap-2"); + + this._button = document.createElement("span") as HTMLSpanElement; + this._el.appendChild(this._button); + this._button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between"); + this._button.innerHTML = ` + + + `; + + this._dropdown = document.createElement("div") as HTMLDivElement; + this._el.appendChild(this._dropdown); + this._dropdown.style.maxHeight = "0"; + this._dropdown.style.opacity = "0"; + this._dropdown.classList.add("settings-dropdown", "unfocused", "flex", "flex-col", "gap-2", "transition-all"); + + this._icon = this._button.querySelector("i.icon"); + this._check = this._button.querySelector("input[type=checkbox]") as HTMLInputElement; + + this._button.onclick = () => { + this.open = !this.open; + }; + this._check.onclick = () => { + this.open = this.open; + } + this.openCloseWithoutAnimation(false); + } +}; + interface Section { section: string; meta: Meta; @@ -595,7 +757,7 @@ export class settingsList { private _buttons: { [name: string]: HTMLSpanElement }; private _groups: { [name: string]: Group }; - private _groupButtons: { [name: string]: HTMLSpanElement }; + private _groupButtons: { [name: string]: groupButton }; private _needsRestart: boolean = false; private _messageEditor = new MessageEditor(); @@ -614,82 +776,12 @@ export class settingsList { // Takes all groups at once since members might contain each other. addGroups = (groups: Group[]) => { groups.forEach((g) => { this._groups[g.group] = g }); - const addGroup = (g: Group, indent: number = 0): HTMLElement => { + const addGroup = (g: Group, indent: number = 0): groupButton => { if (g.group in this._groupButtons) return null; - const container = document.createElement("div") as HTMLDivElement; - container.classList.add("flex", "flex-col", "gap-2"); - - const button = document.createElement("span") as HTMLSpanElement; - container.appendChild(button); - button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between"); - button.innerHTML = ` - ${g.name} - - `; - - const dropdown = document.createElement("div") as HTMLDivElement; - container.appendChild(dropdown); - dropdown.classList.add("ml-" + ((indent+1)*2)); - dropdown.style.maxHeight = "0"; - dropdown.style.opacity = "0"; - dropdown.classList.add("settings-dropdown", "unfocused", "flex", "flex-col", "gap-2", "transition-all"); - - const icon = button.querySelector("i.icon"); - const check = button.querySelector("input[type=checkbox]") as HTMLInputElement; - - - button.onclick = () => { - check.checked = !check.checked; - onCheck(); - }; - // When groups are nested, the outer group's scrollHeight will obviously change when an - // inner group is opened/closed. Instead of traversing the tree and adjusting the maxHeight property - // each open/close, just set the maxHeight to 9999px once the animation is completed. - // On close, quickly set maxHeight back to ~scrollHeight, then animate to 0. - const onCheck = () => { - if (check.checked) { - icon.classList.add("rotated"); - // Hide the scrollbar while we animate - this._sidebar.style.overflowY = "hidden"; - dropdown.classList.remove("unfocused"); - const fullHeight = () => { - dropdown.removeEventListener("transitionend", fullHeight); - dropdown.style.maxHeight = "9999px"; - // Return the scrollbar (or whatever, just don't hide it) - this._sidebar.style.overflowY = ""; - }; - dropdown.addEventListener("transitionend", fullHeight); - dropdown.style.maxHeight = (1.2*dropdown.scrollHeight)+"px"; - dropdown.style.opacity = "100%"; - } else { - icon.classList.remove("rotated"); - const mainTransitionEnd = () => { - dropdown.removeEventListener("transitionend", mainTransitionEnd); - dropdown.classList.add("unfocused"); - // Return the scrollbar (or whatever, just don't hide it) - this._sidebar.style.overflowY = ""; - }; - const mainTransitionStart = () => { - dropdown.removeEventListener("transitionend", mainTransitionStart) - dropdown.style.transitionDuration = ""; - dropdown.addEventListener("transitionend", mainTransitionEnd); - dropdown.style.maxHeight = "0"; - dropdown.style.opacity = "0"; - } - // Hide the scrollbar while we animate - this._sidebar.style.overflowY = "hidden"; - // Disabling transitions then going from 9999 - scrollHeight doesn't work in firefox to me, - // so instead just make the transition duration really short. - dropdown.style.transitionDuration = "1ms"; - dropdown.addEventListener("transitionend", mainTransitionStart); - dropdown.style.maxHeight = (1.2*dropdown.scrollHeight)+"px"; - } - } - check.onchange = onCheck; + const container = new groupButton(this._sidebar); + container.update(g); + container.indent = indent; for (const member of g.members) { if ("group" in member) { @@ -699,12 +791,12 @@ export class settingsList { // Remove from page subgroup.remove(); } - dropdown.appendChild(subgroup); + container.append(subgroup); } else if ("section" in member) { const subsection = this._buttons[member.section]; // Remove from page subsection.remove(); - dropdown.appendChild(subsection); + container.append(subsection); } } @@ -714,7 +806,8 @@ export class settingsList { for (let g of groups) { const container = addGroup(g); if (container) { - this._sidebar.appendChild(container); + this._sidebar.appendChild(container.asElement()); + container.openCloseWithoutAnimation(false); } } } @@ -762,11 +855,27 @@ export class settingsList { this._sidebar.appendChild(this._buttons[name]); } - setOrder(order: Member[]) { + private _traverseMemberList = (list: Member[], func: (sect: string) => void) => { + for (const member of list) { + if ("group" in member) { + for (const group of this._settings.groups) { + if (group.group == member.group) { + this._traverseMemberList(group.members, func); + break; + } + } + } else { + func(member.section); + } + } + } + + setUIOrder(order: Member[]) { this._sidebar.textContent = ``; for (const member of order) { if ("group" in member) { - this._sidebar.appendChild(this._groupButtons[member.group]); + this._sidebar.appendChild(this._groupButtons[member.group].asElement()); + this._groupButtons[member.group].openCloseWithoutAnimation(false); } else if ("section" in member) { if (member.section in this._buttons) { this._sidebar.appendChild(this._buttons[member.section]); @@ -1079,7 +1188,7 @@ export class settingsList { this.addGroups(this._settings.groups); - if ("order" in this._settings && this._settings.order) this.setOrder(this._settings.order); + if ("order" in this._settings && this._settings.order) this.setUIOrder(this._settings.order); removeLoader(this._loader); for (let i = 0; i < this._loader.children.length; i++) { @@ -1097,6 +1206,7 @@ export class settingsList { }) }; + // FIXME: Fix searching groups // FIXME: Search "About" & "User profiles", pseudo-search "User profiles" for things like "Ombi", "Referrals", etc. search = (query: string) => { query = query.toLowerCase().trim(); @@ -1104,11 +1214,20 @@ export class settingsList { if (query.replace(/\s+/g, "") == "") query = ""; let firstVisibleSection = ""; - for (let section of this._settings.sections) { + + // Close and hide all groups to start with + for (const groupButton of Object.values(this._groupButtons)) { + groupButton.openCloseWithoutAnimation(false); + groupButton.hidden = !(groupButton.group.toLowerCase().includes(query) || + groupButton.name.toLowerCase().includes(query) || + groupButton.description.toLowerCase().includes(query)); + } + + const searchSection = (section: Section) => { // Section might be disabled at build-time (like Updates), or deprecated and so not appear. if (!(section.section in this._sections)) { // console.log(`Couldn't find section "${section.section}"`); - continue + return; } const sectionElement = this._sections[section.section].asElement(); let dependencyCard = sectionElement.querySelector(".settings-dependency-message"); @@ -1117,19 +1236,38 @@ export class settingsList { let dependencyList = null; // hide button, unhide if matched - this._buttons[section.section].classList.add("unfocused"); + const button = this._buttons[section.section]; + button.classList.add("unfocused"); + const parentGroup = button.parentElement.getAttribute("data-group"); + let parentGroupButton: groupButton = null; + let matchedGroup = false; + if (parentGroup) { + parentGroupButton = this._groupButtons[parentGroup]; + matchedGroup = !(parentGroupButton.hidden); + } - let matchedSection = false; - - if (section.section.toLowerCase().includes(query) || - section.meta.name.toLowerCase().includes(query) || - section.meta.description.toLowerCase().includes(query)) { - if ((section.meta.advanced && this._advanced) || !(section.meta.advanced)) { - this._buttons[section.section].classList.remove("unfocused"); - firstVisibleSection = firstVisibleSection || section.section; - matchedSection = true; + const show = () => { + button.classList.remove("unfocused"); + if (parentGroupButton) { + if (query != "") parentGroupButton.openCloseWithoutAnimation(true); + parentGroupButton.hidden = false; } } + const hide = () => { + button.classList.add("unfocused"); + } + + let matchedSection = matchedGroup || + section.section.toLowerCase().includes(query) || + section.meta.name.toLowerCase().includes(query) || + section.meta.description.toLowerCase().includes(query); + matchedSection &&= ((section.meta.advanced && this._advanced) || !(section.meta.advanced)); + + if (matchedSection) { + show(); + firstVisibleSection = firstVisibleSection || section.section; + } + for (let setting of section.settings) { if (setting.type == "note") continue; const element = sectionElement.querySelector(`div[data-name="${setting.setting}"]`) as HTMLElement; @@ -1153,7 +1291,7 @@ export class settingsList { setting.description.toLowerCase().includes(query) || String(setting.value).toLowerCase().includes(query)) { if ((section.meta.advanced && this._advanced) || !(section.meta.advanced)) { - this._buttons[section.section].classList.remove("unfocused"); + show(); firstVisibleSection = firstVisibleSection || section.section; } const shouldShow = (query != "" && @@ -1198,7 +1336,12 @@ export class settingsList { } } } + }; + + for (let section of this._settings.sections) { + searchSection(section); } + if (firstVisibleSection && (query != "" || this._visibleSection == "")) { this._buttons[firstVisibleSection].onclick(null); this._noResultsPanel.classList.add("unfocused");