diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 4f03ee94..a9f8095f 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService" import { ExportArcanes, ExportAvionics, + ExportBoosters, ExportCustoms, ExportDrones, ExportGear, @@ -55,6 +56,7 @@ interface ItemLists { KubrowPets: ListedItem[]; EvolutionProgress: ListedItem[]; mods: ListedItem[]; + Boosters: ListedItem[]; } const relicQualitySuffixes: Record = { @@ -86,7 +88,8 @@ const getItemListsController: RequestHandler = (req, response) => { QuestKeys: [], KubrowPets: [], EvolutionProgress: [], - mods: [] + mods: [], + Boosters: [] }; for (const [uniqueName, item] of Object.entries(ExportWarframes)) { res[item.productCategory].push({ @@ -296,6 +299,13 @@ const getItemListsController: RequestHandler = (req, response) => { }); } + for (const item of Object.values(ExportBoosters)) { + res.Boosters.push({ + uniqueName: item.typeName, + name: getString(item.name, lang) + }); + } + response.json(res); }; diff --git a/src/controllers/custom/setBoosterController.ts b/src/controllers/custom/setBoosterController.ts new file mode 100644 index 00000000..f19a3093 --- /dev/null +++ b/src/controllers/custom/setBoosterController.ts @@ -0,0 +1,37 @@ +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getInventory } from "@/src/services/inventoryService"; +import { RequestHandler } from "express"; +import { ExportBoosters } from "warframe-public-export-plus"; + +const I32_MAX = 0x7fffffff; + +export const setBoosterController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const requests = req.body as { ItemType: string; ExpiryDate: number }[]; + const inventory = await getInventory(accountId); + const boosters = inventory.Boosters; + if ( + requests.some(request => { + if (typeof request.ItemType !== "string") return true; + if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined) + return true; + if (typeof request.ExpiryDate !== "number") return true; + if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true; + return false; + }) + ) { + res.status(400).send("Invalid ItemType provided."); + return; + } + // Remove if ExpiryDate lower than current time? + for (const { ItemType, ExpiryDate } of requests) { + const boosterItem = boosters.find(item => item.ItemType === ItemType); + if (boosterItem) { + boosterItem.ExpiryDate = ExpiryDate; + } else { + boosters.push({ ItemType, ExpiryDate }); + } + } + await inventory.save(); + res.end(); +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 8411d996..7d8c7c82 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -23,6 +23,7 @@ import { setEvolutionProgressController } from "@/src/controllers/custom/setEvol import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController"; +import { setBoosterController } from "../controllers/custom/setBoosterController"; const customRouter = express.Router(); @@ -46,6 +47,7 @@ customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); customRouter.post("/manageQuests", manageQuestsController); customRouter.post("/setEvolutionProgress", setEvolutionProgressController); +customRouter.post("/setBooster", setBoosterController); customRouter.get("/config", getConfigDataController); customRouter.post("/config", updateConfigDataController); diff --git a/static/webui/index.html b/static/webui/index.html index 1b42793e..643270a7 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -416,6 +416,20 @@ +
+
+
+
+
+ + +
+ + +
+
+
+
@@ -804,6 +818,7 @@ + diff --git a/static/webui/script.js b/static/webui/script.js index f4abcb9f..c0550a4f 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1011,6 +1011,78 @@ function updateInventory() { } } document.getElementById("changeSyndicate").value = data.SupportedSyndicate ?? ""; + + + document.getElementById("Boosters-list").innerHTML = ""; + const now = Math.floor(Date.now() / 1000); + data.Boosters.forEach(({ItemType, ExpiryDate}) => { + if (ExpiryDate < now) { + // Booster has expired, skip it + return; + } + const tr = document.createElement("tr"); + { + const td = document.createElement("td"); + td.textContent = itemMap[ItemType]?.name ?? ItemType; + tr.appendChild(td); + } + { + const td = document.createElement("td"); + td.classList = "text-end text-nowrap"; + const timeString = formatDatetime("%Y-%m-%d %H:%M:%s", ExpiryDate * 1000); + const inlineForm = document.createElement("form"); + const input = document.createElement("input"); + const a = document.createElement("a"); + + a.href = "#"; + a.onclick = (event)=>{ + event.preventDefault(); + if (inlineForm.style.display === "none") { + inlineForm.style.display = "inline"; + input.value = timeString; + a.style.display = "none"; + input.focus(); + } else { + inlineForm.style.display = "none"; + a.style.display = "inline"; + input.value = ""; + } + }; + a.textContent = timeString; + a.title = loc("code_changeExpiry"); + a.classList.add("text-decoration-none"); + td.appendChild(a); + + const submit = ()=>{ + if (doChangeBoosterExpiry(ItemType, input.value)){ + inlineForm.style.display = "none"; + input.value = ""; + a.style.display = "inline"; + } + }; + + inlineForm.style.display = "none"; + inlineForm.onsubmit = function (event) { + event.preventDefault(); + submit(); + }; + input.type = "datetime-local"; + input.classList.add("form-control"); + input.classList.add("form-control-sm"); + input.value = timeString; + input.onblur = function () { + if (inlineForm.style.display === "inline") { + submit(); + } + } + inlineForm.appendChild(input); + + td.appendChild(inlineForm); + + tr.appendChild(td); + } + document.getElementById("Boosters-list").appendChild(tr); + }) }); }); } @@ -2027,3 +2099,71 @@ function handleModularSelection(category) { }); }); } + +function setBooster(ItemType, ExpiryDate) { + revalidateAuthz(() => { + $.post({ + url: "/custom/setBooster?" + window.authz, + contentType: "application/json", + data: JSON.stringify([{ + ItemType, + ExpiryDate + }]) + }).done(function () { + updateInventory(); + }); + }); +} + +function doAcquireBoosters() { + const uniqueName = getKey(document.getElementById("acquire-type-Boosters")); + if (!uniqueName) { + $("#acquire-type-Boosters").addClass("is-invalid").focus(); + return; + } + const ExpiryDate = (Date.now() / 1000) + 3 * 24 * 60 * 60; // default 3 days + setBooster(uniqueName, ExpiryDate); +} + +function doChangeBoosterExpiry(ItemType, ExpiryDateInput) { + console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput); + // cast local datetime string to unix timestamp + const ExpiryDate = new Date(ExpiryDateInput).getTime() / 1000; + if (isNaN(ExpiryDate)) { + $("#expiry-date-" + ItemType).addClass("is-invalid").focus(); + return false; + } + setBooster(ItemType, ExpiryDate); + return true; +} + +function formatDatetime(fmt, date) { + if (typeof date === 'number') date = new Date(date); + return fmt.replace(/(%[yY]|%m|%[Dd]|%H|%h|%M|%[Ss]|%[Pp])/g, match => { + switch (match) { + case '%Y': + return date.getFullYear().toString(); + case '%y': + return date.getFullYear().toString().slice(-2); + case '%m': + return (date.getMonth() + 1).toString().padStart(2, '0'); + case '%D': + case '%d': + return date.getDate().toString().padStart(2, '0'); + case '%H': + return date.getHours().toString().padStart(2, '0'); + case '%h': + return (date.getHours() % 12).toString().padStart(2, '0'); + case '%M': + return date.getMinutes().toString().padStart(2, '0'); + case '%S': + case '%s': + return date.getSeconds().toString().padStart(2, '0'); + case '%P': + case '%p': + return date.getHours() < 12 ? 'am' : 'pm'; + default: + return match; + } + }) +} \ No newline at end of file diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index eb721391..4e1b8a2d 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -98,6 +98,7 @@ dict = { inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`, inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`, inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`, + inventory_Boosters: `[UNTRANSLATED] Boosters`, quests_list: `Quests`, quests_completeAll: `Alle Quests abschließen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index cd718917..79cec6e7 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -97,6 +97,7 @@ dict = { inventory_bulkRankUpSentinels: `Max Rank All Sentinels`, inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`, inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`, + inventory_Boosters: `Boosters`, quests_list: `Quests`, quests_completeAll: `Complete All Quests`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 652a850c..72d8453b 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -98,6 +98,7 @@ dict = { inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`, inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`, inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`, + inventory_Boosters: `[UNTRANSLATED] Boosters`, quests_list: `Misiones`, quests_completeAll: `Completar todas las misiones`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 5a61111e..bc079ef0 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -98,6 +98,7 @@ dict = { inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`, inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`, inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`, + inventory_Boosters: `[UNTRANSLATED] Boosters`, quests_list: `Quêtes`, quests_completeAll: `Compléter toutes les quêtes`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 445c519a..c99ca154 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -98,6 +98,7 @@ dict = { inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`, inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`, inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`, + inventory_Boosters: `[UNTRANSLATED] Boosters`, quests_list: `Квесты`, quests_completeAll: `Завершить все квесты`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index d46ce77d..d6eb68b3 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -98,6 +98,7 @@ dict = { inventory_bulkRankUpSentinels: `所有守护升满级`, inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, inventory_bulkRankUpEvolutionProgress: `所有灵化之源最大等级`, + inventory_Boosters: `加成器`, quests_list: `任务`, quests_completeAll: `完成所有任务`,