@tomorrowos/sdk 1.1.5 → 1.1.7
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/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/playlist-catalog.d.ts +5 -0
- package/dist/playlist-catalog.d.ts.map +1 -1
- package/dist/playlist-catalog.js +22 -1
- package/dist/screen-references.d.ts +37 -0
- package/dist/screen-references.d.ts.map +1 -0
- package/dist/screen-references.js +228 -0
- package/dist/screen-tags.d.ts +95 -0
- package/dist/screen-tags.d.ts.map +1 -0
- package/dist/screen-tags.js +835 -0
- package/dist/store/memory-store.d.ts +2 -0
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +11 -0
- package/dist/store/postgres-store.d.ts +2 -0
- package/dist/store/postgres-store.d.ts.map +1 -1
- package/dist/store/postgres-store.js +9 -0
- package/dist/store/sqlite-store.d.ts +2 -0
- package/dist/store/sqlite-store.d.ts.map +1 -1
- package/dist/store/sqlite-store.js +12 -0
- package/dist/store/types.d.ts +20 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +7 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +56 -1
- package/package.json +1 -1
- package/templates/cms-starter/package-lock.json +3 -3
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +86 -1
- package/templates/cms-starter/public/methods.js +782 -11
- package/templates/cms-starter/public/panel.css +356 -0
- package/templates/cms-starter-v0/cms-panel/index.html +86 -1
- package/templates/cms-starter-v0/cms-panel/methods.js +782 -11
- package/templates/cms-starter-v0/cms-panel/panel.css +356 -0
- package/templates/cms-starter-v0/package-lock.json +3 -3
- package/templates/cms-starter-v0/package.json +2 -2
|
@@ -12,9 +12,36 @@ let selectedPlaylistId = null;
|
|
|
12
12
|
/** True while creating a new playlist locally (assets allowed before Save). */
|
|
13
13
|
let playlistDraftActive = false;
|
|
14
14
|
|
|
15
|
-
/** @type {{ id: string, assetId?: string, url: string, name: string, type: string, durationMs: number }[]} */
|
|
15
|
+
/** @type {{ id: string, assetId?: string, url: string, name: string, type: string, durationMs: number, tagRule: { match: string, tags: string[] } | null, schedule: { startDate?: string, endDate?: string, startTime?: string, endTime?: string, days?: boolean[] } | null }[]} */
|
|
16
16
|
let editorItems = [];
|
|
17
17
|
|
|
18
|
+
/** @type {Record<string, string[]>} */
|
|
19
|
+
let screenTagsByDevice = {};
|
|
20
|
+
|
|
21
|
+
/** Draft text for the per-device tag field, kept across the device list refresh. */
|
|
22
|
+
const tagDraftByDevice = {};
|
|
23
|
+
/** @type {string|null} */
|
|
24
|
+
let tagInputFocusDeviceId = null;
|
|
25
|
+
let renderingDeviceCards = false;
|
|
26
|
+
|
|
27
|
+
const TAG_RULE_OPS = [
|
|
28
|
+
{ value: "any", label: "any of" },
|
|
29
|
+
{ value: "all", label: "all of" },
|
|
30
|
+
{ value: "notAll", label: "not all of" },
|
|
31
|
+
{ value: "notAny", label: "none of" }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const ITEM_SCHEDULE_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
35
|
+
|
|
36
|
+
/** @type {string[]} */
|
|
37
|
+
let tagCatalog = [];
|
|
38
|
+
/** Tags created in this page session before the catalog refresh returns. */
|
|
39
|
+
const createdScreenTags = new Set();
|
|
40
|
+
/** @type {string|null} */
|
|
41
|
+
let tagRuleModalItemId = null;
|
|
42
|
+
/** @type {{ tagRule: { match: string, tags: string[] } | null, schedule: { startDate?: string, endDate?: string, startTime?: string, endTime?: string, days?: boolean[] } | null } | null} */
|
|
43
|
+
let tagRuleModalSnapshot = null;
|
|
44
|
+
|
|
18
45
|
/** Item id currently being dragged in the asset list (reorder). */
|
|
19
46
|
let draggingEditorItemId = null;
|
|
20
47
|
|
|
@@ -783,6 +810,329 @@ function updatePlaylistEditorVisibility() {
|
|
|
783
810
|
section.classList.toggle("hidden", !isPlaylistEditorOpen());
|
|
784
811
|
}
|
|
785
812
|
|
|
813
|
+
function normalizeEditorTag(raw) {
|
|
814
|
+
const tag = String(raw || "").trim().toLowerCase();
|
|
815
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(tag)) return "";
|
|
816
|
+
return tag;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function parseEditorTagList(text) {
|
|
820
|
+
const tags = [];
|
|
821
|
+
const seen = new Set();
|
|
822
|
+
for (const part of String(text || "").split(",")) {
|
|
823
|
+
const tag = normalizeEditorTag(part);
|
|
824
|
+
if (!tag || seen.has(tag)) continue;
|
|
825
|
+
seen.add(tag);
|
|
826
|
+
tags.push(tag);
|
|
827
|
+
}
|
|
828
|
+
return tags;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function editorTagRuleFromItem(item) {
|
|
832
|
+
const rule = item?.tagRule;
|
|
833
|
+
if (!rule || typeof rule !== "object") return null;
|
|
834
|
+
const matchValue = rule.match || rule.op;
|
|
835
|
+
const match = TAG_RULE_OPS.some((option) => option.value === matchValue) ? matchValue : "";
|
|
836
|
+
const tags = Array.isArray(rule.tags) ? parseEditorTagList(rule.tags.join(",")) : [];
|
|
837
|
+
if (!match || tags.length === 0) return null;
|
|
838
|
+
return { match, tags };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function knownScreenTags() {
|
|
842
|
+
const tags = [];
|
|
843
|
+
const seen = new Set();
|
|
844
|
+
const add = (raw) => {
|
|
845
|
+
const tag = normalizeEditorTag(raw);
|
|
846
|
+
if (!tag || seen.has(tag)) return;
|
|
847
|
+
seen.add(tag);
|
|
848
|
+
tags.push(tag);
|
|
849
|
+
};
|
|
850
|
+
for (const list of Object.values(screenTagsByDevice)) {
|
|
851
|
+
for (const tag of list || []) add(tag);
|
|
852
|
+
}
|
|
853
|
+
for (const playlist of playlistsCatalog) {
|
|
854
|
+
for (const entry of playlist.items || []) {
|
|
855
|
+
for (const tag of entry.tagRule?.tags || []) add(tag);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const entry of editorItems) {
|
|
859
|
+
for (const tag of entry.tagRule?.tags || []) add(tag);
|
|
860
|
+
}
|
|
861
|
+
for (const tag of tagCatalog) add(tag);
|
|
862
|
+
for (const tag of createdScreenTags) add(tag);
|
|
863
|
+
tags.sort();
|
|
864
|
+
return tags;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function registerCatalogTag(tag) {
|
|
868
|
+
const res = await fetch("/tags", {
|
|
869
|
+
method: "POST",
|
|
870
|
+
headers: { "Content-Type": "application/json" },
|
|
871
|
+
body: JSON.stringify({ tag })
|
|
872
|
+
});
|
|
873
|
+
const data = await res.json();
|
|
874
|
+
if (!res.ok) {
|
|
875
|
+
alert(data.error || "Could not create tag");
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
if (Array.isArray(data.tags)) tagCatalog = data.tags;
|
|
879
|
+
createdScreenTags.add(tag);
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function normalizeClockValue(value) {
|
|
884
|
+
const match = /^(\d{2}):(\d{2})/.exec(String(value || "").trim());
|
|
885
|
+
if (!match) return "";
|
|
886
|
+
const hour = Number(match[1]);
|
|
887
|
+
const minute = Number(match[2]);
|
|
888
|
+
if (hour > 23 || minute > 59) return "";
|
|
889
|
+
return `${match[1]}:${match[2]}`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function editorScheduleFromItem(item) {
|
|
893
|
+
const schedule = item?.schedule;
|
|
894
|
+
if (!schedule || typeof schedule !== "object") return null;
|
|
895
|
+
const next = {};
|
|
896
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(schedule.startDate || "")) next.startDate = schedule.startDate;
|
|
897
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(schedule.endDate || "")) next.endDate = schedule.endDate;
|
|
898
|
+
const startTime = normalizeClockValue(schedule.startTime);
|
|
899
|
+
const endTime = normalizeClockValue(schedule.endTime);
|
|
900
|
+
if (startTime) next.startTime = startTime;
|
|
901
|
+
if (endTime) next.endTime = endTime;
|
|
902
|
+
if (
|
|
903
|
+
Array.isArray(schedule.days) &&
|
|
904
|
+
schedule.days.length === 7 &&
|
|
905
|
+
schedule.days.every((day) => typeof day === "boolean") &&
|
|
906
|
+
schedule.days.some((day) => !day)
|
|
907
|
+
) {
|
|
908
|
+
next.days = schedule.days.slice();
|
|
909
|
+
}
|
|
910
|
+
return Object.keys(next).length ? next : null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function formatEditorSchedule(schedule) {
|
|
914
|
+
if (!schedule) return "";
|
|
915
|
+
const parts = [];
|
|
916
|
+
if (schedule.startDate || schedule.endDate) {
|
|
917
|
+
parts.push(`${schedule.startDate || "any date"}–${schedule.endDate || "any date"}`);
|
|
918
|
+
}
|
|
919
|
+
if (schedule.startTime || schedule.endTime) {
|
|
920
|
+
parts.push(`${schedule.startTime || "00:00"}–${schedule.endTime || "24:00"}`);
|
|
921
|
+
}
|
|
922
|
+
if (Array.isArray(schedule.days)) {
|
|
923
|
+
const labels = ITEM_SCHEDULE_DAYS.filter((_, index) => schedule.days[index]);
|
|
924
|
+
parts.push(labels.length ? labels.join(" ") : "no days");
|
|
925
|
+
}
|
|
926
|
+
return parts.join(" · ");
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function readItemScheduleFromForm() {
|
|
930
|
+
const startDate = document.getElementById("itemScheduleStartDate")?.value || "";
|
|
931
|
+
const endDate = document.getElementById("itemScheduleEndDate")?.value || "";
|
|
932
|
+
const startTime = normalizeClockValue(document.getElementById("itemScheduleStartTime")?.value);
|
|
933
|
+
const endTime = normalizeClockValue(document.getElementById("itemScheduleEndTime")?.value);
|
|
934
|
+
const days = [...document.querySelectorAll("[data-item-schedule-day]")].map(
|
|
935
|
+
(button) => button.getAttribute("aria-pressed") === "true"
|
|
936
|
+
);
|
|
937
|
+
const schedule = {};
|
|
938
|
+
if (startDate) schedule.startDate = startDate;
|
|
939
|
+
if (endDate) schedule.endDate = endDate;
|
|
940
|
+
if (startTime) schedule.startTime = startTime;
|
|
941
|
+
if (endTime) schedule.endTime = endTime;
|
|
942
|
+
if (days.length === 7 && days.some((day) => !day)) schedule.days = days;
|
|
943
|
+
return Object.keys(schedule).length ? schedule : null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function loadItemScheduleIntoForm(schedule) {
|
|
947
|
+
const startDate = document.getElementById("itemScheduleStartDate");
|
|
948
|
+
const endDate = document.getElementById("itemScheduleEndDate");
|
|
949
|
+
const startTime = document.getElementById("itemScheduleStartTime");
|
|
950
|
+
const endTime = document.getElementById("itemScheduleEndTime");
|
|
951
|
+
if (startDate) startDate.value = schedule?.startDate || "";
|
|
952
|
+
if (endDate) endDate.value = schedule?.endDate || "";
|
|
953
|
+
if (startTime) startTime.value = schedule?.startTime || "";
|
|
954
|
+
if (endTime) endTime.value = schedule?.endTime || "";
|
|
955
|
+
const days = Array.isArray(schedule?.days) && schedule.days.length === 7
|
|
956
|
+
? schedule.days
|
|
957
|
+
: [true, true, true, true, true, true, true];
|
|
958
|
+
document.querySelectorAll("[data-item-schedule-day]").forEach((button, index) => {
|
|
959
|
+
const on = days[index] !== false;
|
|
960
|
+
button.setAttribute("aria-pressed", on ? "true" : "false");
|
|
961
|
+
button.classList.toggle("is-on", on);
|
|
962
|
+
});
|
|
963
|
+
const error = document.getElementById("itemScheduleError");
|
|
964
|
+
if (error) error.textContent = "";
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function applyItemScheduleFromForm() {
|
|
968
|
+
const item = tagRuleModalItem();
|
|
969
|
+
if (!item) return;
|
|
970
|
+
item.schedule = readItemScheduleFromForm();
|
|
971
|
+
const error = document.getElementById("itemScheduleError");
|
|
972
|
+
if (!error) return;
|
|
973
|
+
const schedule = item.schedule;
|
|
974
|
+
error.textContent = schedule?.startDate && schedule?.endDate && schedule.startDate > schedule.endDate
|
|
975
|
+
? "The until date is before the from date."
|
|
976
|
+
: "";
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function tagRuleModalItem() {
|
|
980
|
+
return editorItems.find((entry) => entry.id === tagRuleModalItemId) || null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function snapshotTagRuleState(item) {
|
|
984
|
+
const schedule = editorScheduleFromItem(item);
|
|
985
|
+
return {
|
|
986
|
+
tagRule: item.tagRule?.tags?.length
|
|
987
|
+
? { match: item.tagRule.match, tags: item.tagRule.tags.slice() }
|
|
988
|
+
: null,
|
|
989
|
+
schedule: schedule
|
|
990
|
+
? { ...schedule, ...(Array.isArray(schedule.days) ? { days: schedule.days.slice() } : {}) }
|
|
991
|
+
: null
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function openTagRuleModal(item) {
|
|
996
|
+
tagRuleModalItemId = item.id;
|
|
997
|
+
tagRuleModalSnapshot = snapshotTagRuleState(item);
|
|
998
|
+
const modal = document.getElementById("tagRuleModal");
|
|
999
|
+
const name = document.getElementById("tagRuleAssetName");
|
|
1000
|
+
const match = document.getElementById("tagRuleMatch");
|
|
1001
|
+
const input = document.getElementById("tagRuleAddInput");
|
|
1002
|
+
if (!modal || !name || !match || !input) return;
|
|
1003
|
+
name.textContent = item.name || "asset";
|
|
1004
|
+
match.value = TAG_RULE_OPS.some((option) => option.value === item.tagRule?.match)
|
|
1005
|
+
? item.tagRule.match
|
|
1006
|
+
: "any";
|
|
1007
|
+
input.value = "";
|
|
1008
|
+
renderTagRuleSelection();
|
|
1009
|
+
hideTagRuleOptions();
|
|
1010
|
+
loadItemScheduleIntoForm(item.schedule);
|
|
1011
|
+
modal.classList.remove("hidden");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function closeTagRuleModal() {
|
|
1015
|
+
document.getElementById("tagRuleModal")?.classList.add("hidden");
|
|
1016
|
+
hideTagRuleOptions();
|
|
1017
|
+
tagRuleModalItemId = null;
|
|
1018
|
+
tagRuleModalSnapshot = null;
|
|
1019
|
+
renderEditorAssets();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function cancelTagRuleModal() {
|
|
1023
|
+
const item = tagRuleModalItem();
|
|
1024
|
+
if (item && tagRuleModalSnapshot) {
|
|
1025
|
+
item.tagRule = tagRuleModalSnapshot.tagRule
|
|
1026
|
+
? { match: tagRuleModalSnapshot.tagRule.match, tags: tagRuleModalSnapshot.tagRule.tags.slice() }
|
|
1027
|
+
: null;
|
|
1028
|
+
const schedule = tagRuleModalSnapshot.schedule;
|
|
1029
|
+
item.schedule = schedule
|
|
1030
|
+
? { ...schedule, ...(Array.isArray(schedule.days) ? { days: schedule.days.slice() } : {}) }
|
|
1031
|
+
: null;
|
|
1032
|
+
}
|
|
1033
|
+
closeTagRuleModal();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function applyTagRuleModal() {
|
|
1037
|
+
applyItemScheduleFromForm();
|
|
1038
|
+
const error = document.getElementById("itemScheduleError");
|
|
1039
|
+
if (error?.textContent) return;
|
|
1040
|
+
const item = tagRuleModalItem();
|
|
1041
|
+
const match = document.getElementById("tagRuleMatch");
|
|
1042
|
+
if (item?.tagRule?.tags?.length && match) {
|
|
1043
|
+
item.tagRule = { match: match.value, tags: item.tagRule.tags.slice() };
|
|
1044
|
+
}
|
|
1045
|
+
closeTagRuleModal();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function renderTagRuleSelection() {
|
|
1049
|
+
const item = tagRuleModalItem();
|
|
1050
|
+
const selected = document.getElementById("tagRuleSelected");
|
|
1051
|
+
if (!item || !selected) return;
|
|
1052
|
+
selected.innerHTML = "";
|
|
1053
|
+
for (const tag of item.tagRule?.tags || []) {
|
|
1054
|
+
const chip = document.createElement("button");
|
|
1055
|
+
chip.type = "button";
|
|
1056
|
+
chip.className = "device-tag-chip";
|
|
1057
|
+
chip.textContent = `${tag} ×`;
|
|
1058
|
+
chip.title = `Remove ${tag}`;
|
|
1059
|
+
chip.addEventListener("click", () => removeTagFromOpenRule(tag));
|
|
1060
|
+
selected.appendChild(chip);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function hideTagRuleOptions() {
|
|
1065
|
+
document.getElementById("tagRuleOptions")?.classList.add("hidden");
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function showTagRuleOptions() {
|
|
1069
|
+
const item = tagRuleModalItem();
|
|
1070
|
+
const input = document.getElementById("tagRuleAddInput");
|
|
1071
|
+
const list = document.getElementById("tagRuleOptions");
|
|
1072
|
+
if (!item || !input || !list) return;
|
|
1073
|
+
const query = String(input.value || "").trim().toLowerCase();
|
|
1074
|
+
const selected = new Set(item.tagRule?.tags || []);
|
|
1075
|
+
const choices = knownScreenTags().filter((tag) => !selected.has(tag) && (!query || tag.includes(query)));
|
|
1076
|
+
const typed = normalizeEditorTag(input.value);
|
|
1077
|
+
list.innerHTML = "";
|
|
1078
|
+
|
|
1079
|
+
for (const tag of choices) {
|
|
1080
|
+
const li = document.createElement("li");
|
|
1081
|
+
const button = document.createElement("button");
|
|
1082
|
+
button.type = "button";
|
|
1083
|
+
button.textContent = tag;
|
|
1084
|
+
button.addEventListener("mousedown", (event) => {
|
|
1085
|
+
event.preventDefault();
|
|
1086
|
+
addTagToOpenRule(tag);
|
|
1087
|
+
});
|
|
1088
|
+
li.appendChild(button);
|
|
1089
|
+
list.appendChild(li);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (typed && !knownScreenTags().includes(typed) && !selected.has(typed)) {
|
|
1093
|
+
const li = document.createElement("li");
|
|
1094
|
+
const button = document.createElement("button");
|
|
1095
|
+
button.type = "button";
|
|
1096
|
+
button.textContent = `Create “${typed}”`;
|
|
1097
|
+
button.addEventListener("mousedown", (event) => {
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
void (async () => {
|
|
1100
|
+
if (!(await registerCatalogTag(typed))) return;
|
|
1101
|
+
addTagToOpenRule(typed);
|
|
1102
|
+
})();
|
|
1103
|
+
});
|
|
1104
|
+
li.appendChild(button);
|
|
1105
|
+
list.appendChild(li);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
list.classList.toggle("hidden", list.childElementCount === 0);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function addTagToOpenRule(tag) {
|
|
1112
|
+
const item = tagRuleModalItem();
|
|
1113
|
+
const match = document.getElementById("tagRuleMatch");
|
|
1114
|
+
const input = document.getElementById("tagRuleAddInput");
|
|
1115
|
+
if (!item || !match) return;
|
|
1116
|
+
const normalized = normalizeEditorTag(tag);
|
|
1117
|
+
if (!normalized) return;
|
|
1118
|
+
const tags = [...(item.tagRule?.tags || [])];
|
|
1119
|
+
if (!tags.includes(normalized)) tags.push(normalized);
|
|
1120
|
+
item.tagRule = { match: match.value || "any", tags };
|
|
1121
|
+
if (input) input.value = "";
|
|
1122
|
+
renderTagRuleSelection();
|
|
1123
|
+
showTagRuleOptions();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function removeTagFromOpenRule(tag) {
|
|
1127
|
+
const item = tagRuleModalItem();
|
|
1128
|
+
const match = document.getElementById("tagRuleMatch");
|
|
1129
|
+
if (!item || !match) return;
|
|
1130
|
+
const tags = (item.tagRule?.tags || []).filter((entry) => entry !== tag);
|
|
1131
|
+
item.tagRule = tags.length ? { match: match.value || item.tagRule?.match || "any", tags } : null;
|
|
1132
|
+
renderTagRuleSelection();
|
|
1133
|
+
showTagRuleOptions();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
786
1136
|
function loadEditorFromSelection() {
|
|
787
1137
|
const pl = getSelectedPlaylist();
|
|
788
1138
|
const nameInput = document.getElementById("playlistName");
|
|
@@ -815,7 +1165,9 @@ function loadEditorFromSelection() {
|
|
|
815
1165
|
url: item.url,
|
|
816
1166
|
name: item.url?.split("/").pop() || "asset",
|
|
817
1167
|
type: item.type || "image",
|
|
818
|
-
durationMs: normalizeDurationMs(item)
|
|
1168
|
+
durationMs: normalizeDurationMs(item),
|
|
1169
|
+
tagRule: editorTagRuleFromItem(item),
|
|
1170
|
+
schedule: editorScheduleFromItem(item)
|
|
819
1171
|
}));
|
|
820
1172
|
renderEditorAssets();
|
|
821
1173
|
updatePlaylistEditorVisibility();
|
|
@@ -972,11 +1324,31 @@ function renderEditorAssets() {
|
|
|
972
1324
|
renderEditorAssets();
|
|
973
1325
|
});
|
|
974
1326
|
|
|
1327
|
+
const ruleSummary = document.createElement("p");
|
|
1328
|
+
ruleSummary.className = "playlist-item-rule-summary";
|
|
1329
|
+
const summaryLines = [];
|
|
1330
|
+
if (item.tagRule?.tags?.length) {
|
|
1331
|
+
const label = TAG_RULE_OPS.find((option) => option.value === item.tagRule.match)?.label || item.tagRule.match;
|
|
1332
|
+
summaryLines.push(`${label} ${item.tagRule.tags.join(", ")}`);
|
|
1333
|
+
}
|
|
1334
|
+
const when = formatEditorSchedule(item.schedule);
|
|
1335
|
+
if (when) summaryLines.push(when);
|
|
1336
|
+
if (summaryLines.length) ruleSummary.textContent = summaryLines.join("\n");
|
|
1337
|
+
else ruleSummary.classList.add("hidden");
|
|
1338
|
+
|
|
1339
|
+
const addRuleBtn = document.createElement("button");
|
|
1340
|
+
addRuleBtn.type = "button";
|
|
1341
|
+
addRuleBtn.className = "playlist-item-btn playlist-item-add-rule";
|
|
1342
|
+
addRuleBtn.textContent = "Add rule";
|
|
1343
|
+
addRuleBtn.addEventListener("click", () => openTagRuleModal(item));
|
|
1344
|
+
|
|
975
1345
|
actions.appendChild(durInput);
|
|
976
1346
|
actions.appendChild(downloadBtn);
|
|
977
1347
|
actions.appendChild(removeBtn);
|
|
978
1348
|
li.appendChild(name);
|
|
979
1349
|
li.appendChild(meta);
|
|
1350
|
+
li.appendChild(ruleSummary);
|
|
1351
|
+
li.appendChild(addRuleBtn);
|
|
980
1352
|
li.appendChild(actions);
|
|
981
1353
|
attachPlaylistItemDragDrop(li, item);
|
|
982
1354
|
list.appendChild(li);
|
|
@@ -996,12 +1368,19 @@ async function saveCurrentPlaylist() {
|
|
|
996
1368
|
|
|
997
1369
|
let items;
|
|
998
1370
|
try {
|
|
999
|
-
items = editorItems.map((item) =>
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1371
|
+
items = editorItems.map((item) => {
|
|
1372
|
+
const payload = {
|
|
1373
|
+
url: absoluteMediaUrl(item.url),
|
|
1374
|
+
assetId: item.assetId,
|
|
1375
|
+
type: item.type,
|
|
1376
|
+
durationMs: item.durationMs
|
|
1377
|
+
};
|
|
1378
|
+
if (item.tagRule?.match && item.tagRule.tags?.length) {
|
|
1379
|
+
payload.tagRule = { match: item.tagRule.match, tags: item.tagRule.tags };
|
|
1380
|
+
}
|
|
1381
|
+
if (item.schedule) payload.schedule = item.schedule;
|
|
1382
|
+
return payload;
|
|
1383
|
+
});
|
|
1005
1384
|
} catch (err) {
|
|
1006
1385
|
alert(err.message);
|
|
1007
1386
|
return;
|
|
@@ -1041,7 +1420,7 @@ async function deleteCurrentPlaylist() {
|
|
|
1041
1420
|
const pl = getSelectedPlaylist();
|
|
1042
1421
|
if (
|
|
1043
1422
|
!confirm(
|
|
1044
|
-
`Delete playlist "${pl?.name}"? Devices already playing it keep their cached copy until Clear or reboot without sync. New devices cannot receive it.`
|
|
1423
|
+
`Delete playlist "${pl?.name}"? Devices already playing it keep their cached copy until Clear or reboot without sync. Clear removes it from the database once no screen still has it. New devices cannot receive it.`
|
|
1045
1424
|
)
|
|
1046
1425
|
) {
|
|
1047
1426
|
return;
|
|
@@ -1086,6 +1465,18 @@ function newPlaylistDraft() {
|
|
|
1086
1465
|
});
|
|
1087
1466
|
}
|
|
1088
1467
|
|
|
1468
|
+
async function fetchScreenTags() {
|
|
1469
|
+
const [screenRes, catalogRes] = await Promise.all([fetch("/screen-tags"), fetch("/tags")]);
|
|
1470
|
+
const screenData = await screenRes.json();
|
|
1471
|
+
if (screenRes.ok && screenData.tagsByDevice && typeof screenData.tagsByDevice === "object") {
|
|
1472
|
+
screenTagsByDevice = screenData.tagsByDevice;
|
|
1473
|
+
}
|
|
1474
|
+
if (catalogRes.ok) {
|
|
1475
|
+
const catalogData = await catalogRes.json();
|
|
1476
|
+
if (Array.isArray(catalogData.tags)) tagCatalog = catalogData.tags;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1089
1480
|
async function fetchDevices() {
|
|
1090
1481
|
try {
|
|
1091
1482
|
const res = await fetch("/devices");
|
|
@@ -1100,6 +1491,11 @@ async function fetchDevices() {
|
|
|
1100
1491
|
}
|
|
1101
1492
|
if (Array.isArray(data.devices)) {
|
|
1102
1493
|
devicesCache = data.devices;
|
|
1494
|
+
try {
|
|
1495
|
+
await fetchScreenTags();
|
|
1496
|
+
} catch {
|
|
1497
|
+
// Tags are optional for the card list; the next poll retries.
|
|
1498
|
+
}
|
|
1103
1499
|
renderDeviceCards();
|
|
1104
1500
|
scheduleReconnectGraceRerender();
|
|
1105
1501
|
}
|
|
@@ -1207,20 +1603,249 @@ function createDeviceScreenshotThumb(device) {
|
|
|
1207
1603
|
return slot;
|
|
1208
1604
|
}
|
|
1209
1605
|
|
|
1606
|
+
function showDeviceTagOptions(deviceId) {
|
|
1607
|
+
const input = document.querySelector(`[data-tag-input="${CSS.escape(deviceId)}"]`);
|
|
1608
|
+
const list = document.querySelector(`[data-tag-options="${CSS.escape(deviceId)}"]`);
|
|
1609
|
+
if (!(input instanceof HTMLInputElement) || !list) return;
|
|
1610
|
+
const query = String(input.value || "").trim().toLowerCase();
|
|
1611
|
+
const selected = new Set(screenTagsByDevice[deviceId] || []);
|
|
1612
|
+
const choices = knownScreenTags().filter((tag) => !selected.has(tag) && (!query || tag.includes(query)));
|
|
1613
|
+
const typed = normalizeEditorTag(input.value);
|
|
1614
|
+
list.innerHTML = "";
|
|
1615
|
+
for (const tag of choices) {
|
|
1616
|
+
const li = document.createElement("li");
|
|
1617
|
+
const button = document.createElement("button");
|
|
1618
|
+
button.type = "button";
|
|
1619
|
+
button.textContent = tag;
|
|
1620
|
+
button.addEventListener("mousedown", (event) => {
|
|
1621
|
+
event.preventDefault();
|
|
1622
|
+
tagDraftByDevice[deviceId] = tag;
|
|
1623
|
+
void addDeviceTag(deviceId);
|
|
1624
|
+
});
|
|
1625
|
+
li.appendChild(button);
|
|
1626
|
+
list.appendChild(li);
|
|
1627
|
+
}
|
|
1628
|
+
if (typed && !knownScreenTags().includes(typed) && !selected.has(typed)) {
|
|
1629
|
+
const li = document.createElement("li");
|
|
1630
|
+
const button = document.createElement("button");
|
|
1631
|
+
button.type = "button";
|
|
1632
|
+
button.textContent = `Create “${typed}”`;
|
|
1633
|
+
button.addEventListener("mousedown", (event) => {
|
|
1634
|
+
event.preventDefault();
|
|
1635
|
+
tagDraftByDevice[deviceId] = typed;
|
|
1636
|
+
void addDeviceTag(deviceId);
|
|
1637
|
+
});
|
|
1638
|
+
li.appendChild(button);
|
|
1639
|
+
list.appendChild(li);
|
|
1640
|
+
}
|
|
1641
|
+
list.classList.toggle("hidden", list.childElementCount === 0);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
let manageTagEditing = "";
|
|
1645
|
+
|
|
1646
|
+
async function openManageTagsModal() {
|
|
1647
|
+
manageTagEditing = "";
|
|
1648
|
+
await fetchScreenTags();
|
|
1649
|
+
renderManageTags();
|
|
1650
|
+
const error = document.getElementById("manageTagError");
|
|
1651
|
+
if (error) error.textContent = "";
|
|
1652
|
+
document.getElementById("manageTagsModal")?.classList.remove("hidden");
|
|
1653
|
+
document.getElementById("manageTagInput")?.focus();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function closeManageTagsModal() {
|
|
1657
|
+
manageTagEditing = "";
|
|
1658
|
+
document.getElementById("manageTagsModal")?.classList.add("hidden");
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function renderManageTags() {
|
|
1662
|
+
const list = document.getElementById("manageTagList");
|
|
1663
|
+
if (!list) return;
|
|
1664
|
+
list.innerHTML = "";
|
|
1665
|
+
if (tagCatalog.length === 0) {
|
|
1666
|
+
const empty = document.createElement("li");
|
|
1667
|
+
empty.className = "manage-tag-empty";
|
|
1668
|
+
empty.textContent = "No tags yet.";
|
|
1669
|
+
list.appendChild(empty);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
for (const tag of tagCatalog) {
|
|
1673
|
+
const li = document.createElement("li");
|
|
1674
|
+
li.className = "manage-tag-row";
|
|
1675
|
+
if (manageTagEditing === tag) {
|
|
1676
|
+
const input = document.createElement("input");
|
|
1677
|
+
input.type = "text";
|
|
1678
|
+
input.value = tag;
|
|
1679
|
+
input.className = "manage-tag-rename";
|
|
1680
|
+
const save = document.createElement("button");
|
|
1681
|
+
save.type = "button";
|
|
1682
|
+
save.className = "primary";
|
|
1683
|
+
save.textContent = "Save";
|
|
1684
|
+
save.addEventListener("click", () => void renameManagedTag(tag, input.value));
|
|
1685
|
+
const cancel = document.createElement("button");
|
|
1686
|
+
cancel.type = "button";
|
|
1687
|
+
cancel.textContent = "Cancel";
|
|
1688
|
+
cancel.addEventListener("click", () => {
|
|
1689
|
+
manageTagEditing = "";
|
|
1690
|
+
renderManageTags();
|
|
1691
|
+
});
|
|
1692
|
+
li.appendChild(input);
|
|
1693
|
+
li.appendChild(save);
|
|
1694
|
+
li.appendChild(cancel);
|
|
1695
|
+
} else {
|
|
1696
|
+
const name = document.createElement("span");
|
|
1697
|
+
name.textContent = tag;
|
|
1698
|
+
const edit = document.createElement("button");
|
|
1699
|
+
edit.type = "button";
|
|
1700
|
+
edit.textContent = "Edit";
|
|
1701
|
+
edit.addEventListener("click", () => {
|
|
1702
|
+
manageTagEditing = tag;
|
|
1703
|
+
renderManageTags();
|
|
1704
|
+
});
|
|
1705
|
+
const remove = document.createElement("button");
|
|
1706
|
+
remove.type = "button";
|
|
1707
|
+
remove.className = "danger";
|
|
1708
|
+
remove.textContent = "Delete";
|
|
1709
|
+
remove.addEventListener("click", () => void deleteManagedTag(tag));
|
|
1710
|
+
li.appendChild(name);
|
|
1711
|
+
li.appendChild(edit);
|
|
1712
|
+
li.appendChild(remove);
|
|
1713
|
+
}
|
|
1714
|
+
list.appendChild(li);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async function addManagedTag() {
|
|
1719
|
+
const input = document.getElementById("manageTagInput");
|
|
1720
|
+
const tag = normalizeEditorTag(input?.value || "");
|
|
1721
|
+
const error = document.getElementById("manageTagError");
|
|
1722
|
+
if (!tag) {
|
|
1723
|
+
if (error) error.textContent = "Use letters, numbers, hyphens, or underscores.";
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (tagCatalog.includes(tag)) {
|
|
1727
|
+
if (error) error.textContent = `Tag “${tag}” already exists.`;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (!(await registerCatalogTag(tag))) return;
|
|
1731
|
+
if (input) input.value = "";
|
|
1732
|
+
if (error) error.textContent = "";
|
|
1733
|
+
renderManageTags();
|
|
1734
|
+
renderDeviceCards();
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function renameManagedTag(from, rawNext) {
|
|
1738
|
+
const next = normalizeEditorTag(rawNext);
|
|
1739
|
+
const error = document.getElementById("manageTagError");
|
|
1740
|
+
if (!next) {
|
|
1741
|
+
if (error) error.textContent = "Use letters, numbers, hyphens, or underscores.";
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (next !== from && tagCatalog.includes(next)) {
|
|
1745
|
+
if (error) error.textContent = `Tag “${next}” already exists.`;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const res = await fetch(`/tags/${encodeURIComponent(from)}`, {
|
|
1749
|
+
method: "PATCH",
|
|
1750
|
+
headers: { "Content-Type": "application/json" },
|
|
1751
|
+
body: JSON.stringify({ tag: next })
|
|
1752
|
+
});
|
|
1753
|
+
const data = await res.json();
|
|
1754
|
+
if (!res.ok) {
|
|
1755
|
+
if (error) error.textContent = data.error || "Could not rename tag";
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (Array.isArray(data.tags)) tagCatalog = data.tags;
|
|
1759
|
+
manageTagEditing = "";
|
|
1760
|
+
if (error) error.textContent = "";
|
|
1761
|
+
await fetchPlaylists();
|
|
1762
|
+
await fetchDevices();
|
|
1763
|
+
if (isPlaylistEditorOpen()) loadEditorFromSelection();
|
|
1764
|
+
renderManageTags();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
async function deleteManagedTag(tag) {
|
|
1768
|
+
const error = document.getElementById("manageTagError");
|
|
1769
|
+
const res = await fetch(`/tags/${encodeURIComponent(tag)}`, { method: "DELETE" });
|
|
1770
|
+
const data = await res.json();
|
|
1771
|
+
if (!res.ok) {
|
|
1772
|
+
if (error) error.textContent = data.error || "Could not delete tag";
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (Array.isArray(data.tags)) tagCatalog = data.tags;
|
|
1776
|
+
createdScreenTags.delete(tag);
|
|
1777
|
+
if (error) error.textContent = "";
|
|
1778
|
+
renderManageTags();
|
|
1779
|
+
renderDeviceCards();
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function addDeviceTag(deviceId) {
|
|
1783
|
+
const tag = normalizeEditorTag(tagDraftByDevice[deviceId] || "");
|
|
1784
|
+
if (!tag) {
|
|
1785
|
+
alert("Enter a tag such as screen-a, drive-thru, or breakfast.");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const res = await fetch(`/device/${encodeURIComponent(deviceId)}/tags`, {
|
|
1789
|
+
method: "POST",
|
|
1790
|
+
headers: { "Content-Type": "application/json" },
|
|
1791
|
+
body: JSON.stringify({ tag })
|
|
1792
|
+
});
|
|
1793
|
+
const data = await res.json();
|
|
1794
|
+
showResult(data);
|
|
1795
|
+
if (!res.ok) {
|
|
1796
|
+
alert(data.error || "Could not add tag");
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
delete tagDraftByDevice[deviceId];
|
|
1800
|
+
await fetchDevices();
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async function removeDeviceTag(deviceId, tag) {
|
|
1804
|
+
const res = await fetch(
|
|
1805
|
+
`/device/${encodeURIComponent(deviceId)}/tags/${encodeURIComponent(tag)}`,
|
|
1806
|
+
{ method: "DELETE" }
|
|
1807
|
+
);
|
|
1808
|
+
const data = await res.json();
|
|
1809
|
+
showResult(data);
|
|
1810
|
+
if (!res.ok) {
|
|
1811
|
+
alert(data.error || "Could not remove tag");
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
await fetchDevices();
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1210
1817
|
function renderDeviceCards() {
|
|
1211
1818
|
const grid = document.getElementById("devicesGrid");
|
|
1212
1819
|
if (!grid) return;
|
|
1820
|
+
renderingDeviceCards = true;
|
|
1213
1821
|
grid.innerHTML = "";
|
|
1214
1822
|
|
|
1823
|
+
const filterTag = normalizeEditorTag(document.getElementById("screenTagFilter")?.value || "");
|
|
1824
|
+
const visibleDevices = filterTag
|
|
1825
|
+
? devicesCache.filter((device) =>
|
|
1826
|
+
(screenTagsByDevice[device.deviceId] || []).includes(filterTag)
|
|
1827
|
+
)
|
|
1828
|
+
: devicesCache;
|
|
1829
|
+
|
|
1215
1830
|
if (devicesCache.length === 0) {
|
|
1216
1831
|
const empty = document.createElement("p");
|
|
1217
1832
|
empty.className = "devices-empty";
|
|
1218
1833
|
empty.textContent = "No paired devices yet.";
|
|
1219
1834
|
grid.appendChild(empty);
|
|
1835
|
+
renderingDeviceCards = false;
|
|
1220
1836
|
return;
|
|
1221
1837
|
}
|
|
1222
1838
|
|
|
1223
|
-
|
|
1839
|
+
if (visibleDevices.length === 0) {
|
|
1840
|
+
const empty = document.createElement("p");
|
|
1841
|
+
empty.className = "devices-empty";
|
|
1842
|
+
empty.textContent = `No screens tagged ${filterTag}.`;
|
|
1843
|
+
grid.appendChild(empty);
|
|
1844
|
+
renderingDeviceCards = false;
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
for (const device of visibleDevices) {
|
|
1224
1849
|
const card = document.createElement("article");
|
|
1225
1850
|
card.className = "device-card";
|
|
1226
1851
|
|
|
@@ -1428,12 +2053,100 @@ function renderDeviceCards() {
|
|
|
1428
2053
|
actions.appendChild(timerBtn);
|
|
1429
2054
|
actions.appendChild(unpairBtn);
|
|
1430
2055
|
|
|
2056
|
+
const tagsBlock = document.createElement("div");
|
|
2057
|
+
tagsBlock.className = "device-tags";
|
|
2058
|
+
const tagsLabel = document.createElement("div");
|
|
2059
|
+
tagsLabel.className = "device-tags-label";
|
|
2060
|
+
tagsLabel.textContent = "Screen tags";
|
|
2061
|
+
const chips = document.createElement("div");
|
|
2062
|
+
chips.className = "device-tag-chips";
|
|
2063
|
+
const deviceTags = screenTagsByDevice[device.deviceId] || [];
|
|
2064
|
+
if (deviceTags.length === 0) {
|
|
2065
|
+
const none = document.createElement("span");
|
|
2066
|
+
none.className = "device-tag-empty";
|
|
2067
|
+
none.textContent = "No tags — plays items with no screen rule.";
|
|
2068
|
+
chips.appendChild(none);
|
|
2069
|
+
} else {
|
|
2070
|
+
for (const tag of deviceTags) {
|
|
2071
|
+
const chip = document.createElement("button");
|
|
2072
|
+
chip.type = "button";
|
|
2073
|
+
chip.className = "device-tag-chip";
|
|
2074
|
+
chip.textContent = `${tag} ×`;
|
|
2075
|
+
chip.title = `Remove ${tag}`;
|
|
2076
|
+
chip.addEventListener("click", () => void removeDeviceTag(device.deviceId, tag));
|
|
2077
|
+
chips.appendChild(chip);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const tagRow = document.createElement("div");
|
|
2081
|
+
tagRow.className = "device-tag-add";
|
|
2082
|
+
const picker = document.createElement("div");
|
|
2083
|
+
picker.className = "device-tag-picker";
|
|
2084
|
+
const tagInput = document.createElement("input");
|
|
2085
|
+
tagInput.type = "text";
|
|
2086
|
+
tagInput.placeholder = "Add a tag";
|
|
2087
|
+
tagInput.autocomplete = "off";
|
|
2088
|
+
tagInput.dataset.tagInput = device.deviceId;
|
|
2089
|
+
tagInput.value = tagDraftByDevice[device.deviceId] || "";
|
|
2090
|
+
const tagOptions = document.createElement("ul");
|
|
2091
|
+
tagOptions.className = "tag-rule-options hidden";
|
|
2092
|
+
tagOptions.dataset.tagOptions = device.deviceId;
|
|
2093
|
+
const showOptions = () => showDeviceTagOptions(device.deviceId);
|
|
2094
|
+
tagInput.addEventListener("input", () => {
|
|
2095
|
+
tagDraftByDevice[device.deviceId] = tagInput.value;
|
|
2096
|
+
showOptions();
|
|
2097
|
+
});
|
|
2098
|
+
tagInput.addEventListener("focus", () => {
|
|
2099
|
+
tagInputFocusDeviceId = device.deviceId;
|
|
2100
|
+
showOptions();
|
|
2101
|
+
});
|
|
2102
|
+
tagInput.addEventListener("click", showOptions);
|
|
2103
|
+
tagInput.addEventListener("blur", () => {
|
|
2104
|
+
if (!renderingDeviceCards && tagInputFocusDeviceId === device.deviceId) {
|
|
2105
|
+
tagInputFocusDeviceId = null;
|
|
2106
|
+
}
|
|
2107
|
+
setTimeout(() => {
|
|
2108
|
+
if (tagInputFocusDeviceId !== device.deviceId) tagOptions.classList.add("hidden");
|
|
2109
|
+
}, 150);
|
|
2110
|
+
});
|
|
2111
|
+
tagInput.addEventListener("keydown", (ev) => {
|
|
2112
|
+
if (ev.key === "Escape") {
|
|
2113
|
+
tagOptions.classList.add("hidden");
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (ev.key === "Enter") {
|
|
2117
|
+
ev.preventDefault();
|
|
2118
|
+
void addDeviceTag(device.deviceId);
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
picker.appendChild(tagInput);
|
|
2122
|
+
picker.appendChild(tagOptions);
|
|
2123
|
+
const addTagBtn = document.createElement("button");
|
|
2124
|
+
addTagBtn.type = "button";
|
|
2125
|
+
addTagBtn.textContent = "Add tag";
|
|
2126
|
+
addTagBtn.addEventListener("click", () => void addDeviceTag(device.deviceId));
|
|
2127
|
+
tagRow.appendChild(picker);
|
|
2128
|
+
tagRow.appendChild(addTagBtn);
|
|
2129
|
+
tagsBlock.appendChild(tagsLabel);
|
|
2130
|
+
tagsBlock.appendChild(chips);
|
|
2131
|
+
tagsBlock.appendChild(tagRow);
|
|
2132
|
+
|
|
1431
2133
|
card.appendChild(header);
|
|
1432
2134
|
card.appendChild(published);
|
|
1433
2135
|
card.appendChild(metaBlock);
|
|
2136
|
+
card.appendChild(tagsBlock);
|
|
1434
2137
|
card.appendChild(actions);
|
|
1435
2138
|
grid.appendChild(card);
|
|
1436
2139
|
}
|
|
2140
|
+
|
|
2141
|
+
renderingDeviceCards = false;
|
|
2142
|
+
if (tagInputFocusDeviceId) {
|
|
2143
|
+
const el = grid.querySelector(`[data-tag-input="${CSS.escape(tagInputFocusDeviceId)}"]`);
|
|
2144
|
+
if (el instanceof HTMLInputElement) {
|
|
2145
|
+
el.focus();
|
|
2146
|
+
const end = el.value.length;
|
|
2147
|
+
el.setSelectionRange(end, end);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
1437
2150
|
}
|
|
1438
2151
|
|
|
1439
2152
|
function openPublishModal(deviceId) {
|
|
@@ -2000,7 +2713,9 @@ async function addAssetFromFile(file, queueIndex, queueTotal) {
|
|
|
2000
2713
|
url: data.url,
|
|
2001
2714
|
name: file.name,
|
|
2002
2715
|
type,
|
|
2003
|
-
durationMs
|
|
2716
|
+
durationMs,
|
|
2717
|
+
tagRule: null,
|
|
2718
|
+
schedule: null
|
|
2004
2719
|
});
|
|
2005
2720
|
renderEditorAssets();
|
|
2006
2721
|
showResult({
|
|
@@ -2650,6 +3365,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2650
3365
|
?.addEventListener("click", () => void fetchServerStatus());
|
|
2651
3366
|
|
|
2652
3367
|
document.getElementById("newPlaylistBtn")?.addEventListener("click", newPlaylistDraft);
|
|
3368
|
+
document.getElementById("screenTagFilter")?.addEventListener("input", () => renderDeviceCards());
|
|
3369
|
+
document.getElementById("manageTagsBtn")?.addEventListener("click", () => void openManageTagsModal());
|
|
3370
|
+
document.querySelectorAll("[data-close-manage-tags]").forEach((el) => {
|
|
3371
|
+
el.addEventListener("click", closeManageTagsModal);
|
|
3372
|
+
});
|
|
3373
|
+
document.getElementById("manageTagAddBtn")?.addEventListener("click", () => void addManagedTag());
|
|
3374
|
+
document.getElementById("manageTagInput")?.addEventListener("keydown", (event) => {
|
|
3375
|
+
if (event.key === "Enter") {
|
|
3376
|
+
event.preventDefault();
|
|
3377
|
+
void addManagedTag();
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
2653
3380
|
document.getElementById("savePlaylistBtn")?.addEventListener("click", () => void saveCurrentPlaylist());
|
|
2654
3381
|
document.getElementById("deletePlaylistBtn")?.addEventListener("click", () => void deleteCurrentPlaylist());
|
|
2655
3382
|
document.getElementById("publishConfirmBtn")?.addEventListener("click", () => void confirmPublishModal());
|
|
@@ -2657,6 +3384,50 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2657
3384
|
document.querySelectorAll("[data-close-modal]").forEach((el) => {
|
|
2658
3385
|
el.addEventListener("click", closePublishModal);
|
|
2659
3386
|
});
|
|
3387
|
+
document.querySelectorAll("[data-close-tag-rule-modal]").forEach((el) => {
|
|
3388
|
+
el.addEventListener("click", cancelTagRuleModal);
|
|
3389
|
+
});
|
|
3390
|
+
document.getElementById("tagRuleCancelBtn")?.addEventListener("click", cancelTagRuleModal);
|
|
3391
|
+
document.getElementById("tagRuleApplyBtn")?.addEventListener("click", applyTagRuleModal);
|
|
3392
|
+
document.getElementById("tagRuleMatch")?.addEventListener("change", () => {
|
|
3393
|
+
const item = tagRuleModalItem();
|
|
3394
|
+
const match = document.getElementById("tagRuleMatch");
|
|
3395
|
+
if (!item?.tagRule?.tags?.length || !match) return;
|
|
3396
|
+
item.tagRule = { match: match.value, tags: item.tagRule.tags };
|
|
3397
|
+
});
|
|
3398
|
+
const tagRuleAddInput = document.getElementById("tagRuleAddInput");
|
|
3399
|
+
tagRuleAddInput?.addEventListener("focus", showTagRuleOptions);
|
|
3400
|
+
tagRuleAddInput?.addEventListener("click", showTagRuleOptions);
|
|
3401
|
+
tagRuleAddInput?.addEventListener("input", showTagRuleOptions);
|
|
3402
|
+
tagRuleAddInput?.addEventListener("keydown", async (event) => {
|
|
3403
|
+
if (event.key === "Escape") {
|
|
3404
|
+
hideTagRuleOptions();
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
if (event.key !== "Enter") return;
|
|
3408
|
+
event.preventDefault();
|
|
3409
|
+
const typed = normalizeEditorTag(tagRuleAddInput.value);
|
|
3410
|
+
if (!typed) return;
|
|
3411
|
+
if (!knownScreenTags().includes(typed)) {
|
|
3412
|
+
if (!(await registerCatalogTag(typed))) return;
|
|
3413
|
+
}
|
|
3414
|
+
addTagToOpenRule(typed);
|
|
3415
|
+
});
|
|
3416
|
+
tagRuleAddInput?.addEventListener("blur", () => {
|
|
3417
|
+
setTimeout(hideTagRuleOptions, 150);
|
|
3418
|
+
});
|
|
3419
|
+
["itemScheduleStartDate", "itemScheduleEndDate", "itemScheduleStartTime", "itemScheduleEndTime"].forEach((id) => {
|
|
3420
|
+
document.getElementById(id)?.addEventListener("input", applyItemScheduleFromForm);
|
|
3421
|
+
document.getElementById(id)?.addEventListener("change", applyItemScheduleFromForm);
|
|
3422
|
+
});
|
|
3423
|
+
document.querySelectorAll("[data-item-schedule-day]").forEach((button) => {
|
|
3424
|
+
button.addEventListener("click", () => {
|
|
3425
|
+
const on = button.getAttribute("aria-pressed") !== "true";
|
|
3426
|
+
button.setAttribute("aria-pressed", on ? "true" : "false");
|
|
3427
|
+
button.classList.toggle("is-on", on);
|
|
3428
|
+
applyItemScheduleFromForm();
|
|
3429
|
+
});
|
|
3430
|
+
});
|
|
2660
3431
|
document.querySelectorAll("[data-close-screenshot-modal]").forEach((el) => {
|
|
2661
3432
|
el.addEventListener("click", closeScreenshotModal);
|
|
2662
3433
|
});
|