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
|
@@ -41,56 +41,62 @@ export function generateChannelsSubtabScript() {
|
|
|
41
41
|
" switchChannelsSubtab(this.getAttribute('data-channels-subtab'));",
|
|
42
42
|
" });",
|
|
43
43
|
" }",
|
|
44
|
+
// Replace the Custom Profiles panel content with server-rendered HTML. Called after profile mutations to update the panel without a page reload.
|
|
45
|
+
// Re-runs processProviderDisplays() so dynamically swapped provider icon elements render correctly.
|
|
46
|
+
" function applyProfilePanel(html) {",
|
|
47
|
+
" var panel = document.querySelector('#channels-subtab-custom-profiles .section');",
|
|
48
|
+
" if(panel) {",
|
|
49
|
+
" panel.innerHTML = html;",
|
|
50
|
+
" if(window.processProviderDisplays) { processProviderDisplays(); }",
|
|
51
|
+
" }",
|
|
52
|
+
" }",
|
|
44
53
|
// Delete a user profile with confirmation.
|
|
45
|
-
" window.deleteUserProfile = function(key) {",
|
|
54
|
+
" window.deleteUserProfile = async function(key) {",
|
|
46
55
|
" if(!confirm('Delete profile \"' + key + '\" and all its domain mappings?')) return;",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
" .
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
56
|
+
" try {",
|
|
57
|
+
" var res = await fetch('/config/profiles/' + encodeURIComponent(key), { method: 'DELETE' });",
|
|
58
|
+
" var data = await res.json();",
|
|
59
|
+
" if(data.success) {",
|
|
60
|
+
" showToast(data.message || 'Profile deleted.', 'success');",
|
|
61
|
+
" if(data.panelHtml) { applyProfilePanel(data.panelHtml); }",
|
|
62
|
+
" } else { showToast('Failed to delete profile: ' + (data.error || 'Unknown error.'), 'error'); }",
|
|
63
|
+
" } catch(err) { console.error('Profile delete error:', err); showToast('Failed to delete profile.', 'error'); }",
|
|
54
64
|
" };",
|
|
55
65
|
// Edit an existing user profile by pre-populating the wizard.
|
|
56
|
-
" window.editUserProfile = function(key) {",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
" .
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
" }",
|
|
66
|
+
" window.editUserProfile = async function(key) {",
|
|
67
|
+
" try {",
|
|
68
|
+
" var res = await fetch('/config/profiles');",
|
|
69
|
+
" var data = await res.json();",
|
|
70
|
+
" if(!data.success) { showToast('Failed to load profiles.', 'error'); return; }",
|
|
71
|
+
" var profiles = data.profiles || [];",
|
|
72
|
+
" var match = profiles.find(function(p) { return p.key === key; });",
|
|
73
|
+
" if(!match || !match.profile) { showToast('Profile not found.', 'error'); return; }",
|
|
74
|
+
" var p = match.profile;",
|
|
75
|
+
" var editCs = p.channelSelection || {};",
|
|
76
|
+
" var strat = editCs.strategy || 'none';",
|
|
77
|
+
" var strategyFields = {};",
|
|
78
|
+
" var editStrat = window.__wizardStrategies.find(function(st) { return st.id === strat; });",
|
|
79
|
+
" if(editStrat) {",
|
|
80
|
+
" for(var i = 0; i < editStrat.fields.length; i++) {",
|
|
81
|
+
" var ef = editStrat.fields[i];",
|
|
82
|
+
" if(ef.type === 'text' && editCs[ef.id]) { strategyFields[ef.id] = editCs[ef.id]; }",
|
|
83
|
+
" else if(ef.type === 'boolean' && editCs[ef.id]) { strategyFields[ef.id] = true; }",
|
|
75
84
|
" }",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
" renderWizardStep();",
|
|
92
|
-
" })",
|
|
93
|
-
" .catch(function() { showToast('Failed to load profile data.', 'error'); });",
|
|
85
|
+
" }",
|
|
86
|
+
" var flags = {};",
|
|
87
|
+
" var wizFlags = window.__wizardFlags;",
|
|
88
|
+
" for(var i = 0; i < wizFlags.length; i++) { if(p[wizFlags[i].id]) flags[wizFlags[i].id] = true; }",
|
|
89
|
+
" var domains = (match.domains && match.domains.length > 0) ?",
|
|
90
|
+
" match.domains.map(function(d) { return { domain: d.domain, provider: d.provider || '', providerTag: d.providerTag || '' }; }) :",
|
|
91
|
+
" [{ domain: '', provider: '', providerTag: '' }];",
|
|
92
|
+
" profileWizard.state = {",
|
|
93
|
+
" editKey: key, editMode: true, profileName: key,",
|
|
94
|
+
" baseProfile: p.extends || '', strategy: strat,",
|
|
95
|
+
" strategyFields: strategyFields, flags: flags,",
|
|
96
|
+
" hideSelector: p.hideSelector || '', domains: domains, description: p.description || ''",
|
|
97
|
+
" };",
|
|
98
|
+
" profileWizard.open({ highestStep: 5, title: 'Edit Provider Profile' });",
|
|
99
|
+
" } catch(e) { showToast('Failed to load profile data.', 'error'); }",
|
|
94
100
|
" };",
|
|
95
101
|
// Pending import data stored while the import preview modal is open.
|
|
96
102
|
" var pendingImportData = null;",
|
|
@@ -141,69 +147,67 @@ export function generateChannelsSubtabScript() {
|
|
|
141
147
|
" pendingImportData = null;",
|
|
142
148
|
" };",
|
|
143
149
|
// Execute the import after the user confirms in the preview modal.
|
|
144
|
-
" window.executeImport = function() {",
|
|
150
|
+
" window.executeImport = async function() {",
|
|
145
151
|
" if(!pendingImportData) return;",
|
|
146
152
|
" var skipChannels = false;",
|
|
147
153
|
" var cb = document.getElementById('import-include-channels');",
|
|
148
154
|
" if(cb && !cb.checked) skipChannels = true;",
|
|
149
155
|
" var body = Object.assign({}, pendingImportData);",
|
|
150
156
|
" if(skipChannels) body.skipChannels = true;",
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
" .
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
" .catch(function() { closeImportModal(); showToast('Import request failed.', 'error'); });",
|
|
157
|
+
" try {",
|
|
158
|
+
" var res = await fetch('/config/profiles/import', {",
|
|
159
|
+
" method: 'POST',",
|
|
160
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
161
|
+
" body: JSON.stringify(body)",
|
|
162
|
+
" });",
|
|
163
|
+
" var result = await res.json();",
|
|
164
|
+
" closeImportModal();",
|
|
165
|
+
" if(result.success) {",
|
|
166
|
+
" var msg = 'Imported ' + plural(result.summary.profilesAdded, 'profile') + ', ' +",
|
|
167
|
+
" plural(result.summary.domainsAdded, 'domain') + ', ' + plural(result.summary.channelsAdded, 'channel') + '.';",
|
|
168
|
+
" if(result.warnings && result.warnings.length > 0) msg += ' Warnings: ' + result.warnings.join('; ');",
|
|
169
|
+
" showToast(msg, 'success');",
|
|
170
|
+
" if(result.panelHtml) { applyProfilePanel(result.panelHtml); }",
|
|
171
|
+
" } else {",
|
|
172
|
+
" showToast('Import failed: ' + (result.error || 'Unknown error.'), 'error');",
|
|
173
|
+
" }",
|
|
174
|
+
" } catch(err) { console.error('Provider pack import error:', err); closeImportModal(); showToast('Import request failed.', 'error'); }",
|
|
170
175
|
" };",
|
|
171
176
|
// Export a provider pack. Opens a modal with profile checklist and options. Shows provider name prominently for each profile with domain and channel counts.
|
|
172
177
|
// The select-all row is hidden when there is only one profile since the single checkbox makes it redundant.
|
|
173
|
-
" window.startProviderExport = function() {",
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
" .
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
" }",
|
|
189
|
-
" var meta = [];",
|
|
190
|
-
" if(providerName) meta.push(esc(providerName));",
|
|
191
|
-
" meta.push(plural(domainCount, 'domain'));",
|
|
192
|
-
" meta.push(plural(channelCount, 'channel'));",
|
|
193
|
-
" html += '<label class=\"export-profile-item\">';",
|
|
194
|
-
" html += '<input type=\"checkbox\" class=\"export-profile-cb\" value=\"' + esc(p.key) + '\" checked>';",
|
|
195
|
-
" html += '<div class=\"export-profile-info\">';",
|
|
196
|
-
" html += '<div class=\"export-profile-name\">' + esc(p.key) + '</div>';",
|
|
197
|
-
" html += '<div class=\"export-profile-meta\">' + meta.join(' · ') + '</div>';",
|
|
198
|
-
" html += '</div></label>';",
|
|
178
|
+
" window.startProviderExport = async function() {",
|
|
179
|
+
" try {",
|
|
180
|
+
" var res = await fetch('/config/profiles');",
|
|
181
|
+
" var data = await res.json();",
|
|
182
|
+
" var profiles = data.profiles || [];",
|
|
183
|
+
" if(profiles.length === 0) { showToast('No user profiles to export.', 'error'); return; }",
|
|
184
|
+
" var listDiv = document.getElementById('export-profile-list');",
|
|
185
|
+
" var html = '';",
|
|
186
|
+
" for(var i = 0; i < profiles.length; i++) {",
|
|
187
|
+
" var p = profiles[i];",
|
|
188
|
+
" var domainCount = p.domains ? p.domains.length : 0;",
|
|
189
|
+
" var channelCount = p.channelCount || 0;",
|
|
190
|
+
" var providerName = '';",
|
|
191
|
+
" for(var di = 0; di < (p.domains || []).length; di++) {",
|
|
192
|
+
" if(p.domains[di].provider) { providerName = p.domains[di].provider; break; }",
|
|
199
193
|
" }",
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
194
|
+
" var meta = [];",
|
|
195
|
+
" if(providerName) meta.push(esc(providerName));",
|
|
196
|
+
" meta.push(plural(domainCount, 'domain'));",
|
|
197
|
+
" meta.push(plural(channelCount, 'channel'));",
|
|
198
|
+
" html += '<label class=\"export-profile-item\">';",
|
|
199
|
+
" html += '<input type=\"checkbox\" class=\"export-profile-cb\" value=\"' + esc(p.key) + '\" checked>';",
|
|
200
|
+
" html += '<div class=\"export-profile-info\">';",
|
|
201
|
+
" html += '<div class=\"export-profile-name\">' + esc(p.key) + '</div>';",
|
|
202
|
+
" html += '<div class=\"export-profile-meta\">' + meta.join(' · ') + '</div>';",
|
|
203
|
+
" html += '</div></label>';",
|
|
204
|
+
" }",
|
|
205
|
+
" listDiv.innerHTML = html;",
|
|
206
|
+
" document.getElementById('export-select-all').checked = true;",
|
|
207
|
+
" document.getElementById('export-select-all-row').style.display = (profiles.length > 1) ? '' : 'none';",
|
|
208
|
+
" document.getElementById('export-include-channels').checked = false;",
|
|
209
|
+
" document.getElementById('export-modal').style.display = 'flex';",
|
|
210
|
+
" } catch(e) { showToast('Failed to load profiles.', 'error'); }",
|
|
207
211
|
" };",
|
|
208
212
|
// Toggle all export profile checkboxes.
|
|
209
213
|
" window.toggleExportAll = function(master) {",
|
|
@@ -225,21 +229,6 @@ export function generateChannelsSubtabScript() {
|
|
|
225
229
|
" closeExportModal();",
|
|
226
230
|
" window.location.href = url;",
|
|
227
231
|
" };",
|
|
228
|
-
// Profile builder wizard state and rendering. highestStep tracks the furthest step the user has reached, allowing click-to-navigate back to visited steps.
|
|
229
|
-
" var wizardState = {",
|
|
230
|
-
" currentStep: 1,",
|
|
231
|
-
" editKey: null,",
|
|
232
|
-
" editMode: false,",
|
|
233
|
-
" highestStep: 1,",
|
|
234
|
-
" profileName: '',",
|
|
235
|
-
" baseProfile: '',",
|
|
236
|
-
" strategy: 'none',",
|
|
237
|
-
" strategyFields: {},",
|
|
238
|
-
" flags: {},",
|
|
239
|
-
" hideSelector: '',",
|
|
240
|
-
" domains: [{ domain: '', provider: '', providerTag: '' }],",
|
|
241
|
-
" description: ''",
|
|
242
|
-
" };",
|
|
243
232
|
// Auto-generate a provider tag from a provider name: lowercase, strip non-alphanumeric.
|
|
244
233
|
" function autoProviderTag(name) {",
|
|
245
234
|
" return name.toLowerCase().replace(/[^a-z0-9]/g, '');",
|
|
@@ -254,45 +243,39 @@ export function generateChannelsSubtabScript() {
|
|
|
254
243
|
" function plural(n, word) {",
|
|
255
244
|
" return n + ' ' + word + (n === 1 ? '' : 's');",
|
|
256
245
|
" }",
|
|
257
|
-
//
|
|
258
|
-
|
|
246
|
+
// Profile builder wizard. Uses the shared wizard controller for step navigation, indicator updates, and error display. The state object holds wizard-specific
|
|
247
|
+
// form data (profile name, base profile, strategy, flags, domains, description).
|
|
248
|
+
" var profileWizard = createWizardController({",
|
|
249
|
+
" contentId: 'wizard-content',",
|
|
250
|
+
" errorId: 'wizard-error',",
|
|
251
|
+
" modalId: 'wizard-modal',",
|
|
252
|
+
" onRender: renderProfileStep,",
|
|
253
|
+
" onValidate: validateProfileStep,",
|
|
254
|
+
" stepCount: 5,",
|
|
255
|
+
" titleId: 'wizard-title'",
|
|
256
|
+
" });",
|
|
257
|
+
// Render the current profile wizard step into the content area. Manages button visibility and delegates to the step-specific content renderer.
|
|
258
|
+
" function renderProfileStep(step) {",
|
|
259
259
|
" var content = document.getElementById('wizard-content');",
|
|
260
|
-
" var errDiv = document.getElementById('wizard-error');",
|
|
261
|
-
" var backBtn = document.getElementById('wizard-back');",
|
|
262
|
-
" var nextBtn = document.getElementById('wizard-next');",
|
|
263
|
-
" var saveBtn = document.getElementById('wizard-save');",
|
|
264
|
-
" var saveTestBtn = document.getElementById('wizard-save-test');",
|
|
265
260
|
" if(!content) return;",
|
|
266
|
-
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
" var stepNum = parseInt(steps[i].getAttribute('data-step'), 10);",
|
|
272
|
-
" steps[i].classList.remove('active', 'completed', 'clickable');",
|
|
273
|
-
" if(stepNum < wizardState.currentStep) steps[i].classList.add('completed');",
|
|
274
|
-
" if(stepNum === wizardState.currentStep) steps[i].classList.add('active');",
|
|
275
|
-
" if(stepNum !== wizardState.currentStep && stepNum <= wizardState.highestStep) steps[i].classList.add('clickable');",
|
|
276
|
-
" steps[i].onclick = (function(n) { return function() { wizardGoToStep(n); }; })(stepNum);",
|
|
277
|
-
" }",
|
|
278
|
-
// Show/hide navigation buttons.
|
|
279
|
-
" backBtn.style.display = (wizardState.currentStep > 1) ? '' : 'none';",
|
|
280
|
-
" nextBtn.style.display = (wizardState.currentStep < 5) ? '' : 'none';",
|
|
281
|
-
" saveBtn.style.display = (wizardState.currentStep === 5) ? '' : 'none';",
|
|
282
|
-
" saveTestBtn.style.display = (wizardState.currentStep === 5) ? '' : 'none';",
|
|
261
|
+
// Show/hide navigation buttons based on the current step.
|
|
262
|
+
" if(step > 1) { profileWizard.show('wizard-back'); } else { profileWizard.hide('wizard-back'); }",
|
|
263
|
+
" if(step < 5) { profileWizard.show('wizard-next'); } else { profileWizard.hide('wizard-next'); }",
|
|
264
|
+
" if(step === 5) { profileWizard.show('wizard-save'); profileWizard.show('wizard-save-test'); }",
|
|
265
|
+
" else { profileWizard.hide('wizard-save'); profileWizard.hide('wizard-save-test'); }",
|
|
283
266
|
" var html = '';",
|
|
284
|
-
" var s =
|
|
267
|
+
" var s = profileWizard.state;",
|
|
285
268
|
// Step 1: Base Profile.
|
|
286
|
-
" if(
|
|
269
|
+
" if(step === 1) {",
|
|
287
270
|
" html += '<div class=\"field-group\">';",
|
|
288
|
-
" html += '<label for=\"wizard-profile-name\">Profile Name</label>';",
|
|
271
|
+
" html += '<label class=\"wizard-label\" for=\"wizard-profile-name\">Profile Name</label>';",
|
|
289
272
|
" var nameReadonly = s.editMode ? ' readonly style=\"opacity: 0.6; cursor: not-allowed;\"' : '';",
|
|
290
273
|
" html += '<input type=\"text\" id=\"wizard-profile-name\" value=\"' + s.profileName.replace(/\"/g, '"') +",
|
|
291
274
|
" '\" placeholder=\"e.g., myProvider\"' + nameReadonly + '>';",
|
|
292
275
|
" html += '<div class=\"field-hint\">' + (s.editMode ? 'Profile name cannot be changed.' :",
|
|
293
276
|
" 'Unique identifier. Letters, numbers, and hyphens only.') + '</div>';",
|
|
294
277
|
" html += '</div>';",
|
|
295
|
-
" html += '<label>Base Profile</label>';",
|
|
278
|
+
" html += '<label class=\"wizard-label\">Base Profile</label>';",
|
|
296
279
|
" var profiles = window.__wizardProfiles || {};",
|
|
297
280
|
" var cats = [",
|
|
298
281
|
" { key: 'api', title: 'Fullscreen API',",
|
|
@@ -326,8 +309,8 @@ export function generateChannelsSubtabScript() {
|
|
|
326
309
|
" }",
|
|
327
310
|
" }",
|
|
328
311
|
// Step 2: Channel Selection Strategy. Strategies and their fields are rendered from the server-side registry (window.__wizardStrategies).
|
|
329
|
-
" if(
|
|
330
|
-
" html += '<label>Channel Selection Strategy</label>';",
|
|
312
|
+
" if(step === 2) {",
|
|
313
|
+
" html += '<label class=\"wizard-label\">Channel Selection Strategy</label>';",
|
|
331
314
|
" var strategies = window.__wizardStrategies;",
|
|
332
315
|
" for(var si = 0; si < strategies.length; si++) {",
|
|
333
316
|
" var st = strategies[si];",
|
|
@@ -347,7 +330,7 @@ export function generateChannelsSubtabScript() {
|
|
|
347
330
|
" var val = (s.strategyFields[f.id] || '').replace(/\"/g, '"');",
|
|
348
331
|
" var optLabel = f.required ? '' : ' <span style=\"color: var(--text-muted);\">(optional)</span>';",
|
|
349
332
|
" html += '<div class=\"field-group\">';",
|
|
350
|
-
" html += '<label for=\"wizard-sf-' + f.id + '\">' + f.label + optLabel + '</label>';",
|
|
333
|
+
" html += '<label class=\"wizard-label\" for=\"wizard-sf-' + f.id + '\">' + f.label + optLabel + '</label>';",
|
|
351
334
|
" html += '<input type=\"text\" id=\"wizard-sf-' + f.id + '\" data-field=\"' + f.id + '\" value=\"' + val + '\"';",
|
|
352
335
|
" if(f.placeholder) html += ' placeholder=\"' + f.placeholder.replace(/\"/g, '"') + '\"';",
|
|
353
336
|
" html += '>';",
|
|
@@ -365,8 +348,8 @@ export function generateChannelsSubtabScript() {
|
|
|
365
348
|
" }",
|
|
366
349
|
" }",
|
|
367
350
|
// Step 3: Override Flags. Flags are rendered from the server-side registry (window.__wizardFlags).
|
|
368
|
-
" if(
|
|
369
|
-
" html += '<label>Profile Flags</label>';",
|
|
351
|
+
" if(step === 3) {",
|
|
352
|
+
" html += '<label class=\"wizard-label\">Profile Flags</label>';",
|
|
370
353
|
" html += '<div class=\"field-hint\" style=\"margin-bottom: 12px;\">These override the base profile. ' +",
|
|
371
354
|
" 'Only change what differs from ' + s.baseProfile + '.</div>';",
|
|
372
355
|
" var wizFlags = window.__wizardFlags;",
|
|
@@ -380,40 +363,40 @@ export function generateChannelsSubtabScript() {
|
|
|
380
363
|
" html += '</label>';",
|
|
381
364
|
" }",
|
|
382
365
|
" html += '<div class=\"field-group\" style=\"margin-top: 12px;\">';",
|
|
383
|
-
" html += '<label for=\"wizard-hide-selector\">Hide Selector (CSS) <span style=\"color: var(--text-muted);\">(optional)</span></label>';",
|
|
366
|
+
" html += '<label class=\"wizard-label\" for=\"wizard-hide-selector\">Hide Selector (CSS) <span style=\"color: var(--text-muted);\">(optional)</span></label>';",
|
|
384
367
|
" html += '<input type=\"text\" id=\"wizard-hide-selector\" value=\"' + s.hideSelector.replace(/\"/g, '"') +",
|
|
385
368
|
" '\" placeholder=\"e.g., .overlay, .ad-banner\">';",
|
|
386
369
|
" html += '<div class=\"field-hint\">Overlay elements to hide during capture.</div>';",
|
|
387
370
|
" html += '</div>';",
|
|
388
371
|
" }",
|
|
389
372
|
// Step 4: Domain Mapping.
|
|
390
|
-
" if(
|
|
391
|
-
" html += '<label>Domain Mapping</label>';",
|
|
373
|
+
" if(step === 4) {",
|
|
374
|
+
" html += '<label class=\"wizard-label\">Domain Mapping</label>';",
|
|
392
375
|
" html += '<div class=\"field-hint\" style=\"margin-bottom: 12px;\">Map a streaming site domain to this profile.</div>';",
|
|
393
376
|
" for(var di = 0; di < s.domains.length; di++) {",
|
|
394
377
|
" var d = s.domains[di];",
|
|
395
378
|
" html += '<div class=\"wizard-domain-row\" data-domain-idx=\"' + di + '\">';",
|
|
396
379
|
" html += '<div class=\"field-group\">';",
|
|
397
|
-
" html += '<label>Domain</label>';",
|
|
380
|
+
" html += '<label class=\"wizard-label\">Domain</label>';",
|
|
398
381
|
" html += '<input type=\"text\" class=\"domain-input\" value=\"' + d.domain.replace(/\"/g, '"') + '\" placeholder=\"e.g., watch.sling.com\">';",
|
|
399
382
|
" html += '</div>';",
|
|
400
383
|
" html += '<div class=\"field-group\">';",
|
|
401
|
-
" html += '<label>Provider Name</label>';",
|
|
384
|
+
" html += '<label class=\"wizard-label\">Provider Name</label>';",
|
|
402
385
|
" html += '<input type=\"text\" class=\"provider-input\" value=\"' + d.provider.replace(/\"/g, '"') + '\" placeholder=\"e.g., Sling TV\">';",
|
|
403
386
|
" html += '</div>';",
|
|
404
387
|
" html += '<div class=\"field-group\">';",
|
|
405
|
-
" html += '<label>Provider Tag <span style=\"color: var(--text-muted);\">(auto-filled)</span></label>';",
|
|
388
|
+
" html += '<label class=\"wizard-label\">Provider Tag <span style=\"color: var(--text-muted);\">(auto-filled)</span></label>';",
|
|
406
389
|
" html += '<input type=\"text\" class=\"provider-tag-input\" value=\"' + d.providerTag.replace(/\"/g, '"') +",
|
|
407
390
|
" '\" placeholder=\"auto-generated\">';",
|
|
408
391
|
" html += '<div class=\"field-hint\">Identifier for the provider filter. Auto-generated from provider name.</div>';",
|
|
409
392
|
" html += '</div>';",
|
|
410
393
|
" html += '</div>';",
|
|
411
394
|
" }",
|
|
412
|
-
" html += '<button type=\"button\" class=\"wizard-add-domain\"
|
|
395
|
+
" html += '<button type=\"button\" class=\"wizard-add-domain\" id=\"wizard-add-domain-btn\">+ Add another domain</button>';",
|
|
413
396
|
" }",
|
|
414
397
|
// Step 5: Review and Save.
|
|
415
|
-
" if(
|
|
416
|
-
" html += '<label>Review</label>';",
|
|
398
|
+
" if(step === 5) {",
|
|
399
|
+
" html += '<label class=\"wizard-label\">Review</label>';",
|
|
417
400
|
" html += '<table class=\"wizard-review-table\">';",
|
|
418
401
|
" html += '<tr><td>Profile</td><td>' + esc(s.profileName) + '</td></tr>';",
|
|
419
402
|
" html += '<tr><td>Extends</td><td>' + esc(s.baseProfile) + '</td></tr>';",
|
|
@@ -443,17 +426,17 @@ export function generateChannelsSubtabScript() {
|
|
|
443
426
|
" if(s.hideSelector) html += '<tr><td>Hide Selector</td><td><code>' + esc(s.hideSelector) + '</code></td></tr>';",
|
|
444
427
|
" html += '</table>';",
|
|
445
428
|
" html += '<div class=\"field-group\" style=\"margin-top: 16px;\">';",
|
|
446
|
-
" html += '<label for=\"wizard-description\">Description <span style=\"color: var(--text-muted);\">(optional)</span></label>';",
|
|
429
|
+
" html += '<label class=\"wizard-label\" for=\"wizard-description\">Description <span style=\"color: var(--text-muted);\">(optional)</span></label>';",
|
|
447
430
|
" html += '<textarea id=\"wizard-description\" placeholder=\"Optional description for documentation.\">' + esc(s.description) + '</textarea>';",
|
|
448
431
|
" html += '</div>';",
|
|
449
432
|
" }",
|
|
450
433
|
" content.innerHTML = html;",
|
|
451
434
|
// Attach live event handlers after rendering.
|
|
452
|
-
"
|
|
435
|
+
" attachProfileHandlers();",
|
|
453
436
|
" }",
|
|
454
|
-
// Attach event handlers to dynamically rendered wizard inputs.
|
|
455
|
-
" function
|
|
456
|
-
" var s =
|
|
437
|
+
// Attach event handlers to dynamically rendered profile wizard inputs.
|
|
438
|
+
" function attachProfileHandlers() {",
|
|
439
|
+
" var s = profileWizard.state;",
|
|
457
440
|
// Step 1: profile name and base profile radio.
|
|
458
441
|
" var nameInput = document.getElementById('wizard-profile-name');",
|
|
459
442
|
" if(nameInput) nameInput.addEventListener('input', function() { s.profileName = this.value; });",
|
|
@@ -461,10 +444,11 @@ export function generateChannelsSubtabScript() {
|
|
|
461
444
|
" for(var i = 0; i < radios.length; i++) {",
|
|
462
445
|
" radios[i].addEventListener('change', function() { s.baseProfile = this.value; });",
|
|
463
446
|
" }",
|
|
464
|
-
// Step 2: strategy radio and strategy field inputs. Text and boolean fields are bound by data-field attribute.
|
|
447
|
+
// Step 2: strategy radio and strategy field inputs. Text and boolean fields are bound by data-field attribute. Strategy radio triggers a re-render to show
|
|
448
|
+
// the selected strategy's fields.
|
|
465
449
|
" var stratRadios = document.querySelectorAll('input[name=\"strategy\"]');",
|
|
466
450
|
" for(var i = 0; i < stratRadios.length; i++) {",
|
|
467
|
-
" stratRadios[i].addEventListener('change', function() { s.strategy = this.value;
|
|
451
|
+
" stratRadios[i].addEventListener('change', function() { s.strategy = this.value; renderProfileStep(profileWizard.getStep()); });",
|
|
468
452
|
" }",
|
|
469
453
|
" var sfTextInputs = document.querySelectorAll('#wizard-content input[type=\"text\"][data-field]');",
|
|
470
454
|
" for(var i = 0; i < sfTextInputs.length; i++) {",
|
|
@@ -487,7 +471,9 @@ export function generateChannelsSubtabScript() {
|
|
|
487
471
|
" }",
|
|
488
472
|
" var hideInput = document.getElementById('wizard-hide-selector');",
|
|
489
473
|
" if(hideInput) hideInput.addEventListener('input', function() { s.hideSelector = this.value; });",
|
|
490
|
-
// Step 4: domain inputs with auto-fill for provider tag.
|
|
474
|
+
// Step 4: add-domain button and domain inputs with auto-fill for provider tag.
|
|
475
|
+
" var addDomainBtn = document.getElementById('wizard-add-domain-btn');",
|
|
476
|
+
" if(addDomainBtn) addDomainBtn.addEventListener('click', addWizardDomain);",
|
|
491
477
|
" var domainRows = document.querySelectorAll('.wizard-domain-row');",
|
|
492
478
|
" for(var i = 0; i < domainRows.length; i++) {",
|
|
493
479
|
" (function(idx) {",
|
|
@@ -509,31 +495,10 @@ export function generateChannelsSubtabScript() {
|
|
|
509
495
|
" var descInput = document.getElementById('wizard-description');",
|
|
510
496
|
" if(descInput) descInput.addEventListener('input', function() { s.description = this.value; });",
|
|
511
497
|
" }",
|
|
512
|
-
//
|
|
513
|
-
"
|
|
514
|
-
"
|
|
515
|
-
"
|
|
516
|
-
" strategyFields: {}, flags: {}, hideSelector: '',",
|
|
517
|
-
" domains: [{ domain: '', provider: '', providerTag: '' }], description: ''",
|
|
518
|
-
" };",
|
|
519
|
-
" var title = document.getElementById('wizard-title');",
|
|
520
|
-
" if(title) title.textContent = 'New Provider Profile';",
|
|
521
|
-
" document.getElementById('wizard-modal').style.display = 'flex';",
|
|
522
|
-
" renderWizardStep();",
|
|
523
|
-
" };",
|
|
524
|
-
// Close the wizard modal.
|
|
525
|
-
" window.closeWizard = function() {",
|
|
526
|
-
" document.getElementById('wizard-modal').style.display = 'none';",
|
|
527
|
-
" };",
|
|
528
|
-
// Add another domain row.
|
|
529
|
-
" window.addWizardDomain = function() {",
|
|
530
|
-
" wizardState.domains.push({ domain: '', provider: '', providerTag: '' });",
|
|
531
|
-
" renderWizardStep();",
|
|
532
|
-
" };",
|
|
533
|
-
// Validate current step. Returns error message or empty string.
|
|
534
|
-
" function validateStep() {",
|
|
535
|
-
" var s = wizardState;",
|
|
536
|
-
" if(s.currentStep === 1) {",
|
|
498
|
+
// Validate the current profile wizard step. Returns an error message or empty string if valid.
|
|
499
|
+
" function validateProfileStep(step) {",
|
|
500
|
+
" var s = profileWizard.state;",
|
|
501
|
+
" if(step === 1) {",
|
|
537
502
|
" if(!s.editMode) {",
|
|
538
503
|
" if(!s.profileName.trim()) return 'Profile name is required.';",
|
|
539
504
|
" if(!/^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(s.profileName))",
|
|
@@ -542,7 +507,7 @@ export function generateChannelsSubtabScript() {
|
|
|
542
507
|
" }",
|
|
543
508
|
" if(!s.baseProfile) return 'Select a base profile.';",
|
|
544
509
|
" }",
|
|
545
|
-
" if(
|
|
510
|
+
" if(step === 2) {",
|
|
546
511
|
" var valStrat = window.__wizardStrategies.find(function(st) { return st.id === s.strategy; });",
|
|
547
512
|
" if(valStrat) {",
|
|
548
513
|
" for(var vi = 0; vi < valStrat.fields.length; vi++) {",
|
|
@@ -553,54 +518,29 @@ export function generateChannelsSubtabScript() {
|
|
|
553
518
|
" }",
|
|
554
519
|
" }",
|
|
555
520
|
" }",
|
|
556
|
-
" if(
|
|
521
|
+
" if(step === 4) {",
|
|
557
522
|
" var hasDomain = s.domains.some(function(d) { return d.domain.trim(); });",
|
|
558
523
|
" if(!hasDomain) return 'At least one domain is required.';",
|
|
559
524
|
" }",
|
|
560
525
|
" return '';",
|
|
561
526
|
" }",
|
|
562
|
-
//
|
|
563
|
-
" window.
|
|
564
|
-
"
|
|
565
|
-
"
|
|
566
|
-
"
|
|
567
|
-
"
|
|
568
|
-
"
|
|
569
|
-
"
|
|
570
|
-
" }",
|
|
571
|
-
" if(wizardState.currentStep < 5) {",
|
|
572
|
-
" wizardState.currentStep++;",
|
|
573
|
-
" if(wizardState.currentStep > wizardState.highestStep) wizardState.highestStep = wizardState.currentStep;",
|
|
574
|
-
" renderWizardStep();",
|
|
575
|
-
" }",
|
|
576
|
-
" };",
|
|
577
|
-
// Navigate to previous step.
|
|
578
|
-
" window.wizardBack = function() {",
|
|
579
|
-
" if(wizardState.currentStep > 1) {",
|
|
580
|
-
" wizardState.currentStep--;",
|
|
581
|
-
" renderWizardStep();",
|
|
582
|
-
" }",
|
|
527
|
+
// Open the profile builder wizard and reset state for a new profile.
|
|
528
|
+
" window.openWizard = function() {",
|
|
529
|
+
" profileWizard.state = {",
|
|
530
|
+
" editKey: null, editMode: false, profileName: '', baseProfile: '', strategy: 'none',",
|
|
531
|
+
" strategyFields: {}, flags: {}, hideSelector: '',",
|
|
532
|
+
" domains: [{ domain: '', provider: '', providerTag: '' }], description: ''",
|
|
533
|
+
" };",
|
|
534
|
+
" profileWizard.open({ title: 'New Provider Profile' });",
|
|
583
535
|
" };",
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
"
|
|
587
|
-
"
|
|
588
|
-
" if(targetStep > wizardState.highestStep) return;",
|
|
589
|
-
" if(targetStep > wizardState.currentStep) {",
|
|
590
|
-
" var err = validateStep();",
|
|
591
|
-
" if(err) {",
|
|
592
|
-
" var errDiv = document.getElementById('wizard-error');",
|
|
593
|
-
" errDiv.textContent = err;",
|
|
594
|
-
" errDiv.style.display = 'block';",
|
|
595
|
-
" return;",
|
|
596
|
-
" }",
|
|
597
|
-
" }",
|
|
598
|
-
" wizardState.currentStep = targetStep;",
|
|
599
|
-
" renderWizardStep();",
|
|
536
|
+
// Add another domain row to step 4 and re-render.
|
|
537
|
+
" function addWizardDomain() {",
|
|
538
|
+
" profileWizard.state.domains.push({ domain: '', provider: '', providerTag: '' });",
|
|
539
|
+
" renderProfileStep(profileWizard.getStep());",
|
|
600
540
|
" }",
|
|
601
541
|
// Save the profile to the server.
|
|
602
|
-
" window.saveProfile = function(andTest) {",
|
|
603
|
-
" var s =
|
|
542
|
+
" window.saveProfile = async function(andTest) {",
|
|
543
|
+
" var s = profileWizard.state;",
|
|
604
544
|
" var profile = { extends: s.baseProfile };",
|
|
605
545
|
" if(s.strategy !== 'none') {",
|
|
606
546
|
" var cs = { strategy: s.strategy };",
|
|
@@ -629,107 +569,627 @@ export function generateChannelsSubtabScript() {
|
|
|
629
569
|
" }",
|
|
630
570
|
" }",
|
|
631
571
|
" var body = { domains: domains, key: s.profileName, profile: profile };",
|
|
632
|
-
"
|
|
633
|
-
"
|
|
634
|
-
"
|
|
635
|
-
"
|
|
636
|
-
"
|
|
637
|
-
" .then(function(r) { return r.json(); })",
|
|
638
|
-
" .then(function(data) {",
|
|
639
|
-
" if(data.success) {",
|
|
640
|
-
" closeWizard();",
|
|
641
|
-
" if(andTest && s.domains[0] && s.domains[0].domain) {",
|
|
642
|
-
" startProfileTest(s.domains[0].domain);",
|
|
643
|
-
" } else {",
|
|
644
|
-
" location.reload();",
|
|
645
|
-
" }",
|
|
646
|
-
" } else {",
|
|
647
|
-
" var errDiv = document.getElementById('wizard-error');",
|
|
648
|
-
" errDiv.textContent = data.error || 'Failed to save profile.';",
|
|
649
|
-
" errDiv.style.display = 'block';",
|
|
650
|
-
" }",
|
|
651
|
-
" })",
|
|
652
|
-
" .catch(function() {",
|
|
653
|
-
" var errDiv = document.getElementById('wizard-error');",
|
|
654
|
-
" errDiv.textContent = 'Request failed.';",
|
|
655
|
-
" errDiv.style.display = 'block';",
|
|
572
|
+
" try {",
|
|
573
|
+
" var res = await fetch('/config/profiles', {",
|
|
574
|
+
" method: 'POST',",
|
|
575
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
576
|
+
" body: JSON.stringify(body)",
|
|
656
577
|
" });",
|
|
578
|
+
" var data = await res.json();",
|
|
579
|
+
" if(data.success) {",
|
|
580
|
+
" profileWizard.close();",
|
|
581
|
+
" if(data.panelHtml) { applyProfilePanel(data.panelHtml); }",
|
|
582
|
+
" if(andTest && s.domains[0] && s.domains[0].domain) {",
|
|
583
|
+
" startProfileTest(s.domains[0].domain);",
|
|
584
|
+
" }",
|
|
585
|
+
" } else {",
|
|
586
|
+
" profileWizard.setError(data.error || 'Failed to save profile.');",
|
|
587
|
+
" }",
|
|
588
|
+
" } catch(err) {",
|
|
589
|
+
" console.error('Profile save error:', err);",
|
|
590
|
+
" profileWizard.setError('Request failed.');",
|
|
591
|
+
" }",
|
|
657
592
|
" };",
|
|
658
593
|
// Test flow: open a browser to a URL with the profile applied and allow selector checking.
|
|
659
594
|
" var testSelectors = {};",
|
|
660
595
|
// Start a profile test by opening the browser to a URL.
|
|
661
|
-
" window.startProfileTest = function(url) {",
|
|
662
|
-
" var s =
|
|
596
|
+
" window.startProfileTest = async function(url) {",
|
|
597
|
+
" var s = profileWizard.state;",
|
|
663
598
|
" testSelectors = {};",
|
|
664
599
|
" if(s.strategy !== 'none' && s.strategyFields.matchSelector) testSelectors.matchSelector = s.strategyFields.matchSelector;",
|
|
665
600
|
" if(s.strategyFields.playSelector) testSelectors.playSelector = s.strategyFields.playSelector;",
|
|
666
601
|
" if(s.hideSelector) testSelectors.hideSelector = s.hideSelector;",
|
|
667
602
|
" var testUrl = url.indexOf('://') === -1 ? 'https://' + url : url;",
|
|
668
|
-
"
|
|
669
|
-
"
|
|
670
|
-
"
|
|
671
|
-
"
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
" .
|
|
675
|
-
"
|
|
676
|
-
"
|
|
677
|
-
"
|
|
678
|
-
"
|
|
679
|
-
"
|
|
680
|
-
"
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"
|
|
684
|
-
" .catch(function() { showToast('Failed to start test.', 'error'); });",
|
|
603
|
+
" try {",
|
|
604
|
+
" var res = await fetch('/config/profiles/test', {",
|
|
605
|
+
" method: 'POST',",
|
|
606
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
607
|
+
" body: JSON.stringify({ url: testUrl })",
|
|
608
|
+
" });",
|
|
609
|
+
" var data = await res.json();",
|
|
610
|
+
" if(data.success) {",
|
|
611
|
+
" document.getElementById('test-modal').style.display = 'flex';",
|
|
612
|
+
" document.getElementById('test-selector-results').style.display = 'none';",
|
|
613
|
+
" document.getElementById('test-check-btn').style.display = ",
|
|
614
|
+
" Object.keys(testSelectors).length > 0 ? '' : 'none';",
|
|
615
|
+
" } else {",
|
|
616
|
+
" showToast('Failed to start test: ' + (data.error || 'Unknown error.'), 'error');",
|
|
617
|
+
" }",
|
|
618
|
+
" } catch(e) { showToast('Failed to start test.', 'error'); }",
|
|
685
619
|
" };",
|
|
686
620
|
// Check CSS selectors against the live page.
|
|
687
|
-
" window.checkSelectors = function() {",
|
|
621
|
+
" window.checkSelectors = async function() {",
|
|
688
622
|
" if(Object.keys(testSelectors).length === 0) return;",
|
|
689
|
-
"
|
|
690
|
-
"
|
|
691
|
-
"
|
|
692
|
-
"
|
|
693
|
-
"
|
|
694
|
-
" .then(function(r) { return r.json(); })",
|
|
695
|
-
" .then(function(data) {",
|
|
696
|
-
" var resultsDiv = document.getElementById('test-selector-results');",
|
|
697
|
-
" if(!data.success || !data.results) {",
|
|
698
|
-
" resultsDiv.innerHTML = '<p style=\"color: var(--status-error-text);\">Check failed.</p>';",
|
|
699
|
-
" resultsDiv.style.display = 'block';",
|
|
700
|
-
" return;",
|
|
701
|
-
" }",
|
|
702
|
-
" var html = '<table style=\"width: 100%; font-size: 13px; margin: 12px 0;\">';",
|
|
703
|
-
" var names = Object.keys(data.results);",
|
|
704
|
-
" for(var i = 0; i < names.length; i++) {",
|
|
705
|
-
" var name = names[i];",
|
|
706
|
-
" var r = data.results[name];",
|
|
707
|
-
" var color = r.valid && r.count > 0 ? 'var(--status-success-text)' : 'var(--status-error-text)';",
|
|
708
|
-
" var text = !r.valid ? 'Invalid selector' : r.count + ' element(s)';",
|
|
709
|
-
" html += '<tr><td style=\"padding: 4px 8px;\">' + esc(name) + '</td>';",
|
|
710
|
-
" html += '<td style=\"padding: 4px 8px; color: ' + color + ';\">' + text + '</td></tr>';",
|
|
711
|
-
" }",
|
|
712
|
-
" html += '</table>';",
|
|
713
|
-
" resultsDiv.innerHTML = html;",
|
|
714
|
-
" resultsDiv.style.display = 'block';",
|
|
715
|
-
" })",
|
|
716
|
-
" .catch(function() {",
|
|
717
|
-
" var resultsDiv = document.getElementById('test-selector-results');",
|
|
718
|
-
" resultsDiv.innerHTML = '<p style=\"color: var(--status-error-text);\">Check request failed.</p>';",
|
|
719
|
-
" resultsDiv.style.display = 'block';",
|
|
623
|
+
" try {",
|
|
624
|
+
" var res = await fetch('/config/profiles/test/check', {",
|
|
625
|
+
" method: 'POST',",
|
|
626
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
627
|
+
" body: JSON.stringify({ selectors: testSelectors })",
|
|
720
628
|
" });",
|
|
629
|
+
" var data = await res.json();",
|
|
630
|
+
" var resultsDiv = document.getElementById('test-selector-results');",
|
|
631
|
+
" if(!data.success || !data.results) {",
|
|
632
|
+
" resultsDiv.innerHTML = '<p style=\"color: var(--status-error-text);\">Check failed.</p>';",
|
|
633
|
+
" resultsDiv.style.display = 'block';",
|
|
634
|
+
" return;",
|
|
635
|
+
" }",
|
|
636
|
+
" var html = '<table style=\"width: 100%; font-size: 13px; margin: 12px 0;\">';",
|
|
637
|
+
" var names = Object.keys(data.results);",
|
|
638
|
+
" for(var i = 0; i < names.length; i++) {",
|
|
639
|
+
" var name = names[i];",
|
|
640
|
+
" var r = data.results[name];",
|
|
641
|
+
" var color = r.valid && r.count > 0 ? 'var(--status-success-text)' : 'var(--status-error-text)';",
|
|
642
|
+
" var text = !r.valid ? 'Invalid selector' : r.count + ' element(s)';",
|
|
643
|
+
" html += '<tr><td style=\"padding: 4px 8px;\">' + esc(name) + '</td>';",
|
|
644
|
+
" html += '<td style=\"padding: 4px 8px; color: ' + color + ';\">' + text + '</td></tr>';",
|
|
645
|
+
" }",
|
|
646
|
+
" html += '</table>';",
|
|
647
|
+
" resultsDiv.innerHTML = html;",
|
|
648
|
+
" resultsDiv.style.display = 'block';",
|
|
649
|
+
" } catch(e) {",
|
|
650
|
+
" var resultsDiv = document.getElementById('test-selector-results');",
|
|
651
|
+
" resultsDiv.innerHTML = '<p style=\"color: var(--status-error-text);\">Check request failed.</p>';",
|
|
652
|
+
" resultsDiv.style.display = 'block';",
|
|
653
|
+
" }",
|
|
721
654
|
" };",
|
|
722
655
|
// End the test flow and close the test dialog.
|
|
723
|
-
" window.endProfileTest = function() {",
|
|
724
|
-
"
|
|
725
|
-
"
|
|
726
|
-
"
|
|
727
|
-
"
|
|
728
|
-
" }
|
|
729
|
-
"
|
|
730
|
-
"
|
|
656
|
+
" window.endProfileTest = async function() {",
|
|
657
|
+
" try {",
|
|
658
|
+
" var res = await fetch('/config/profiles/test/done', { method: 'POST' });",
|
|
659
|
+
" var data = await res.json();",
|
|
660
|
+
" document.getElementById('test-modal').style.display = 'none';",
|
|
661
|
+
" if(data.panelHtml) { applyProfilePanel(data.panelHtml); }",
|
|
662
|
+
" } catch(err) {",
|
|
663
|
+
" console.error('End profile test error:', err);",
|
|
664
|
+
" document.getElementById('test-modal').style.display = 'none';",
|
|
665
|
+
" }",
|
|
666
|
+
" };",
|
|
667
|
+
// Read embedded JSON data for the browse modal. Provider list and guide URLs are embedded server-side. Channel state (lineup matching) is provided by
|
|
668
|
+
// the annotated discovery response, not by embedded data.
|
|
669
|
+
" var browseProviders = [];",
|
|
670
|
+
" var browseGuideUrls = {};",
|
|
671
|
+
" try {",
|
|
672
|
+
" var pEl = document.getElementById('browse-providers-data');",
|
|
673
|
+
" if(pEl) browseProviders = JSON.parse(pEl.textContent || '[]');",
|
|
674
|
+
" var gEl = document.getElementById('browse-guide-urls-data');",
|
|
675
|
+
" if(gEl) browseGuideUrls = JSON.parse(gEl.textContent || '{}');",
|
|
676
|
+
" } catch(e) {}",
|
|
677
|
+
// Browse Channels wizard. Two steps: Provider (picker grid) and Channels (discovery + channel list). Uses the shared wizard controller for step navigation
|
|
678
|
+
// and indicator updates. The state object tracks the selected provider slug and label, and the filtered provider list for the current session.
|
|
679
|
+
" var browseWizard = createWizardController({",
|
|
680
|
+
" contentId: 'browse-content',",
|
|
681
|
+
" modalId: 'browse-modal',",
|
|
682
|
+
" onRender: renderBrowseStep,",
|
|
683
|
+
" onValidate: function(step) { return (step === 1 && !browseWizard.state.slug) ? 'Select a provider.' : ''; },",
|
|
684
|
+
" stepCount: 2,",
|
|
685
|
+
" titleId: 'browse-title'",
|
|
686
|
+
" });",
|
|
687
|
+
// Open the browse modal. If only one provider is available (or in the filter), skip step 1 and open directly on step 2 with the provider pre-selected.
|
|
688
|
+
" window.openBrowseModal = function() {",
|
|
689
|
+
// Determine which providers to show. Filter to providers that are enabled in the provider filter if a filter is active.
|
|
690
|
+
" var enabledFilter = [];",
|
|
691
|
+
" var chips = document.querySelectorAll('.provider-chip');",
|
|
692
|
+
" for(var i = 0; i < chips.length; i++) {",
|
|
693
|
+
" var tag = chips[i].getAttribute('data-tag');",
|
|
694
|
+
" if(tag) enabledFilter.push(tag);",
|
|
695
|
+
" }",
|
|
696
|
+
" var available = browseProviders;",
|
|
697
|
+
" if(enabledFilter.length > 0) {",
|
|
698
|
+
" available = browseProviders.filter(function(p) { return enabledFilter.indexOf(p.slug) !== -1; });",
|
|
699
|
+
" }",
|
|
700
|
+
// If no discoverable providers match the filter, show all providers.
|
|
701
|
+
" if(available.length === 0) available = browseProviders;",
|
|
702
|
+
// If exactly one provider is available, skip the picker and go straight to channel discovery on step 2.
|
|
703
|
+
" if(available.length === 1) {",
|
|
704
|
+
" browseWizard.state = { available: available, label: available[0].label, slug: available[0].slug };",
|
|
705
|
+
" browseWizard.open({ highestStep: 2, step: 2 });",
|
|
706
|
+
" return;",
|
|
707
|
+
" }",
|
|
708
|
+
" browseWizard.state = { available: available, label: null, slug: null };",
|
|
709
|
+
" browseWizard.open();",
|
|
710
|
+
" };",
|
|
711
|
+
// Select a provider from the picker grid and advance to step 2.
|
|
712
|
+
" function selectBrowseProvider(slug, label) {",
|
|
713
|
+
" browseWizard.state.slug = slug;",
|
|
714
|
+
" browseWizard.state.label = label;",
|
|
715
|
+
" browseWizard.next();",
|
|
716
|
+
" }",
|
|
717
|
+
// Render the current browse step. Step 1: provider picker grid. Step 2: channel discovery and list.
|
|
718
|
+
" function renderBrowseStep(step) {",
|
|
719
|
+
" var content = document.getElementById('browse-content');",
|
|
720
|
+
// Show/hide navigation buttons. Back is only available on step 2 when there are multiple providers.
|
|
721
|
+
" if(step > 1 && browseWizard.state.available.length > 1) { browseWizard.show('browse-back'); }",
|
|
722
|
+
" else { browseWizard.hide('browse-back'); }",
|
|
723
|
+
// Reset title and apply button on each render.
|
|
724
|
+
" browseWizard.setTitle('Browse Channels');",
|
|
725
|
+
" browseWizard.hide('browse-add-btn');",
|
|
726
|
+
// Step 1: Provider picker grid. Clear the provider selection so the user must pick again (prevents the step indicator from jumping to step 2 with a stale
|
|
727
|
+
// provider after navigating back).
|
|
728
|
+
" if(step === 1) {",
|
|
729
|
+
" browseWizard.state.slug = null;",
|
|
730
|
+
" browseWizard.state.label = null;",
|
|
731
|
+
" var available = browseWizard.state.available;",
|
|
732
|
+
" var html = '<p class=\"wizard-hint\">Select a provider to see its available channels.</p>';",
|
|
733
|
+
" html += '<div class=\"wizard-provider-grid\">';",
|
|
734
|
+
" for(var j = 0; j < available.length; j++) {",
|
|
735
|
+
" var p = available[j];",
|
|
736
|
+
" html += '<div class=\"wizard-provider-card\" data-slug=\"' + esc(p.slug) + '\" data-label=\"' + esc(p.label) + '\">';",
|
|
737
|
+
" html += providerIconHtml(p.domain, esc(p.label), 'provider-icon', 'provider-icon-text', 'both', p.iconUrl || '');",
|
|
738
|
+
" html += '</div>';",
|
|
739
|
+
" }",
|
|
740
|
+
" html += '</div>';",
|
|
741
|
+
" content.innerHTML = html;",
|
|
742
|
+
// Attach click handlers to provider cards programmatically. The handler reads slug and label from data attributes, avoiding inline onclick and global scope.
|
|
743
|
+
" var cards = content.querySelectorAll('.wizard-provider-card');",
|
|
744
|
+
" for(var ci = 0; ci < cards.length; ci++) {",
|
|
745
|
+
" cards[ci].addEventListener('click', function() {",
|
|
746
|
+
" selectBrowseProvider(this.getAttribute('data-slug'), this.getAttribute('data-label'));",
|
|
747
|
+
" });",
|
|
748
|
+
" }",
|
|
749
|
+
" return;",
|
|
750
|
+
" }",
|
|
751
|
+
// Step 2: Fetch discovered channels and render the channel list.
|
|
752
|
+
" var slug = browseWizard.state.slug;",
|
|
753
|
+
" var label = browseWizard.state.label;",
|
|
754
|
+
// Show a spinner while fetching channels.
|
|
755
|
+
" content.innerHTML = '<div class=\"wizard-spinner\">Discovering channels from ' + esc(label) + '...</div>';",
|
|
756
|
+
// Fetch discovered channels from the provider endpoint.
|
|
757
|
+
" (async function() {",
|
|
758
|
+
" try {",
|
|
759
|
+
" var r = await fetch('/providers/' + encodeURIComponent(slug) + '/channels?lineup=true');",
|
|
760
|
+
" if(!r.ok) { var d = await r.json(); throw new Error(d.error || 'Discovery failed.'); }",
|
|
761
|
+
" var channels = await r.json();",
|
|
762
|
+
" if(browseWizard.state.slug !== slug) return;",
|
|
763
|
+
" renderBrowseChannelList(channels, slug, label);",
|
|
764
|
+
" } catch(err) {",
|
|
765
|
+
" if(browseWizard.state.slug !== slug) return;",
|
|
766
|
+
" content.innerHTML = '<div class=\"wizard-empty\"><div class=\"wizard-empty-title\">Could not discover channels.</div>' +",
|
|
767
|
+
" esc(err.message || 'Check that the browser is running and you are signed in to ' + label + '.') + '</div>';",
|
|
768
|
+
" }",
|
|
769
|
+
" })();",
|
|
770
|
+
" }",
|
|
771
|
+
// Render the channel list with three-state checkboxes. Each discovered channel carries an optional lineup field from the server's annotation layer.
|
|
772
|
+
// When lineup is present, the channel exists in the user's lineup. When lineup.currentTag matches the browsed provider slug, the channel is "current"
|
|
773
|
+
// (checked). When lineup is present but the tag differs, the channel is "switch" (indeterminate). When lineup is absent, the channel is "new" (empty).
|
|
774
|
+
" function renderBrowseChannelList(channels, slug, label) {",
|
|
775
|
+
" if(!channels || channels.length === 0) {",
|
|
776
|
+
" document.getElementById('browse-content').innerHTML =",
|
|
777
|
+
" '<div class=\"wizard-empty\"><div class=\"wizard-empty-title\">No channels found.</div>' +",
|
|
778
|
+
" 'Make sure you are signed in to ' + esc(label) + ' in the browser.</div>';",
|
|
779
|
+
" return;",
|
|
780
|
+
" }",
|
|
781
|
+
// Build the channel list HTML with hint, legend, search filter, and three-state checkboxes.
|
|
782
|
+
" var html = '';",
|
|
783
|
+
" html += '<p class=\"wizard-hint\">Check to add or switch to this provider. Uncheck to remove. Changes take effect when you click Apply.</p>';",
|
|
784
|
+
" html += '<div class=\"browse-toolbar\">';",
|
|
785
|
+
" html += '<label class=\"browse-select-all\">';",
|
|
786
|
+
" html += '<input type=\"checkbox\" id=\"browse-select-all-cb\">';",
|
|
787
|
+
" html += 'Select All</label>';",
|
|
788
|
+
" html += '<input type=\"text\" class=\"browse-search\" id=\"browse-search\" placeholder=\"Filter channels...\">';",
|
|
789
|
+
" html += '</div>';",
|
|
790
|
+
" html += '<div class=\"browse-legend\">';",
|
|
791
|
+
" html += '<span class=\"browse-legend-item\"><input type=\"checkbox\" disabled> New channel</span>';",
|
|
792
|
+
" html += '<span class=\"browse-legend-item\"><input type=\"checkbox\" class=\"browse-legend-indeterminate\" disabled> Available via another provider</span>';",
|
|
793
|
+
" html += '<span class=\"browse-legend-item\"><input type=\"checkbox\" checked disabled> Active on this provider</span>';",
|
|
794
|
+
" html += '</div>';",
|
|
795
|
+
" html += '<div class=\"browse-channel-list\" id=\"browse-channel-list\">';",
|
|
796
|
+
// Render each discovered channel. The lineup field from the server annotation determines the checkbox state.
|
|
797
|
+
" for(var i = 0; i < channels.length; i++) {",
|
|
798
|
+
" var ch = channels[i];",
|
|
799
|
+
" var lu = ch.lineup;",
|
|
800
|
+
// Determine the channel's state from the server-provided lineup annotation. Four states: new (no lineup data), disabled (predefined but disabled),
|
|
801
|
+
// switch (exists via a different provider), current (already using this provider).
|
|
802
|
+
" var state = 'new';",
|
|
803
|
+
" var stateLabel = '';",
|
|
804
|
+
" if(lu) {",
|
|
805
|
+
" if(!lu.enabled) {",
|
|
806
|
+
" state = 'disabled';",
|
|
807
|
+
" stateLabel = 'Disabled';",
|
|
808
|
+
" } else if(lu.currentTag === slug) {",
|
|
809
|
+
" state = 'current';",
|
|
810
|
+
" stateLabel = lu.source === 'predefined' ? 'Predefined' : 'User channel';",
|
|
811
|
+
" } else {",
|
|
812
|
+
" state = 'switch';",
|
|
813
|
+
" stateLabel = 'via ' + lu.currentProvider;",
|
|
814
|
+
" }",
|
|
815
|
+
" }",
|
|
816
|
+
" var itemClass = 'browse-channel-item browse-state-' + state;",
|
|
817
|
+
" var searchName = ((lu && lu.displayName) ? lu.displayName : ch.name).toLowerCase();",
|
|
818
|
+
" html += '<label class=\"' + itemClass + '\" data-name=\"' + esc(searchName).replace(/\"/g, '"') + '\">';",
|
|
819
|
+
// Checkbox with data attributes for state tracking and submission.
|
|
820
|
+
" html += '<input type=\"checkbox\" class=\"browse-channel-cb\"';",
|
|
821
|
+
" html += ' data-name=\"' + esc(ch.name).replace(/\"/g, '"') + '\"';",
|
|
822
|
+
" html += ' data-selector=\"' + esc(ch.channelSelector).replace(/\"/g, '"') + '\"';",
|
|
823
|
+
" html += ' data-original=\"' + state + '\"';",
|
|
824
|
+
" if(lu) html += ' data-canonical=\"' + esc(lu.canonicalKey) + '\" data-has-alternatives=\"' + (lu.hasAlternatives ? 'true' : 'false') + '\"';",
|
|
825
|
+
" if(ch.stationId) html += ' data-station-id=\"' + esc(ch.stationId) + '\"';",
|
|
826
|
+
" if(state === 'current') html += ' checked';",
|
|
827
|
+
" html += '>';",
|
|
828
|
+
// Channel name with optional logo and affiliate. When the channel matches a lineup entry, use the predefined display name so the browse modal shows the
|
|
829
|
+
// same names as the channels table. For new channels (no lineup match), use the raw discovery name.
|
|
830
|
+
" var channelName = (lu && lu.displayName) ? lu.displayName : ch.name;",
|
|
831
|
+
" html += '<span class=\"browse-channel-name\">';",
|
|
832
|
+
" html += channelDisplayHtml(lu ? lu.logoUrl || '' : '', esc(channelName), 'browse-channel-logo', 'browse-channel-text', 'both');",
|
|
833
|
+
" if(ch.affiliate) html += ' <span class=\"browse-affiliate\">(' + esc(ch.affiliate) + ')</span>';",
|
|
834
|
+
" html += '</span>';",
|
|
835
|
+
// Right side: state label and tier badge.
|
|
836
|
+
" html += '<span class=\"browse-channel-meta\">';",
|
|
837
|
+
" if(stateLabel) html += '<span class=\"browse-state-label\">' + esc(stateLabel) + '</span>';",
|
|
838
|
+
" if(ch.tier === 'free') html += '<span class=\"browse-tier-badge tier-free\">Free</span>';",
|
|
839
|
+
" html += '</span>';",
|
|
840
|
+
" html += '</label>';",
|
|
841
|
+
" }",
|
|
842
|
+
" html += '</div>';",
|
|
843
|
+
" document.getElementById('browse-content').innerHTML = html;",
|
|
844
|
+
" browseWizard.setTitle('Browse Channels <span class=\"browse-header-count\">(' + esc(label) + ' \\u2014 ' + channels.length + ' channels)</span>');",
|
|
845
|
+
// Set indeterminate state on checkboxes for channels that exist via a different provider. The indeterminate property cannot be set via HTML attributes —
|
|
846
|
+
// it must be set via JavaScript after the elements are in the DOM.
|
|
847
|
+
" var switchCbs = document.querySelectorAll('.browse-channel-cb[data-original=\"switch\"]');",
|
|
848
|
+
" for(var j = 0; j < switchCbs.length; j++) switchCbs[j].indeterminate = true;",
|
|
849
|
+
// Set the legend's indeterminate checkbox visual.
|
|
850
|
+
" var legendInd = document.querySelector('.browse-legend-indeterminate');",
|
|
851
|
+
" if(legendInd) legendInd.indeterminate = true;",
|
|
852
|
+
// Attach event handlers programmatically to keep them scoped to the IIFE closure.
|
|
853
|
+
" var selectAllCb = document.getElementById('browse-select-all-cb');",
|
|
854
|
+
" if(selectAllCb) selectAllCb.addEventListener('change', function() { toggleBrowseSelectAll(this.checked); });",
|
|
855
|
+
" var searchInput = document.getElementById('browse-search');",
|
|
856
|
+
" if(searchInput) searchInput.addEventListener('input', filterBrowseChannels);",
|
|
857
|
+
" var channelCbs = document.querySelectorAll('.browse-channel-cb');",
|
|
858
|
+
" for(var ci = 0; ci < channelCbs.length; ci++) {",
|
|
859
|
+
" channelCbs[ci].addEventListener('change', function() { onBrowseCheckboxChange(this); });",
|
|
860
|
+
" }",
|
|
861
|
+
// Show the apply button and update counts.
|
|
862
|
+
" browseWizard.show('browse-add-btn');",
|
|
863
|
+
" updateBrowseCount();",
|
|
864
|
+
" }",
|
|
865
|
+
// Handle checkbox state changes. On uncheck, the visual depends on the channel's state: switch channels restore to indeterminate (they arrived that
|
|
866
|
+
// way). Current channels show indeterminate if alternatives exist (channel persists elsewhere) or empty if not (channel will be disabled). New and
|
|
867
|
+
// disabled channels stay empty on uncheck.
|
|
868
|
+
" function onBrowseCheckboxChange(cb) {",
|
|
869
|
+
" if(!cb.checked) {",
|
|
870
|
+
" var original = cb.getAttribute('data-original');",
|
|
871
|
+
" var hasAlt = cb.getAttribute('data-has-alternatives') === 'true';",
|
|
872
|
+
" if((original === 'switch') || ((original === 'current') && hasAlt)) {",
|
|
873
|
+
" cb.indeterminate = true;",
|
|
874
|
+
" }",
|
|
875
|
+
" }",
|
|
876
|
+
" updateBrowseCount();",
|
|
877
|
+
" }",
|
|
878
|
+
// Toggle all actionable checkboxes. Select-all checks all channels and clears indeterminate. Deselect-all unchecks new and disabled channels, and restores
|
|
879
|
+
// switch channels and current channels with alternatives to indeterminate (indicating the channel exists but is not selected for this provider).
|
|
880
|
+
" function toggleBrowseSelectAll(checked) {",
|
|
881
|
+
" var cbs = document.querySelectorAll('.browse-channel-cb');",
|
|
882
|
+
" for(var i = 0; i < cbs.length; i++) {",
|
|
883
|
+
" var cb = cbs[i];",
|
|
884
|
+
" var original = cb.getAttribute('data-original');",
|
|
885
|
+
" var row = cb.closest('.browse-channel-item');",
|
|
886
|
+
" if(row && row.style.display === 'none') continue;",
|
|
887
|
+
" if(checked) {",
|
|
888
|
+
" cb.checked = true;",
|
|
889
|
+
" cb.indeterminate = false;",
|
|
890
|
+
" } else {",
|
|
891
|
+
" var hasAlt = cb.getAttribute('data-has-alternatives') === 'true';",
|
|
892
|
+
" cb.checked = false;",
|
|
893
|
+
" cb.indeterminate = (original === 'switch') || ((original === 'current') && hasAlt);",
|
|
894
|
+
" }",
|
|
895
|
+
" }",
|
|
896
|
+
" updateBrowseCount();",
|
|
897
|
+
" }",
|
|
898
|
+
// Filter the channel list by search text. Hides rows whose channel name does not contain the search term.
|
|
899
|
+
" function filterBrowseChannels() {",
|
|
900
|
+
" var query = (document.getElementById('browse-search').value || '').toLowerCase();",
|
|
901
|
+
" var rows = document.querySelectorAll('.browse-channel-item');",
|
|
902
|
+
" for(var i = 0; i < rows.length; i++) {",
|
|
903
|
+
" var name = rows[i].getAttribute('data-name') || '';",
|
|
904
|
+
" rows[i].style.display = (!query || name.indexOf(query) !== -1) ? '' : 'none';",
|
|
905
|
+
" }",
|
|
906
|
+
" }",
|
|
907
|
+
// Update the apply button text with categorized action counts. Computes add, switch, and remove counts based on checkbox state vs data-original.
|
|
908
|
+
" function updateBrowseCount() {",
|
|
909
|
+
" var cbs = document.querySelectorAll('.browse-channel-cb');",
|
|
910
|
+
" var addCount = 0;",
|
|
911
|
+
" var switchCount = 0;",
|
|
912
|
+
" var removeCount = 0;",
|
|
913
|
+
" for(var i = 0; i < cbs.length; i++) {",
|
|
914
|
+
" var cb = cbs[i];",
|
|
915
|
+
" var original = cb.getAttribute('data-original');",
|
|
916
|
+
" var row = cb.closest('.browse-channel-item');",
|
|
917
|
+
" if(row && row.style.display === 'none') continue;",
|
|
918
|
+
" if((original === 'new' || original === 'disabled') && cb.checked) addCount++;",
|
|
919
|
+
" if(original === 'switch' && cb.checked && !cb.indeterminate) switchCount++;",
|
|
920
|
+
" if(original === 'current' && !cb.checked) removeCount++;",
|
|
921
|
+
" }",
|
|
922
|
+
" var btn = document.getElementById('browse-add-btn');",
|
|
923
|
+
" var total = addCount + switchCount + removeCount;",
|
|
924
|
+
" var parts = [];",
|
|
925
|
+
" if(addCount > 0) parts.push('add ' + addCount);",
|
|
926
|
+
" if(switchCount > 0) parts.push('switch ' + switchCount);",
|
|
927
|
+
" if(removeCount > 0) parts.push('remove ' + removeCount);",
|
|
928
|
+
" btn.textContent = total > 0 ? 'Apply (' + parts.join(', ') + ')' : 'Apply';",
|
|
929
|
+
" btn.disabled = total === 0;",
|
|
930
|
+
// Update select-all checkbox state.
|
|
931
|
+
" var allCbs = document.querySelectorAll('.browse-channel-cb');",
|
|
932
|
+
" var allChecked = true;",
|
|
933
|
+
" var visibleCount = 0;",
|
|
934
|
+
" for(var j = 0; j < allCbs.length; j++) {",
|
|
935
|
+
" var r = allCbs[j].closest('.browse-channel-item');",
|
|
936
|
+
" if(r && r.style.display === 'none') continue;",
|
|
937
|
+
" visibleCount++;",
|
|
938
|
+
" if(!allCbs[j].checked) allChecked = false;",
|
|
939
|
+
" }",
|
|
940
|
+
" var selectAll = document.getElementById('browse-select-all-cb');",
|
|
941
|
+
" if(selectAll) selectAll.checked = (visibleCount > 0) && allChecked;",
|
|
942
|
+
" }",
|
|
943
|
+
// Submit channel changes via the modify endpoint. Collects action types: add (new channels checked), enable (disabled channels checked), switch
|
|
944
|
+
// remove (current channels unchecked). Each entry includes an action field so the server can handle them appropriately.
|
|
945
|
+
" window.submitBrowseChannels = function() {",
|
|
946
|
+
" var cbs = document.querySelectorAll('.browse-channel-cb');",
|
|
947
|
+
" var guideUrl = browseGuideUrls[browseWizard.state.slug] || '';",
|
|
948
|
+
" var entries = [];",
|
|
949
|
+
" for(var i = 0; i < cbs.length; i++) {",
|
|
950
|
+
" var cb = cbs[i];",
|
|
951
|
+
" var original = cb.getAttribute('data-original');",
|
|
952
|
+
" var row = cb.closest('.browse-channel-item');",
|
|
953
|
+
" if(row && row.style.display === 'none') continue;",
|
|
954
|
+
" var action = null;",
|
|
955
|
+
" if(original === 'new' && cb.checked) action = 'add';",
|
|
956
|
+
" if(original === 'disabled' && cb.checked) action = 'enable';",
|
|
957
|
+
" if(original === 'switch' && cb.checked && !cb.indeterminate) action = 'switch';",
|
|
958
|
+
" if(original === 'current' && !cb.checked) action = 'remove';",
|
|
959
|
+
" if(!action) continue;",
|
|
960
|
+
" var entry = {",
|
|
961
|
+
" action: action,",
|
|
962
|
+
" canonicalKey: cb.getAttribute('data-canonical') || '',",
|
|
963
|
+
" channelSelector: cb.getAttribute('data-selector'),",
|
|
964
|
+
" name: cb.getAttribute('data-name'),",
|
|
965
|
+
" providerSlug: browseWizard.state.slug,",
|
|
966
|
+
" url: guideUrl",
|
|
967
|
+
" };",
|
|
968
|
+
" var sid = cb.getAttribute('data-station-id');",
|
|
969
|
+
" if(sid) entry.stationId = sid;",
|
|
970
|
+
" entries.push(entry);",
|
|
971
|
+
" }",
|
|
972
|
+
" if(entries.length === 0) return;",
|
|
973
|
+
// Disable the button to prevent double submission.
|
|
974
|
+
" var btn = document.getElementById('browse-add-btn');",
|
|
975
|
+
" btn.disabled = true;",
|
|
976
|
+
" btn.textContent = 'Applying...';",
|
|
977
|
+
" (async function() {",
|
|
978
|
+
" try {",
|
|
979
|
+
" var res = await fetch('/config/channels/modify', {",
|
|
980
|
+
" method: 'POST',",
|
|
981
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
982
|
+
" body: JSON.stringify({ channels: entries })",
|
|
983
|
+
" });",
|
|
984
|
+
" var data = await res.json();",
|
|
985
|
+
" if(data.success) {",
|
|
986
|
+
" browseWizard.close();",
|
|
987
|
+
" showToast(data.message || 'Channels added.', 'success');",
|
|
988
|
+
" if(data.patch) { applyChannelPatch(data.patch); }",
|
|
989
|
+
" } else {",
|
|
990
|
+
" btn.disabled = false;",
|
|
991
|
+
" updateBrowseCount();",
|
|
992
|
+
" showToast('Failed to add channels: ' + (data.error || 'Unknown error.'), 'error');",
|
|
993
|
+
" }",
|
|
994
|
+
" } catch(err) {",
|
|
995
|
+
" console.error('Browse channels error:', err);",
|
|
996
|
+
" btn.disabled = false;",
|
|
997
|
+
" updateBrowseCount();",
|
|
998
|
+
" showToast('Failed to add channels.', 'error');",
|
|
999
|
+
" }",
|
|
1000
|
+
" })();",
|
|
1001
|
+
" };",
|
|
1002
|
+
// Read embedded provider data for the setup wizard. Each entry includes domain, iconUrl, displayName, tag, and enabled state from the server.
|
|
1003
|
+
" var setupProviders = [];",
|
|
1004
|
+
" try {",
|
|
1005
|
+
" var spEl = document.getElementById('setup-providers-data');",
|
|
1006
|
+
" if(spEl) setupProviders = JSON.parse(spEl.textContent || '[]');",
|
|
1007
|
+
" } catch(e) {}",
|
|
1008
|
+
// Validate the current setup step. Step 1 collects selected providers, validates at least one is selected, and saves the provider filter to the server
|
|
1009
|
+
// asynchronously. Returns a Promise that resolves to an error string or empty string. Step 2+ always passes (no validation needed).
|
|
1010
|
+
" async function validateSetupStep(step) {",
|
|
1011
|
+
" if(step === 1) {",
|
|
1012
|
+
" var selected = [];",
|
|
1013
|
+
" var cbs = document.querySelectorAll('.setup-provider-cb:checked');",
|
|
1014
|
+
" for(var i = 0; i < cbs.length; i++) selected.push(cbs[i].value);",
|
|
1015
|
+
" if(selected.length === 0) return 'Select at least one provider.';",
|
|
1016
|
+
" setupWizard.state.selectedProviders = selected;",
|
|
1017
|
+
" setupWizard.state.authIndex = 0;",
|
|
1018
|
+
" try {",
|
|
1019
|
+
" await fetch('/config/provider-filter', {",
|
|
1020
|
+
" method: 'POST',",
|
|
1021
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
1022
|
+
" body: JSON.stringify({ enabledProviders: selected })",
|
|
1023
|
+
" });",
|
|
1024
|
+
" return '';",
|
|
1025
|
+
" } catch(e) { return 'Failed to save provider selection.'; }",
|
|
1026
|
+
" }",
|
|
1027
|
+
" return '';",
|
|
1028
|
+
" }",
|
|
1029
|
+
// Provider Setup wizard. Uses the shared wizard controller for step navigation and indicator updates. The state object tracks selected providers and
|
|
1030
|
+
// the current auth index for the sequential sign-in flow.
|
|
1031
|
+
" var setupWizard = createWizardController({",
|
|
1032
|
+
" contentId: 'setup-content',",
|
|
1033
|
+
" errorId: 'setup-error',",
|
|
1034
|
+
" modalId: 'setup-modal',",
|
|
1035
|
+
" onClose: function() {",
|
|
1036
|
+
" if(setupWizard.getStep() === 2) { fetch('/auth/done', { method: 'POST' }).catch(function() {}); }",
|
|
1037
|
+
" },",
|
|
1038
|
+
" onRender: renderSetupStep,",
|
|
1039
|
+
" onValidate: validateSetupStep,",
|
|
1040
|
+
" stepCount: 3,",
|
|
1041
|
+
" stepsId: 'setup-steps',",
|
|
1042
|
+
" titleId: 'setup-title'",
|
|
1043
|
+
" });",
|
|
1044
|
+
// Open the setup wizard and render the first step. Pre-populate selected providers from the embedded enabled state so re-running the wizard shows the
|
|
1045
|
+
// current provider filter as pre-checked.
|
|
1046
|
+
" window.openSetupWizard = function() {",
|
|
1047
|
+
" var preSelected = setupProviders.filter(function(p) { return p.enabled; }).map(function(p) { return p.tag; });",
|
|
1048
|
+
" setupWizard.state = { selectedProviders: preSelected, authIndex: 0 };",
|
|
1049
|
+
" setupWizard.open();",
|
|
1050
|
+
" };",
|
|
1051
|
+
// Skip the entire setup flow and mark as completed.
|
|
1052
|
+
" window.skipSetup = async function() {",
|
|
1053
|
+
" try { await fetch('/config/channels/setup-completed', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); }",
|
|
1054
|
+
" catch(e) {}",
|
|
1055
|
+
" setupWizard.close();",
|
|
1056
|
+
" };",
|
|
1057
|
+
// Authenticate the current provider in step 2. Opens the browser and shows a waiting prompt.
|
|
1058
|
+
" async function setupAuthProvider() {",
|
|
1059
|
+
" var slug = setupWizard.state.selectedProviders[setupWizard.state.authIndex];",
|
|
1060
|
+
" var provider = setupProviders.find(function(p) { return p.tag === slug; });",
|
|
1061
|
+
" var url = provider && provider.domain ? 'https://' + provider.domain + '/' : '';",
|
|
1062
|
+
" if(!url) { setupAdvanceAuth(); return; }",
|
|
1063
|
+
" try {",
|
|
1064
|
+
" var res = await fetch('/auth/login', {",
|
|
1065
|
+
" method: 'POST',",
|
|
1066
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
1067
|
+
" body: JSON.stringify({ url: url })",
|
|
731
1068
|
" });",
|
|
1069
|
+
" var data = await res.json();",
|
|
1070
|
+
" if(data.success) renderSetupAuthWaiting();",
|
|
1071
|
+
" else showToast('Failed to start login: ' + (data.error || 'Unknown error.'), 'error');",
|
|
1072
|
+
" } catch(e) { showToast('Failed to start login.', 'error'); }",
|
|
1073
|
+
" }",
|
|
1074
|
+
// Mark the current provider auth as done and advance to the next.
|
|
1075
|
+
" function setupAuthDone() {",
|
|
1076
|
+
" fetch('/auth/done', { method: 'POST' }).catch(function() {});",
|
|
1077
|
+
" setupAdvanceAuth();",
|
|
1078
|
+
" }",
|
|
1079
|
+
// Skip the current provider auth and advance.
|
|
1080
|
+
" function setupAuthSkip() {",
|
|
1081
|
+
" fetch('/auth/done', { method: 'POST' }).catch(function() {});",
|
|
1082
|
+
" setupAdvanceAuth();",
|
|
1083
|
+
" }",
|
|
1084
|
+
// Advance to the next provider or to step 3 if all are done.
|
|
1085
|
+
" function setupAdvanceAuth() {",
|
|
1086
|
+
" setupWizard.state.authIndex++;",
|
|
1087
|
+
" if(setupWizard.state.authIndex >= setupWizard.state.selectedProviders.length) {",
|
|
1088
|
+
" setupWizard.next();",
|
|
1089
|
+
" } else {",
|
|
1090
|
+
" renderSetupStep(setupWizard.getStep());",
|
|
1091
|
+
" }",
|
|
1092
|
+
" }",
|
|
1093
|
+
// Finish the setup wizard. Mark as completed and refresh the channel table.
|
|
1094
|
+
" window.finishSetup = async function() {",
|
|
1095
|
+
" try {",
|
|
1096
|
+
" var res = await fetch('/config/channels/setup-completed', { method: 'POST', headers: { 'Content-Type': 'application/json' } });",
|
|
1097
|
+
" var data = await res.json();",
|
|
1098
|
+
" setupWizard.close();",
|
|
1099
|
+
" if(data.patch) { applyChannelPatch(data.patch); }",
|
|
1100
|
+
" } catch(err) { console.error('Setup finish error:', err); setupWizard.close(); }",
|
|
732
1101
|
" };",
|
|
1102
|
+
// Render the auth waiting state - shows a message while the user authenticates in Chrome.
|
|
1103
|
+
" function renderSetupAuthWaiting() {",
|
|
1104
|
+
" var slug = setupWizard.state.selectedProviders[setupWizard.state.authIndex];",
|
|
1105
|
+
" var provider = setupProviders.find(function(p) { return p.tag === slug; });",
|
|
1106
|
+
" var label = provider ? provider.displayName : slug;",
|
|
1107
|
+
" var content = document.getElementById('setup-content');",
|
|
1108
|
+
" content.innerHTML = '<div class=\"wizard-spinner\">Complete authentication for ' + esc(label) +",
|
|
1109
|
+
" ' in the Chrome browser window...</div>' +",
|
|
1110
|
+
" '<div class=\"wizard-step-centered-compact\">' +",
|
|
1111
|
+
" '<button type=\"button\" class=\"btn btn-primary\" data-action=\"auth-done\">Done</button> ' +",
|
|
1112
|
+
" '<button type=\"button\" class=\"btn btn-secondary\" data-action=\"auth-skip\">Skip</button></div>';",
|
|
1113
|
+
// Attach handlers to the auth waiting buttons programmatically.
|
|
1114
|
+
" var waitActions = content.querySelectorAll('[data-action]');",
|
|
1115
|
+
" for(var wi = 0; wi < waitActions.length; wi++) {",
|
|
1116
|
+
" (function(btn) {",
|
|
1117
|
+
" var action = btn.getAttribute('data-action');",
|
|
1118
|
+
" if(action === 'auth-done') { btn.onclick = function() { setupAuthDone(); }; }",
|
|
1119
|
+
" else if(action === 'auth-skip') { btn.onclick = function() { setupAuthSkip(); }; }",
|
|
1120
|
+
" })(waitActions[wi]);",
|
|
1121
|
+
" }",
|
|
1122
|
+
" }",
|
|
1123
|
+
// Render the current setup step content and update navigation buttons. The controller handles step indicator updates and error clearing.
|
|
1124
|
+
" function renderSetupStep(step) {",
|
|
1125
|
+
" var content = document.getElementById('setup-content');",
|
|
1126
|
+
// Show/hide navigation buttons based on the current step.
|
|
1127
|
+
" if(step > 1) { setupWizard.show('setup-back'); } else { setupWizard.hide('setup-back'); }",
|
|
1128
|
+
" if(step < 3) { setupWizard.show('setup-next'); setupWizard.show('setup-skip'); }",
|
|
1129
|
+
" else { setupWizard.hide('setup-next'); setupWizard.hide('setup-skip'); }",
|
|
1130
|
+
" if(step === 3) { setupWizard.show('setup-finish'); } else { setupWizard.hide('setup-finish'); }",
|
|
1131
|
+
" var html = '';",
|
|
1132
|
+
// Step 1: Provider selection with multi-select cards.
|
|
1133
|
+
" if(step === 1) {",
|
|
1134
|
+
" html += '<p class=\"wizard-hint\">Select the streaming services you subscribe to. " +
|
|
1135
|
+
"This determines which channels are available in your lineup.</p>';",
|
|
1136
|
+
" html += '<div class=\"wizard-provider-grid\">';",
|
|
1137
|
+
" for(var j = 0; j < setupProviders.length; j++) {",
|
|
1138
|
+
" var p = setupProviders[j];",
|
|
1139
|
+
" var checked = (setupWizard.state.selectedProviders.indexOf(p.tag) !== -1) ? ' checked' : '';",
|
|
1140
|
+
" html += '<label class=\"wizard-provider-card\">';",
|
|
1141
|
+
" html += '<input type=\"checkbox\" class=\"setup-provider-cb\" value=\"' + p.tag + '\"' + checked + '>';",
|
|
1142
|
+
" html += providerIconHtml(p.domain || '', esc(p.displayName), 'provider-icon', 'provider-icon-text', 'both', p.iconUrl || '');",
|
|
1143
|
+
" html += '</label>';",
|
|
1144
|
+
" }",
|
|
1145
|
+
" html += '</div>';",
|
|
1146
|
+
" }",
|
|
1147
|
+
// Step 2: Sequential authentication prompts.
|
|
1148
|
+
" if(step === 2) {",
|
|
1149
|
+
" var slug = setupWizard.state.selectedProviders[setupWizard.state.authIndex];",
|
|
1150
|
+
" var provider = setupProviders.find(function(p) { return p.tag === slug; });",
|
|
1151
|
+
" var label = provider ? provider.displayName : slug;",
|
|
1152
|
+
" var total = setupWizard.state.selectedProviders.length;",
|
|
1153
|
+
" var current = setupWizard.state.authIndex + 1;",
|
|
1154
|
+
" html += '<p class=\"wizard-hint\">Sign in to each provider in the Chrome browser window. " +
|
|
1155
|
+
"PrismCast uses your login session to access live streams.</p>';",
|
|
1156
|
+
" html += '<div class=\"wizard-step-centered\">';",
|
|
1157
|
+
" html += '<p class=\"wizard-step-provider-name\">' + esc(label) + '</p>';",
|
|
1158
|
+
" html += '<p class=\"wizard-step-provider-count\">Provider ' + current + ' of ' + total + '</p>';",
|
|
1159
|
+
" html += '<button type=\"button\" class=\"btn btn-primary\" data-action=\"auth-start\">Sign In</button> ';",
|
|
1160
|
+
" html += '<button type=\"button\" class=\"btn btn-secondary\" data-action=\"auth-skip\">Skip</button>';",
|
|
1161
|
+
" html += '</div>';",
|
|
1162
|
+
" }",
|
|
1163
|
+
// Step 3: Summary and finish.
|
|
1164
|
+
" if(step === 3) {",
|
|
1165
|
+
" html += '<p class=\"wizard-hint\">Your providers are ready. Click Finish to complete setup. " +
|
|
1166
|
+
"You can browse and manage channels anytime using the Browse Channels button.</p>';",
|
|
1167
|
+
" html += '<div class=\"wizard-step-centered\">';",
|
|
1168
|
+
" html += '<p class=\"wizard-step-summary\">Selected ' + setupWizard.state.selectedProviders.length + ' provider' +",
|
|
1169
|
+
" (setupWizard.state.selectedProviders.length === 1 ? '' : 's') + '.</p>';",
|
|
1170
|
+
" html += '</div>';",
|
|
1171
|
+
" }",
|
|
1172
|
+
" content.innerHTML = html;",
|
|
1173
|
+
// Attach event handlers to dynamically-rendered buttons programmatically. Uses data-action attributes to identify buttons without inline onclick.
|
|
1174
|
+
" var actions = content.querySelectorAll('[data-action]');",
|
|
1175
|
+
" for(var ai = 0; ai < actions.length; ai++) {",
|
|
1176
|
+
" (function(btn) {",
|
|
1177
|
+
" var action = btn.getAttribute('data-action');",
|
|
1178
|
+
" if(action === 'auth-start') { btn.onclick = function() { setupAuthProvider(); }; }",
|
|
1179
|
+
" else if(action === 'auth-skip') { btn.onclick = function() { setupAuthSkip(); }; }",
|
|
1180
|
+
" else if(action === 'auth-done') { btn.onclick = function() { setupAuthDone(); }; }",
|
|
1181
|
+
" })(actions[ai]);",
|
|
1182
|
+
" }",
|
|
1183
|
+
" }",
|
|
1184
|
+
// Auto-show the setup wizard on first visit if setupCompleted is false.
|
|
1185
|
+
" var setupModal = document.getElementById('setup-modal');",
|
|
1186
|
+
" if(setupModal && setupModal.getAttribute('data-setup-completed') === 'false') {",
|
|
1187
|
+
" openSetupWizard();",
|
|
1188
|
+
" }",
|
|
1189
|
+
// Process channel logos and provider icons on page load. These shared functions are also called after DOM mutations (row insertion, chip rebuild) to ensure
|
|
1190
|
+
// dynamically added elements get the same icon rendering as the initial server-rendered content.
|
|
1191
|
+
" processChannelLogos();",
|
|
1192
|
+
" processProviderDisplays();",
|
|
733
1193
|
// Initialize channels subtab on load: hash > localStorage > default.
|
|
734
1194
|
" var initialSubtab = window.initialChannelsHashSubtab;",
|
|
735
1195
|
" if(!initialSubtab) {",
|