From 024b806af1f0821f06b19230e8ee1e9243d5adb3 Mon Sep 17 00:00:00 2001
From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Date: Sun, 17 Aug 2025 13:12:06 -0700
Subject: [PATCH] feat(webui): display Favorite items first (#2656)
Closes #2653
Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2656
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
---
 static/webui/script.js | 302 +++++++++++++++++++++--------------------
 1 file changed, 155 insertions(+), 147 deletions(-)
diff --git a/static/webui/script.js b/static/webui/script.js
index ebad506f1..33fae7493 100644
--- a/static/webui/script.js
+++ b/static/webui/script.js
@@ -662,166 +662,174 @@ function updateInventory() {
                 "KubrowPets"
             ].forEach(category => {
                 document.getElementById(category + "-list").innerHTML = "";
-                data[category].forEach(item => {
-                    const tr = document.createElement("tr");
-                    tr.setAttribute("data-item-type", item.ItemType);
-                    {
-                        const td = document.createElement("td");
-                        td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
-                        if (item.ItemName) {
-                            const pipeIndex = item.ItemName.indexOf("|");
-                            if (pipeIndex != -1) {
-                                td.textContent = item.ItemName.substr(1 + pipeIndex) + " " + td.textContent;
-                            } else {
-                                td.textContent = item.ItemName + " (" + td.textContent + ")";
+                data[category]
+                    .sort((a, b) => (b.Favorite ? 1 : 0) - (a.Favorite ? 1 : 0))
+                    .forEach(item => {
+                        const tr = document.createElement("tr");
+                        tr.setAttribute("data-item-type", item.ItemType);
+                        {
+                            const td = document.createElement("td");
+                            td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
+                            if (item.ItemName) {
+                                const pipeIndex = item.ItemName.indexOf("|");
+                                if (pipeIndex != -1) {
+                                    td.textContent = item.ItemName.substr(1 + pipeIndex) + " " + td.textContent;
+                                } else {
+                                    td.textContent = item.ItemName + " (" + td.textContent + ")";
+                                }
                             }
+                            if (item.Details?.Name) {
+                                td.textContent = item.Details.Name + " (" + td.textContent + ")";
+                            }
+                            if (item.ModularParts && item.ModularParts.length) {
+                                td.textContent += " [";
+                                item.ModularParts.forEach(part => {
+                                    td.textContent += " " + (itemMap[part]?.name ?? part) + ",";
+                                });
+                                td.textContent = td.textContent.slice(0, -1) + " ]";
+                            }
+                            tr.appendChild(td);
                         }
-                        if (item.Details?.Name) {
-                            td.textContent = item.Details.Name + " (" + td.textContent + ")";
-                        }
-                        if (item.ModularParts && item.ModularParts.length) {
-                            td.textContent += " [";
-                            item.ModularParts.forEach(part => {
-                                td.textContent += " " + (itemMap[part]?.name ?? part) + ",";
-                            });
-                            td.textContent = td.textContent.slice(0, -1) + " ]";
-                        }
-                        tr.appendChild(td);
-                    }
-                    {
-                        const td = document.createElement("td");
-                        td.classList = "text-end text-nowrap";
+                        {
+                            const td = document.createElement("td");
+                            td.classList = "text-end text-nowrap";
 
-                        let maxXP = Math.pow(uniqueLevelCaps[item.ItemType] ?? 30, 2) * 1000;
-                        if (
-                            category != "Suits" &&
-                            category != "SpaceSuits" &&
-                            category != "Sentinels" &&
-                            category != "Hoverboards" &&
-                            category != "MechSuits" &&
-                            category != "MoaPets" &&
-                            category != "KubrowPets"
-                        ) {
-                            maxXP /= 2;
-                        }
-                        let anyExaltedMissingXP = false;
-                        if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) {
-                            for (const exaltedType of itemMap[item.ItemType].exalted) {
-                                const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
-                                if (exaltedItem) {
-                                    const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
-                                    if (exaltedItem.XP < exaltedCap) {
-                                        anyExaltedMissingXP = true;
-                                        break;
+                            let maxXP = Math.pow(uniqueLevelCaps[item.ItemType] ?? 30, 2) * 1000;
+                            if (
+                                category != "Suits" &&
+                                category != "SpaceSuits" &&
+                                category != "Sentinels" &&
+                                category != "Hoverboards" &&
+                                category != "MechSuits" &&
+                                category != "MoaPets" &&
+                                category != "KubrowPets"
+                            ) {
+                                maxXP /= 2;
+                            }
+                            let anyExaltedMissingXP = false;
+                            if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) {
+                                for (const exaltedType of itemMap[item.ItemType].exalted) {
+                                    const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
+                                    if (exaltedItem) {
+                                        const exaltedCap =
+                                            itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
+                                        if (exaltedItem.XP < exaltedCap) {
+                                            anyExaltedMissingXP = true;
+                                            break;
+                                        }
                                     }
                                 }
                             }
-                        }
-                        if (item.XP < maxXP || anyExaltedMissingXP) {
-                            const a = document.createElement("a");
-                            a.href = "#";
-                            a.onclick = function (event) {
-                                event.preventDefault();
-                                revalidateAuthz().then(() => {
-                                    const promises = [];
-                                    if (item.XP < maxXP) {
-                                        promises.push(addGearExp(category, item.ItemId.$oid, maxXP - item.XP));
-                                    }
-                                    if ("exalted" in itemMap[item.ItemType]) {
-                                        for (const exaltedType of itemMap[item.ItemType].exalted) {
-                                            const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType);
-                                            if (exaltedItem) {
-                                                const exaltedCap =
-                                                    itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
-                                                if (exaltedItem.XP < exaltedCap) {
-                                                    promises.push(
-                                                        addGearExp(
-                                                            "SpecialItems",
-                                                            exaltedItem.ItemId.$oid,
-                                                            exaltedCap - exaltedItem.XP
-                                                        )
-                                                    );
+                            if (item.XP < maxXP || anyExaltedMissingXP) {
+                                const a = document.createElement("a");
+                                a.href = "#";
+                                a.onclick = function (event) {
+                                    event.preventDefault();
+                                    revalidateAuthz().then(() => {
+                                        const promises = [];
+                                        if (item.XP < maxXP) {
+                                            promises.push(addGearExp(category, item.ItemId.$oid, maxXP - item.XP));
+                                        }
+                                        if ("exalted" in itemMap[item.ItemType]) {
+                                            for (const exaltedType of itemMap[item.ItemType].exalted) {
+                                                const exaltedItem = data.SpecialItems.find(
+                                                    x => x.ItemType == exaltedType
+                                                );
+                                                if (exaltedItem) {
+                                                    const exaltedCap =
+                                                        itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
+                                                    if (exaltedItem.XP < exaltedCap) {
+                                                        promises.push(
+                                                            addGearExp(
+                                                                "SpecialItems",
+                                                                exaltedItem.ItemId.$oid,
+                                                                exaltedCap - exaltedItem.XP
+                                                            )
+                                                        );
+                                                    }
                                                 }
                                             }
                                         }
-                                    }
-                                    Promise.all(promises).then(() => {
-                                        updateInventory();
+                                        Promise.all(promises).then(() => {
+                                            updateInventory();
+                                        });
                                     });
-                                });
-                            };
-                            a.title = loc("code_maxRank");
-                            a.innerHTML = ``;
-                            td.appendChild(a);
-                        }
-
-                        if (
-                            ["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category) ||
-                            modularWeapons.includes(item.ItemType)
-                        ) {
-                            const a = document.createElement("a");
-                            a.href = "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid;
-                            a.innerHTML = ``;
-                            td.appendChild(a);
-                        }
-
-                        if (!(item.Features & 8) && modularWeapons.includes(item.ItemType)) {
-                            const a = document.createElement("a");
-                            a.href = "#";
-                            a.onclick = function (event) {
-                                event.preventDefault();
-                                gildEquipment(category, item.ItemId.$oid);
-                            };
-                            a.title = loc("code_gild");
-                            a.innerHTML = ``;
-                            td.appendChild(a);
-                        }
-                        if (category == "KubrowPets") {
-                            const a = document.createElement("a");
-                            a.href = "#";
-                            a.onclick = function (event) {
-                                event.preventDefault();
-                                maturePet(item.ItemId.$oid, !item.Details.IsPuppy);
-                            };
-                            if (item.Details.IsPuppy) {
-                                a.title = loc("code_mature");
-                                a.innerHTML = ``;
-                            } else {
-                                a.title = loc("code_unmature");
-                                a.innerHTML = ``;
+                                };
+                                a.title = loc("code_maxRank");
+                                a.innerHTML = ``;
+                                td.appendChild(a);
                             }
-                            td.appendChild(a);
-                        }
-                        {
-                            const a = document.createElement("a");
-                            a.href = "#";
-                            a.onclick = function (event) {
-                                event.preventDefault();
-                                const name = prompt(loc("code_renamePrompt"));
-                                if (name !== null) {
-                                    renameGear(category, item.ItemId.$oid, name);
+
+                            if (
+                                ["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(
+                                    category
+                                ) ||
+                                modularWeapons.includes(item.ItemType)
+                            ) {
+                                const a = document.createElement("a");
+                                a.href =
+                                    "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid;
+                                a.innerHTML = ``;
+                                td.appendChild(a);
+                            }
+
+                            if (!(item.Features & 8) && modularWeapons.includes(item.ItemType)) {
+                                const a = document.createElement("a");
+                                a.href = "#";
+                                a.onclick = function (event) {
+                                    event.preventDefault();
+                                    gildEquipment(category, item.ItemId.$oid);
+                                };
+                                a.title = loc("code_gild");
+                                a.innerHTML = ``;
+                                td.appendChild(a);
+                            }
+                            if (category == "KubrowPets") {
+                                const a = document.createElement("a");
+                                a.href = "#";
+                                a.onclick = function (event) {
+                                    event.preventDefault();
+                                    maturePet(item.ItemId.$oid, !item.Details.IsPuppy);
+                                };
+                                if (item.Details.IsPuppy) {
+                                    a.title = loc("code_mature");
+                                    a.innerHTML = ``;
+                                } else {
+                                    a.title = loc("code_unmature");
+                                    a.innerHTML = ``;
                                 }
-                            };
-                            a.title = loc("code_rename");
-                            a.innerHTML = ``;
-                            td.appendChild(a);
+                                td.appendChild(a);
+                            }
+                            {
+                                const a = document.createElement("a");
+                                a.href = "#";
+                                a.onclick = function (event) {
+                                    event.preventDefault();
+                                    const name = prompt(loc("code_renamePrompt"));
+                                    if (name !== null) {
+                                        renameGear(category, item.ItemId.$oid, name);
+                                    }
+                                };
+                                a.title = loc("code_rename");
+                                a.innerHTML = ``;
+                                td.appendChild(a);
+                            }
+                            {
+                                const a = document.createElement("a");
+                                a.href = "#";
+                                a.onclick = function (event) {
+                                    event.preventDefault();
+                                    document.getElementById(category + "-list").removeChild(tr);
+                                    disposeOfGear(category, item.ItemId.$oid);
+                                };
+                                a.title = loc("code_remove");
+                                a.innerHTML = ``;
+                                td.appendChild(a);
+                            }
+                            tr.appendChild(td);
                         }
-                        {
-                            const a = document.createElement("a");
-                            a.href = "#";
-                            a.onclick = function (event) {
-                                event.preventDefault();
-                                document.getElementById(category + "-list").removeChild(tr);
-                                disposeOfGear(category, item.ItemId.$oid);
-                            };
-                            a.title = loc("code_remove");
-                            a.innerHTML = ``;
-                            td.appendChild(a);
-                        }
-                        tr.appendChild(td);
-                    }
-                    document.getElementById(category + "-list").appendChild(tr);
-                });
+                        document.getElementById(category + "-list").appendChild(tr);
+                    });
             });
 
             document.getElementById("EvolutionProgress-list").innerHTML = "";