homebridge-sonos-scenes 0.1.7 → 0.1.8
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/homebridge-ui/public/index.html +92 -50
- package/package.json +1 -1
|
@@ -365,6 +365,33 @@
|
|
|
365
365
|
max-width: 132px;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
.scene-member-flags {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-wrap: wrap;
|
|
371
|
+
gap: 0.35rem;
|
|
372
|
+
margin-top: 0.2rem;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.scene-member-flag {
|
|
376
|
+
display: inline-flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
padding: 0.15rem 0.45rem;
|
|
379
|
+
border-radius: 999px;
|
|
380
|
+
font-size: 0.72rem;
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
line-height: 1.2;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.scene-member-flag.source {
|
|
386
|
+
background: rgba(10, 132, 255, 0.12);
|
|
387
|
+
color: #0a84ff;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.scene-member-flag.primary {
|
|
391
|
+
background: rgba(168, 85, 247, 0.16);
|
|
392
|
+
color: #7c3aed;
|
|
393
|
+
}
|
|
394
|
+
|
|
368
395
|
.scene-log {
|
|
369
396
|
max-height: 280px;
|
|
370
397
|
overflow: auto;
|
|
@@ -631,7 +658,7 @@
|
|
|
631
658
|
<input class="form-control" id="scene-name" type="text">
|
|
632
659
|
</div>
|
|
633
660
|
<input id="scene-id" type="hidden">
|
|
634
|
-
<div class="col-
|
|
661
|
+
<div class="col-12">
|
|
635
662
|
<label class="form-label" for="household-select">
|
|
636
663
|
<span class="scene-label-row">
|
|
637
664
|
<span>Household</span>
|
|
@@ -643,25 +670,13 @@
|
|
|
643
670
|
</label>
|
|
644
671
|
<select class="form-select" id="household-select"></select>
|
|
645
672
|
</div>
|
|
646
|
-
<div class="col-md-6">
|
|
647
|
-
<label class="form-label" for="coordinator-select">
|
|
648
|
-
<span class="scene-label-row">
|
|
649
|
-
<span>Coordinator Room</span>
|
|
650
|
-
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Coordinator Room">
|
|
651
|
-
?
|
|
652
|
-
<span class="scene-tooltip">The main room that anchors the group and receives the source change first. Choose the room that should lead the scene.</span>
|
|
653
|
-
</span>
|
|
654
|
-
</span>
|
|
655
|
-
</label>
|
|
656
|
-
<select class="form-select" id="coordinator-select"></select>
|
|
657
|
-
</div>
|
|
658
673
|
<div class="col-12">
|
|
659
674
|
<label class="form-label">
|
|
660
675
|
<span class="scene-label-row">
|
|
661
|
-
<span>
|
|
676
|
+
<span>Scene Rooms</span>
|
|
662
677
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Group Members">
|
|
663
678
|
?
|
|
664
|
-
<span class="scene-tooltip">
|
|
679
|
+
<span class="scene-tooltip">Pick the rooms that should be part of this scene. For line-in and TV scenes, the source room is included automatically. One selected room becomes the internal lead room behind the scenes.</span>
|
|
665
680
|
</span>
|
|
666
681
|
</span>
|
|
667
682
|
</label>
|
|
@@ -673,7 +688,7 @@
|
|
|
673
688
|
<span>Source Kind</span>
|
|
674
689
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Source Kind">
|
|
675
690
|
?
|
|
676
|
-
<span class="scene-tooltip">The type of thing this scene should load. The list
|
|
691
|
+
<span class="scene-tooltip">The type of thing this scene should load. The list is based on what is available in this household, such as favorites, line-in, or TV.</span>
|
|
677
692
|
</span>
|
|
678
693
|
</span>
|
|
679
694
|
</label>
|
|
@@ -694,10 +709,10 @@
|
|
|
694
709
|
<div class="col-md-3">
|
|
695
710
|
<label class="form-label" for="coordinator-volume">
|
|
696
711
|
<span class="scene-label-row">
|
|
697
|
-
<span>
|
|
712
|
+
<span>Lead Room Volume</span>
|
|
698
713
|
<span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Coordinator Volume">
|
|
699
714
|
?
|
|
700
|
-
<span class="scene-tooltip">Optional starting volume for the
|
|
715
|
+
<span class="scene-tooltip">Optional starting volume for the scene's lead room. Leave this blank to keep whatever volume that room already has.</span>
|
|
701
716
|
</span>
|
|
702
717
|
</span>
|
|
703
718
|
</label>
|
|
@@ -811,7 +826,6 @@
|
|
|
811
826
|
sceneName: document.getElementById("scene-name"),
|
|
812
827
|
sceneId: document.getElementById("scene-id"),
|
|
813
828
|
householdSelect: document.getElementById("household-select"),
|
|
814
|
-
coordinatorSelect: document.getElementById("coordinator-select"),
|
|
815
829
|
memberList: document.getElementById("member-list"),
|
|
816
830
|
sourceKind: document.getElementById("source-kind"),
|
|
817
831
|
sourceTarget: document.getElementById("source-target"),
|
|
@@ -958,14 +972,43 @@
|
|
|
958
972
|
return (household?.favorites || []).filter((favorite) => favorite.playable !== false);
|
|
959
973
|
}
|
|
960
974
|
|
|
975
|
+
function getSelectedSceneRoomIdsFromDraft() {
|
|
976
|
+
const selected = Array.from(new Set([state.draft.coordinatorPlayerId, ...(state.draft.memberPlayerIds || [])].filter(Boolean)));
|
|
977
|
+
const source = state.draft.source;
|
|
978
|
+
if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
|
|
979
|
+
if (!selected.includes(source.deviceId)) {
|
|
980
|
+
selected.unshift(source.deviceId);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return selected;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function resolveCoordinatorPlayerId(selectedRoomIds, source) {
|
|
987
|
+
const uniqueRooms = Array.from(new Set((selectedRoomIds || []).filter(Boolean)));
|
|
988
|
+
|
|
989
|
+
if ((source?.kind === "line_in" || source?.kind === "tv") && source.deviceId) {
|
|
990
|
+
return source.deviceId;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (state.draft?.coordinatorPlayerId && uniqueRooms.includes(state.draft.coordinatorPlayerId)) {
|
|
994
|
+
return state.draft.coordinatorPlayerId;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return uniqueRooms[0] || "";
|
|
998
|
+
}
|
|
999
|
+
|
|
961
1000
|
function serializeDraft() {
|
|
962
1001
|
serializeCloudConfig();
|
|
963
1002
|
const draft = clone(state.draft);
|
|
964
1003
|
draft.name = elements.sceneName.value.trim() || "New Scene";
|
|
965
1004
|
draft.householdId = elements.householdSelect.value;
|
|
966
|
-
draft.coordinatorPlayerId = elements.coordinatorSelect.value;
|
|
967
|
-
draft.memberPlayerIds = Array.from(elements.memberList.querySelectorAll("input[type='checkbox']:checked")).map((checkbox) => checkbox.value);
|
|
968
1005
|
draft.source = buildSourcePayload();
|
|
1006
|
+
const selectedRoomIds = Array.from(elements.memberList.querySelectorAll("input[type='checkbox']:checked")).map((checkbox) => checkbox.value);
|
|
1007
|
+
draft.coordinatorPlayerId = resolveCoordinatorPlayerId(selectedRoomIds, draft.source);
|
|
1008
|
+
if ((draft.source?.kind === "line_in" || draft.source?.kind === "tv") && draft.source.deviceId && !selectedRoomIds.includes(draft.source.deviceId)) {
|
|
1009
|
+
selectedRoomIds.unshift(draft.source.deviceId);
|
|
1010
|
+
}
|
|
1011
|
+
draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== draft.coordinatorPlayerId);
|
|
969
1012
|
draft.coordinatorVolume = elements.coordinatorVolume.value === "" ? undefined : Number(elements.coordinatorVolume.value);
|
|
970
1013
|
draft.settleMs = Number(elements.settleMs.value || 0);
|
|
971
1014
|
draft.retryCount = Number(elements.retryCount.value || 0);
|
|
@@ -974,6 +1017,7 @@
|
|
|
974
1017
|
draft.offBehavior = { kind: elements.offBehavior.value };
|
|
975
1018
|
draft.playerVolumes = Array.from(elements.memberList.querySelectorAll("input[data-player-id]"))
|
|
976
1019
|
.filter((input) => input.value !== "")
|
|
1020
|
+
.filter((input) => selectedRoomIds.includes(input.dataset.playerId) && input.dataset.playerId !== draft.coordinatorPlayerId)
|
|
977
1021
|
.map((input) => ({
|
|
978
1022
|
playerId: input.dataset.playerId,
|
|
979
1023
|
volume: Number(input.value),
|
|
@@ -1127,40 +1171,40 @@
|
|
|
1127
1171
|
.join("");
|
|
1128
1172
|
}
|
|
1129
1173
|
|
|
1130
|
-
function renderCoordinatorOptions() {
|
|
1131
|
-
const household = getActiveHousehold();
|
|
1132
|
-
const players = household?.players || [];
|
|
1133
|
-
const currentCoordinator = players.some((player) => player.id === state.draft.coordinatorPlayerId)
|
|
1134
|
-
? state.draft.coordinatorPlayerId
|
|
1135
|
-
: players[0]?.id || "";
|
|
1136
|
-
state.draft.coordinatorPlayerId = currentCoordinator;
|
|
1137
|
-
elements.coordinatorSelect.innerHTML = players
|
|
1138
|
-
.map((player) => `<option value="${player.id}" ${player.id === currentCoordinator ? "selected" : ""}>${player.name}</option>`)
|
|
1139
|
-
.join("");
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
1174
|
function renderMemberOptions() {
|
|
1143
1175
|
const household = getActiveHousehold();
|
|
1144
|
-
const
|
|
1145
|
-
const selected = new Set(
|
|
1176
|
+
const selectedRoomIds = getSelectedSceneRoomIdsFromDraft();
|
|
1177
|
+
const selected = new Set(selectedRoomIds);
|
|
1178
|
+
const coordinatorId = resolveCoordinatorPlayerId(selectedRoomIds, state.draft.source);
|
|
1179
|
+
state.draft.coordinatorPlayerId = coordinatorId;
|
|
1180
|
+
state.draft.memberPlayerIds = selectedRoomIds.filter((playerId) => playerId !== coordinatorId);
|
|
1146
1181
|
const values = new Map((state.draft.playerVolumes || []).map((entry) => [entry.playerId, entry.volume]));
|
|
1147
|
-
const
|
|
1182
|
+
const forcedSourceId = (state.draft.source?.kind === "line_in" || state.draft.source?.kind === "tv")
|
|
1183
|
+
? state.draft.source.deviceId
|
|
1184
|
+
: "";
|
|
1185
|
+
const players = household?.players || [];
|
|
1148
1186
|
elements.memberList.innerHTML = players.length === 0
|
|
1149
|
-
? `<div class="scene-help">No
|
|
1187
|
+
? `<div class="scene-help">No rooms are available in this household.</div>`
|
|
1150
1188
|
: players
|
|
1151
1189
|
.map(
|
|
1152
1190
|
(player) => {
|
|
1153
|
-
const
|
|
1191
|
+
const isForcedSource = forcedSourceId === player.id;
|
|
1192
|
+
const isSelected = isForcedSource || selected.has(player.id);
|
|
1193
|
+
const flags = [
|
|
1194
|
+
isForcedSource ? `<span class="scene-member-flag source">Source Room</span>` : "",
|
|
1195
|
+
isSelected && coordinatorId === player.id ? `<span class="scene-member-flag primary">Lead Room</span>` : "",
|
|
1196
|
+
].filter(Boolean).join("");
|
|
1154
1197
|
return `
|
|
1155
1198
|
<div class="scene-member-pill ${isSelected ? "selected" : ""}">
|
|
1156
1199
|
<label class="scene-member-row scene-member-toggle">
|
|
1157
|
-
<input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""}>
|
|
1200
|
+
<input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""} ${isForcedSource ? "disabled" : ""}>
|
|
1158
1201
|
<span class="scene-member-copy">
|
|
1159
1202
|
<span class="scene-member-title">${player.name}</span>
|
|
1160
1203
|
<span class="scene-member-meta">${player.model || "Unknown model"} - ${((player.sourceOptions || []).join(", ") || "favorite").replaceAll("_", " ")}</span>
|
|
1204
|
+
${flags ? `<span class="scene-member-flags">${flags}</span>` : ""}
|
|
1161
1205
|
</span>
|
|
1162
1206
|
</label>
|
|
1163
|
-
${isSelected ? `
|
|
1207
|
+
${isSelected && coordinatorId !== player.id ? `
|
|
1164
1208
|
<div class="scene-member-volume">
|
|
1165
1209
|
<label class="scene-member-volume-label" for="member-volume-${player.id}">Volume Override</label>
|
|
1166
1210
|
<input
|
|
@@ -1183,8 +1227,8 @@
|
|
|
1183
1227
|
|
|
1184
1228
|
function renderSourceControls() {
|
|
1185
1229
|
const household = getActiveHousehold();
|
|
1186
|
-
const
|
|
1187
|
-
const supportedKinds = ["favorite", ...new Set((
|
|
1230
|
+
const players = household?.players || [];
|
|
1231
|
+
const supportedKinds = ["favorite", ...new Set(players.flatMap((player) => player.sourceOptions || []).filter((kind) => kind !== "favorite"))];
|
|
1188
1232
|
const sourceKind = supportedKinds.includes(state.draft.source?.kind) ? state.draft.source.kind : supportedKinds[0] || "favorite";
|
|
1189
1233
|
state.draft.source = state.draft.source || { kind: sourceKind, favoriteId: "" };
|
|
1190
1234
|
state.draft.source.kind = sourceKind;
|
|
@@ -1351,9 +1395,8 @@
|
|
|
1351
1395
|
elements.sceneName.value = state.draft.name || "";
|
|
1352
1396
|
elements.sceneId.value = state.draft.id || "";
|
|
1353
1397
|
renderHouseholdOptions();
|
|
1354
|
-
renderCoordinatorOptions();
|
|
1355
|
-
renderMemberOptions();
|
|
1356
1398
|
renderSourceControls();
|
|
1399
|
+
renderMemberOptions();
|
|
1357
1400
|
elements.coordinatorVolume.value = state.draft.coordinatorVolume === "" ? "" : state.draft.coordinatorVolume ?? "";
|
|
1358
1401
|
elements.settleMs.value = state.draft.settleMs ?? 750;
|
|
1359
1402
|
elements.retryCount.value = state.draft.retryCount ?? 3;
|
|
@@ -1552,12 +1595,6 @@
|
|
|
1552
1595
|
render();
|
|
1553
1596
|
});
|
|
1554
1597
|
|
|
1555
|
-
elements.coordinatorSelect.addEventListener("change", () => {
|
|
1556
|
-
serializeDraft();
|
|
1557
|
-
state.draft.memberPlayerIds = state.draft.memberPlayerIds.filter((playerId) => playerId !== state.draft.coordinatorPlayerId);
|
|
1558
|
-
render();
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
1598
|
elements.sourceKind.addEventListener("change", () => {
|
|
1562
1599
|
serializeDraft();
|
|
1563
1600
|
state.draft.source = elements.sourceKind.value === "favorite"
|
|
@@ -1566,6 +1603,11 @@
|
|
|
1566
1603
|
render();
|
|
1567
1604
|
});
|
|
1568
1605
|
|
|
1606
|
+
elements.sourceTarget.addEventListener("change", () => {
|
|
1607
|
+
serializeDraft();
|
|
1608
|
+
render();
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1569
1611
|
homebridge.addEventListener("scene-test-result", (event) => {
|
|
1570
1612
|
state.lastRun = event.data;
|
|
1571
1613
|
render();
|
package/package.json
CHANGED