prismcast 1.10.2 → 1.10.3
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/app.d.ts +5 -3
- package/dist/app.js +42 -41
- package/dist/app.js.map +1 -1
- package/dist/browser/hlsPlaylistObserver.d.ts +38 -0
- package/dist/browser/hlsPlaylistObserver.js +113 -0
- package/dist/browser/hlsPlaylistObserver.js.map +1 -0
- package/dist/browser/index.d.ts +26 -16
- package/dist/browser/index.js +95 -70
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/manifestInterceptor.d.ts +29 -29
- package/dist/browser/manifestInterceptor.js +106 -212
- package/dist/browser/manifestInterceptor.js.map +1 -1
- package/dist/browser/tabNetworkObserver.d.ts +48 -0
- package/dist/browser/tabNetworkObserver.js +140 -0
- package/dist/browser/tabNetworkObserver.js.map +1 -0
- package/dist/config/consistencyProbe.js +13 -11
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/paths.d.ts +3 -8
- package/dist/config/paths.js +3 -10
- package/dist/config/paths.js.map +1 -1
- package/dist/config/profiles.d.ts +1 -1
- package/dist/config/profiles.js +6 -6
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/services.d.ts +5 -5
- package/dist/config/services.js +8 -8
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +2 -2
- package/dist/config/sites.js +6 -6
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +108 -14
- package/dist/config/userChannels.js +291 -37
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.js +1 -1
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +3 -3
- package/dist/config/userProfiles.js +19 -19
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/index.js +7 -9
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +9 -18
- package/dist/native/index.js.map +1 -1
- package/dist/native/proxy.d.ts +1 -4
- package/dist/native/proxy.js +0 -9
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/clientActions.d.ts +97 -0
- package/dist/routes/clientActions.js +113 -0
- package/dist/routes/clientActions.js.map +1 -0
- package/dist/routes/components.d.ts +4 -3
- package/dist/routes/components.js +36 -39
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/crud.js +37 -72
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
- package/dist/routes/config/channels/healthBridge.d.ts +23 -0
- package/dist/routes/config/channels/healthBridge.js +71 -0
- package/dist/routes/config/channels/healthBridge.js.map +1 -0
- package/dist/routes/config/channels/table.js +97 -84
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/services.js +25 -22
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.js +9 -6
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +277 -139
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/logs.js +1 -1
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/root/content.js +18 -14
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +17 -14
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +50 -21
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +72 -3
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +60 -0
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +5 -27
- package/dist/routes/root/scripts/status.handlers.js +8 -60
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/scripts/status.js +6 -1
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/streams.js +5 -3
- package/dist/routes/streams.js.map +1 -1
- package/dist/service/commands.context.js +1 -1
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/generators.context.js +1 -1
- package/dist/service/generators.context.js.map +1 -1
- package/dist/service/generators.d.ts +1 -1
- package/dist/service/generators.js +3 -11
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/hls.js +1 -5
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +1 -1
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/preroll.d.ts +1 -1
- package/dist/streaming/preroll.js +1 -5
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +5 -8
- package/dist/streaming/statusEmitter.js +0 -5
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +9 -0
- package/dist/types/channels.js.map +1 -1
- package/dist/types/profiles.d.ts +2 -2
- package/dist/upgrade/commands.context.js +14 -10
- package/dist/upgrade/commands.context.js.map +1 -1
- package/dist/upgrade/commands.d.ts +7 -15
- package/dist/upgrade/commands.js +27 -28
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.js +2 -1
- package/dist/upgrade/detection.context.js.map +1 -1
- package/dist/upgrade/detection.d.ts +2 -1
- package/dist/upgrade/detection.js +20 -51
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +1 -0
- package/dist/upgrade/index.js +1 -0
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/lifecycle.context.d.ts +19 -0
- package/dist/upgrade/lifecycle.context.js +47 -0
- package/dist/upgrade/lifecycle.context.js.map +1 -0
- package/dist/upgrade/lifecycle.d.ts +72 -0
- package/dist/upgrade/lifecycle.js +176 -0
- package/dist/upgrade/lifecycle.js.map +1 -0
- package/dist/upgrade/pathHandle.d.ts +28 -0
- package/dist/upgrade/pathHandle.js +101 -0
- package/dist/upgrade/pathHandle.js.map +1 -0
- package/dist/utils/bootSession.context.d.ts +15 -0
- package/dist/utils/bootSession.context.js +59 -0
- package/dist/utils/bootSession.context.js.map +1 -0
- package/dist/utils/bootSession.d.ts +16 -0
- package/dist/utils/bootSession.js +20 -0
- package/dist/utils/bootSession.js.map +1 -0
- package/dist/utils/clock.js +1 -1
- package/dist/utils/ffmpeg.js +1 -1
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markup.d.ts +10 -0
- package/dist/utils/markup.js +23 -0
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/pid.d.ts +2 -25
- package/dist/utils/pid.js +10 -49
- package/dist/utils/pid.js.map +1 -1
- package/dist/utils/processInspector.context.d.ts +7 -0
- package/dist/utils/processInspector.context.js +104 -0
- package/dist/utils/processInspector.context.js.map +1 -0
- package/dist/utils/processInspector.d.ts +54 -0
- package/dist/utils/processInspector.js +125 -0
- package/dist/utils/processInspector.js.map +1 -0
- package/dist/utils/retry.d.ts +1 -1
- package/dist/utils/retry.js +1 -1
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/runtimeIdentity.context.d.ts +6 -0
- package/dist/utils/runtimeIdentity.context.js +13 -0
- package/dist/utils/runtimeIdentity.context.js.map +1 -0
- package/dist/utils/runtimeIdentity.d.ts +96 -0
- package/dist/utils/runtimeIdentity.js +171 -0
- package/dist/utils/runtimeIdentity.js.map +1 -0
- package/dist/utils/timing.d.ts +1 -1
- package/dist/utils/timing.js +0 -5
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.js +40 -10
- package/dist/utils/version.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import { getActiveTagVocabulary, getChannelCustomizations, getChannelEffectiveTa
|
|
|
5
5
|
import { getCachedProviderChannels, getProviderDomainMap, getProviderGuideUrls, getProviderModuleInfo } from "../../../browser/channelSelection.js";
|
|
6
6
|
import { getChannelHealth, getDomainAuth } from "../../../config/health.js";
|
|
7
7
|
import { getProfileForChannel, getProfiles } from "../../../config/profiles.js";
|
|
8
|
+
import { ACTIONS } from "../../clientActions.js";
|
|
8
9
|
import { CONFIG } from "../../../config/index.js";
|
|
9
10
|
import { PREDEFINED_CHANNELS } from "../../../channels/index.js";
|
|
10
11
|
import { categorizeProfiles } from "../index.js";
|
|
@@ -61,7 +62,7 @@ function generateTextField(id, name, label, value, options = {}) {
|
|
|
61
62
|
const modifiedDot = isModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
|
|
62
63
|
const defaultAttr = (options.defaultValue !== undefined) ? " data-default=\"" + escapeHtml(options.defaultValue) + "\"" : "";
|
|
63
64
|
const resetBtn = isModified ? "<button type=\"button\" class=\"btn-reset\" title=\"Reset to predefined default\" aria-label=\"Reset to predefined default\" " +
|
|
64
|
-
"
|
|
65
|
+
"data-click-action=\"" + ACTIONS.resetChannelField + "\" data-field-id=\"" + id + "\">↻</button>" : "";
|
|
65
66
|
lines.push("<div class=\"form-row" + modifiedClass + "\">");
|
|
66
67
|
lines.push(modifiedDot + "<label for=\"" + id + "\">" + label + "</label>");
|
|
67
68
|
lines.push("<input class=\"form-input\" type=\"" + inputType + "\" id=\"" + id + "\" name=\"" + name + "\"" + required + listAttr + pattern +
|
|
@@ -97,7 +98,7 @@ function generateProfileDropdown(id, selectedProfile, profiles, showHint = true,
|
|
|
97
98
|
const modifiedDot = isModified ? "<span class=\"modified-dot\" title=\"Modified from predefined default\"></span>" : "";
|
|
98
99
|
const defaultAttr = (defaultProfile !== undefined) ? " data-default=\"" + escapeHtml(defaultProfile) + "\"" : "";
|
|
99
100
|
const resetBtn = isModified ? "<button type=\"button\" class=\"btn-reset\" title=\"Reset to predefined default\" aria-label=\"Reset to predefined default\" " +
|
|
100
|
-
"
|
|
101
|
+
"data-click-action=\"" + ACTIONS.resetChannelField + "\" data-field-id=\"" + id + "\">↻</button>" : "";
|
|
101
102
|
// Helper to generate option elements for a profile.
|
|
102
103
|
const renderOption = (profile) => {
|
|
103
104
|
const selected = (profile.name === selectedProfile) ? " selected" : "";
|
|
@@ -154,7 +155,7 @@ function generateProfileDropdown(id, selectedProfile, profiles, showHint = true,
|
|
|
154
155
|
if (showHint) {
|
|
155
156
|
lines.push("<div class=\"hint\">Autodetect uses predefined profiles for known sites. If video doesn't play or fullscreen fails, " +
|
|
156
157
|
"try experimenting with different profiles. ");
|
|
157
|
-
lines.push("<a href=\"#\"
|
|
158
|
+
lines.push("<a href=\"#\" data-click-action=\"" + ACTIONS.toggleProfileReference + "\" data-click-prevent-default>View profile reference</a></div>");
|
|
158
159
|
}
|
|
159
160
|
return lines;
|
|
160
161
|
}
|
|
@@ -170,7 +171,8 @@ function generateProfileReference(profiles) {
|
|
|
170
171
|
lines.push("<div id=\"profile-reference\" class=\"profile-reference\" style=\"display: none;\">");
|
|
171
172
|
lines.push("<div class=\"profile-reference-header\">");
|
|
172
173
|
lines.push("<h3>Profile Reference</h3>");
|
|
173
|
-
lines.push("<button type=\"button\" class=\"profile-reference-close\" aria-label=\"Close\"
|
|
174
|
+
lines.push("<button type=\"button\" class=\"profile-reference-close\" aria-label=\"Close\" data-click-action=\"" +
|
|
175
|
+
ACTIONS.toggleProfileReference + "\">\u2715</button>");
|
|
174
176
|
lines.push("</div>");
|
|
175
177
|
lines.push("<p class=\"reference-intro\">Profiles configure how PrismCast interacts with different video players. Autodetect uses predefined ");
|
|
176
178
|
lines.push("profiles for known sites. If video doesn't play or fullscreen fails, use this reference to experiment with different profiles.</p>");
|
|
@@ -284,9 +286,8 @@ function generateAdvancedFields(idPrefix, options = {}) {
|
|
|
284
286
|
const { channelNumberValue = "", channelSelectorValue = "", guideTitleValue = "", hdhrEnabled = true, logoUrlValue = "", showHints = true, stationIdValue = "", tagsValue = "" } = options;
|
|
285
287
|
const lines = [];
|
|
286
288
|
// Advanced fields toggle.
|
|
287
|
-
lines.push("<div class=\"advanced-toggle\"
|
|
288
|
-
"-advanced
|
|
289
|
-
"'Hide Advanced Options' : 'Show Advanced Options';\">Show Advanced Options</div>");
|
|
289
|
+
lines.push("<div class=\"advanced-toggle\" data-click-action=\"" + ACTIONS.toggleAdvancedFields + "\" data-advanced-id=\"" + idPrefix +
|
|
290
|
+
"-advanced\">Show Advanced Options</div>");
|
|
290
291
|
lines.push("<div id=\"" + idPrefix + "-advanced\" class=\"advanced-fields\">");
|
|
291
292
|
// Station ID.
|
|
292
293
|
const stationIdHint = showHints ? "Optional Gracenote station ID for guide data (tvc-guide-stationid)." : undefined;
|
|
@@ -335,7 +336,7 @@ function generateAdvancedFields(idPrefix, options = {}) {
|
|
|
335
336
|
const checked = currentTags.some((t) => tagsMatch(t, tag)) ? " checked" : "";
|
|
336
337
|
lines.push("<label class=\"tag-checkbox-label\">" +
|
|
337
338
|
"<input type=\"checkbox\" class=\"tag-checkbox\" data-tag=\"" + escapeHtml(tag) + "\"" + checked +
|
|
338
|
-
"
|
|
339
|
+
" data-change-action=\"" + ACTIONS.updateTagsHidden + "\">" +
|
|
339
340
|
"<span class=\"tag-badge\">" + escapeHtml(tag) + "</span></label>");
|
|
340
341
|
}
|
|
341
342
|
lines.push("</div>");
|
|
@@ -433,13 +434,14 @@ export function generateTagFilterContent() {
|
|
|
433
434
|
const vocabulary = getActiveTagVocabulary();
|
|
434
435
|
const lines = [];
|
|
435
436
|
for (const tag of vocabulary) {
|
|
436
|
-
lines.push("<label class=\"provider-option\"
|
|
437
|
-
"<input type=\"checkbox\" class=\"tag-filter-checkbox\" data-tag=\"" + escapeHtml(tag) + "\" checked
|
|
438
|
-
escapeHtml(tag) + "</label>");
|
|
437
|
+
lines.push("<label class=\"provider-option\" data-click-stop-propagation>" +
|
|
438
|
+
"<input type=\"checkbox\" class=\"tag-filter-checkbox\" data-tag=\"" + escapeHtml(tag) + "\" checked data-change-action=\"" +
|
|
439
|
+
ACTIONS.applyTagColumnFilter + "\"> " + escapeHtml(tag) + "</label>");
|
|
439
440
|
}
|
|
440
441
|
if (vocabulary.length > 0) {
|
|
441
442
|
lines.push("<div class=\"dropdown-divider\"></div>");
|
|
442
|
-
lines.push("<div class=\"dropdown-item\" id=\"tag-filter-toggle\"
|
|
443
|
+
lines.push("<div class=\"dropdown-item\" id=\"tag-filter-toggle\" data-click-action=\"" + ACTIONS.toggleTagColumnFilter +
|
|
444
|
+
"\" data-click-stop-propagation>Show None</div>");
|
|
443
445
|
}
|
|
444
446
|
return lines.join("");
|
|
445
447
|
}
|
|
@@ -455,26 +457,27 @@ export function generateTagManagerBody() {
|
|
|
455
457
|
const tagListItems = [];
|
|
456
458
|
for (const tag of vocabulary) {
|
|
457
459
|
tagListItems.push("<div class=\"tag-manager-item\" data-tag=\"" + escapeHtml(tag) + "\">" +
|
|
458
|
-
"<span class=\"tag-badge tag-editable\" title=\"Click to rename\"
|
|
459
|
-
"
|
|
460
|
-
"<button type=\"button\" class=\"btn-icon btn-icon-delete\" title=\"Delete tag\"
|
|
461
|
-
"
|
|
460
|
+
"<span class=\"tag-badge tag-editable\" title=\"Click to rename\" data-click-action=\"" + ACTIONS.startTagRename +
|
|
461
|
+
"\" data-tag-name=\"" + escapeHtml(tag) + "\">" + escapeHtml(tag) + "</span>" +
|
|
462
|
+
"<button type=\"button\" class=\"btn-icon btn-icon-delete\" title=\"Delete tag\" data-click-action=\"" +
|
|
463
|
+
ACTIONS.deleteTag + "\" data-tag-name=\"" + escapeHtml(tag) +
|
|
464
|
+
"\">" + ICON_DELETE + "</button></div>");
|
|
462
465
|
}
|
|
463
466
|
// Deleted predefined tags section - show restore option for tags the user has deleted.
|
|
464
467
|
const deletedItems = [];
|
|
465
468
|
for (const tag of registry.deletedTags) {
|
|
466
469
|
deletedItems.push("<div class=\"tag-manager-item tag-deleted\" data-tag=\"" + escapeHtml(tag) + "\">" +
|
|
467
470
|
"<span class=\"tag-badge tag-badge-deleted\">" + escapeHtml(tag) + "</span> <span class=\"tag-annotation\">(deleted)</span>" +
|
|
468
|
-
"<button type=\"button\" class=\"btn-icon\" title=\"Restore tag\"
|
|
469
|
-
"
|
|
471
|
+
"<button type=\"button\" class=\"btn-icon\" title=\"Restore tag\" data-click-action=\"" + ACTIONS.restoreTag + "\" data-tag-name=\"" + escapeHtml(tag) +
|
|
472
|
+
"\">" + ICON_REVERT + "</button></div>");
|
|
470
473
|
}
|
|
471
474
|
return "<div class=\"tag-manager\">" +
|
|
472
475
|
"<p class=\"wizard-hint\">Create and manage the tag vocabulary. Tags can be assigned to channels via the channel table " +
|
|
473
476
|
"for playlist filtering and organization.</p>" +
|
|
474
477
|
"<div class=\"tag-manager-add\">" +
|
|
475
478
|
"<input type=\"text\" id=\"tag-manager-input\" placeholder=\"New tag name\" maxlength=\"30\" " +
|
|
476
|
-
"pattern=\"[a-zA-Z0-9]([a-zA-Z0-9 -]*[a-zA-Z0-9])?\"
|
|
477
|
-
"<button type=\"button\" class=\"btn btn-primary btn-sm\"
|
|
479
|
+
"pattern=\"[a-zA-Z0-9]([a-zA-Z0-9 -]*[a-zA-Z0-9])?\" data-keydown-action=\"" + ACTIONS.createTagOnEnter + "\">" +
|
|
480
|
+
"<button type=\"button\" class=\"btn btn-primary btn-sm\" data-click-action=\"" + ACTIONS.createTag + "\">Add</button>" +
|
|
478
481
|
"</div>" +
|
|
479
482
|
"<div id=\"tag-manager-error\" class=\"wizard-error\" style=\"display: none;\"></div>" +
|
|
480
483
|
"<div id=\"tag-manager-list\" class=\"tag-manager-list\">" +
|
|
@@ -492,11 +495,11 @@ function generateTagManagementModal() {
|
|
|
492
495
|
return generateWizardModal({
|
|
493
496
|
body: generateTagManagerBody(),
|
|
494
497
|
buttons: [
|
|
495
|
-
{
|
|
498
|
+
{ action: ACTIONS.closeTagManager, label: "Done", position: "right", variant: "primary" }
|
|
496
499
|
],
|
|
500
|
+
closeAction: ACTIONS.closeTagManager,
|
|
497
501
|
id: "tag-manager-modal",
|
|
498
502
|
maxWidth: "420px",
|
|
499
|
-
onClose: "closeTagManager()",
|
|
500
503
|
title: "Manage Tags"
|
|
501
504
|
});
|
|
502
505
|
}
|
|
@@ -514,7 +517,7 @@ function generateBrowseModal() {
|
|
|
514
517
|
buttons: [
|
|
515
518
|
{ id: "browse-back", label: "Back", position: "left", role: "back", visible: false },
|
|
516
519
|
{ label: "Cancel", position: "right", role: "close" },
|
|
517
|
-
{ id: "browse-add-btn", label: "Apply",
|
|
520
|
+
{ action: ACTIONS.submitBrowseChannels, id: "browse-add-btn", label: "Apply", position: "right", variant: "primary", visible: false }
|
|
518
521
|
],
|
|
519
522
|
contentId: "browse-content",
|
|
520
523
|
dataBlocks: [
|
|
@@ -543,9 +546,9 @@ function generateSetupWizardModal() {
|
|
|
543
546
|
return generateWizardModal({
|
|
544
547
|
buttons: [
|
|
545
548
|
{ id: "setup-back", label: "Back", position: "left", role: "back", visible: false },
|
|
546
|
-
{ id: "setup-skip", label: "Skip Setup",
|
|
549
|
+
{ action: ACTIONS.skipSetup, id: "setup-skip", label: "Skip Setup", position: "right" },
|
|
547
550
|
{ id: "setup-next", label: "Next", position: "right", role: "next", variant: "primary" },
|
|
548
|
-
{ id: "setup-finish", label: "Finish",
|
|
551
|
+
{ action: ACTIONS.finishSetup, id: "setup-finish", label: "Finish", position: "right", variant: "primary", visible: false }
|
|
549
552
|
],
|
|
550
553
|
contentId: "setup-content",
|
|
551
554
|
dataAttributes: { "setup-completed": CONFIG.channels.setupCompleted ? "true" : "false" },
|
|
@@ -629,7 +632,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
629
632
|
// to remove them from the DOM.
|
|
630
633
|
const currentSelection = resolveServiceKey(key);
|
|
631
634
|
displayLines.push("<select class=\"provider-select\" data-channel=\"" + escapeHtml(key) +
|
|
632
|
-
"\" title=\"Choose which streaming service delivers this channel\"
|
|
635
|
+
"\" title=\"Choose which streaming service delivers this channel\" data-change-action=\"" + ACTIONS.updateServiceSelection + "\"" + contentHidden + ">");
|
|
633
636
|
for (const variant of serviceGroup.variants) {
|
|
634
637
|
const selected = (variant.key === currentSelection) ? " selected" : "";
|
|
635
638
|
const optionHidden = !isServiceTagEnabled(variant.tag) ? " hidden" : "";
|
|
@@ -640,7 +643,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
640
643
|
}
|
|
641
644
|
else {
|
|
642
645
|
// Single-service: wrap the service name in a span so client-side JS can toggle it with the no-service label. Uses profile-aware label resolution so
|
|
643
|
-
// channels with explicit profile assignments show the profile's service name rather than the
|
|
646
|
+
// channels with explicit profile assignments show the profile's service name rather than the builtin name for the URL domain.
|
|
644
647
|
displayLines.push("<span class=\"provider-name\"" + contentHidden + ">" +
|
|
645
648
|
escapeHtml(getChannelServiceLabel(channel)) + "</span>");
|
|
646
649
|
}
|
|
@@ -649,16 +652,17 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
649
652
|
const cellKey = escapeHtml(key);
|
|
650
653
|
displayLines.push("<td class=\"col-chnum editable-cell\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "channelNumber")) +
|
|
651
654
|
"\" data-field=\"channelNumber\" data-key=\"" + cellKey + "\" data-value=\"" + (channel.channelNumber ? escapeHtml(String(channel.channelNumber)) : "") +
|
|
652
|
-
"\"
|
|
653
|
-
"</td>");
|
|
655
|
+
"\" data-click-action=\"" + ACTIONS.startInlineEdit + "\">" +
|
|
656
|
+
(channel.channelNumber ? escapeHtml(String(channel.channelNumber)) : "<span class=\"text-muted\">–</span>") + "</td>");
|
|
654
657
|
// HDHR column: inline checkbox for quick toggling. The checkbox submits changes via the toggleHdhr() client-side function.
|
|
655
658
|
const hdhrCheckedAttr = getEffectiveHdhrEnabled(channel) ? " checked" : "";
|
|
656
659
|
displayLines.push("<td class=\"col-hdhr\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "hdhrEnabled")) +
|
|
657
660
|
"\"><input type=\"checkbox\" data-key=\"" + cellKey + "\"" + hdhrCheckedAttr +
|
|
658
|
-
"
|
|
661
|
+
" data-change-action=\"" + ACTIONS.toggleHdhr + "\" title=\"Include in HDHomeRun/Plex lineup\"></td>");
|
|
659
662
|
displayLines.push("<td class=\"col-stationid editable-cell\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "stationId")) +
|
|
660
663
|
"\" data-field=\"stationId\" data-key=\"" + cellKey + "\" data-value=\"" + (channel.stationId ? escapeHtml(channel.stationId) : "") +
|
|
661
|
-
"\"
|
|
664
|
+
"\" data-click-action=\"" + ACTIONS.startInlineEdit + "\">" +
|
|
665
|
+
(channel.stationId ? escapeHtml(channel.stationId) : "<span class=\"text-muted\">–</span>") + "</td>");
|
|
662
666
|
// Profile column: show explicit profile as-is, or the auto-resolved friendly name with "(auto)" suffix in muted style.
|
|
663
667
|
const profileSortKey = escapeHtml(getChannelSortKey(channel, key, "profile"));
|
|
664
668
|
if (channel.profile) {
|
|
@@ -684,7 +688,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
684
688
|
"<span class=\"text-muted\">–</span>";
|
|
685
689
|
displayLines.push("<td class=\"col-tags editable-cell dropdown\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "tags")) +
|
|
686
690
|
"\" data-key=\"" + cellKey + "\" data-tags=\"" + escapeHtml(effectiveTags.join(",")) +
|
|
687
|
-
"\"
|
|
691
|
+
"\" data-click-action=\"" + ACTIONS.toggleInlineTagDropdown + "\">" + tagsHtml + "</td>");
|
|
688
692
|
// Actions column with icon buttons. Five positions per row: Edit (always), Login/placeholder, Health/placeholder, context-sensitive, and Copy URL.
|
|
689
693
|
displayLines.push("<td>");
|
|
690
694
|
displayLines.push("<div class=\"btn-group\">");
|
|
@@ -693,17 +697,21 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
693
697
|
const variantKey = resolveServiceKey(key);
|
|
694
698
|
const authDomain = getAuthDomainForChannel(variantKey);
|
|
695
699
|
// Position 1: Edit (all channels).
|
|
696
|
-
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-edit\" title=\"Edit\" aria-label=\"Edit\"
|
|
697
|
-
"
|
|
700
|
+
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-edit\" title=\"Edit\" aria-label=\"Edit\" " +
|
|
701
|
+
"data-click-action=\"" + ACTIONS.showEditForm + "\" data-channel-key=\"" + escapedKey + "\">" + ICON_EDIT + "</button>");
|
|
698
702
|
// Position 2: Login for enabled channels (with domain auth color), placeholder for disabled predefined. Custom channels (user-defined, not predefined) skip
|
|
699
703
|
// login coloring because they have no service concept.
|
|
700
704
|
if (!isDisabled) {
|
|
701
705
|
const authTimestamp = (isPredefined || isOverride) ? getDomainAuth(authDomain) : null;
|
|
702
706
|
const loginColorClass = authTimestamp ? " health-success" : "";
|
|
703
|
-
|
|
707
|
+
// Lead the title with the action so users discover that the icon is interactive...the trailing fact reports the service-scoped verified state, since the
|
|
708
|
+
// auth domain is shared across every channel of the same service.
|
|
709
|
+
const serviceLabel = getChannelServiceLabel(channel);
|
|
710
|
+
const loginTitle = "Click to open this channel in PrismCast's Chrome to sign in or test. " + serviceLabel +
|
|
711
|
+
(authTimestamp ? " verified " + formatTimeAgo(authTimestamp) : " not yet verified") + ".";
|
|
704
712
|
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-login" + loginColorClass + "\" data-auth-domain=\"" +
|
|
705
|
-
escapeHtml(authDomain) + "\" title=\"" + loginTitle + "\" aria-label=\"Login\" " +
|
|
706
|
-
"
|
|
713
|
+
escapeHtml(authDomain) + "\" title=\"" + escapeHtml(loginTitle) + "\" aria-label=\"Login\" " +
|
|
714
|
+
"data-click-action=\"" + ACTIONS.startChannelLogin + "\" data-channel-key=\"" + escapedKey + "\">" + ICON_LOGIN + "</button>");
|
|
707
715
|
}
|
|
708
716
|
else {
|
|
709
717
|
displayLines.push("<span class=\"btn-icon-placeholder\"></span>");
|
|
@@ -713,7 +721,8 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
713
721
|
const channelHealthResult = getChannelHealth(key, authDomain);
|
|
714
722
|
const healthColorClass = (channelHealthResult?.status === "success") ? " health-success" : (channelHealthResult?.status === "failed") ? " health-failed" : "";
|
|
715
723
|
const healthTitle = channelHealthResult ?
|
|
716
|
-
(channelHealthResult.status === "success" ? "
|
|
724
|
+
"Last tune " + (channelHealthResult.status === "success" ? "succeeded " : "failed ") + formatTimeAgo(channelHealthResult.timestamp) + "." :
|
|
725
|
+
"Not yet tuned.";
|
|
717
726
|
displayLines.push("<span class=\"btn-icon btn-icon-health" + healthColorClass + "\" title=\"" + healthTitle +
|
|
718
727
|
"\" aria-label=\"Channel health\">" + ICON_HEALTH + "</span>");
|
|
719
728
|
}
|
|
@@ -724,25 +733,27 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
724
733
|
// property overrides are independent concerns - a predefined channel can be disabled AND have customizations, and the toggle always reflects the visibility state.
|
|
725
734
|
if (isPredefined) {
|
|
726
735
|
if (isDisabled) {
|
|
727
|
-
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-enable\" title=\"Enable\" aria-label=\"Enable\"
|
|
728
|
-
escapedKey + "
|
|
736
|
+
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-enable\" title=\"Enable\" aria-label=\"Enable\" " +
|
|
737
|
+
"data-click-action=\"" + ACTIONS.togglePredefinedChannel + "\" data-channel-key=\"" + escapedKey + "\" data-enabled=\"true\">" + ICON_ENABLE + "</button>");
|
|
729
738
|
}
|
|
730
739
|
else {
|
|
731
|
-
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-disable\" title=\"Disable\" aria-label=\"Disable\"
|
|
732
|
-
escapedKey + "
|
|
740
|
+
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-disable\" title=\"Disable\" aria-label=\"Disable\" " +
|
|
741
|
+
"data-click-action=\"" + ACTIONS.togglePredefinedChannel + "\" data-channel-key=\"" + escapedKey + "\" data-enabled=\"false\">" + ICON_DISABLE + "</button>");
|
|
733
742
|
}
|
|
734
743
|
}
|
|
735
744
|
else if (isUser) {
|
|
736
|
-
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-delete\" title=\"Delete\" aria-label=\"Delete\"
|
|
737
|
-
escapedKey + "
|
|
745
|
+
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-delete\" title=\"Delete\" aria-label=\"Delete\" " +
|
|
746
|
+
"data-click-action=\"" + ACTIONS.deleteChannel + "\" data-channel-key=\"" + escapedKey + "\">" + ICON_DELETE + "</button>");
|
|
738
747
|
}
|
|
739
748
|
// Position 5: Copy URL dropdown (all channels).
|
|
740
749
|
displayLines.push("<div class=\"dropdown copy-dropdown\">");
|
|
741
750
|
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-copy\" title=\"Copy stream URL\" aria-label=\"Copy stream URL\" " +
|
|
742
|
-
"
|
|
751
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\">" + ICON_COPY + "</button>");
|
|
743
752
|
displayLines.push("<div class=\"dropdown-menu copy-url-menu\">");
|
|
744
|
-
displayLines.push("<div class=\"dropdown-item\"
|
|
745
|
-
|
|
753
|
+
displayLines.push("<div class=\"dropdown-item\" data-click-action=\"" + ACTIONS.copyStreamUrl + "\" data-protocol=\"hls\" data-channel-key=\"" +
|
|
754
|
+
escapedKey + "\">Copy HLS URL</div>");
|
|
755
|
+
displayLines.push("<div class=\"dropdown-item\" data-click-action=\"" + ACTIONS.copyStreamUrl + "\" data-protocol=\"mpegts\" data-channel-key=\"" +
|
|
756
|
+
escapedKey + "\">Copy MPEG-TS URL</div>");
|
|
746
757
|
displayLines.push("</div>");
|
|
747
758
|
displayLines.push("</div>");
|
|
748
759
|
displayLines.push("</div>");
|
|
@@ -756,7 +767,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
756
767
|
editLines.push("<td colspan=\"" + String(TOTAL_COLUMN_COUNT) + "\">");
|
|
757
768
|
editLines.push("<div class=\"channel-form\" style=\"margin: 0;\">");
|
|
758
769
|
editLines.push("<h3>Edit Channel: " + escapedKey + "</h3>");
|
|
759
|
-
editLines.push("<form id=\"edit-channel-form-" + escapedKey + "\"
|
|
770
|
+
editLines.push("<form id=\"edit-channel-form-" + escapedKey + "\" data-submit-action=\"" + ACTIONS.submitChannelFormEdit + "\" data-submit-prevent-default>");
|
|
760
771
|
editLines.push("<input type=\"hidden\" name=\"action\" value=\"edit\">");
|
|
761
772
|
editLines.push("<input type=\"hidden\" name=\"key\" value=\"" + escapedKey + "\">");
|
|
762
773
|
// For override channels, fetch the customization provenance once. The accessor returns which fields the user has explicitly customized (across canonical and
|
|
@@ -831,10 +842,11 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
831
842
|
// Form buttons.
|
|
832
843
|
editLines.push("<div class=\"form-buttons\">");
|
|
833
844
|
editLines.push("<button type=\"submit\" class=\"btn btn-primary\">Save Changes</button>");
|
|
834
|
-
editLines.push("<button type=\"button\" class=\"btn btn-secondary\"
|
|
845
|
+
editLines.push("<button type=\"button\" class=\"btn btn-secondary\" data-click-action=\"" + ACTIONS.hideEditForm +
|
|
846
|
+
"\" data-channel-key=\"" + escapedKey + "\">Cancel</button>");
|
|
835
847
|
if (isOverride) {
|
|
836
|
-
editLines.push("<button type=\"button\" class=\"btn btn-secondary btn-revert\"
|
|
837
|
-
"
|
|
848
|
+
editLines.push("<button type=\"button\" class=\"btn btn-secondary btn-revert\" data-click-action=\"" + ACTIONS.revertChannel + "\" data-channel-key=\"" + escapedKey +
|
|
849
|
+
"\">Revert to Defaults</button>");
|
|
838
850
|
}
|
|
839
851
|
editLines.push("</div>");
|
|
840
852
|
editLines.push("</form>");
|
|
@@ -974,7 +986,7 @@ export function generateServiceFilterToolbar() {
|
|
|
974
986
|
lines.push("<div class=\"dropdown provider-dropdown\">");
|
|
975
987
|
const buttonText = hasFilter ? "Filtered" : "All Services";
|
|
976
988
|
lines.push("<button type=\"button\" class=\"btn btn-sm toolbar-icon-btn\" id=\"provider-filter-btn\" " +
|
|
977
|
-
"title=\"Filter channels by streaming service\"
|
|
989
|
+
"title=\"Filter channels by streaming service\" data-click-action=\"" + ACTIONS.toggleDropdown + "\">" + ICON_FILTER + " " + buttonText + " ▾</button>");
|
|
978
990
|
lines.push("<div class=\"dropdown-menu provider-dropdown-menu\">");
|
|
979
991
|
for (const tagInfo of allTags) {
|
|
980
992
|
const isDirectTag = tagInfo.tag === "direct";
|
|
@@ -983,7 +995,7 @@ export function generateServiceFilterToolbar() {
|
|
|
983
995
|
const disabledAttr = isDirectTag ? " disabled" : "";
|
|
984
996
|
lines.push("<label class=\"provider-option\">");
|
|
985
997
|
lines.push("<input type=\"checkbox\" data-tag=\"" + escapeHtml(tagInfo.tag) + "\"" + checkedAttr + disabledAttr +
|
|
986
|
-
"
|
|
998
|
+
" data-change-action=\"" + ACTIONS.toggleServiceTag + "\"> " + serviceDisplaySpan(tagInfo.displayName, tagInfo.domain, tagInfo.iconUrl));
|
|
987
999
|
lines.push("</label>");
|
|
988
1000
|
}
|
|
989
1001
|
lines.push("</div>");
|
|
@@ -1000,7 +1012,7 @@ export function generateServiceFilterToolbar() {
|
|
|
1000
1012
|
lines.push("<span class=\"provider-chip\" data-tag=\"" + escapeHtml(tag) + "\">" +
|
|
1001
1013
|
serviceDisplaySpan(displayName, chipTag?.domain, chipTag?.iconUrl, true) +
|
|
1002
1014
|
"<button type=\"button\" class=\"chip-close\" title=\"Remove " + escapeHtml(displayName) + " from filter\" aria-label=\"Remove " +
|
|
1003
|
-
escapeHtml(displayName) + "\"
|
|
1015
|
+
escapeHtml(displayName) + "\" data-click-action=\"" + ACTIONS.removeServiceChip + "\" data-tag-name=\"" + escapeHtml(tag) + "\">×</button></span>");
|
|
1004
1016
|
}
|
|
1005
1017
|
}
|
|
1006
1018
|
lines.push("</div>");
|
|
@@ -1039,7 +1051,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1039
1051
|
// Manage Channels dropdown - primary channel creation and setup actions.
|
|
1040
1052
|
lines.push("<div class=\"dropdown\">");
|
|
1041
1053
|
lines.push("<button type=\"button\" class=\"btn btn-primary btn-sm toolbar-icon-btn\" title=\"Add, browse, or set up channels\" " +
|
|
1042
|
-
"
|
|
1054
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\">" + ICON_MANAGE + " Manage Channels ▾</button>");
|
|
1043
1055
|
lines.push("<div class=\"dropdown-menu\">");
|
|
1044
1056
|
// Dropdown item icons: tinted variants of the shared icons for visual differentiation in menu items. Each uses a distinct color so items are scannable at
|
|
1045
1057
|
// a glance. These are local to this function because the tint colors are context-specific to this dropdown.
|
|
@@ -1054,42 +1066,42 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1054
1066
|
const TINTED_TAG = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#9b59b6\" stroke-width=\"1.5\" " +
|
|
1055
1067
|
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1.5 8.7V2.5a1 1 0 011-1h6.2a1 1 0 01.7.3l5.1 5.1a1 1 0 010 1.4l-5.5 5.5a1 " +
|
|
1056
1068
|
"1 0 01-1.4 0L1.8 9.4a1 1 0 01-.3-.7z\"/><circle cx=\"5\" cy=\"5\" r=\"1\"/></svg>";
|
|
1057
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1058
|
-
"
|
|
1059
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1069
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.showAddChannelForm + "\" data-click-close-dropdown>" + TINTED_ADD +
|
|
1070
|
+
" Add Channel</div>");
|
|
1071
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.openBrowseModal + "\" data-click-close-dropdown>" + TINTED_BROWSE +
|
|
1060
1072
|
" Browse Service Channels</div>");
|
|
1061
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1073
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.openTagManager + "\" data-click-close-dropdown>" + TINTED_TAG +
|
|
1062
1074
|
" Manage Tags</div>");
|
|
1063
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1075
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.openSetupWizard + "\" data-click-close-dropdown>" + TINTED_SETUP +
|
|
1064
1076
|
" Service Setup</div>");
|
|
1065
1077
|
lines.push("</div>");
|
|
1066
1078
|
lines.push("</div>");
|
|
1067
1079
|
// Import / Export dropdown - data I/O operations.
|
|
1068
1080
|
lines.push("<div class=\"dropdown\">");
|
|
1069
1081
|
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-icon-btn\" title=\"Import or export channel data\" " +
|
|
1070
|
-
"
|
|
1082
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\">" + ICON_TRANSFER + " Import / Export ▾</button>");
|
|
1071
1083
|
lines.push("<div class=\"dropdown-menu\">");
|
|
1072
1084
|
const TINTED_IMPORT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#5b8def\" stroke-width=\"1.5\" " +
|
|
1073
1085
|
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 2v8M5 7l3 3 3-3\"/><path d=\"M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2\"/></svg>";
|
|
1074
1086
|
const TINTED_EXPORT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#22a563\" stroke-width=\"1.5\" " +
|
|
1075
1087
|
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 10V2M5 5l3-3 3 3\"/><path d=\"M2 11v2a1 1 0 001 1h10a1 1 0 001-1v-2\"/></svg>";
|
|
1076
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1088
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.triggerChannelsImport + "\" data-click-close-dropdown>" +
|
|
1077
1089
|
TINTED_IMPORT + " Import Channels (JSON)</div>");
|
|
1078
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1090
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.triggerM3uImport + "\" data-click-close-dropdown>" +
|
|
1079
1091
|
TINTED_IMPORT + " Import M3U Playlist</div>");
|
|
1080
1092
|
lines.push("<label class=\"dropdown-option\"><input type=\"checkbox\" id=\"m3u-replace-duplicates\"> Replace duplicates on M3U import</label>");
|
|
1081
1093
|
lines.push("<div class=\"dropdown-divider\"></div>");
|
|
1082
|
-
lines.push("<div class=\"dropdown-item dropdown-item-icon\"
|
|
1094
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" data-click-action=\"" + ACTIONS.exportChannels + "\" data-click-close-dropdown>" +
|
|
1083
1095
|
TINTED_EXPORT + " Export Channels (JSON)</div>");
|
|
1084
1096
|
lines.push("</div>");
|
|
1085
1097
|
lines.push("</div>");
|
|
1086
|
-
lines.push("<input type=\"file\" id=\"import-m3u-file\" accept=\".m3u,.m3u8\" style=\"display: none;\"
|
|
1098
|
+
lines.push("<input type=\"file\" id=\"import-m3u-file\" accept=\".m3u,.m3u8\" style=\"display: none;\" data-change-action=\"" + ACTIONS.importM3u + "\">");
|
|
1087
1099
|
// Visible column set, used by Quick Actions to gate column-dependent options and by the table header to set initial hide classes.
|
|
1088
1100
|
const visibleCols = new Set(CONFIG.channels.visibleColumns);
|
|
1089
1101
|
// Quick Actions dropdown - bulk operations for predefined channels.
|
|
1090
1102
|
lines.push("<div class=\"dropdown quick-actions-dropdown\">");
|
|
1091
1103
|
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-icon-btn\" title=\"Bulk operations for predefined channels\" " +
|
|
1092
|
-
"
|
|
1104
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\">" + ICON_BOLT + " Quick Actions ▾</button>");
|
|
1093
1105
|
lines.push("<div class=\"dropdown-menu\">");
|
|
1094
1106
|
// Compute initial toggle counts for predefined channel scopes. The server is the single source of truth - the client renders what we return here.
|
|
1095
1107
|
const scopeCounts = getPredefinedScopeCounts();
|
|
@@ -1102,8 +1114,8 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1102
1114
|
];
|
|
1103
1115
|
for (const s of scopes) {
|
|
1104
1116
|
const checked = (s.count === s.total) ? " checked" : "";
|
|
1105
|
-
lines.push("<label class=\"provider-option\"
|
|
1106
|
-
"
|
|
1117
|
+
lines.push("<label class=\"provider-option\" data-click-action=\"" + ACTIONS.bulkTogglePredefined + "\" data-scope=\"" + s.scope + "\" data-click-prevent-default>" +
|
|
1118
|
+
"<input type=\"checkbox\" class=\"scope-toggle\" data-scope=\"" + s.scope + "\"" + checked + "> " +
|
|
1107
1119
|
s.label + "<span class=\"quick-action-count\" data-scope=\"" + s.scope + "\" data-enabled=\"" + String(s.count) + "\" data-total=\"" + String(s.total) +
|
|
1108
1120
|
"\">" + String(s.count) + " of " + String(s.total) + " enabled</span></label>");
|
|
1109
1121
|
}
|
|
@@ -1115,7 +1127,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1115
1127
|
lines.push("<div class=\"bulk-assign-row\">");
|
|
1116
1128
|
lines.push("<span>Set all channels to:</span>");
|
|
1117
1129
|
lines.push("<select id=\"bulk-assign-select\" class=\"bulk-assign-select\" title=\"Switch all multi-service channels to this service\" " +
|
|
1118
|
-
"
|
|
1130
|
+
"data-change-action=\"" + ACTIONS.bulkAssignService + "\">");
|
|
1119
1131
|
lines.push("<option value=\"\">Select service</option>");
|
|
1120
1132
|
for (const tagInfo of allTags) {
|
|
1121
1133
|
const hidden = (hasFilter && !enabled.includes(tagInfo.tag) && (tagInfo.tag !== "direct")) ? " hidden" : "";
|
|
@@ -1133,8 +1145,9 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1133
1145
|
lines.push("<div class=\"bulk-assign-row\">");
|
|
1134
1146
|
lines.push("<span>Auto-number from:</span>");
|
|
1135
1147
|
lines.push("<input type=\"number\" id=\"auto-number-start\" class=\"auto-number-input\" value=\"1\" min=\"0\" max=\"99999\" " +
|
|
1136
|
-
"placeholder=\"Clear\"
|
|
1137
|
-
lines.push("<button type=\"button\" class=\"btn btn-sm btn-secondary\"
|
|
1148
|
+
"placeholder=\"Clear\" data-click-stop-propagation>");
|
|
1149
|
+
lines.push("<button type=\"button\" class=\"btn btn-sm btn-secondary\" data-click-action=\"" + ACTIONS.autoNumberChannels +
|
|
1150
|
+
"\" data-click-close-dropdown>Apply</button>");
|
|
1138
1151
|
lines.push("</div>");
|
|
1139
1152
|
lines.push("</div>");
|
|
1140
1153
|
// HDHR bulk toggle - tri-state checkbox to enable/disable all channels for the HDHomeRun lineup. Visible when the HDHR column is shown.
|
|
@@ -1143,7 +1156,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1143
1156
|
const hdhrCheckedAttr = (hdhrCounts.enabled === hdhrCounts.total) ? " checked" : "";
|
|
1144
1157
|
const hdhrIndeterminate = ((hdhrCounts.enabled > 0) && (hdhrCounts.enabled < hdhrCounts.total)) ? " data-indeterminate=\"true\"" : "";
|
|
1145
1158
|
lines.push("<div id=\"quick-action-hdhr\"" + hdhrVisible + ">");
|
|
1146
|
-
lines.push("<label class=\"provider-option\"
|
|
1159
|
+
lines.push("<label class=\"provider-option\" data-click-action=\"" + ACTIONS.bulkToggleHdhr + "\" data-click-prevent-default>");
|
|
1147
1160
|
lines.push("<input type=\"checkbox\" id=\"hdhr-bulk-toggle\"" + hdhrCheckedAttr + hdhrIndeterminate + "> ");
|
|
1148
1161
|
lines.push("Include all in HDHR/Plex");
|
|
1149
1162
|
lines.push("<span class=\"quick-action-count\" id=\"hdhr-bulk-count\">" + String(hdhrCounts.enabled) + " of " + String(hdhrCounts.total) + "</span>");
|
|
@@ -1177,8 +1190,8 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1177
1190
|
const count = tagCountMap.get(tag) ?? 0;
|
|
1178
1191
|
const checkedAttr = (count === totalEnabled) ? " checked" : "";
|
|
1179
1192
|
const indeterminate = ((count > 0) && (count < totalEnabled)) ? " data-indeterminate=\"true\"" : "";
|
|
1180
|
-
lines.push("<label class=\"provider-option\"
|
|
1181
|
-
"
|
|
1193
|
+
lines.push("<label class=\"provider-option\" data-click-action=\"" + ACTIONS.bulkToggleTag +
|
|
1194
|
+
"\" data-tag-name=\"" + escapeHtml(tag) + "\" data-click-prevent-default>" +
|
|
1182
1195
|
"<input type=\"checkbox\" class=\"tag-bulk-toggle\" data-tag=\"" + escapeHtml(tag) + "\"" + checkedAttr + indeterminate + "> " +
|
|
1183
1196
|
escapeHtml(tag) +
|
|
1184
1197
|
"<span class=\"quick-action-count\" data-tag-count=\"" + escapeHtml(tag) + "\">" +
|
|
@@ -1232,7 +1245,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1232
1245
|
const addFormVisible = (showAddForm === true) || (formErrors && formErrors.has("key") && !editingChannelKey);
|
|
1233
1246
|
lines.push("<div id=\"add-channel-form\" class=\"channel-form\" style=\"display: " + (addFormVisible ? "block" : "none") + ";\">");
|
|
1234
1247
|
lines.push("<h3>Add New Channel</h3>");
|
|
1235
|
-
lines.push("<form id=\"add-channel-form-el\"
|
|
1248
|
+
lines.push("<form id=\"add-channel-form-el\" data-submit-action=\"" + ACTIONS.submitChannelFormAdd + "\" data-submit-prevent-default>");
|
|
1236
1249
|
lines.push("<input type=\"hidden\" name=\"action\" value=\"add\">");
|
|
1237
1250
|
// Service pills. Clicking a service auto-fills the URL field, which triggers the existing URL-change infrastructure (datalist population, profile
|
|
1238
1251
|
// resolution). The pills use serviceDisplaySpan for icon rendering via the shared processServiceDisplays path. Guide-grid services fill with their
|
|
@@ -1247,7 +1260,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1247
1260
|
}
|
|
1248
1261
|
const pillUrl = addFormGuideUrls[tagInfo.tag] ?? (tagInfo.domain ? "https://" + tagInfo.domain + "/" : "");
|
|
1249
1262
|
lines.push("<button type=\"button\" class=\"provider-pill\" data-slug=\"" + escapeHtml(tagInfo.tag) + "\" data-url=\"" + escapeHtml(pillUrl) +
|
|
1250
|
-
"\"
|
|
1263
|
+
"\" data-click-action=\"" + ACTIONS.selectServicePill + "\">" + serviceDisplaySpan(tagInfo.displayName, tagInfo.domain, tagInfo.iconUrl) + "</button>");
|
|
1251
1264
|
}
|
|
1252
1265
|
lines.push("</div>");
|
|
1253
1266
|
lines.push("</div>");
|
|
@@ -1287,7 +1300,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1287
1300
|
// Form buttons.
|
|
1288
1301
|
lines.push("<div class=\"form-buttons\">");
|
|
1289
1302
|
lines.push("<button type=\"submit\" class=\"btn btn-primary\">Add Channel</button>");
|
|
1290
|
-
lines.push("<button type=\"button\" class=\"btn btn-secondary\"
|
|
1303
|
+
lines.push("<button type=\"button\" class=\"btn btn-secondary\" data-click-action=\"" + ACTIONS.hideAddChannelForm + "\">Cancel</button>");
|
|
1291
1304
|
lines.push("</div>");
|
|
1292
1305
|
lines.push("</form>");
|
|
1293
1306
|
// Close the add-channel-form wrapper opened at the top of this builder.
|
|
@@ -1333,7 +1346,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1333
1346
|
const isActive = (sortField === hdr.field);
|
|
1334
1347
|
const activeIndicator = isActive ? ((sortDir === "asc") ? " ▲" : " ▼") : "";
|
|
1335
1348
|
lines.push("<th class=\"" + hdr.cssClass + " sortable\" data-sort-field=\"" + hdr.field +
|
|
1336
|
-
"\"
|
|
1349
|
+
"\" data-click-action=\"" + ACTIONS.channelTableSort + "\" data-field=\"" + hdr.field + "\">");
|
|
1337
1350
|
lines.push("<span class=\"sort-label\">" + hdr.label + activeIndicator + "</span>");
|
|
1338
1351
|
// Tags header: additional filter dropdown alongside the sort label. The dropdown is a client-side-only view filter (transient, not persisted) that
|
|
1339
1352
|
// shows/hides rows based on their data-channel-tags attribute. The dropdown content comes from generateTagFilterContent() - the single source of truth
|
|
@@ -1341,7 +1354,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1341
1354
|
if (hdr.field === "tags") {
|
|
1342
1355
|
lines.push("<div class=\"dropdown tag-filter-dropdown\" style=\"display: inline;\">");
|
|
1343
1356
|
lines.push("<button type=\"button\" class=\"btn-icon btn-tag-filter\" title=\"Filter by tag\" " +
|
|
1344
|
-
"
|
|
1357
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\" data-click-stop-propagation>" + ICON_FILTER + "</button>");
|
|
1345
1358
|
lines.push("<div class=\"dropdown-menu\" id=\"tag-filter-menu\">" + generateTagFilterContent() + "</div>");
|
|
1346
1359
|
lines.push("</div>");
|
|
1347
1360
|
// Playlist hint icon. Hidden by default, shown by applyTagColumnFilter() when the filter is active. Clicking opens a popover with the playlist URL
|
|
@@ -1349,7 +1362,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1349
1362
|
lines.push("<div class=\"dropdown\" style=\"display: inline;\">");
|
|
1350
1363
|
lines.push("<button type=\"button\" class=\"btn-icon btn-playlist-hint\" id=\"playlist-hint-btn\" " +
|
|
1351
1364
|
"title=\"Playlist URL for this filter\" style=\"display: none;\" " +
|
|
1352
|
-
"
|
|
1365
|
+
"data-click-action=\"" + ACTIONS.showPlaylistHint + "\" data-click-stop-propagation>" + ICON_LINK + "</button>");
|
|
1353
1366
|
lines.push("<div class=\"dropdown-menu playlist-hint-menu\"></div>");
|
|
1354
1367
|
lines.push("</div>");
|
|
1355
1368
|
}
|
|
@@ -1359,15 +1372,15 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1359
1372
|
lines.push("<th class=\"col-actions\"><span>Actions</span>");
|
|
1360
1373
|
lines.push("<div class=\"dropdown column-picker\">");
|
|
1361
1374
|
lines.push("<button type=\"button\" class=\"btn-icon btn-col-picker\" title=\"Table options\" aria-label=\"Table options\" " +
|
|
1362
|
-
"
|
|
1375
|
+
"data-click-action=\"" + ACTIONS.toggleDropdown + "\">⋮</button>");
|
|
1363
1376
|
lines.push("<div class=\"dropdown-menu column-picker-menu\">");
|
|
1364
|
-
lines.push("<label class=\"provider-option\"><input type=\"checkbox\" id=\"show-disabled-toggle\"
|
|
1377
|
+
lines.push("<label class=\"provider-option\"><input type=\"checkbox\" id=\"show-disabled-toggle\" data-change-action=\"" + ACTIONS.toggleDisabledVisibility + "\"> " +
|
|
1365
1378
|
"Show disabled channels</label>");
|
|
1366
1379
|
lines.push("<div class=\"dropdown-divider\"></div>");
|
|
1367
1380
|
for (const col of OPTIONAL_COLUMNS) {
|
|
1368
1381
|
const checked = visibleCols.has(col.field) ? " checked" : "";
|
|
1369
1382
|
lines.push("<label class=\"provider-option\"><input type=\"checkbox\" data-col-class=\"" + col.cssClass + "\" data-col-field=\"" + col.field +
|
|
1370
|
-
"\"
|
|
1383
|
+
"\" data-change-action=\"" + ACTIONS.toggleColumn + "\"" + checked + "> " + col.label + "</label>");
|
|
1371
1384
|
}
|
|
1372
1385
|
lines.push("</div>");
|
|
1373
1386
|
lines.push("</div>");
|
|
@@ -1403,7 +1416,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
1403
1416
|
const vocabulary = getActiveTagVocabulary();
|
|
1404
1417
|
lines.push("<div id=\"inline-tag-portal\" class=\"dropdown-menu inline-tag-menu\">");
|
|
1405
1418
|
for (const tag of vocabulary) {
|
|
1406
|
-
lines.push("<label class=\"provider-option\"
|
|
1419
|
+
lines.push("<label class=\"provider-option\" data-click-stop-propagation>" +
|
|
1407
1420
|
"<input type=\"checkbox\" class=\"inline-tag-checkbox\" data-tag=\"" + escapeHtml(tag) + "\"> " + escapeHtml(tag) + "</label>");
|
|
1408
1421
|
}
|
|
1409
1422
|
lines.push("</div>");
|