Skip to content

Commit 20485df

Browse files
feat: add status badges and i18n for Classes menu
1 parent 057c075 commit 20485df

20 files changed

Lines changed: 262 additions & 81 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ Group modules to show/hide together with a single action. See the [Classes Guide
270270
}
271271
```
272272
273+
**Example of Classes in the web interface:**
274+
275+
![Classes Example](img/menu_classes.png)
276+
273277
### Custom Menu Items
274278
275279
Create custom buttons in the web interface. See the [Custom Menus Guide](docs/guide/custom-menus.md) for details.

img/main_screenshot.png

-3.67 KB
Loading

img/menu_classes.png

31.1 KB
Loading

remote.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,3 +1293,40 @@ pre {
12931293
box-shadow: var(--accent-glow);
12941294
}
12951295
}
1296+
1297+
/* ===== Classes Menu Status Badges ===== */
1298+
.class-status-badge {
1299+
margin-left: auto;
1300+
padding: 0.3em 0.6em;
1301+
border-radius: 1em;
1302+
font-size: 0.75em;
1303+
font-weight: 600;
1304+
letter-spacing: 0.05em;
1305+
border: 1px solid;
1306+
min-width: 2.5em;
1307+
text-align: center;
1308+
}
1309+
1310+
.class-active .class-status-badge {
1311+
background: rgba(76, 175, 80, 0.2);
1312+
border-color: #4caf50;
1313+
color: #4caf50;
1314+
}
1315+
1316+
.class-inactive .class-status-badge {
1317+
background: rgba(244, 67, 54, 0.2);
1318+
border-color: #f44336;
1319+
color: #f44336;
1320+
}
1321+
1322+
.class-mixed .class-status-badge {
1323+
background: rgba(255, 152, 0, 0.2);
1324+
border-color: #ff9800;
1325+
color: #ff9800;
1326+
}
1327+
1328+
.class-empty .class-status-badge {
1329+
background: rgba(158, 158, 158, 0.2);
1330+
border-color: #9e9e9e;
1331+
color: #9e9e9e;
1332+
}

remote.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@
6767
<span class="fa fa-fw fa-angle-right" aria-hidden="true"></span>
6868
</div>
6969
<div id="classes-button" class="button" role="button" tabindex="0">
70-
<span class="fa fa-fw fa-wrench" aria-hidden="true"></span>
71-
<span class="text">Classes Menu</span>
70+
<span class="fa fa-fw fa-object-group" aria-hidden="true"></span>
71+
<span class="text">%%TRANSLATE:CLASSES_MENU_NAME%%</span>
7272
<span class="fa fa-fw fa-angle-right" aria-hidden="true"></span>
7373
</div>
7474
<div id="update-button" class="button" role="button" tabindex="0">

remote.js

Lines changed: 205 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,94 @@ const Remote = {
726726
return {status, modules: modules.join(", ")};
727727
},
728728

729+
/**
730+
* Determines the status of a class based on visible/hidden modules
731+
* @param {object} classData - Class data with show/hide/toggle arrays
732+
* @returns {object} Status object with status and details
733+
*/
734+
getClassStatus (classData) {
735+
if (!classData || !this.savedData.modules) {
736+
return {status: "unknown", details: ""};
737+
}
738+
739+
// Create map for fast module access
740+
const moduleMap = {};
741+
for (const module of this.savedData.modules) {
742+
moduleMap[module.name] = module;
743+
moduleMap[module.identifier] = module;
744+
}
745+
746+
let visibleCount = 0;
747+
let hiddenCount = 0;
748+
let totalCount = 0;
749+
750+
// Check show modules
751+
if (classData.show) {
752+
const showModules = Array.isArray(classData.show) ? classData.show : [classData.show];
753+
for (const moduleName of showModules) {
754+
const module = moduleMap[moduleName];
755+
if (module) {
756+
totalCount++;
757+
if (module.hidden) {
758+
hiddenCount++;
759+
} else {
760+
visibleCount++;
761+
}
762+
}
763+
}
764+
}
765+
766+
// Check hide modules
767+
if (classData.hide) {
768+
const hideModules = Array.isArray(classData.hide) ? classData.hide : [classData.hide];
769+
for (const moduleName of hideModules) {
770+
const module = moduleMap[moduleName];
771+
if (module) {
772+
totalCount++;
773+
if (module.hidden) {
774+
hiddenCount++;
775+
} else {
776+
visibleCount++;
777+
}
778+
}
779+
}
780+
}
781+
782+
// Check toggle modules (we just show if they're visible/hidden)
783+
if (classData.toggle) {
784+
const toggleModules = Array.isArray(classData.toggle) ? classData.toggle : [classData.toggle];
785+
for (const moduleName of toggleModules) {
786+
const module = moduleMap[moduleName];
787+
if (module) {
788+
totalCount++;
789+
if (module.hidden) {
790+
hiddenCount++;
791+
} else {
792+
visibleCount++;
793+
}
794+
}
795+
}
796+
}
797+
798+
// Determine status
799+
let status = "class-mixed";
800+
if (totalCount === 0) {
801+
status = "class-empty";
802+
} else if (hiddenCount === 0 && visibleCount > 0) {
803+
status = "class-active";
804+
} else if (visibleCount === 0 && hiddenCount > 0) {
805+
status = "class-inactive";
806+
}
807+
808+
return {
809+
status,
810+
details: `${visibleCount}/${totalCount}`,
811+
visibleCount,
812+
hiddenCount,
813+
totalCount
814+
};
815+
},
816+
729817
addToggleElements (parent) {
730818
const outerSpan = document.createElement("span");
731819
outerSpan.className = "stack fa-fw";
@@ -1401,7 +1489,11 @@ const Remote = {
14011489

14021490
async loadClasses () {
14031491
try {
1492+
// Always reload module data to get fresh status
1493+
const {data: moduleData} = await this.loadList("visible-modules", "modules");
1494+
this.savedData.modules = moduleData;
14041495
const {data: classes} = await this.loadList("classes", "classes");
1496+
14051497
for (const index in classes) {
14061498
const node = document.createElement("div");
14071499
node.id = "classes-before-result";
@@ -1418,7 +1510,8 @@ const Remote = {
14181510
payload: {
14191511
classes: index
14201512
}
1421-
}
1513+
},
1514+
classData: classes[index] // Store class data for status checking
14221515
};
14231516

14241517
const existingButton = document.getElementById(`${content.id}-button`);
@@ -1787,6 +1880,102 @@ const Remote = {
17871880
}
17881881
},
17891882

1883+
addClassStatusBadge (item, classData) {
1884+
const classStatus = this.getClassStatus(classData);
1885+
const statusBadge = document.createElement("span");
1886+
statusBadge.className = `class-status-badge ${classStatus.status}`;
1887+
statusBadge.textContent = classStatus.details;
1888+
statusBadge.title = `${classStatus.visibleCount} visible, ${classStatus.hiddenCount} hidden`;
1889+
item.append(statusBadge);
1890+
item.classList.add(classStatus.status);
1891+
},
1892+
1893+
createMenuTypeElement (item, content, menu) {
1894+
const mcmArrow = document.createElement("span");
1895+
mcmArrow.className = "fa fa-fw fa-angle-right";
1896+
mcmArrow.setAttribute("aria-hidden", "true");
1897+
item.append(mcmArrow);
1898+
item.dataset.parent = menu;
1899+
item.dataset.type = "menu";
1900+
document.querySelector("#back-button").classList.add(`${content.id}-menu`);
1901+
const menuContent = document.querySelector(".menu-content");
1902+
if (menuContent) {
1903+
menuContent.classList.add(`${content.id}-menu`);
1904+
}
1905+
item.addEventListener("click", () => {
1906+
globalThis.location.hash = `${content.id}-menu`;
1907+
});
1908+
},
1909+
1910+
createSliderElement (item, content) {
1911+
const contain = document.createElement("div");
1912+
contain.classList.add("flex-1");
1913+
1914+
const slide = document.createElement("input");
1915+
slide.id = `${content.id}-slider`;
1916+
slide.className = "slider";
1917+
slide.type = "range";
1918+
slide.min = content.min || 0;
1919+
slide.max = content.max || 100;
1920+
slide.step = content.step || 10;
1921+
slide.value = content.defaultValue || 50;
1922+
1923+
slide.addEventListener("change", () => {
1924+
this.sendSocketNotification("REMOTE_ACTION", {
1925+
action: content.action.toUpperCase(),
1926+
...content.content,
1927+
payload: {
1928+
...content.content === undefined ? {} : (typeof content.content.payload === "string" ? {string: content.content.payload} : content.content.payload),
1929+
value: slide.value
1930+
},
1931+
value: slide.value
1932+
});
1933+
});
1934+
1935+
contain.append(slide);
1936+
item.append(contain);
1937+
},
1938+
1939+
createInputElement (content, menu) {
1940+
const input = document.createElement("input");
1941+
input.id = `${content.id}-input`;
1942+
input.className = `menu-element ${menu}-menu medium`;
1943+
input.type = "text";
1944+
input.placeholder = content.text;
1945+
1946+
input.addEventListener("focusout", () => {
1947+
this.sendSocketNotification("REMOTE_ACTION", {
1948+
action: content.action.toUpperCase(),
1949+
...content.content,
1950+
payload: {
1951+
...content.content === undefined ? {} : (typeof content.content.payload === "string" ? {string: content.content.payload} : content.content.payload),
1952+
value: input.value
1953+
},
1954+
value: input.value
1955+
});
1956+
});
1957+
1958+
return input;
1959+
},
1960+
1961+
addItemClickHandler (item, content, menu) {
1962+
item.dataset.type = "item";
1963+
item.addEventListener("click", () => {
1964+
this.sendSocketNotification("REMOTE_ACTION", {
1965+
action: content.action.toUpperCase(),
1966+
payload: {},
1967+
...content.content
1968+
});
1969+
1970+
// Reload classes menu after executing class action to update status badges
1971+
if (content.action === "MANAGE_CLASSES" && menu === "classes") {
1972+
setTimeout(() => {
1973+
this.loadClasses();
1974+
}, 1000);
1975+
}
1976+
});
1977+
},
1978+
17901979
createMenuElement (content, menu, insertAfter) {
17911980
if (!content) { return; }
17921981
const item = document.createElement("div");
@@ -1807,87 +1996,24 @@ const Remote = {
18071996
item.append(mcmText);
18081997
}
18091998

1810-
switch (content.type) {
1811-
case "menu": {
1812-
const mcmArrow = document.createElement("span");
1813-
mcmArrow.className = "fa fa-fw fa-angle-right";
1814-
mcmArrow.setAttribute("aria-hidden", "true");
1815-
item.append(mcmArrow);
1816-
item.dataset.parent = menu;
1817-
item.dataset.type = "menu";
1818-
document.querySelector("#back-button").classList.add(`${content.id}-menu`);
1819-
const menuContent = document.querySelector(".menu-content");
1820-
if (menuContent) {
1821-
menuContent.classList.add(`${content.id}-menu`);
1822-
}
1823-
item.addEventListener("click", () => {
1824-
globalThis.location.hash = `${content.id}-menu`;
1825-
});
1999+
// Add status badge for Classes menu
2000+
if (menu === "classes" && content.classData) {
2001+
this.addClassStatusBadge(item, content.classData);
2002+
}
18262003

2004+
switch (content.type) {
2005+
case "menu":
2006+
this.createMenuTypeElement(item, content, menu);
18272007
break;
1828-
}
1829-
case "slider": {
1830-
const contain = document.createElement("div");
1831-
contain.classList.add("flex-1");
1832-
1833-
const slide = document.createElement("input");
1834-
slide.id = `${content.id}-slider`;
1835-
slide.className = "slider";
1836-
slide.type = "range";
1837-
slide.min = content.min || 0;
1838-
slide.max = content.max || 100;
1839-
slide.step = content.step || 10;
1840-
slide.value = content.defaultValue || 50;
1841-
1842-
slide.addEventListener("change", () => {
1843-
this.sendSocketNotification("REMOTE_ACTION", {
1844-
action: content.action.toUpperCase(),
1845-
...content.content,
1846-
payload: {
1847-
...content.content === undefined ? {} : (typeof content.content.payload === "string" ? {string: content.content.payload} : content.content.payload),
1848-
value: slide.value
1849-
},
1850-
value: slide.value
1851-
});
1852-
});
1853-
1854-
contain.append(slide);
1855-
item.append(contain);
1856-
2008+
case "slider":
2009+
this.createSliderElement(item, content);
18572010
break;
1858-
}
1859-
case "input": {
1860-
const input = document.createElement("input");
1861-
input.id = `${content.id}-input`;
1862-
input.className = `menu-element ${menu}-menu medium`;
1863-
input.type = "text";
1864-
input.placeholder = content.text;
1865-
1866-
input.addEventListener("focusout", () => {
1867-
this.sendSocketNotification("REMOTE_ACTION", {
1868-
action: content.action.toUpperCase(),
1869-
...content.content,
1870-
payload: {
1871-
...content.content === undefined ? {} : (typeof content.content.payload === "string" ? {string: content.content.payload} : content.content.payload),
1872-
value: input.value
1873-
},
1874-
value: input.value
1875-
});
1876-
});
1877-
1878-
return input;
1879-
}
1880-
default: if (content.action && content.content) {
1881-
item.dataset.type = "item";
1882-
item.addEventListener("click", () => {
1883-
this.sendSocketNotification("REMOTE_ACTION", {
1884-
action: content.action.toUpperCase(),
1885-
payload: {},
1886-
...content.content
1887-
});
1888-
});
1889-
}
1890-
2011+
case "input":
2012+
return this.createInputElement(content, menu);
2013+
default:
2014+
if (content.action && content.content) {
2015+
this.addItemClickHandler(item, content, menu);
2016+
}
18912017
}
18922018

18932019
if (!globalThis.location.hash && menu !== "main" ||

translations/ca.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"EDIT_MENU_NAME": "Editar Vista",
55
"SHUTDOWN_MENU_NAME": "Encendre/Apagar",
66
"CONFIGURE_MENU_NAME": "Editar config.js",
7+
"CLASSES_MENU_NAME": "Classes",
78
"VIEW_MIRROR": "MagicMirror²",
89
"LINKS": "Enllaços",
910
"API_DOCS": "Documentació de l'API",

0 commit comments

Comments
 (0)