homebridge-plugin-utils 1.27.2 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/eslint-rules.mjs +5 -0
- package/dist/featureoptions.js +7 -8
- package/dist/featureoptions.js.map +1 -1
- package/dist/ffmpeg/codecs.d.ts +10 -10
- package/dist/ffmpeg/codecs.js +37 -43
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/options.d.ts +27 -3
- package/dist/ffmpeg/options.js +191 -110
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +1 -1
- package/dist/ffmpeg/process.js +4 -9
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +6 -0
- package/dist/ffmpeg/record.js +22 -11
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/rtp.js +2 -4
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/mqttclient.js.map +1 -1
- package/dist/service.js +1 -4
- package/dist/service.js.map +1 -1
- package/dist/ui/featureoptions.js +7 -8
- package/dist/ui/featureoptions.js.map +1 -1
- package/dist/ui/webUi-featureoptions.mjs +190 -13
- package/package.json +7 -7
|
@@ -156,6 +156,9 @@ import { FeatureOptions} from "./featureoptions.js";
|
|
|
156
156
|
*/
|
|
157
157
|
export class webUiFeatureOptions {
|
|
158
158
|
|
|
159
|
+
// Map of category UI state indexed by context key (serial number or "global").
|
|
160
|
+
#categoryStates;
|
|
161
|
+
|
|
159
162
|
// Table containing the currently displayed feature options.
|
|
160
163
|
#configTable;
|
|
161
164
|
|
|
@@ -241,6 +244,7 @@ export class webUiFeatureOptions {
|
|
|
241
244
|
} = options;
|
|
242
245
|
|
|
243
246
|
// Initialize all our properties. We cache DOM elements for performance and maintain state for the current controller and device context.
|
|
247
|
+
this.#categoryStates = {};
|
|
244
248
|
this.#configTable = document.getElementById("configTable");
|
|
245
249
|
this.#controller = null;
|
|
246
250
|
this.#controllersContainer = document.getElementById("controllersContainer");
|
|
@@ -476,19 +480,18 @@ export class webUiFeatureOptions {
|
|
|
476
480
|
|
|
477
481
|
const isCollapsed = tbody.style.display === "none";
|
|
478
482
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const arrow = table.querySelector(".arrow");
|
|
483
|
+
// Use the shared method to update the state.
|
|
484
|
+
this.#setCategoryState(table, !isCollapsed);
|
|
482
485
|
|
|
483
|
-
|
|
486
|
+
document.getElementById("toggleAllCategories")?.updateState?.();
|
|
484
487
|
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
// Save the state after toggling this category.
|
|
489
|
+
const currentContext = this.#getCurrentContextKey();
|
|
487
490
|
|
|
488
|
-
|
|
489
|
-
headerCell.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
|
|
491
|
+
if(currentContext) {
|
|
490
492
|
|
|
491
|
-
|
|
493
|
+
this.#saveCategoryStates(currentContext);
|
|
494
|
+
}
|
|
492
495
|
}
|
|
493
496
|
|
|
494
497
|
return;
|
|
@@ -556,13 +559,11 @@ export class webUiFeatureOptions {
|
|
|
556
559
|
|
|
557
560
|
const searchInput = event.target;
|
|
558
561
|
|
|
559
|
-
if(
|
|
562
|
+
if(searchInput._searchTimeout) {
|
|
560
563
|
|
|
561
|
-
searchInput._searchTimeout
|
|
564
|
+
clearTimeout(searchInput._searchTimeout);
|
|
562
565
|
}
|
|
563
566
|
|
|
564
|
-
clearTimeout(searchInput._searchTimeout);
|
|
565
|
-
|
|
566
567
|
searchInput._searchTimeout = setTimeout(() => {
|
|
567
568
|
|
|
568
569
|
this.#handleSearch(searchInput.value.trim(),
|
|
@@ -691,6 +692,35 @@ export class webUiFeatureOptions {
|
|
|
691
692
|
}
|
|
692
693
|
}
|
|
693
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Set the expansion state of a category table.
|
|
697
|
+
*
|
|
698
|
+
* @param {HTMLTableElement} table - The category table element.
|
|
699
|
+
* @param {boolean} isCollapsed - True to collapse, false to expand.
|
|
700
|
+
* @private
|
|
701
|
+
*/
|
|
702
|
+
#setCategoryState(table, isCollapsed) {
|
|
703
|
+
|
|
704
|
+
const tbody = table.querySelector("tbody");
|
|
705
|
+
const arrow = table.querySelector(".arrow");
|
|
706
|
+
const headerCell = table.querySelector("thead th[role='button']");
|
|
707
|
+
|
|
708
|
+
if(tbody) {
|
|
709
|
+
|
|
710
|
+
tbody.style.display = isCollapsed ? "none" : "";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if(arrow) {
|
|
714
|
+
|
|
715
|
+
arrow.textContent = isCollapsed ? "\u25B6 " : "\u25BC ";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if(headerCell) {
|
|
719
|
+
|
|
720
|
+
headerCell.setAttribute("aria-expanded", isCollapsed ? "false" : "true");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
694
724
|
/**
|
|
695
725
|
* Hide the feature options webUI and clean up all resources.
|
|
696
726
|
*
|
|
@@ -726,6 +756,13 @@ export class webUiFeatureOptions {
|
|
|
726
756
|
*/
|
|
727
757
|
#showGlobalOptions() {
|
|
728
758
|
|
|
759
|
+
// Save the current UI state before switching contexts.
|
|
760
|
+
const previousContext = this.#getCurrentContextKey();
|
|
761
|
+
|
|
762
|
+
if(previousContext && this.#configTable.querySelector("table[data-category]")) {
|
|
763
|
+
|
|
764
|
+
this.#saveCategoryStates(previousContext);
|
|
765
|
+
}
|
|
729
766
|
|
|
730
767
|
// Clear the devices container since global options don't have associated devices, but only when we have controllers defined.
|
|
731
768
|
if(this.#getControllers) {
|
|
@@ -750,6 +787,14 @@ export class webUiFeatureOptions {
|
|
|
750
787
|
*/
|
|
751
788
|
async #showControllerOptions(controllerSerial) {
|
|
752
789
|
|
|
790
|
+
// Save the current UI state before switching contexts.
|
|
791
|
+
const previousContext = this.#getCurrentContextKey();
|
|
792
|
+
|
|
793
|
+
if(previousContext && this.#configTable.querySelector("table[data-category]")) {
|
|
794
|
+
|
|
795
|
+
this.#saveCategoryStates(previousContext);
|
|
796
|
+
}
|
|
797
|
+
|
|
753
798
|
const entry = (await this.#getControllers())?.find(c => c.serialNumber === controllerSerial);
|
|
754
799
|
|
|
755
800
|
if(!entry) {
|
|
@@ -788,6 +833,23 @@ export class webUiFeatureOptions {
|
|
|
788
833
|
// Make sure we have the refreshed configuration. This ensures we're always working with the latest saved settings.
|
|
789
834
|
this.currentConfig = await homebridge.getPluginConfig();
|
|
790
835
|
|
|
836
|
+
// Load any persisted UI states from localStorage.
|
|
837
|
+
try {
|
|
838
|
+
|
|
839
|
+
const storageKey = "homebridge-" + (this.currentConfig[0]?.platform ?? "plugin") + "-category-states";
|
|
840
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
841
|
+
|
|
842
|
+
if(stored) {
|
|
843
|
+
|
|
844
|
+
this.#categoryStates = JSON.parse(stored);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// eslint-disable-next-line no-unused-vars
|
|
848
|
+
} catch(error) {
|
|
849
|
+
|
|
850
|
+
this.#categoryStates = {};
|
|
851
|
+
}
|
|
852
|
+
|
|
791
853
|
// Keep our revert snapshot aligned with whatever was *last saved* (not just first render).
|
|
792
854
|
// We compare to the current config and update the snapshot if it differs, so "Revert to Saved" reflects the latest saved state.
|
|
793
855
|
const loadedOptions = (this.currentConfig[0]?.options ?? []);
|
|
@@ -1281,6 +1343,15 @@ export class webUiFeatureOptions {
|
|
|
1281
1343
|
|
|
1282
1344
|
homebridge.showSpinner();
|
|
1283
1345
|
|
|
1346
|
+
// Retrieve our current context before we change the active link.
|
|
1347
|
+
const previousActive = this.#devicesContainer.querySelector(".nav-link.active[data-navigation='device']");
|
|
1348
|
+
|
|
1349
|
+
// Save the current category UI state before switching contexts.
|
|
1350
|
+
if(previousActive && (previousActive.name !== deviceId) && this.#configTable.querySelector("table[data-category]")) {
|
|
1351
|
+
|
|
1352
|
+
this.#saveCategoryStates(previousActive.name);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1284
1355
|
// Clean up event listeners from previous option displays. This ensures we don't accumulate listeners as users navigate between devices.
|
|
1285
1356
|
this.#cleanupOptionEventListeners();
|
|
1286
1357
|
|
|
@@ -1301,6 +1372,9 @@ export class webUiFeatureOptions {
|
|
|
1301
1372
|
// Create option tables for each category. Categories group related options together for better organization.
|
|
1302
1373
|
this.#createOptionTables(currentDevice);
|
|
1303
1374
|
|
|
1375
|
+
// Restore saved category UI state context for this device.
|
|
1376
|
+
this.#restoreCategoryStates(deviceId);
|
|
1377
|
+
|
|
1304
1378
|
// Set up search functionality if available. This includes debounced search and keyboard shortcuts.
|
|
1305
1379
|
this.#setupSearchFunctionality();
|
|
1306
1380
|
|
|
@@ -1809,6 +1883,14 @@ export class webUiFeatureOptions {
|
|
|
1809
1883
|
}
|
|
1810
1884
|
|
|
1811
1885
|
toggleBtn.updateState();
|
|
1886
|
+
|
|
1887
|
+
// Save the category UI state after toggling all categories.
|
|
1888
|
+
const currentContext = this.#getCurrentContextKey();
|
|
1889
|
+
|
|
1890
|
+
if(currentContext) {
|
|
1891
|
+
|
|
1892
|
+
this.#saveCategoryStates(currentContext);
|
|
1893
|
+
}
|
|
1812
1894
|
}
|
|
1813
1895
|
|
|
1814
1896
|
/**
|
|
@@ -1855,6 +1937,7 @@ export class webUiFeatureOptions {
|
|
|
1855
1937
|
// Create a unique id for the tbody so that the header can reference it for accessibility.
|
|
1856
1938
|
const tbodyId = "tbody-" + category.name.replace(/\s+/g, "-");
|
|
1857
1939
|
|
|
1940
|
+
// We default category tables to collapsed.
|
|
1858
1941
|
const tbody = this.#createElement("tbody", {
|
|
1859
1942
|
|
|
1860
1943
|
classList: [ "border", "category-border" ],
|
|
@@ -1935,6 +2018,7 @@ export class webUiFeatureOptions {
|
|
|
1935
2018
|
|
|
1936
2019
|
const scopeLabel = !currentDevice ? " (Global)" : (this.#ui.isController(currentDevice) ? " (Controller-specific)" : " (Device-specific)");
|
|
1937
2020
|
|
|
2021
|
+
// We default category tables to collapsed.
|
|
1938
2022
|
const th = this.#createElement("th", {
|
|
1939
2023
|
|
|
1940
2024
|
"aria-controls": tbodyId,
|
|
@@ -2204,6 +2288,98 @@ export class webUiFeatureOptions {
|
|
|
2204
2288
|
return label;
|
|
2205
2289
|
}
|
|
2206
2290
|
|
|
2291
|
+
/**
|
|
2292
|
+
* Save the current category UI state for the given context.
|
|
2293
|
+
*
|
|
2294
|
+
* @param {string} contextKey - The context identifier (device serial or "Global Options").
|
|
2295
|
+
* @private
|
|
2296
|
+
*/
|
|
2297
|
+
#saveCategoryStates(contextKey) {
|
|
2298
|
+
|
|
2299
|
+
const states = {};
|
|
2300
|
+
const tables = document.querySelectorAll("#configTable table[data-category]");
|
|
2301
|
+
|
|
2302
|
+
for(const table of tables) {
|
|
2303
|
+
|
|
2304
|
+
const categoryName = table.getAttribute("data-category");
|
|
2305
|
+
const tbody = table.querySelector("tbody");
|
|
2306
|
+
|
|
2307
|
+
if(tbody) {
|
|
2308
|
+
|
|
2309
|
+
// Store true if collapsed (display: none), false if expanded.
|
|
2310
|
+
states[categoryName] = tbody.style.display === "none";
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
this.#categoryStates[contextKey] = states;
|
|
2315
|
+
|
|
2316
|
+
// Finally, we persist to localStorage to remember user preferences.
|
|
2317
|
+
try {
|
|
2318
|
+
|
|
2319
|
+
const storageKey = "homebridge-" + (this.currentConfig[0]?.platform ?? "plugin") + "-category-states";
|
|
2320
|
+
|
|
2321
|
+
window.localStorage.setItem(storageKey, JSON.stringify(this.#categoryStates));
|
|
2322
|
+
|
|
2323
|
+
// eslint-disable-next-line no-empty, no-unused-vars
|
|
2324
|
+
} catch(error) {
|
|
2325
|
+
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Restore saved category UI state for the given context.
|
|
2331
|
+
*
|
|
2332
|
+
* @param {string} contextKey - The context identifier (device serial or "Global Options").
|
|
2333
|
+
* @private
|
|
2334
|
+
*/
|
|
2335
|
+
#restoreCategoryStates(contextKey) {
|
|
2336
|
+
|
|
2337
|
+
const savedStates = this.#categoryStates[contextKey];
|
|
2338
|
+
|
|
2339
|
+
if(!savedStates) {
|
|
2340
|
+
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
const tables = document.querySelectorAll("#configTable table[data-category]");
|
|
2345
|
+
|
|
2346
|
+
for(const table of tables) {
|
|
2347
|
+
|
|
2348
|
+
const categoryName = table.getAttribute("data-category");
|
|
2349
|
+
|
|
2350
|
+
if(categoryName in savedStates) {
|
|
2351
|
+
|
|
2352
|
+
this.#setCategoryState(table, savedStates[categoryName]);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Update the toggle button state after restoring.
|
|
2357
|
+
document.getElementById("toggleAllCategories")?.updateState?.();
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/**
|
|
2361
|
+
* Get the current context key for UI state tracking.
|
|
2362
|
+
*
|
|
2363
|
+
* @returns {string} The context key for the current view.
|
|
2364
|
+
* @private
|
|
2365
|
+
*/
|
|
2366
|
+
#getCurrentContextKey() {
|
|
2367
|
+
|
|
2368
|
+
// Check devices first - if there's an active device, that's our context.
|
|
2369
|
+
const activeDevice = this.#devicesContainer.querySelector(".nav-link.active[data-navigation='device']");
|
|
2370
|
+
|
|
2371
|
+
if(activeDevice) {
|
|
2372
|
+
|
|
2373
|
+
return activeDevice.name;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// No active device, so check for either the active controller or global context.
|
|
2377
|
+
const activeController = this.#controllersContainer.querySelector(".nav-link.active[data-navigation]");
|
|
2378
|
+
|
|
2379
|
+
// Default to our global options if we don't have a current controller.
|
|
2380
|
+
return (activeController?.name !== "Global Options") ? this.#devices[0].serialNumber : "Global Options";
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2207
2383
|
/**
|
|
2208
2384
|
* Handle option state changes with full hierarchy and dependency management.
|
|
2209
2385
|
*
|
|
@@ -3511,6 +3687,7 @@ export class webUiFeatureOptions {
|
|
|
3511
3687
|
*/
|
|
3512
3688
|
cleanup() {
|
|
3513
3689
|
|
|
3690
|
+
this.#categoryStates = {};
|
|
3514
3691
|
this.#cleanupEventListeners();
|
|
3515
3692
|
this.#eventListeners.clear();
|
|
3516
3693
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-plugin-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"displayName": "Homebridge Plugin Utilities",
|
|
5
5
|
"description": "Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.",
|
|
6
6
|
"author": {
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
},
|
|
41
41
|
"main": "dist/index.js",
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@stylistic/eslint-plugin": "5.
|
|
44
|
-
"@types/node": "24.
|
|
45
|
-
"eslint": "9.
|
|
43
|
+
"@stylistic/eslint-plugin": "5.3.1",
|
|
44
|
+
"@types/node": "24.3.0",
|
|
45
|
+
"eslint": "9.34.0",
|
|
46
46
|
"homebridge": "1.11.0",
|
|
47
47
|
"shx": "0.4.0",
|
|
48
|
-
"typedoc": "0.28.
|
|
49
|
-
"typedoc-plugin-markdown": "4.8.
|
|
48
|
+
"typedoc": "0.28.12",
|
|
49
|
+
"typedoc-plugin-markdown": "4.8.1",
|
|
50
50
|
"typescript": "5.9.2",
|
|
51
|
-
"typescript-eslint": "8.
|
|
51
|
+
"typescript-eslint": "8.41.0"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"mqtt": "5.14.0"
|