From 26be26b295f40ed6829ef4557fc0b65b7ac62a11 Mon Sep 17 00:00:00 2001 From: Daymi Date: Mon, 4 May 2026 22:52:54 +0200 Subject: [PATCH] Add hide empty mod categories setting --- .../plugins/ROSE-CustomWheel/index.js | 132 +++++++++++++++--- .../plugins/ROSE-SettingsPanel/index.js | 37 ++++- pengu/communication/message_handler.py | 9 +- 3 files changed, 155 insertions(+), 23 deletions(-) diff --git a/Pengu Loader/plugins/ROSE-CustomWheel/index.js b/Pengu Loader/plugins/ROSE-CustomWheel/index.js index db5e274e..51f0e141 100644 --- a/Pengu Loader/plugins/ROSE-CustomWheel/index.js +++ b/Pengu Loader/plugins/ROSE-CustomWheel/index.js @@ -27,6 +27,10 @@ let selectedMapId = null; let selectedFontId = null; let selectedAnnouncerId = null; + let hideEmptyCategories = false; + let lastMapsMods = null; + let lastFontsMods = null; + let lastAnnouncersMods = null; // Per-category multi-selection (UI / Voiceover / Loading Screen / VFX / SFX / Others). // These are first-class categories in the UI; they just share the same list rendering logic. let selectedCategoryIds = Object.create(null); @@ -133,6 +137,67 @@ return SUMMARY_TABS.find((t) => t.id === tabId)?.label || String(tabId || ""); } + function tabHasInstalledMods(tabId) { + if (tabId === "skins") return true; + if (tabId === "maps") return Array.isArray(lastMapsMods) && lastMapsMods.length > 0; + if (tabId === "fonts") return Array.isArray(lastFontsMods) && lastFontsMods.length > 0; + if (tabId === "announcers") return Array.isArray(lastAnnouncersMods) && lastAnnouncersMods.length > 0; + + if (OTHER_CATEGORY_TABS.some((t) => t.id === tabId)) { + if (!Object.prototype.hasOwnProperty.call(lastCategoryModsById, tabId)) return false; + const mods = lastCategoryModsById[tabId]; + return Array.isArray(mods) && mods.length > 0; + } + + return true; + } + + function getVisibleSummaryTabs() { + if (!hideEmptyCategories) return SUMMARY_TABS; + return SUMMARY_TABS.filter((tab) => tab.id === "skins" || tabHasInstalledMods(tab.id)); + } + + function isSummaryTabVisible(tabId) { + return getVisibleSummaryTabs().some((tab) => tab.id === tabId); + } + + function ensureActiveTabVisible() { + if (!isSummaryTabVisible(activeTab)) { + activeTab = "skins"; + } + } + + function syncActiveTabContent() { + if (!panel) return; + panel.querySelectorAll(".tab-content").forEach((content) => { + if (content && content.dataset && content.dataset.tab === activeTab) { + content.classList.add("active"); + } else if (content) { + content.classList.remove("active"); + } + }); + } + + function syncSummaryRowVisibility() { + if (!panel || !panel._summaryRowsByTab) return; + const visibleIds = new Set(getVisibleSummaryTabs().map((tab) => tab.id)); + for (const tab of SUMMARY_TABS) { + const row = panel._summaryRowsByTab[tab.id]; + if (row) { + row.style.display = visibleIds.has(tab.id) ? "" : "none"; + } + } + } + + function applyVisibleCategoryState() { + syncSummaryRowVisibility(); + if (rightPaneMode === "picker" && !isSummaryTabVisible(activeTab)) { + ensureActiveTabVisible(); + syncActiveTabContent(); + setRightPaneMode("picker"); + } + } + function refreshSummaryValues() { if (!panel || !panel._summaryValuesByTab) return; for (const tab of SUMMARY_TABS) { @@ -151,11 +216,17 @@ } } } + syncSummaryRowVisibility(); // Keep the button badge in sync even when the panel is closed. refreshButtonBadgeFromSelections(); } function setRightPaneMode(mode) { + if (mode === "picker") { + ensureActiveTabVisible(); + syncActiveTabContent(); + } + rightPaneMode = mode; if (!panel) return; @@ -767,22 +838,12 @@ const isOtherCategoryTab = (tabName) => OTHER_CATEGORY_TABS.some((t) => t.id === tabName); const switchTab = (tabName) => { + if (!isSummaryTabVisible(tabName)) { + tabName = "skins"; + } activeTab = tabName; // Update tab content - const allContents = [ - panel._modsContent, - panel._mapsContent, - panel._fontsContent, - panel._announcersContent, - ...OTHER_CATEGORY_TABS.map((t) => panel[`_${t.id}Content`]).filter(Boolean), - ]; - allContents.forEach((content) => { - if (content && content.dataset && content.dataset.tab === tabName) { - content.classList.add("active"); - } else if (content) { - content.classList.remove("active"); - } - }); + syncActiveTabContent(); // Request data for the active tab (always request fresh data) if (tabName === "skins") { requestModsForCurrentSkin(); @@ -1033,6 +1094,7 @@ panel._summaryRowsByTab[tab.id] = row; summaryView.appendChild(row); }); + syncSummaryRowVisibility(); // Picker view (reuses existing scrollable with tab contents) const pickerView = document.createElement("div"); @@ -2034,6 +2096,9 @@ return; } + // Settings changes are intentionally applied on the next wheel open. + requestSettings(); + // Create panel if it doesn't exist if (!panel.parentNode) { document.body.appendChild(panel); @@ -2049,19 +2114,14 @@ activeTab = "skins"; isFirstOpenInSession = false; } + ensureActiveTabVisible(); // Always start in summary view when opening the panel setRightPaneMode("summary"); refreshSummaryValues(); // Update tab content based on activeTab (generic) - panel.querySelectorAll(".tab-content").forEach((content) => { - if (content && content.dataset && content.dataset.tab === activeTab) { - content.classList.add("active"); - } else if (content) { - content.classList.remove("active"); - } - }); + syncActiveTabContent(); // Request data for the active tab if (activeTab === "skins") { @@ -2291,12 +2351,30 @@ updateButtonBadge(getSelectedModsCount()); } + function requestSettings() { + if (bridge) bridge.send({ type: "settings-request" }); + } + + function handleSettingsData(event) { + const detail = event?.detail; + if (!detail || detail.type !== "settings-data") { + return; + } + + hideEmptyCategories = Boolean(detail.hideEmptyCategories); + applyVisibleCategoryState(); + refreshSummaryValues(); + } + function handleModsResponse(event) { const detail = event?.detail; if (!detail || detail.type !== "skin-mods-response") { return; } + hideEmptyCategories = Boolean(detail.hideEmptyCategories); + applyVisibleCategoryState(); + const championId = Number(detail?.championId); const skinId = Number(detail?.skinId); if (!championId || !skinId) { @@ -2389,6 +2467,7 @@ } const mapsList = Array.isArray(detail.maps) ? detail.maps : []; + lastMapsMods = mapsList; // Check for historic mod and auto-select it const historicMod = detail.historicMod; @@ -2411,6 +2490,7 @@ refreshSummaryValues(); refreshButtonBadgeFromSelections(); + applyVisibleCategoryState(); if (isOpen && rightPaneMode === "picker" && activeTab === "maps") { updateMapsEntries(mapsList); @@ -2443,6 +2523,7 @@ } const fontsList = Array.isArray(detail.fonts) ? detail.fonts : []; + lastFontsMods = fontsList; // Check for historic mod and auto-select it const historicMod = detail.historicMod; @@ -2462,6 +2543,7 @@ refreshSummaryValues(); refreshButtonBadgeFromSelections(); + applyVisibleCategoryState(); if (isOpen && rightPaneMode === "picker" && activeTab === "fonts") { updateFontsEntries(fontsList); @@ -2494,6 +2576,7 @@ } const announcersList = Array.isArray(detail.announcers) ? detail.announcers : []; + lastAnnouncersMods = announcersList; // Check for historic mod and auto-select it const historicMod = detail.historicMod; @@ -2513,6 +2596,7 @@ refreshSummaryValues(); refreshButtonBadgeFromSelections(); + applyVisibleCategoryState(); if (isOpen && rightPaneMode === "picker" && activeTab === "announcers") { updateAnnouncersEntries(announcersList); @@ -2578,6 +2662,7 @@ refreshSummaryValues(); refreshButtonBadgeFromSelections(); + applyVisibleCategoryState(); if (!isOpen || rightPaneMode !== "picker" || !OTHER_CATEGORY_TABS.some((t) => t.id === activeTab)) { return; @@ -2650,6 +2735,7 @@ refreshSummaryValues(); refreshButtonBadgeFromSelections(); + applyVisibleCategoryState(); if (!isOpen || rightPaneMode !== "picker" || activeTab !== category) { return; @@ -2734,6 +2820,7 @@ // Subscribe to bridge messages instead of window CustomEvents if (bridge) { + bridge.subscribe("settings-data", (data) => handleSettingsData({ detail: data })); bridge.subscribe("skin-mods-response", (data) => handleModsResponse({ detail: data })); bridge.subscribe("maps-response", (data) => handleMapsResponse({ detail: data })); bridge.subscribe("fonts-response", (data) => handleFontsResponse({ detail: data })); @@ -2758,8 +2845,11 @@ } }); + requestSettings(); + // Request initial data on every (re)connect bridge.onReady(() => { + requestSettings(); requestMaps(); requestFonts(); requestAnnouncers(); diff --git a/Pengu Loader/plugins/ROSE-SettingsPanel/index.js b/Pengu Loader/plugins/ROSE-SettingsPanel/index.js index d03fb909..c5da8395 100644 --- a/Pengu Loader/plugins/ROSE-SettingsPanel/index.js +++ b/Pengu Loader/plugins/ROSE-SettingsPanel/index.js @@ -50,6 +50,7 @@ threshold: 0.5, monitorAutoResumeTimeout: 60, autostart: false, + hideEmptyCategories: false, gamePath: "", gamePathValid: false, version: "", @@ -952,6 +953,7 @@ threshold: payload.threshold || 0.5, monitorAutoResumeTimeout: payload.monitorAutoResumeTimeout || 60, autostart: payload.autostart || false, + hideEmptyCategories: payload.hideEmptyCategories || false, gamePath: payload.gamePath || "", gamePathValid: payload.gamePathValid || false, version: payload.version || "", @@ -1798,6 +1800,30 @@ autostartSection.appendChild(autostartWrapper); form.appendChild(autostartSection); + // Custom mods wheel section + const customWheelSection = document.createElement("div"); + customWheelSection.className = "settings-section"; + + const customWheelLabel = document.createElement("label"); + customWheelLabel.className = "settings-label"; + customWheelLabel.textContent = "Custom mods wheel:"; + customWheelSection.appendChild(customWheelLabel); + + const hideEmptyCategoriesWrapper = document.createElement("div"); + hideEmptyCategoriesWrapper.className = "settings-checkbox-wrapper"; + + const hideEmptyCategoriesCheckbox = document.createElement("input"); + hideEmptyCategoriesCheckbox.type = "checkbox"; + hideEmptyCategoriesCheckbox.className = "settings-checkbox"; + hideEmptyCategoriesCheckbox.id = "hide-empty-categories-checkbox"; + hideEmptyCategoriesWrapper.appendChild(hideEmptyCategoriesCheckbox); + + const hideEmptyCategoriesText = document.createElement("span"); + hideEmptyCategoriesText.textContent = "Hide empty mod categories"; + hideEmptyCategoriesWrapper.appendChild(hideEmptyCategoriesText); + customWheelSection.appendChild(hideEmptyCategoriesWrapper); + form.appendChild(customWheelSection); + // Game path section const pathSection = document.createElement("div"); pathSection.className = "settings-section"; @@ -2386,6 +2412,7 @@ const timeoutButton = timeoutSlider?.closest('.lol-settings-slider')?.querySelector('.lol-uikit-slider-button'); const timeoutFill = timeoutSlider?.closest('.lol-settings-slider')?.querySelector('.lol-uikit-slider-fill'); const autostartCheckbox = document.getElementById("autostart-checkbox"); + const hideEmptyCategoriesCheckbox = document.getElementById("hide-empty-categories-checkbox"); const pathInput = document.getElementById("game-path-input"); if (thresholdSlider && thresholdValue && thresholdButton && thresholdFill) { @@ -2417,6 +2444,10 @@ autostartCheckbox.checked = currentSettings.autostart; } + if (hideEmptyCategoriesCheckbox) { + hideEmptyCategoriesCheckbox.checked = currentSettings.hideEmptyCategories; + } + if (pathInput) { pathInput.value = currentSettings.gamePath || ""; // Update status based on validation result from settings data @@ -2490,6 +2521,7 @@ const thresholdSlider = document.getElementById("threshold-slider"); const timeoutSlider = document.getElementById("timeout-slider"); const autostartCheckbox = document.getElementById("autostart-checkbox"); + const hideEmptyCategoriesCheckbox = document.getElementById("hide-empty-categories-checkbox"); const pathInput = document.getElementById("game-path-input"); const threshold = thresholdSlider @@ -2499,6 +2531,7 @@ ? parseInt(timeoutSlider.value) : 60; const autostart = autostartCheckbox ? autostartCheckbox.checked : false; + const hideEmptyCategories = hideEmptyCategoriesCheckbox ? hideEmptyCategoriesCheckbox.checked : false; const gamePath = pathInput ? pathInput.value.trim() : ""; // Clamp threshold between 0.30 and 2.0 @@ -2514,6 +2547,7 @@ threshold: clampedThreshold, monitorAutoResumeTimeout: clampedTimeout, autostart: autostart, + hideEmptyCategories: hideEmptyCategories, gamePath: gamePath, }); @@ -2521,6 +2555,7 @@ threshold: clampedThreshold, monitorAutoResumeTimeout: clampedTimeout, autostart, + hideEmptyCategories, gamePath, }); } @@ -3658,4 +3693,4 @@ log("error", "Init failed:", err); }); } -})(); \ No newline at end of file +})(); diff --git a/pengu/communication/message_handler.py b/pengu/communication/message_handler.py index 45e30d5e..35322c8e 100644 --- a/pengu/communication/message_handler.py +++ b/pengu/communication/message_handler.py @@ -364,6 +364,7 @@ def _handle_settings_request(self, payload: dict) -> None: threshold = get_config_float("General", "injection_threshold", 0.5) monitor_auto_resume_timeout = get_config_float("General", "monitor_auto_resume_timeout", 60.0) autostart = is_registered_for_autostart() + hide_empty_categories = (get_config_option("General", "hide_empty_categories", "false") or "false").lower() == "true" game_path = get_config_option("General", "leaguePath") or "" diagnostics_errors = self._compute_diagnostics_errors() @@ -377,6 +378,7 @@ def _handle_settings_request(self, payload: dict) -> None: "threshold": threshold, "monitorAutoResumeTimeout": int(monitor_auto_resume_timeout), "autostart": autostart, + "hideEmptyCategories": hide_empty_categories, "gamePath": game_path, "gamePathValid": path_valid, "hasErrors": len(diagnostics_errors) > 0, @@ -385,7 +387,7 @@ def _handle_settings_request(self, payload: dict) -> None: } self._send_response(json.dumps(response_payload)) - log.info(f"[SkinMonitor] Settings data sent: threshold={threshold}, monitor_auto_resume_timeout={monitor_auto_resume_timeout}, autostart={autostart}, gamePath={game_path}, valid={path_valid}") + log.info(f"[SkinMonitor] Settings data sent: threshold={threshold}, monitor_auto_resume_timeout={monitor_auto_resume_timeout}, autostart={autostart}, hide_empty_categories={hide_empty_categories}, gamePath={game_path}, valid={path_valid}") except Exception as e: log.error(f"[SkinMonitor] Failed to handle settings request: {e}") @@ -827,6 +829,7 @@ def _handle_request_skin_mods(self, payload: dict) -> None: "championId": champion_id, "skinId": skin_id, "mods": mods_payload, + "hideEmptyCategories": (get_config_option("General", "hide_empty_categories", "false") or "false").lower() == "true", "historicMod": historic_mod_path, # Add historic mod path if available "timestamp": int(time.time() * 1000), } @@ -1752,6 +1755,7 @@ def _handle_settings_save(self, payload: dict) -> None: threshold = max(0.0, min(2.0, float(payload.get("threshold", 0.5)))) monitor_auto_resume_timeout = max(1, min(180, int(payload.get("monitorAutoResumeTimeout", 60)))) autostart = payload.get("autostart", False) + hide_empty_categories = bool(payload.get("hideEmptyCategories", False)) game_path = payload.get("gamePath", "") set_config_option("General", "injection_threshold", f"{threshold:.2f}") @@ -1759,6 +1763,9 @@ def _handle_settings_save(self, payload: dict) -> None: set_config_option("General", "monitor_auto_resume_timeout", str(monitor_auto_resume_timeout)) log.info(f"[SkinMonitor] Monitor auto-resume timeout updated to {monitor_auto_resume_timeout}s") + + set_config_option("General", "hide_empty_categories", "true" if hide_empty_categories else "false") + log.info(f"[SkinMonitor] Hide empty mod categories updated to {hide_empty_categories}") if game_path and game_path.strip(): if not self._is_valid_local_league_path(game_path):