prismcast 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -28
- package/dist/app.d.ts +5 -0
- package/dist/app.js +63 -5
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +1 -1
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +10 -34
- package/dist/browser/channelSelection.js +31 -96
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +19 -0
- package/dist/browser/display.js +16 -11
- package/dist/browser/display.js.map +1 -1
- package/dist/browser/index.d.ts +31 -54
- package/dist/browser/index.js +334 -272
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +65 -0
- package/dist/browser/login.js +193 -0
- package/dist/browser/login.js.map +1 -0
- package/dist/browser/precaching.js +15 -3
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +23 -0
- package/dist/browser/tuning/comcastPolymer.js +655 -0
- package/dist/browser/tuning/comcastPolymer.js.map +1 -0
- package/dist/browser/tuning/cox.d.ts +2 -0
- package/dist/browser/tuning/cox.js +14 -0
- package/dist/browser/tuning/cox.js.map +1 -0
- package/dist/browser/tuning/directv.js +14 -1
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.js +14 -2
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.js +12 -1
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +24 -1
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +46 -0
- package/dist/browser/tuning/shared.js +100 -0
- package/dist/browser/tuning/shared.js.map +1 -0
- package/dist/browser/tuning/sling.js +35 -5
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.js +13 -1
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/thumbnailRow.js.map +1 -1
- package/dist/browser/tuning/tileClick.js +1 -1
- package/dist/browser/tuning/tileClick.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +2 -0
- package/dist/browser/tuning/xfinity.js +14 -0
- package/dist/browser/tuning/xfinity.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +15 -1
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -0
- package/dist/browser/video.js +168 -124
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +2 -2
- package/dist/channels/index.js +2075 -683
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +28 -28
- package/dist/config/health.js +72 -54
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.js +29 -64
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +12 -0
- package/dist/config/paths.js +16 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/profiles.d.ts +3 -2
- package/dist/config/profiles.js +45 -34
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providerPacks.js +4 -1
- package/dist/config/providerPacks.js.map +1 -1
- package/dist/config/providers.d.ts +19 -5
- package/dist/config/providers.js +123 -93
- package/dist/config/providers.js.map +1 -1
- package/dist/config/sites.d.ts +28 -0
- package/dist/config/sites.js +104 -127
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +53 -2
- package/dist/config/userChannels.js +222 -13
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +2 -1
- package/dist/config/userConfig.js +6 -15
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +0 -1
- package/dist/config/userProfiles.js +18 -10
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +4 -4
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/index.js +25 -22
- package/dist/index.js.map +1 -1
- package/dist/native/decrypt.d.ts +32 -0
- package/dist/native/decrypt.js +85 -0
- package/dist/native/decrypt.js.map +1 -0
- package/dist/native/index.d.ts +61 -0
- package/dist/native/index.js +342 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/intercept.d.ts +35 -0
- package/dist/native/intercept.js +184 -0
- package/dist/native/intercept.js.map +1 -0
- package/dist/native/probe.d.ts +49 -0
- package/dist/native/probe.js +294 -0
- package/dist/native/probe.js.map +1 -0
- package/dist/native/proxy.d.ts +54 -0
- package/dist/native/proxy.js +947 -0
- package/dist/native/proxy.js.map +1 -0
- package/dist/routes/components.d.ts +43 -0
- package/dist/routes/components.js +122 -0
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +2 -2
- package/dist/routes/config/channels/index.js +1 -1
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/routes.js +443 -65
- package/dist/routes/config/channels/routes.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +76 -0
- package/dist/routes/config/channels/table.js +369 -89
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/index.d.ts +1 -1
- package/dist/routes/config/index.js +1 -1
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/providers.d.ts +6 -6
- package/dist/routes/config/providers.js +73 -99
- package/dist/routes/config/providers.js.map +1 -1
- package/dist/routes/debug.js +5 -5
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/hls.js +12 -2
- package/dist/routes/hls.js.map +1 -1
- package/dist/routes/index.d.ts +0 -15
- package/dist/routes/index.js +2 -16
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/providers.js +106 -3
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/root/content.js +124 -47
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -1
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +772 -312
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +596 -544
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +1 -0
- package/dist/routes/root/scripts/index.js +1 -0
- package/dist/routes/root/scripts/index.js.map +1 -1
- package/dist/routes/root/scripts/shared.d.ts +6 -0
- package/dist/routes/root/scripts/shared.js +391 -0
- package/dist/routes/root/scripts/shared.js.map +1 -0
- package/dist/routes/root/scripts/status.js +49 -33
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +117 -10
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/service/commands.js +2 -1
- package/dist/service/commands.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +5 -0
- package/dist/streaming/fmp4Segmenter.js +146 -42
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +24 -19
- package/dist/streaming/hls.js +603 -297
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +16 -3
- package/dist/streaming/hlsResume.js +35 -8
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +41 -2
- package/dist/streaming/hlsSegments.js +131 -28
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +27 -13
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -0
- package/dist/streaming/monitor.js +862 -474
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +52 -6
- package/dist/streaming/mp4Parser.js +156 -30
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +2 -2
- package/dist/streaming/mpegts.js +156 -109
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +38 -0
- package/dist/streaming/playlistBuilder.js +66 -0
- package/dist/streaming/playlistBuilder.js.map +1 -0
- package/dist/streaming/preroll.d.ts +126 -0
- package/dist/streaming/preroll.js +434 -0
- package/dist/streaming/preroll.js.map +1 -0
- package/dist/streaming/pretune.d.ts +8 -0
- package/dist/streaming/pretune.js +226 -0
- package/dist/streaming/pretune.js.map +1 -0
- package/dist/streaming/recovery.d.ts +0 -6
- package/dist/streaming/recovery.js +0 -8
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +57 -9
- package/dist/streaming/registry.js +26 -7
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +8 -1
- package/dist/streaming/setup.js +71 -27
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +29 -4
- package/dist/streaming/showInfo.js +210 -38
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +20 -6
- package/dist/streaming/statusEmitter.js +31 -7
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +32 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +13 -1
- package/dist/types/selection.d.ts +5 -1
- package/dist/types/shared.d.ts +1 -1
- package/dist/types/streaming.d.ts +7 -0
- package/dist/utils/chromeFetch.d.ts +20 -0
- package/dist/utils/chromeFetch.js +43 -0
- package/dist/utils/chromeFetch.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +0 -5
- package/dist/utils/debugFilter.js +20 -11
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +28 -0
- package/dist/utils/delay.js +35 -1
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +6 -11
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +14 -12
- package/dist/utils/ffmpeg.js +114 -134
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +8 -0
- package/dist/utils/format.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/logEmitter.js +1 -1
- package/dist/utils/logEmitter.js.map +1 -1
- package/dist/utils/pid.d.ts +40 -0
- package/dist/utils/pid.js +83 -0
- package/dist/utils/pid.js.map +1 -0
- package/dist/utils/retry.d.ts +15 -9
- package/dist/utils/retry.js +7 -23
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/timing.d.ts +5 -0
- package/dist/utils/timing.js +13 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/version.js +2 -2
- package/dist/utils/version.js.map +1 -1
- package/package.json +7 -7
|
@@ -2,15 +2,31 @@
|
|
|
2
2
|
*
|
|
3
3
|
* table.ts: Channel table rendering for the PrismCast configuration interface.
|
|
4
4
|
*/
|
|
5
|
-
import { compareChannelSort, getAllProviderTags, getChannelProviderLabel, getChannelProviderTags, getChannelSortKey, getEnabledProviders, getProviderGroup,
|
|
5
|
+
import { compareChannelSort, getAllProviderTags, getAuthDomainForChannel, getChannelProviderLabel, getChannelProviderTags, getChannelSortKey, getEnabledProviders, getProviderGroup, hasMultipleProviders, isChannelAvailableByProvider, isProviderTagEnabled, resolvePredefinedVariant, resolveProviderKey } from "../../../config/providers.js";
|
|
6
6
|
import { escapeHtml, formatTimeAgo } from "../../../utils/index.js";
|
|
7
|
-
import { getCachedProviderChannels, getProviderDomainMap, getProviderGuideUrls } from "../../../browser/channelSelection.js";
|
|
8
|
-
import { getChannelHealth,
|
|
9
|
-
import { getChannelListing, getChannelsParseErrorMessage, getPredefinedScopeCounts, getUserChannelsFilePath, hasChannelsParseError, isPredefinedChannel, isPredefinedChannelDisabled, isUserChannel } from "../../../config/userChannels.js";
|
|
7
|
+
import { getCachedProviderChannels, getProviderDomainMap, getProviderGuideUrls, getProviderModuleInfo } from "../../../browser/channelSelection.js";
|
|
8
|
+
import { getChannelHealth, getDomainAuth } from "../../../config/health.js";
|
|
9
|
+
import { getChannelListing, getChannelLogo, getChannelsParseErrorMessage, getPredefinedScopeCounts, getUserChannelsFilePath, hasChannelsParseError, isPredefinedChannel, isPredefinedChannelDisabled, isUserChannel } from "../../../config/userChannels.js";
|
|
10
10
|
import { getProfileForChannel, getProfiles } from "../../../config/profiles.js";
|
|
11
11
|
import { CONFIG } from "../../../config/index.js";
|
|
12
12
|
import { PREDEFINED_CHANNELS } from "../../../channels/index.js";
|
|
13
13
|
import { categorizeProfiles } from "../index.js";
|
|
14
|
+
import { generateWizardModal } from "../../components.js";
|
|
15
|
+
/**
|
|
16
|
+
* Generates an annotated provider display span. The client-side page-load script processes these elements via providerIconHtml, rendering the appropriate
|
|
17
|
+
* icon + text combination. The server just emits the text with data attributes — all icon rendering is client-side through the single providerIconHtml path.
|
|
18
|
+
* @param name - The provider display name.
|
|
19
|
+
* @param domain - The provider's domain for icon fallback derivation. Undefined for providers without a known domain.
|
|
20
|
+
* @param iconUrl - Optional explicit icon URL to try before domain-derived fallbacks.
|
|
21
|
+
* @param small - When true, uses the small icon variant for chips.
|
|
22
|
+
* @returns HTML span string with data attributes for client-side processing.
|
|
23
|
+
*/
|
|
24
|
+
function providerDisplaySpan(name, domain, iconUrl, small) {
|
|
25
|
+
const domainAttr = domain ? " data-domain=\"" + escapeHtml(domain) + "\"" : "";
|
|
26
|
+
const iconAttr = iconUrl ? " data-icon-url=\"" + escapeHtml(iconUrl) + "\"" : "";
|
|
27
|
+
const sizeAttr = small ? " data-sm" : "";
|
|
28
|
+
return "<span class=\"provider-display\"" + domainAttr + iconAttr + sizeAttr + ">" + escapeHtml(name) + "</span>";
|
|
29
|
+
}
|
|
14
30
|
// SVG icon constants for channel action buttons. Each icon is 14x14px with a 16x16 viewBox, stroke-based with currentColor, and uses round line caps/joins.
|
|
15
31
|
export const ICON_EDIT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" " +
|
|
16
32
|
"stroke-linejoin=\"round\"><path d=\"M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z\"/></svg>";
|
|
@@ -33,12 +49,13 @@ const ICON_COPY = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"
|
|
|
33
49
|
// column order in the table, slotted between Provider and Actions.
|
|
34
50
|
export const OPTIONAL_COLUMNS = [
|
|
35
51
|
{ align: "center", cssClass: "col-chnum", field: "channelNumber", label: "Number", width: "70px" },
|
|
52
|
+
{ align: "center", cssClass: "col-hdhr", field: "hdhrEnabled", label: "HDHR", width: "55px" },
|
|
36
53
|
{ align: "center", cssClass: "col-stationid", field: "stationId", label: "Station ID", width: "100px" },
|
|
37
54
|
{ align: "left", cssClass: "col-profile", field: "profile", label: "Profile", width: "130px" },
|
|
38
55
|
{ align: "left", cssClass: "col-selector", field: "channelSelector", label: "Selector", width: "130px" }
|
|
39
56
|
];
|
|
40
|
-
// Total number of columns in the channels table (4 required +
|
|
41
|
-
const TOTAL_COLUMN_COUNT =
|
|
57
|
+
// Total number of columns in the channels table (4 required + 5 optional).
|
|
58
|
+
const TOTAL_COLUMN_COUNT = 9;
|
|
42
59
|
// Valid optional column field names.
|
|
43
60
|
export const VALID_OPTIONAL_COLUMNS = new Set(OPTIONAL_COLUMNS.map((c) => c.field));
|
|
44
61
|
/**
|
|
@@ -259,16 +276,8 @@ function generateProfileReference(profiles) {
|
|
|
259
276
|
lines.push("</div>");
|
|
260
277
|
return lines.join("\n");
|
|
261
278
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
* @param idPrefix - Prefix for element IDs ("add" or "edit").
|
|
265
|
-
* @param stationIdValue - Current station ID value.
|
|
266
|
-
* @param channelSelectorValue - Current channel selector value.
|
|
267
|
-
* @param channelNumberValue - Current channel number value.
|
|
268
|
-
* @param showHints - Whether to show hint text.
|
|
269
|
-
* @returns Array of HTML strings for the advanced fields section.
|
|
270
|
-
*/
|
|
271
|
-
function generateAdvancedFields(idPrefix, stationIdValue, channelSelectorValue, channelNumberValue, showHints = true) {
|
|
279
|
+
function generateAdvancedFields(idPrefix, options = {}) {
|
|
280
|
+
const { channelNumberValue = "", channelSelectorValue = "", hdhrEnabled = true, showHints = true, stationIdValue = "" } = options;
|
|
272
281
|
const lines = [];
|
|
273
282
|
// Advanced fields toggle.
|
|
274
283
|
lines.push("<div class=\"advanced-toggle\" onclick=\"document.getElementById('" + idPrefix +
|
|
@@ -278,10 +287,11 @@ function generateAdvancedFields(idPrefix, stationIdValue, channelSelectorValue,
|
|
|
278
287
|
// Station ID.
|
|
279
288
|
const stationIdHint = showHints ? "Optional Gracenote station ID for guide data (tvc-guide-stationid)." : undefined;
|
|
280
289
|
lines.push(...generateTextField(idPrefix + "-stationId", "stationId", "Station ID", stationIdValue, { hint: stationIdHint, placeholder: showHints ? "e.g., 12345" : undefined }));
|
|
281
|
-
// Channel selector.
|
|
290
|
+
// Channel selector. The guide-based provider list is derived from the provider module registry so it stays current as providers are added.
|
|
291
|
+
const guideProviderNames = getProviderModuleInfo().map((p) => p.label).sort().join(", ");
|
|
282
292
|
const channelSelectorHint = showHints ?
|
|
283
293
|
"Identifies which channel to select on sites that host multiple live streams. Known values are suggested when the URL matches a supported site. " +
|
|
284
|
-
"For guide-based profiles (
|
|
294
|
+
"For guide-based profiles (" + guideProviderNames + "), use the channel name or station code from the guide. " +
|
|
285
295
|
"For tile and thumbnail profiles, right-click the channel element \u2192 Inspect \u2192 copy a unique value matching the profile's selector pattern " +
|
|
286
296
|
"(typically a portion of the image src URL)." :
|
|
287
297
|
undefined;
|
|
@@ -291,6 +301,18 @@ function generateAdvancedFields(idPrefix, stationIdValue, channelSelectorValue,
|
|
|
291
301
|
"Optional numeric channel number for guide matching in Channels DVR and Plex." :
|
|
292
302
|
undefined;
|
|
293
303
|
lines.push(...generateTextField(idPrefix + "-channelNumber", "channelNumber", "Channel Number", channelNumberValue, { hint: channelNumberHint, placeholder: showHints ? "e.g., 501" : undefined }));
|
|
304
|
+
// HDHomeRun lineup inclusion. A hidden input provides the "false" value when the checkbox is unchecked (unchecked checkboxes are not submitted in FormData).
|
|
305
|
+
// When checked, the checkbox value "true" overwrites the hidden input's "false" since it appears later in DOM order.
|
|
306
|
+
const hdhrChecked = hdhrEnabled ? " checked" : "";
|
|
307
|
+
const hdhrHint = showHints ? "When unchecked, this channel is hidden from the HDHomeRun lineup and not available in Plex." : undefined;
|
|
308
|
+
lines.push("<div class=\"form-row form-row-checkbox\">");
|
|
309
|
+
lines.push("<label for=\"" + idPrefix + "-hdhrEnabled\">Include in HDHomeRun/Plex Lineup</label>");
|
|
310
|
+
lines.push("<input type=\"hidden\" name=\"hdhrEnabled\" value=\"false\">");
|
|
311
|
+
lines.push("<input type=\"checkbox\" id=\"" + idPrefix + "-hdhrEnabled\" name=\"hdhrEnabled\" value=\"true\"" + hdhrChecked + ">");
|
|
312
|
+
lines.push("</div>");
|
|
313
|
+
if (hdhrHint) {
|
|
314
|
+
lines.push("<div class=\"hint\">" + hdhrHint + "</div>");
|
|
315
|
+
}
|
|
294
316
|
lines.push("</div>"); // End advanced fields.
|
|
295
317
|
return lines;
|
|
296
318
|
}
|
|
@@ -305,18 +327,22 @@ function generateAdvancedFields(idPrefix, stationIdValue, channelSelectorValue,
|
|
|
305
327
|
function generateChannelSelectorData() {
|
|
306
328
|
const byDomain = {};
|
|
307
329
|
const seen = {};
|
|
308
|
-
for (const
|
|
309
|
-
if (!
|
|
330
|
+
for (const [key, raw] of Object.entries(PREDEFINED_CHANNELS)) {
|
|
331
|
+
if (!raw.channelSelector) {
|
|
310
332
|
continue;
|
|
311
333
|
}
|
|
334
|
+
// Use the resolved channel (with inheritance from the canonical) so stationId, name, and other inherited fields are populated correctly for variants.
|
|
335
|
+
// The channelSelector is captured from the raw entry (already validated as non-null) to avoid non-null assertions on the resolved channel's optional field.
|
|
336
|
+
const selector = raw.channelSelector;
|
|
337
|
+
const channel = resolvePredefinedVariant(key) ?? raw;
|
|
312
338
|
const hostname = new URL(channel.url).hostname;
|
|
313
339
|
seen[hostname] ??= new Set();
|
|
314
|
-
if (seen[hostname].has(
|
|
340
|
+
if (seen[hostname].has(selector)) {
|
|
315
341
|
continue;
|
|
316
342
|
}
|
|
317
|
-
seen[hostname].add(
|
|
343
|
+
seen[hostname].add(selector);
|
|
318
344
|
byDomain[hostname] ??= [];
|
|
319
|
-
byDomain[hostname].push({ label: channel.name ?? channel.
|
|
345
|
+
byDomain[hostname].push({ label: channel.name ?? selector, stationId: channel.stationId, value: selector });
|
|
320
346
|
}
|
|
321
347
|
// Merge cached provider-discovered channels into the domain map. Predefined entries take precedence — we only add discovered channels whose channelSelector
|
|
322
348
|
// value is not already present for that domain. This enriches the datalist with the full provider lineup when precaching or prior discovery has run.
|
|
@@ -342,6 +368,67 @@ function generateChannelSelectorData() {
|
|
|
342
368
|
"var providerByDomain = " + JSON.stringify(providerByDomain) + ";\n" +
|
|
343
369
|
"var providerGuideUrl = " + JSON.stringify(providerGuideUrl) + ";";
|
|
344
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Generates the Browse Channels 2-step wizard modal. Step 1 is the provider picker grid, step 2 is channel discovery and management. Channel state detection
|
|
373
|
+
* (new, switch, current) is handled server-side by the annotated discovery response - no client-side matching logic is needed. Provider and guide URL data are
|
|
374
|
+
* embedded as JSON data blocks for the client-side wizard controller.
|
|
375
|
+
* @returns HTML string for the browse modal.
|
|
376
|
+
*/
|
|
377
|
+
function generateBrowseModal() {
|
|
378
|
+
// Build the provider info array from the provider module registry. The client uses this to render the provider picker.
|
|
379
|
+
const providers = getProviderModuleInfo();
|
|
380
|
+
const guideUrls = getProviderGuideUrls();
|
|
381
|
+
return generateWizardModal({
|
|
382
|
+
buttons: [
|
|
383
|
+
{ id: "browse-back", label: "Back", position: "left", role: "back", visible: false },
|
|
384
|
+
{ label: "Cancel", position: "right", role: "close" },
|
|
385
|
+
{ id: "browse-add-btn", label: "Apply", onclick: "submitBrowseChannels()", position: "right", variant: "primary", visible: false }
|
|
386
|
+
],
|
|
387
|
+
contentId: "browse-content",
|
|
388
|
+
dataBlocks: [
|
|
389
|
+
"<script type=\"application/json\" id=\"browse-providers-data\">" + JSON.stringify(providers) + "</script>",
|
|
390
|
+
"<script type=\"application/json\" id=\"browse-guide-urls-data\">" + JSON.stringify(guideUrls) + "</script>"
|
|
391
|
+
],
|
|
392
|
+
description: "Manage your channel lineup by provider. Add new channels, switch providers for existing channels, or remove channels you no longer need.",
|
|
393
|
+
id: "browse-modal",
|
|
394
|
+
steps: ["Provider", "Channels"],
|
|
395
|
+
title: "Browse Channels",
|
|
396
|
+
titleId: "browse-title"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Generates the Provider Setup 3-step wizard modal. Steps: Providers (multi-select), Sign In (sequential auth), Channels (summary + finish). The
|
|
401
|
+
* setupCompleted flag is embedded as a data attribute so the client can auto-show the wizard on first visit.
|
|
402
|
+
* @returns HTML string for the setup wizard modal.
|
|
403
|
+
*/
|
|
404
|
+
function generateSetupWizardModal() {
|
|
405
|
+
const enabled = getEnabledProviders();
|
|
406
|
+
// Build provider data with enabled state from the current provider filter. getAllProviderTags is the single source of truth for provider metadata
|
|
407
|
+
// (domain, iconUrl, displayName). The enabled field lets the client pre-check providers on re-run without scraping DOM state.
|
|
408
|
+
const tags = getAllProviderTags()
|
|
409
|
+
.filter((t) => t.tag !== "direct")
|
|
410
|
+
.map((t) => ({ ...t, enabled: enabled.includes(t.tag) }));
|
|
411
|
+
return generateWizardModal({
|
|
412
|
+
buttons: [
|
|
413
|
+
{ id: "setup-back", label: "Back", position: "left", role: "back", visible: false },
|
|
414
|
+
{ id: "setup-skip", label: "Skip Setup", onclick: "skipSetup()", position: "right" },
|
|
415
|
+
{ id: "setup-next", label: "Next", position: "right", role: "next", variant: "primary" },
|
|
416
|
+
{ id: "setup-finish", label: "Finish", onclick: "finishSetup()", position: "right", variant: "primary", visible: false }
|
|
417
|
+
],
|
|
418
|
+
contentId: "setup-content",
|
|
419
|
+
dataAttributes: { "setup-completed": CONFIG.channels.setupCompleted ? "true" : "false" },
|
|
420
|
+
dataBlocks: [
|
|
421
|
+
"<script type=\"application/json\" id=\"setup-providers-data\">" + JSON.stringify(tags) + "</script>"
|
|
422
|
+
],
|
|
423
|
+
description: "Set up your streaming providers in three steps: select your services, sign in, and choose your channels.",
|
|
424
|
+
errorId: "setup-error",
|
|
425
|
+
id: "setup-modal",
|
|
426
|
+
steps: ["Providers", "Sign In", "Channels"],
|
|
427
|
+
stepsId: "setup-steps",
|
|
428
|
+
title: "Provider Setup",
|
|
429
|
+
titleId: "setup-title"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
345
432
|
/**
|
|
346
433
|
* Generates the HTML for a single channel's table rows (display row and edit form row). All channels — predefined, override, and user-defined — get both rows.
|
|
347
434
|
* The edit form is pre-populated with the effective (resolved) values so users see what they're changing. When called from generateChannelsPanel() which already
|
|
@@ -360,10 +447,6 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
360
447
|
return { displayRow: "", editRow: "" };
|
|
361
448
|
}
|
|
362
449
|
const channel = listing.channel;
|
|
363
|
-
// Resolve the selected provider's channel data for display purposes (edit form pre-population). This ensures the values shown reflect the currently selected provider.
|
|
364
|
-
const resolvedKey = resolveProviderKey(key);
|
|
365
|
-
const resolvedChannel = getResolvedChannel(resolvedKey);
|
|
366
|
-
const displayChannel = resolvedChannel ?? channel;
|
|
367
450
|
const isUser = isUserChannel(key);
|
|
368
451
|
const isPredefined = isPredefinedChannel(key);
|
|
369
452
|
const isOverride = isPredefined && isUser;
|
|
@@ -388,7 +471,10 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
388
471
|
const rowClassAttr = (rowClasses.length > 0) ? " class=\"" + rowClasses.join(" ") + "\"" : "";
|
|
389
472
|
displayLines.push("<tr id=\"display-row-" + escapeHtml(key) + "\"" + rowClassAttr + " data-provider-tags=\"" + escapeHtml(providerTags) + "\">");
|
|
390
473
|
displayLines.push("<td class=\"ch-key\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "key")) + "\">" + escapeHtml(key) + "</td>");
|
|
391
|
-
|
|
474
|
+
const channelLogoUrl = getChannelLogo(key) ?? "";
|
|
475
|
+
const logoAttr = channelLogoUrl ? " data-logo=\"" + escapeHtml(channelLogoUrl) + "\"" : "";
|
|
476
|
+
displayLines.push("<td data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "name")) + "\"" + logoAttr +
|
|
477
|
+
"><span class=\"channel-name-cell\">" + escapeHtml(channel.name ?? key) + "</span></td>");
|
|
392
478
|
// Provider column: dropdown for multi-provider channels, static provider name for single-provider. Both states always render a hidden "No available providers"
|
|
393
479
|
// label alongside the provider content so that client-side filterChannelRows() can toggle between them without a page reload.
|
|
394
480
|
displayLines.push("<td data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "provider")) + "\">");
|
|
@@ -399,14 +485,13 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
399
485
|
// Multi-provider: render ALL variants with data-provider-tag attributes so client-side JS can filter options when the provider selection changes. Filtered-out
|
|
400
486
|
// options get the hidden attribute for immediate filtering in Chrome. Safari ignores hidden on option elements, so the page-load JS init calls filterChannelRows()
|
|
401
487
|
// to remove them from the DOM.
|
|
402
|
-
const currentSelection =
|
|
403
|
-
displayLines.push("<select class=\"provider-select\" data-channel=\"" + escapeHtml(key) +
|
|
404
|
-
contentHidden + ">");
|
|
488
|
+
const currentSelection = resolveProviderKey(key);
|
|
489
|
+
displayLines.push("<select class=\"provider-select\" data-channel=\"" + escapeHtml(key) +
|
|
490
|
+
"\" title=\"Choose which streaming provider delivers this channel\" onchange=\"updateProviderSelection(this)\"" + contentHidden + ">");
|
|
405
491
|
for (const variant of providerGroup.variants) {
|
|
406
492
|
const selected = (variant.key === currentSelection) ? " selected" : "";
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
displayLines.push("<option value=\"" + escapeHtml(variant.key) + "\" data-provider-tag=\"" + escapeHtml(tag) + "\"" + selected + optionHidden + ">" +
|
|
493
|
+
const optionHidden = !isProviderTagEnabled(variant.tag) ? " hidden" : "";
|
|
494
|
+
displayLines.push("<option value=\"" + escapeHtml(variant.key) + "\" data-provider-tag=\"" + escapeHtml(variant.tag) + "\"" + selected + optionHidden + ">" +
|
|
410
495
|
escapeHtml(variant.label) + "</option>");
|
|
411
496
|
}
|
|
412
497
|
displayLines.push("</select>");
|
|
@@ -418,22 +503,29 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
418
503
|
escapeHtml(getChannelProviderLabel(channel)) + "</span>");
|
|
419
504
|
}
|
|
420
505
|
displayLines.push("</td>");
|
|
421
|
-
// Optional columns: Number, Station ID, Profile, Selector. All
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
(channel.
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
506
|
+
// Optional columns: Number, HDHR, Station ID, Profile, Selector. All five are always rendered; visibility is controlled by CSS classes on the table element.
|
|
507
|
+
const cellKey = escapeHtml(key);
|
|
508
|
+
displayLines.push("<td class=\"col-chnum editable-cell\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "channelNumber")) +
|
|
509
|
+
"\" data-field=\"channelNumber\" data-key=\"" + cellKey + "\" data-value=\"" + (channel.channelNumber ? escapeHtml(String(channel.channelNumber)) : "") +
|
|
510
|
+
"\" onclick=\"startInlineEdit(this)\">" + (channel.channelNumber ? escapeHtml(String(channel.channelNumber)) : "<span class=\"text-muted\">–</span>") +
|
|
511
|
+
"</td>");
|
|
512
|
+
// HDHR column: inline checkbox for quick toggling. The checkbox submits changes via the toggleHdhr() client-side function.
|
|
513
|
+
const hdhrCheckedAttr = (channel.hdhrEnabled !== false) ? " checked" : "";
|
|
514
|
+
displayLines.push("<td class=\"col-hdhr\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "hdhrEnabled")) +
|
|
515
|
+
"\"><input type=\"checkbox\" data-key=\"" + cellKey + "\"" + hdhrCheckedAttr +
|
|
516
|
+
" onchange=\"toggleHdhr(this)\" title=\"Include in HDHomeRun/Plex lineup\"></td>");
|
|
517
|
+
displayLines.push("<td class=\"col-stationid editable-cell\" data-sort-value=\"" + escapeHtml(getChannelSortKey(channel, key, "stationId")) +
|
|
518
|
+
"\" data-field=\"stationId\" data-key=\"" + cellKey + "\" data-value=\"" + (channel.stationId ? escapeHtml(channel.stationId) : "") +
|
|
519
|
+
"\" onclick=\"startInlineEdit(this)\">" + (channel.stationId ? escapeHtml(channel.stationId) : "<span class=\"text-muted\">–</span>") + "</td>");
|
|
520
|
+
// Profile column: show explicit profile as-is, or the auto-resolved friendly name with "(auto)" suffix in muted style.
|
|
429
521
|
const profileSortKey = escapeHtml(getChannelSortKey(channel, key, "profile"));
|
|
430
|
-
if (
|
|
431
|
-
displayLines.push("<td class=\"col-profile\" data-sort-value=\"" + profileSortKey + "\">" + escapeHtml(
|
|
522
|
+
if (channel.profile) {
|
|
523
|
+
displayLines.push("<td class=\"col-profile\" data-sort-value=\"" + profileSortKey + "\">" + escapeHtml(channel.profile) + "</td>");
|
|
432
524
|
}
|
|
433
525
|
else {
|
|
434
|
-
const resolved = getProfileForChannel(
|
|
526
|
+
const resolved = getProfileForChannel(channel);
|
|
435
527
|
if (resolved.profileName !== "default") {
|
|
436
|
-
const label = getChannelProviderLabel(
|
|
528
|
+
const label = getChannelProviderLabel(channel);
|
|
437
529
|
displayLines.push("<td class=\"col-profile\" data-sort-value=\"" + profileSortKey +
|
|
438
530
|
"\"><span class=\"text-muted\">" + escapeHtml(label + " (auto)") + "</span></td>");
|
|
439
531
|
}
|
|
@@ -447,20 +539,20 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
447
539
|
displayLines.push("<td>");
|
|
448
540
|
displayLines.push("<div class=\"btn-group\">");
|
|
449
541
|
const escapedKey = escapeHtml(key);
|
|
450
|
-
// Resolve the
|
|
542
|
+
// Resolve the auth domain for the currently selected provider variant. Used for both login icon color and health icon lookups.
|
|
451
543
|
const variantKey = resolveProviderKey(key);
|
|
452
|
-
const
|
|
544
|
+
const authDomain = getAuthDomainForChannel(variantKey);
|
|
453
545
|
// Position 1: Edit (all channels).
|
|
454
546
|
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-edit\" title=\"Edit\" aria-label=\"Edit\" onclick=\"showEditForm('" + escapedKey +
|
|
455
547
|
"')\">" + ICON_EDIT + "</button>");
|
|
456
|
-
// Position 2: Login for enabled channels (with
|
|
548
|
+
// Position 2: Login for enabled channels (with domain auth color), placeholder for disabled predefined. Custom channels (user-defined, not predefined) skip
|
|
457
549
|
// login coloring because they have no provider concept.
|
|
458
550
|
if (!isDisabled) {
|
|
459
|
-
const authTimestamp = (isPredefined || isOverride) ?
|
|
551
|
+
const authTimestamp = (isPredefined || isOverride) ? getDomainAuth(authDomain) : null;
|
|
460
552
|
const loginColorClass = authTimestamp ? " health-success" : "";
|
|
461
553
|
const loginTitle = authTimestamp ? "Verified " + formatTimeAgo(authTimestamp) : "Not yet verified";
|
|
462
|
-
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-login" + loginColorClass + "\" data-
|
|
463
|
-
escapeHtml(
|
|
554
|
+
displayLines.push("<button type=\"button\" class=\"btn-icon btn-icon-login" + loginColorClass + "\" data-auth-domain=\"" +
|
|
555
|
+
escapeHtml(authDomain) + "\" title=\"" + loginTitle + "\" aria-label=\"Login\" " +
|
|
464
556
|
"onclick=\"startChannelLogin('" + escapedKey + "')\">" + ICON_LOGIN + "</button>");
|
|
465
557
|
}
|
|
466
558
|
else {
|
|
@@ -468,7 +560,7 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
468
560
|
}
|
|
469
561
|
// Position 3: Channel health indicator. Shows last tune result via color. Non-interactive (span, not button). Disabled channels get a placeholder instead.
|
|
470
562
|
if (!isDisabled) {
|
|
471
|
-
const channelHealthResult = getChannelHealth(key,
|
|
563
|
+
const channelHealthResult = getChannelHealth(key, authDomain);
|
|
472
564
|
const healthColorClass = (channelHealthResult?.status === "success") ? " health-success" : (channelHealthResult?.status === "failed") ? " health-failed" : "";
|
|
473
565
|
const healthTitle = channelHealthResult ?
|
|
474
566
|
(channelHealthResult.status === "success" ? "Succeeded " : "Failed ") + formatTimeAgo(channelHealthResult.timestamp) : "Not yet tuned";
|
|
@@ -524,20 +616,25 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
524
616
|
editLines.push("<input type=\"hidden\" name=\"action\" value=\"edit\">");
|
|
525
617
|
editLines.push("<input type=\"hidden\" name=\"key\" value=\"" + escapedKey + "\">");
|
|
526
618
|
// Channel name.
|
|
527
|
-
editLines.push(...generateTextField("edit-name-" + key, "name", "Display Name",
|
|
619
|
+
editLines.push(...generateTextField("edit-name-" + key, "name", "Display Name", channel.name ?? key, {
|
|
528
620
|
hint: "Friendly name shown in the playlist and UI.",
|
|
529
621
|
required: true
|
|
530
622
|
}));
|
|
531
623
|
// Channel URL.
|
|
532
|
-
editLines.push(...generateTextField("edit-url-" + key, "url", "Stream URL",
|
|
624
|
+
editLines.push(...generateTextField("edit-url-" + key, "url", "Stream URL", channel.url, {
|
|
533
625
|
hint: "The URL of the streaming page to capture.",
|
|
534
626
|
required: true,
|
|
535
627
|
type: "url"
|
|
536
628
|
}));
|
|
537
629
|
// Profile dropdown.
|
|
538
|
-
editLines.push(...generateProfileDropdown("edit-profile-" + key,
|
|
630
|
+
editLines.push(...generateProfileDropdown("edit-profile-" + key, channel.profile ?? "", profiles));
|
|
539
631
|
// Advanced fields.
|
|
540
|
-
editLines.push(...generateAdvancedFields("edit-" + key,
|
|
632
|
+
editLines.push(...generateAdvancedFields("edit-" + key, {
|
|
633
|
+
channelNumberValue: channel.channelNumber ? String(channel.channelNumber) : "",
|
|
634
|
+
channelSelectorValue: channel.channelSelector ?? "",
|
|
635
|
+
hdhrEnabled: channel.hdhrEnabled !== false,
|
|
636
|
+
stationIdValue: channel.stationId ?? ""
|
|
637
|
+
}));
|
|
541
638
|
// Form buttons.
|
|
542
639
|
editLines.push("<div class=\"form-buttons\">");
|
|
543
640
|
editLines.push("<button type=\"submit\" class=\"btn btn-primary\">Save Changes</button>");
|
|
@@ -550,6 +647,80 @@ export function generateChannelRowHtml(key, profiles, entry) {
|
|
|
550
647
|
const editRow = editLines.join("\n");
|
|
551
648
|
return { displayRow, editRow };
|
|
552
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Computes the complete channel table state — summary counts and scope toggle counts — from the current channel listing. This is the server-side single source
|
|
652
|
+
* of truth for all count computation, replacing the client-side DOM-scanning approach. Called once per mutation as part of building a patch.
|
|
653
|
+
* @param listing - Optional pre-fetched listing. When omitted, fetches from getChannelListing(). Passing the listing avoids a redundant computation when
|
|
654
|
+
* buildChannelTablePatch already has it.
|
|
655
|
+
* @returns The channel table counts and scope toggle counts.
|
|
656
|
+
*/
|
|
657
|
+
export function buildChannelTableState(listing) {
|
|
658
|
+
listing ??= getChannelListing();
|
|
659
|
+
let predefined = 0;
|
|
660
|
+
let user = 0;
|
|
661
|
+
let disabled = 0;
|
|
662
|
+
for (const entry of listing) {
|
|
663
|
+
if ((entry.source === "user") || (entry.source === "override")) {
|
|
664
|
+
user++;
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
predefined++;
|
|
668
|
+
}
|
|
669
|
+
if (!entry.enabled || !entry.availableByProvider) {
|
|
670
|
+
disabled++;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const total = listing.length;
|
|
674
|
+
const enabled = total - disabled;
|
|
675
|
+
return {
|
|
676
|
+
counts: { disabled, enabled, predefined, total, user },
|
|
677
|
+
scopeCounts: getPredefinedScopeCounts()
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Counts channels included and excluded from the HDHomeRun lineup. Only counts enabled, provider-available channels (the same set visible in the table).
|
|
682
|
+
* @param listing - The channel listing entries.
|
|
683
|
+
* @returns An object with `enabled` (HDHR-included) and `total` counts.
|
|
684
|
+
*/
|
|
685
|
+
function getHdhrCounts(listing) {
|
|
686
|
+
let enabled = 0;
|
|
687
|
+
let total = 0;
|
|
688
|
+
for (const entry of listing) {
|
|
689
|
+
if (!entry.enabled || !entry.availableByProvider) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
total++;
|
|
693
|
+
if (entry.channel.hdhrEnabled !== false) {
|
|
694
|
+
enabled++;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return { enabled, total };
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Builds a complete channel table patch for the given set of affected channel keys. Generates row HTML for keys that exist in the listing (update action) and
|
|
701
|
+
* remove actions for keys that no longer exist. Includes the current summary counts and scope toggle counts. This is the single function mutation endpoints
|
|
702
|
+
* call to construct their patch response.
|
|
703
|
+
* @param affectedKeys - The channel keys that were added, modified, or removed by the mutation.
|
|
704
|
+
* @param profiles - Available profiles for the edit form dropdown.
|
|
705
|
+
* @returns A complete ChannelTablePatch ready for client-side application.
|
|
706
|
+
*/
|
|
707
|
+
export function buildChannelTablePatch(affectedKeys, profiles) {
|
|
708
|
+
const listing = getChannelListing();
|
|
709
|
+
const listingByKey = new Map(listing.map((entry) => [entry.key, entry]));
|
|
710
|
+
const rows = [];
|
|
711
|
+
for (const key of affectedKeys) {
|
|
712
|
+
const entry = listingByKey.get(key);
|
|
713
|
+
if (entry) {
|
|
714
|
+
const rowHtml = generateChannelRowHtml(key, profiles, entry);
|
|
715
|
+
rows.push({ action: "update", displayRow: rowHtml.displayRow, editRow: rowHtml.editRow, key });
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
rows.push({ action: "remove", key });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const { counts, scopeCounts } = buildChannelTableState(listing);
|
|
722
|
+
return { counts, hdhrCounts: getHdhrCounts(listing), rows, scopeCounts };
|
|
723
|
+
}
|
|
553
724
|
/**
|
|
554
725
|
* Generates the provider filter toolbar HTML with a multi-select dropdown and dismissable chips.
|
|
555
726
|
* @returns HTML string for the provider filter toolbar.
|
|
@@ -565,7 +736,8 @@ export function generateProviderFilterToolbar() {
|
|
|
565
736
|
lines.push("<span class=\"toolbar-label\">Providers:</span>");
|
|
566
737
|
lines.push("<div class=\"dropdown provider-dropdown\">");
|
|
567
738
|
const buttonText = hasFilter ? "Filtered" : "All Providers";
|
|
568
|
-
lines.push("<button type=\"button\" class=\"btn btn-sm\" id=\"provider-filter-btn\"
|
|
739
|
+
lines.push("<button type=\"button\" class=\"btn btn-sm\" id=\"provider-filter-btn\" title=\"Filter channels by streaming provider\" " +
|
|
740
|
+
"onclick=\"toggleDropdown(this)\">" + buttonText + " ▾</button>");
|
|
569
741
|
lines.push("<div class=\"dropdown-menu provider-dropdown-menu\">");
|
|
570
742
|
for (const tagInfo of allTags) {
|
|
571
743
|
const isDirectTag = tagInfo.tag === "direct";
|
|
@@ -574,7 +746,7 @@ export function generateProviderFilterToolbar() {
|
|
|
574
746
|
const disabledAttr = isDirectTag ? " disabled" : "";
|
|
575
747
|
lines.push("<label class=\"provider-option\">");
|
|
576
748
|
lines.push("<input type=\"checkbox\" data-tag=\"" + escapeHtml(tagInfo.tag) + "\"" + checkedAttr + disabledAttr +
|
|
577
|
-
" onchange=\"toggleProviderTag(this)\"> " +
|
|
749
|
+
" onchange=\"toggleProviderTag(this)\"> " + providerDisplaySpan(tagInfo.displayName, tagInfo.domain, tagInfo.iconUrl));
|
|
578
750
|
lines.push("</label>");
|
|
579
751
|
}
|
|
580
752
|
lines.push("</div>");
|
|
@@ -586,10 +758,12 @@ export function generateProviderFilterToolbar() {
|
|
|
586
758
|
if (tag === "direct") {
|
|
587
759
|
continue;
|
|
588
760
|
}
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
761
|
+
const chipTag = allTags.find((t) => t.tag === tag);
|
|
762
|
+
const displayName = chipTag?.displayName ?? tag;
|
|
763
|
+
lines.push("<span class=\"provider-chip\" data-tag=\"" + escapeHtml(tag) + "\">" +
|
|
764
|
+
providerDisplaySpan(displayName, chipTag?.domain, chipTag?.iconUrl, true) +
|
|
765
|
+
"<button type=\"button\" class=\"chip-close\" title=\"Remove " + escapeHtml(displayName) + " from filter\" aria-label=\"Remove " +
|
|
766
|
+
escapeHtml(displayName) + "\" onclick=\"removeProviderChip('" + escapeHtml(tag) + "')\">×</button></span>");
|
|
593
767
|
}
|
|
594
768
|
}
|
|
595
769
|
lines.push("</div>");
|
|
@@ -620,29 +794,72 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
620
794
|
lines.push("<p>Define and manage streaming channels for the playlist. Customized channels are highlighted.</p>");
|
|
621
795
|
lines.push("<p class=\"description-hint\">Tip: Use the <strong>provider filter</strong> above to show only channels from services you subscribe to — ", "this also controls which channels Channels DVR sees in the playlist. Use the <strong>provider dropdown</strong> on any multi-provider channel to choose ", "which streaming service delivers it (e.g., Comedy Central via Hulu vs Sling). Click the <strong>edit icon</strong> to customize any channel's name, ", "Gracenote station ID, URL, or other properties.</p>");
|
|
622
796
|
lines.push("</div>");
|
|
623
|
-
// Toolbar with
|
|
797
|
+
// Toolbar with three dropdown menus: Manage Channels (primary actions), Import/Export (data I/O), and Quick Actions (bulk operations). Each dropdown has an
|
|
798
|
+
// inline SVG icon + label + chevron for visual discoverability. Grouped menus reduce visual clutter and separate channel management from data I/O and bulk
|
|
799
|
+
// operations.
|
|
800
|
+
// Toolbar-level SVG icons (14x14, stroke-based, currentColor). Defined here since they're toolbar-specific and not reused elsewhere.
|
|
801
|
+
const ICON_MANAGE = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" " +
|
|
802
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"1.5\" y=\"5\" width=\"13\" height=\"9.5\" rx=\"1.5\"/>" +
|
|
803
|
+
"<path d=\"M7 4.5L3.5 1\"/><path d=\"M9 4.5L12.5 1\"/></svg>";
|
|
804
|
+
const ICON_TRANSFER = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" " +
|
|
805
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 5h10M10 2l3 3-3 3\"/><path d=\"M13 11H3M6 8l-3 3 3 3\"/></svg>";
|
|
806
|
+
const ICON_BOLT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" " +
|
|
807
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 1.5L4 9h4l-1 5.5L12 7H8l1-5.5z\"/></svg>";
|
|
624
808
|
lines.push("<div class=\"channel-toolbar\">");
|
|
625
|
-
// Channel operations. Import uses a dropdown menu to consolidate M3U and JSON import into a single button.
|
|
626
809
|
lines.push("<div class=\"toolbar-group\">");
|
|
627
|
-
|
|
810
|
+
// Manage Channels dropdown — primary channel creation and setup actions.
|
|
628
811
|
lines.push("<div class=\"dropdown\">");
|
|
629
|
-
lines.push("<button type=\"button\" class=\"btn btn-
|
|
812
|
+
lines.push("<button type=\"button\" class=\"btn btn-primary btn-sm toolbar-dropdown-btn\" title=\"Add, browse, or set up channels\" " +
|
|
813
|
+
"onclick=\"toggleDropdown(this)\">" + ICON_MANAGE + " Manage Channels ▾</button>");
|
|
630
814
|
lines.push("<div class=\"dropdown-menu\">");
|
|
631
|
-
|
|
815
|
+
// Dropdown item icons (14x14, stroke-based, tinted for visual context). Each icon uses a distinct color to differentiate menu items at a glance.
|
|
816
|
+
const ICON_ADD = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#22a563\" stroke-width=\"1.5\" " +
|
|
817
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3v10M3 8h10\"/></svg>";
|
|
818
|
+
const ICON_BROWSE = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#5b8def\" stroke-width=\"1.5\" " +
|
|
819
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"5\" " +
|
|
820
|
+
"height=\"5\" rx=\"1\"/><rect x=\"2\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/></svg>";
|
|
821
|
+
const ICON_SETUP = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#9b8cd8\" stroke-width=\"1.5\" " +
|
|
822
|
+
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"8\" cy=\"8\" r=\"2.5\"/><path d=\"M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.1 " +
|
|
823
|
+
"3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4\"/></svg>";
|
|
824
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); " +
|
|
825
|
+
"document.getElementById('add-channel-form').style.display='block';\">" + ICON_ADD + " Add Channel</div>");
|
|
826
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); openBrowseModal()\">" + ICON_BROWSE +
|
|
827
|
+
" Browse Provider Channels</div>");
|
|
828
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); openSetupWizard()\">" + ICON_SETUP +
|
|
829
|
+
" Provider Setup</div>");
|
|
830
|
+
lines.push("</div>");
|
|
831
|
+
lines.push("</div>");
|
|
832
|
+
// Import / Export dropdown — data I/O operations.
|
|
833
|
+
lines.push("<div class=\"dropdown\">");
|
|
834
|
+
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-dropdown-btn\" title=\"Import or export channel data\" " +
|
|
835
|
+
"onclick=\"toggleDropdown(this)\">" + ICON_TRANSFER + " Import / Export ▾</button>");
|
|
836
|
+
lines.push("<div class=\"dropdown-menu\">");
|
|
837
|
+
const ICON_IMPORT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#5b8def\" stroke-width=\"1.5\" " +
|
|
838
|
+
"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>";
|
|
839
|
+
const ICON_EXPORT = "<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"#22a563\" stroke-width=\"1.5\" " +
|
|
840
|
+
"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>";
|
|
841
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); document.getElementById('import-channels-file').click()\">" +
|
|
842
|
+
ICON_IMPORT + " Import Channels (JSON)</div>");
|
|
843
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); document.getElementById('import-m3u-file').click()\">" +
|
|
844
|
+
ICON_IMPORT + " Import M3U Playlist</div>");
|
|
845
|
+
lines.push("<label class=\"dropdown-option\"><input type=\"checkbox\" id=\"m3u-replace-duplicates\"> Replace duplicates on M3U import</label>");
|
|
632
846
|
lines.push("<div class=\"dropdown-divider\"></div>");
|
|
633
|
-
lines.push("<div class=\"dropdown-item\" onclick=\"closeDropdowns();
|
|
634
|
-
|
|
847
|
+
lines.push("<div class=\"dropdown-item dropdown-item-icon\" onclick=\"closeDropdowns(); exportChannels()\">" +
|
|
848
|
+
ICON_EXPORT + " Export Channels (JSON)</div>");
|
|
635
849
|
lines.push("</div>");
|
|
636
850
|
lines.push("</div>");
|
|
637
|
-
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm\" onclick=\"exportChannels()\">Export</button>");
|
|
638
851
|
lines.push("<input type=\"file\" id=\"import-m3u-file\" accept=\".m3u,.m3u8\" style=\"display: none;\" onchange=\"importM3U(this)\">");
|
|
852
|
+
// Visible column set, used by Quick Actions to gate column-dependent options and by the table header to set initial hide classes.
|
|
853
|
+
const visibleCols = new Set(CONFIG.channels.visibleColumns);
|
|
854
|
+
// Quick Actions dropdown — bulk operations for predefined channels.
|
|
639
855
|
lines.push("<div class=\"dropdown quick-actions-dropdown\">");
|
|
640
|
-
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm\"
|
|
856
|
+
lines.push("<button type=\"button\" class=\"btn btn-secondary btn-sm toolbar-dropdown-btn\" title=\"Bulk operations for predefined channels\" " +
|
|
857
|
+
"onclick=\"toggleDropdown(this)\">" + ICON_BOLT + " Quick Actions ▾</button>");
|
|
641
858
|
lines.push("<div class=\"dropdown-menu\">");
|
|
642
859
|
// Compute initial toggle counts for predefined channel scopes. The server is the single source of truth — the client renders what we return here.
|
|
643
860
|
const scopeCounts = getPredefinedScopeCounts();
|
|
644
|
-
// Three toggle rows: checkbox + label +
|
|
645
|
-
//
|
|
861
|
+
// Three toggle rows: checkbox + label + count. Clicking toggles the group via bulkTogglePredefined(). The onclick uses event.preventDefault() to stop the
|
|
862
|
+
// native checkbox toggle — the server response drives the update.
|
|
646
863
|
const scopes = [
|
|
647
864
|
{ count: scopeCounts.all.enabled, label: "All Predefined", scope: "all", total: scopeCounts.all.total },
|
|
648
865
|
{ count: scopeCounts.east.enabled, label: "East Variants", scope: "east", total: scopeCounts.east.total },
|
|
@@ -651,19 +868,52 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
651
868
|
for (const s of scopes) {
|
|
652
869
|
const checked = (s.count === s.total) ? " checked" : "";
|
|
653
870
|
lines.push("<label class=\"provider-option\" onclick=\"event.preventDefault(); bulkTogglePredefined(!this.querySelector('input').checked, '" + s.scope +
|
|
654
|
-
"')\">" + "<input type=\"checkbox\" class=\"scope-toggle\" data-scope=\"" + s.scope + "\"
|
|
655
|
-
s.label + "<span class=\"quick-action-count\" data-scope=\"" + s.scope + "\"
|
|
871
|
+
"')\">" + "<input type=\"checkbox\" class=\"scope-toggle\" data-scope=\"" + s.scope + "\"" + checked + "> " +
|
|
872
|
+
s.label + "<span class=\"quick-action-count\" data-scope=\"" + s.scope + "\" data-enabled=\"" + String(s.count) + "\" data-total=\"" + String(s.total) +
|
|
873
|
+
"\">" + String(s.count) + " of " + String(s.total) + " enabled</span></label>");
|
|
656
874
|
}
|
|
657
875
|
lines.push("<div class=\"dropdown-divider\"></div>");
|
|
658
|
-
// Bulk assign
|
|
876
|
+
// Bulk assign select — switch all multi-provider channels to a single provider.
|
|
659
877
|
const allTags = getAllProviderTags();
|
|
660
878
|
const enabled = getEnabledProviders();
|
|
661
879
|
const hasFilter = enabled.length > 0;
|
|
880
|
+
lines.push("<div class=\"bulk-assign-row\">");
|
|
881
|
+
lines.push("<span>Set all channels to:</span>");
|
|
882
|
+
lines.push("<select id=\"bulk-assign-select\" class=\"bulk-assign-select\" title=\"Switch all multi-provider channels to this provider\" " +
|
|
883
|
+
"onchange=\"if(this.value) { closeDropdowns(); bulkAssignProvider(this.value); this.value = ''; }\">");
|
|
884
|
+
lines.push("<option value=\"\">Select provider</option>");
|
|
662
885
|
for (const tagInfo of allTags) {
|
|
663
|
-
const hidden = (hasFilter && !enabled.includes(tagInfo.tag) && (tagInfo.tag !== "direct")) ? "
|
|
664
|
-
lines.push("<
|
|
665
|
-
|
|
886
|
+
const hidden = (hasFilter && !enabled.includes(tagInfo.tag) && (tagInfo.tag !== "direct")) ? " hidden" : "";
|
|
887
|
+
lines.push("<option value=\"" + escapeHtml(tagInfo.tag) + "\" data-provider-tag=\"" + escapeHtml(tagInfo.tag) + "\"" + hidden + ">" +
|
|
888
|
+
escapeHtml(tagInfo.displayName) + "</option>");
|
|
666
889
|
}
|
|
890
|
+
lines.push("</select>");
|
|
891
|
+
lines.push("</div>");
|
|
892
|
+
// Divider before column-gated quick actions. Hidden when neither the channel number nor HDHR column is visible.
|
|
893
|
+
const dividerVisible = (visibleCols.has("channelNumber") || visibleCols.has("hdhrEnabled")) ? "" : " style=\"display: none;\"";
|
|
894
|
+
lines.push("<div class=\"dropdown-divider\" id=\"quick-action-divider\"" + dividerVisible + "></div>");
|
|
895
|
+
// Auto-number channels — assign sequential channel numbers to visible channels in current sort order. Visible when the channel number column is shown.
|
|
896
|
+
const autoNumberVisible = visibleCols.has("channelNumber") ? "" : " style=\"display: none;\"";
|
|
897
|
+
lines.push("<div id=\"quick-action-autonumber\"" + autoNumberVisible + ">");
|
|
898
|
+
lines.push("<div class=\"bulk-assign-row\">");
|
|
899
|
+
lines.push("<span>Auto-number from:</span>");
|
|
900
|
+
lines.push("<input type=\"number\" id=\"auto-number-start\" class=\"auto-number-input\" value=\"1\" min=\"0\" max=\"99999\" " +
|
|
901
|
+
"placeholder=\"Clear\" onclick=\"event.stopPropagation()\">");
|
|
902
|
+
lines.push("<button type=\"button\" class=\"btn btn-sm btn-secondary\" onclick=\"closeDropdowns(); autoNumberChannels()\">Apply</button>");
|
|
903
|
+
lines.push("</div>");
|
|
904
|
+
lines.push("</div>");
|
|
905
|
+
// HDHR bulk toggle — tri-state checkbox to enable/disable all channels for the HDHomeRun lineup. Visible when the HDHR column is shown.
|
|
906
|
+
const hdhrVisible = visibleCols.has("hdhrEnabled") ? "" : " style=\"display: none;\"";
|
|
907
|
+
const hdhrCounts = getHdhrCounts(listing);
|
|
908
|
+
const hdhrCheckedAttr = (hdhrCounts.enabled === hdhrCounts.total) ? " checked" : "";
|
|
909
|
+
const hdhrIndeterminate = ((hdhrCounts.enabled > 0) && (hdhrCounts.enabled < hdhrCounts.total)) ? " data-indeterminate=\"true\"" : "";
|
|
910
|
+
lines.push("<div id=\"quick-action-hdhr\"" + hdhrVisible + ">");
|
|
911
|
+
lines.push("<label class=\"provider-option\" onclick=\"event.preventDefault(); bulkToggleHdhr()\">");
|
|
912
|
+
lines.push("<input type=\"checkbox\" id=\"hdhr-bulk-toggle\"" + hdhrCheckedAttr + hdhrIndeterminate + "> ");
|
|
913
|
+
lines.push("Include all in HDHR/Plex");
|
|
914
|
+
lines.push("<span class=\"quick-action-count\" id=\"hdhr-bulk-count\">" + String(hdhrCounts.enabled) + " of " + String(hdhrCounts.total) + "</span>");
|
|
915
|
+
lines.push("</label>");
|
|
916
|
+
lines.push("</div>");
|
|
667
917
|
lines.push("</div>");
|
|
668
918
|
lines.push("</div>");
|
|
669
919
|
lines.push("</div>");
|
|
@@ -712,6 +962,24 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
712
962
|
lines.push("<h3>Add New Channel</h3>");
|
|
713
963
|
lines.push("<form id=\"add-channel-form-el\" onsubmit=\"return submitChannelForm(event, 'add')\">");
|
|
714
964
|
lines.push("<input type=\"hidden\" name=\"action\" value=\"add\">");
|
|
965
|
+
// Provider pills. Clicking a provider auto-fills the URL field, which triggers the existing URL-change infrastructure (datalist population, profile
|
|
966
|
+
// resolution). The pills use providerDisplaySpan for icon rendering via the shared processProviderDisplays path. Guide-grid providers fill with their
|
|
967
|
+
// guideUrl; non-guide-grid providers fill with https://{domain}/.
|
|
968
|
+
const addFormTags = getAllProviderTags();
|
|
969
|
+
const addFormGuideUrls = getProviderGuideUrls();
|
|
970
|
+
lines.push("<div class=\"form-row\"><label>Provider</label>");
|
|
971
|
+
lines.push("<div class=\"provider-pills\">");
|
|
972
|
+
for (const tagInfo of addFormTags) {
|
|
973
|
+
if (tagInfo.tag === "direct") {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const pillUrl = addFormGuideUrls[tagInfo.tag] ?? (tagInfo.domain ? "https://" + tagInfo.domain + "/" : "");
|
|
977
|
+
lines.push("<button type=\"button\" class=\"provider-pill\" data-slug=\"" + escapeHtml(tagInfo.tag) + "\" data-url=\"" + escapeHtml(pillUrl) +
|
|
978
|
+
"\" onclick=\"selectProviderPill(this)\">" + providerDisplaySpan(tagInfo.displayName, tagInfo.domain, tagInfo.iconUrl) + "</button>");
|
|
979
|
+
}
|
|
980
|
+
lines.push("</div>");
|
|
981
|
+
lines.push("</div>");
|
|
982
|
+
lines.push("<div class=\"hint\">Select a provider to auto-fill the URL, or enter one manually below.</div>");
|
|
715
983
|
// Channel key (add form only).
|
|
716
984
|
lines.push(...generateTextField("add-key", "key", "Channel Key", formValues?.get("key") ?? "", {
|
|
717
985
|
hint: "Lowercase letters, numbers, and hyphens only. Used in the URL: /stream/channel-key",
|
|
@@ -734,22 +1002,24 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
734
1002
|
}));
|
|
735
1003
|
// Profile dropdown.
|
|
736
1004
|
lines.push(...generateProfileDropdown("add-profile", formValues?.get("profile") ?? "", profiles));
|
|
737
|
-
// Advanced fields (station ID, channel selector, channel number).
|
|
738
|
-
lines.push(...generateAdvancedFields("add",
|
|
1005
|
+
// Advanced fields (station ID, channel selector, channel number, HDHR).
|
|
1006
|
+
lines.push(...generateAdvancedFields("add", {
|
|
1007
|
+
channelNumberValue: formValues?.get("channelNumber") ?? "",
|
|
1008
|
+
channelSelectorValue: formValues?.get("channelSelector") ?? "",
|
|
1009
|
+
hdhrEnabled: formValues?.get("hdhrEnabled") !== "false",
|
|
1010
|
+
stationIdValue: formValues?.get("stationId") ?? ""
|
|
1011
|
+
}));
|
|
739
1012
|
// Form buttons.
|
|
740
1013
|
lines.push("<div class=\"form-buttons\">");
|
|
741
1014
|
lines.push("<button type=\"submit\" class=\"btn btn-primary\">Add Channel</button>");
|
|
742
|
-
lines.push("<button type=\"button\" class=\"btn btn-secondary\" onclick=\"document.getElementById('add-channel-form').style.display='none'
|
|
1015
|
+
lines.push("<button type=\"button\" class=\"btn btn-secondary\" onclick=\"document.getElementById('add-channel-form').style.display='none';\">Cancel</button>");
|
|
743
1016
|
lines.push("</div>");
|
|
744
1017
|
lines.push("</form>");
|
|
745
1018
|
lines.push("</div>"); // End add-channel-form.
|
|
746
|
-
// Provider filter toolbar with multi-select dropdown and chips. Placed after the add channel form so the form flows directly from its trigger button.
|
|
747
|
-
lines.push(generateProviderFilterToolbar());
|
|
748
1019
|
// Profile reference section (hidden by default, toggled via link in profile dropdown hint).
|
|
749
1020
|
lines.push(generateProfileReference(profiles));
|
|
750
1021
|
// Channels table. Disabled predefined channels are hidden by default and revealed via the "Show disabled" toggle. The wrapper div enables horizontal scrolling on
|
|
751
1022
|
// small screens. Table classes dynamically include hide-col-* for each hidden optional column.
|
|
752
|
-
const visibleCols = new Set(CONFIG.channels.visibleColumns);
|
|
753
1023
|
const tableClasses = ["channel-table", "hide-disabled"];
|
|
754
1024
|
for (const col of OPTIONAL_COLUMNS) {
|
|
755
1025
|
if (!visibleCols.has(col.field)) {
|
|
@@ -758,13 +1028,17 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
758
1028
|
}
|
|
759
1029
|
const sortField = CONFIG.channels.channelSortField;
|
|
760
1030
|
const sortDir = CONFIG.channels.channelSortDirection;
|
|
761
|
-
//
|
|
762
|
-
|
|
1031
|
+
// Provider filter toolbar with multi-select dropdown and chips.
|
|
1032
|
+
lines.push(generateProviderFilterToolbar());
|
|
1033
|
+
// Channel summary line with predefined/user breakdown, placed directly above the table for visual proximity. The user-count span contains the entire user
|
|
1034
|
+
// portion (comma, count, and label) so the client can toggle it by setting textContent. When there are no user channels, the span is empty.
|
|
763
1035
|
const userPortion = (userCount > 0) ? ", " + String(userCount) + " user" : "";
|
|
764
1036
|
lines.push("<div class=\"channel-summary\"><span id=\"total-count\">" + String(totalCount) + "</span> channels " +
|
|
765
1037
|
"(<span id=\"predefined-count\">" + String(predefinedCount) + "</span> predefined<span id=\"user-count\">" + userPortion + "</span>) · " +
|
|
766
1038
|
"<span id=\"enabled-count\">" + String(enabledCount) + "</span> enabled · " +
|
|
767
1039
|
"<span id=\"disabled-count\">" + String(totalHiddenCount) + "</span> disabled</div>");
|
|
1040
|
+
// Channels table. The wrapper uses fit-content so it shrinks when columns are hidden, with max-width: 100% to prevent viewport overflow. Centered via auto
|
|
1041
|
+
// margins. Dropdown menus escape the container via portal to <body>.
|
|
768
1042
|
lines.push("<div class=\"channel-table-wrapper\">");
|
|
769
1043
|
lines.push("<table class=\"" + tableClasses.join(" ") + "\" data-sort-field=\"" + sortField + "\" data-sort-dir=\"" + sortDir + "\">");
|
|
770
1044
|
lines.push("<thead>");
|
|
@@ -782,7 +1056,7 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
782
1056
|
lines.push("<th class=\"" + hdr.cssClass + " sortable\" data-sort-field=\"" + hdr.field + "\" onclick=\"sortChannelTable('" + hdr.field + "')\">" +
|
|
783
1057
|
hdr.label + indicator + "</th>");
|
|
784
1058
|
}
|
|
785
|
-
// Actions header with table options dropdown.
|
|
1059
|
+
// Actions header with table options dropdown (kebab menu). Contains the show-disabled toggle and column visibility checkboxes.
|
|
786
1060
|
lines.push("<th class=\"col-actions\"><span>Actions</span>");
|
|
787
1061
|
lines.push("<div class=\"dropdown column-picker\">");
|
|
788
1062
|
lines.push("<button type=\"button\" class=\"btn-icon btn-col-picker\" title=\"Table options\" aria-label=\"Table options\" " +
|
|
@@ -816,6 +1090,12 @@ export function generateChannelsPanel(channelMessage, channelError, editingChann
|
|
|
816
1090
|
// Embed channel selector data for datalist population. The client-side JavaScript uses this to offer known selector suggestions when the URL matches a
|
|
817
1091
|
// multi-channel site like Disney+ or USA Network.
|
|
818
1092
|
lines.push("<script>" + generateChannelSelectorData() + "</script>");
|
|
1093
|
+
// Browse Channels modal. The shell is server-rendered using wizard modal CSS classes. The content area (provider picker, channel list) is rendered
|
|
1094
|
+
// client-side after fetching discovered channels from the provider endpoint.
|
|
1095
|
+
lines.push(generateBrowseModal());
|
|
1096
|
+
// Provider Setup wizard modal. Follows the same wizard pattern as the provider profile builder. Three steps: pick providers, authenticate, browse channels.
|
|
1097
|
+
// The setupCompleted flag is embedded as a data attribute so the client can auto-show the wizard on first visit.
|
|
1098
|
+
lines.push(generateSetupWizardModal());
|
|
819
1099
|
return lines.join("\n");
|
|
820
1100
|
}
|
|
821
1101
|
//# sourceMappingURL=table.js.map
|