prismcast 1.4.2 → 1.5.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.
Files changed (178) hide show
  1. package/dist/app.js +18 -1
  2. package/dist/app.js.map +1 -1
  3. package/dist/browser/cdp.d.ts +2 -1
  4. package/dist/browser/cdp.js +44 -23
  5. package/dist/browser/cdp.js.map +1 -1
  6. package/dist/browser/channelSelection.d.ts +45 -1
  7. package/dist/browser/channelSelection.js +164 -27
  8. package/dist/browser/channelSelection.js.map +1 -1
  9. package/dist/browser/display.d.ts +0 -4
  10. package/dist/browser/display.js +0 -7
  11. package/dist/browser/display.js.map +1 -1
  12. package/dist/browser/index.d.ts +5 -0
  13. package/dist/browser/index.js +14 -0
  14. package/dist/browser/index.js.map +1 -1
  15. package/dist/browser/precaching.d.ts +5 -0
  16. package/dist/browser/precaching.js +112 -0
  17. package/dist/browser/precaching.js.map +1 -0
  18. package/dist/browser/tuning/directv.d.ts +2 -0
  19. package/dist/browser/tuning/directv.js +723 -0
  20. package/dist/browser/tuning/directv.js.map +1 -0
  21. package/dist/browser/tuning/fox.d.ts +2 -2
  22. package/dist/browser/tuning/fox.js +141 -22
  23. package/dist/browser/tuning/fox.js.map +1 -1
  24. package/dist/browser/tuning/hbo.d.ts +2 -2
  25. package/dist/browser/tuning/hbo.js +110 -41
  26. package/dist/browser/tuning/hbo.js.map +1 -1
  27. package/dist/browser/tuning/hulu.d.ts +2 -2
  28. package/dist/browser/tuning/hulu.js +442 -168
  29. package/dist/browser/tuning/hulu.js.map +1 -1
  30. package/dist/browser/tuning/sling.d.ts +2 -2
  31. package/dist/browser/tuning/sling.js +200 -56
  32. package/dist/browser/tuning/sling.js.map +1 -1
  33. package/dist/browser/tuning/youtubeTv.d.ts +2 -2
  34. package/dist/browser/tuning/youtubeTv.js +243 -57
  35. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  36. package/dist/browser/video.d.ts +1 -1
  37. package/dist/browser/video.js +4 -2
  38. package/dist/browser/video.js.map +1 -1
  39. package/dist/channels/index.js +280 -34
  40. package/dist/channels/index.js.map +1 -1
  41. package/dist/config/health.d.ts +78 -0
  42. package/dist/config/health.js +202 -0
  43. package/dist/config/health.js.map +1 -0
  44. package/dist/config/paths.d.ts +10 -0
  45. package/dist/config/paths.js +14 -0
  46. package/dist/config/paths.js.map +1 -1
  47. package/dist/config/profiles.d.ts +6 -4
  48. package/dist/config/profiles.js +65 -14
  49. package/dist/config/profiles.js.map +1 -1
  50. package/dist/config/providerPacks.d.ts +46 -0
  51. package/dist/config/providerPacks.js +200 -0
  52. package/dist/config/providerPacks.js.map +1 -0
  53. package/dist/config/providers.d.ts +36 -3
  54. package/dist/config/providers.js +168 -8
  55. package/dist/config/providers.js.map +1 -1
  56. package/dist/config/sites.d.ts +1 -12
  57. package/dist/config/sites.js +38 -5
  58. package/dist/config/sites.js.map +1 -1
  59. package/dist/config/userChannels.d.ts +12 -0
  60. package/dist/config/userChannels.js +81 -14
  61. package/dist/config/userChannels.js.map +1 -1
  62. package/dist/config/userConfig.d.ts +5 -1
  63. package/dist/config/userConfig.js +58 -6
  64. package/dist/config/userConfig.js.map +1 -1
  65. package/dist/config/userProfiles.d.ts +81 -0
  66. package/dist/config/userProfiles.js +424 -0
  67. package/dist/config/userProfiles.js.map +1 -0
  68. package/dist/routes/config/channels/index.d.ts +3 -0
  69. package/dist/routes/config/channels/index.js +3 -0
  70. package/dist/routes/config/channels/index.js.map +1 -0
  71. package/dist/routes/config/channels/routes.d.ts +6 -0
  72. package/dist/routes/config/channels/routes.js +797 -0
  73. package/dist/routes/config/channels/routes.js.map +1 -0
  74. package/dist/routes/config/channels/table.d.ts +46 -0
  75. package/dist/routes/config/channels/table.js +811 -0
  76. package/dist/routes/config/channels/table.js.map +1 -0
  77. package/dist/routes/config/index.d.ts +37 -0
  78. package/dist/routes/config/index.js +81 -0
  79. package/dist/routes/config/index.js.map +1 -0
  80. package/dist/routes/config/providers.d.ts +18 -0
  81. package/dist/routes/config/providers.js +573 -0
  82. package/dist/routes/config/providers.js.map +1 -0
  83. package/dist/routes/config/settings.d.ts +39 -0
  84. package/dist/routes/config/settings.js +820 -0
  85. package/dist/routes/config/settings.js.map +1 -0
  86. package/dist/routes/index.d.ts +3 -2
  87. package/dist/routes/index.js +7 -4
  88. package/dist/routes/index.js.map +1 -1
  89. package/dist/routes/playlist.d.ts +5 -1
  90. package/dist/routes/playlist.js +32 -7
  91. package/dist/routes/playlist.js.map +1 -1
  92. package/dist/routes/providers.d.ts +6 -0
  93. package/dist/routes/providers.js +166 -0
  94. package/dist/routes/providers.js.map +1 -0
  95. package/dist/routes/root/content.d.ts +32 -0
  96. package/dist/routes/root/content.js +857 -0
  97. package/dist/routes/root/content.js.map +1 -0
  98. package/dist/routes/root/index.js +197 -0
  99. package/dist/routes/root/index.js.map +1 -0
  100. package/dist/routes/root/scripts/channels.d.ts +1 -0
  101. package/dist/routes/root/scripts/channels.js +745 -0
  102. package/dist/routes/root/scripts/channels.js.map +1 -0
  103. package/dist/routes/root/scripts/config.d.ts +1 -0
  104. package/dist/routes/root/scripts/config.js +1662 -0
  105. package/dist/routes/root/scripts/config.js.map +1 -0
  106. package/dist/routes/root/scripts/index.d.ts +3 -0
  107. package/dist/routes/root/scripts/index.js +8 -0
  108. package/dist/routes/root/scripts/index.js.map +1 -0
  109. package/dist/routes/root/scripts/status.d.ts +1 -0
  110. package/dist/routes/root/scripts/status.js +436 -0
  111. package/dist/routes/root/scripts/status.js.map +1 -0
  112. package/dist/routes/root/styles.d.ts +5 -0
  113. package/dist/routes/root/styles.js +447 -0
  114. package/dist/routes/root/styles.js.map +1 -0
  115. package/dist/routes/ui.js +8 -3
  116. package/dist/routes/ui.js.map +1 -1
  117. package/dist/service/commands.d.ts +5 -3
  118. package/dist/service/commands.js +117 -33
  119. package/dist/service/commands.js.map +1 -1
  120. package/dist/service/generators.d.ts +27 -0
  121. package/dist/service/generators.js +97 -6
  122. package/dist/service/generators.js.map +1 -1
  123. package/dist/streaming/hls.js +18 -1
  124. package/dist/streaming/hls.js.map +1 -1
  125. package/dist/streaming/lifecycle.js +1 -1
  126. package/dist/streaming/lifecycle.js.map +1 -1
  127. package/dist/streaming/monitor.d.ts +1 -38
  128. package/dist/streaming/monitor.js +2 -289
  129. package/dist/streaming/monitor.js.map +1 -1
  130. package/dist/streaming/recovery.d.ts +143 -0
  131. package/dist/streaming/recovery.js +286 -0
  132. package/dist/streaming/recovery.js.map +1 -0
  133. package/dist/streaming/registry.d.ts +1 -1
  134. package/dist/streaming/setup.d.ts +1 -1
  135. package/dist/streaming/setup.js +3 -1
  136. package/dist/streaming/setup.js.map +1 -1
  137. package/dist/streaming/statusEmitter.d.ts +4 -2
  138. package/dist/streaming/statusEmitter.js +5 -0
  139. package/dist/streaming/statusEmitter.js.map +1 -1
  140. package/dist/types/channels.d.ts +68 -0
  141. package/dist/types/channels.js +2 -0
  142. package/dist/types/channels.js.map +1 -0
  143. package/dist/types/config.d.ts +127 -0
  144. package/dist/types/config.js +2 -0
  145. package/dist/types/config.js.map +1 -0
  146. package/dist/types/index.d.ts +7 -429
  147. package/dist/types/index.js +1 -5
  148. package/dist/types/index.js.map +1 -1
  149. package/dist/types/profiles.d.ts +141 -0
  150. package/dist/types/profiles.js +2 -0
  151. package/dist/types/profiles.js.map +1 -0
  152. package/dist/types/selection.d.ts +101 -0
  153. package/dist/types/selection.js +6 -0
  154. package/dist/types/selection.js.map +1 -0
  155. package/dist/types/shared.d.ts +7 -0
  156. package/dist/types/shared.js +6 -0
  157. package/dist/types/shared.js.map +1 -0
  158. package/dist/types/streaming.d.ts +96 -0
  159. package/dist/types/streaming.js +2 -0
  160. package/dist/types/streaming.js.map +1 -0
  161. package/dist/utils/debugFilter.js +3 -1
  162. package/dist/utils/debugFilter.js.map +1 -1
  163. package/dist/utils/format.d.ts +7 -0
  164. package/dist/utils/format.js +22 -0
  165. package/dist/utils/format.js.map +1 -1
  166. package/dist/utils/index.d.ts +1 -0
  167. package/dist/utils/index.js +1 -0
  168. package/dist/utils/index.js.map +1 -1
  169. package/dist/utils/sanitize.d.ts +14 -0
  170. package/dist/utils/sanitize.js +37 -0
  171. package/dist/utils/sanitize.js.map +1 -0
  172. package/package.json +5 -3
  173. package/dist/routes/config.d.ts +0 -76
  174. package/dist/routes/config.js +0 -2187
  175. package/dist/routes/config.js.map +0 -1
  176. package/dist/routes/root.js +0 -3158
  177. package/dist/routes/root.js.map +0 -1
  178. /package/dist/routes/{root.d.ts → root/index.d.ts} +0 -0
@@ -0,0 +1,745 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * channels.ts: Client-side JavaScript generator for the PrismCast channels subtab.
4
+ */
5
+ export function generateChannelsSubtabScript() {
6
+ return [
7
+ "<script>",
8
+ "(function() {",
9
+ // Channels subtab switching function. Scoped to .channels-subtab-btn / .channels-subtab-panel to avoid colliding with Config tab subtabs.
10
+ " function switchChannelsSubtab(subtab, updateUrl) {",
11
+ " var btns = document.querySelectorAll('.channels-subtab-btn');",
12
+ " var panels = document.querySelectorAll('.channels-subtab-panel');",
13
+ " for(var i = 0; i < btns.length; i++) {",
14
+ " btns[i].classList.remove('active');",
15
+ " btns[i].setAttribute('aria-selected', 'false');",
16
+ " if(btns[i].getAttribute('data-channels-subtab') === subtab) {",
17
+ " btns[i].classList.add('active');",
18
+ " btns[i].setAttribute('aria-selected', 'true');",
19
+ " }",
20
+ " }",
21
+ " for(var j = 0; j < panels.length; j++) {",
22
+ " panels[j].classList.remove('active');",
23
+ " if(panels[j].id === 'channels-subtab-' + subtab) {",
24
+ " panels[j].classList.add('active');",
25
+ " }",
26
+ " }",
27
+ " try { localStorage.setItem('prismcast-channels-subtab', subtab); } catch(e) {}",
28
+ " if(updateUrl !== false) {",
29
+ " var newHash = (subtab === 'channels') ? '#channels' : '#channels/' + subtab;",
30
+ " if(window.location.hash !== newHash) {",
31
+ " window.location.hash = newHash;",
32
+ " }",
33
+ " }",
34
+ " }",
35
+ // Expose for main tab script hashchange handler.
36
+ " window.switchChannelsSubtab = switchChannelsSubtab;",
37
+ // Attach click handlers to channels subtab buttons.
38
+ " var btns = document.querySelectorAll('.channels-subtab-btn');",
39
+ " for(var i = 0; i < btns.length; i++) {",
40
+ " btns[i].addEventListener('click', function() {",
41
+ " switchChannelsSubtab(this.getAttribute('data-channels-subtab'));",
42
+ " });",
43
+ " }",
44
+ // Delete a user profile with confirmation.
45
+ " window.deleteUserProfile = function(key) {",
46
+ " if(!confirm('Delete profile \"' + key + '\" and all its domain mappings?')) return;",
47
+ " fetch('/config/profiles/' + encodeURIComponent(key), { method: 'DELETE' })",
48
+ " .then(function(r) { return r.json(); })",
49
+ " .then(function(data) {",
50
+ " if(data.success) { location.reload(); }",
51
+ " else { showToast('Failed to delete profile: ' + (data.error || 'Unknown error.'), 'error'); }",
52
+ " })",
53
+ " .catch(function() { showToast('Failed to delete profile.', 'error'); });",
54
+ " };",
55
+ // Edit an existing user profile by pre-populating the wizard.
56
+ " window.editUserProfile = function(key) {",
57
+ " fetch('/config/profiles')",
58
+ " .then(function(r) { return r.json(); })",
59
+ " .then(function(data) {",
60
+ " if(!data.success) { showToast('Failed to load profiles.', 'error'); return; }",
61
+ " var profiles = data.profiles || [];",
62
+ " var match = profiles.find(function(p) { return p.key === key; });",
63
+ " if(!match || !match.profile) { showToast('Profile not found.', 'error'); return; }",
64
+ " var p = match.profile;",
65
+ " var editCs = p.channelSelection || {};",
66
+ " var strat = editCs.strategy || 'none';",
67
+ " var strategyFields = {};",
68
+ " var editStrat = window.__wizardStrategies.find(function(st) { return st.id === strat; });",
69
+ " if(editStrat) {",
70
+ " for(var i = 0; i < editStrat.fields.length; i++) {",
71
+ " var ef = editStrat.fields[i];",
72
+ " if(ef.type === 'text' && editCs[ef.id]) { strategyFields[ef.id] = editCs[ef.id]; }",
73
+ " else if(ef.type === 'boolean' && editCs[ef.id]) { strategyFields[ef.id] = true; }",
74
+ " }",
75
+ " }",
76
+ " var flags = {};",
77
+ " var wizFlags = window.__wizardFlags;",
78
+ " for(var i = 0; i < wizFlags.length; i++) { if(p[wizFlags[i].id]) flags[wizFlags[i].id] = true; }",
79
+ " var domains = (match.domains && match.domains.length > 0) ?",
80
+ " match.domains.map(function(d) { return { domain: d.domain, provider: d.provider || '', providerTag: d.providerTag || '' }; }) :",
81
+ " [{ domain: '', provider: '', providerTag: '' }];",
82
+ " wizardState = {",
83
+ " currentStep: 1, editKey: key, editMode: true, highestStep: 5, profileName: key,",
84
+ " baseProfile: p.extends || '', strategy: strat,",
85
+ " strategyFields: strategyFields, flags: flags,",
86
+ " hideSelector: p.hideSelector || '', domains: domains, description: p.description || ''",
87
+ " };",
88
+ " var title = document.getElementById('wizard-title');",
89
+ " if(title) title.textContent = 'Edit Provider Profile';",
90
+ " document.getElementById('wizard-modal').style.display = 'flex';",
91
+ " renderWizardStep();",
92
+ " })",
93
+ " .catch(function() { showToast('Failed to load profile data.', 'error'); });",
94
+ " };",
95
+ // Pending import data stored while the import preview modal is open.
96
+ " var pendingImportData = null;",
97
+ // Import a provider pack via file picker — shows a preview modal before importing.
98
+ " window.startProviderImport = function() {",
99
+ " var input = document.createElement('input');",
100
+ " input.type = 'file';",
101
+ " input.accept = '.json,application/json';",
102
+ " input.onchange = function() {",
103
+ " if(!input.files || !input.files[0]) return;",
104
+ " var reader = new FileReader();",
105
+ " reader.onload = function() {",
106
+ " try {",
107
+ " var data = JSON.parse(reader.result);",
108
+ " pendingImportData = data;",
109
+ " var profileCount = data.profiles ? Object.keys(data.profiles).length : 0;",
110
+ " var domainCount = data.domains ? Object.keys(data.domains).length : 0;",
111
+ " var channelCount = data.channels ? Object.keys(data.channels).length : 0;",
112
+ " var html = '<table class=\"import-summary-table\">';",
113
+ " html += '<tr><td class=\"import-summary-label\">Pack name</td>';",
114
+ " html += '<td>' + esc(data.name || 'Unnamed') + '</td></tr>';",
115
+ " html += '<tr><td class=\"import-summary-label\">Profiles</td>';",
116
+ " html += '<td>' + plural(profileCount, 'profile') + '</td></tr>';",
117
+ " html += '<tr><td class=\"import-summary-label\">Domains</td>';",
118
+ " html += '<td>' + plural(domainCount, 'domain') + '</td></tr>';",
119
+ " html += '<tr><td class=\"import-summary-label\">Channels</td>';",
120
+ " html += '<td>' + plural(channelCount, 'channel') + '</td></tr>';",
121
+ " html += '</table>';",
122
+ " if(channelCount > 0) {",
123
+ " html += '<div class=\"export-divider\"></div>';",
124
+ " html += '<label class=\"export-option-label\">';",
125
+ " html += '<input type=\"checkbox\" id=\"import-include-channels\" checked> Include ' + plural(channelCount, 'channel') + '</label>';",
126
+ " html += '<div class=\"export-hint\">Import the channel definitions from this pack. Uncheck to import only the provider profiles and domains.</div>';",
127
+ " }",
128
+ " document.getElementById('import-modal-body').innerHTML = html;",
129
+ " document.getElementById('import-modal').style.display = 'flex';",
130
+ " } catch(e) {",
131
+ " showToast('Invalid JSON file.', 'error');",
132
+ " }",
133
+ " };",
134
+ " reader.readAsText(input.files[0]);",
135
+ " };",
136
+ " input.click();",
137
+ " };",
138
+ // Close the import preview modal.
139
+ " window.closeImportModal = function() {",
140
+ " document.getElementById('import-modal').style.display = 'none';",
141
+ " pendingImportData = null;",
142
+ " };",
143
+ // Execute the import after the user confirms in the preview modal.
144
+ " window.executeImport = function() {",
145
+ " if(!pendingImportData) return;",
146
+ " var skipChannels = false;",
147
+ " var cb = document.getElementById('import-include-channels');",
148
+ " if(cb && !cb.checked) skipChannels = true;",
149
+ " var body = Object.assign({}, pendingImportData);",
150
+ " if(skipChannels) body.skipChannels = true;",
151
+ " fetch('/config/profiles/import', {",
152
+ " method: 'POST',",
153
+ " headers: { 'Content-Type': 'application/json' },",
154
+ " body: JSON.stringify(body)",
155
+ " })",
156
+ " .then(function(r) { return r.json(); })",
157
+ " .then(function(result) {",
158
+ " closeImportModal();",
159
+ " if(result.success) {",
160
+ " var msg = 'Imported ' + plural(result.summary.profilesAdded, 'profile') + ', ' +",
161
+ " plural(result.summary.domainsAdded, 'domain') + ', ' + plural(result.summary.channelsAdded, 'channel') + '.';",
162
+ " if(result.warnings && result.warnings.length > 0) msg += ' Warnings: ' + result.warnings.join('; ');",
163
+ " showToast(msg, 'success');",
164
+ " location.reload();",
165
+ " } else {",
166
+ " showToast('Import failed: ' + (result.error || 'Unknown error.'), 'error');",
167
+ " }",
168
+ " })",
169
+ " .catch(function() { closeImportModal(); showToast('Import request failed.', 'error'); });",
170
+ " };",
171
+ // 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
+ // The select-all row is hidden when there is only one profile since the single checkbox makes it redundant.
173
+ " window.startProviderExport = function() {",
174
+ " fetch('/config/profiles')",
175
+ " .then(function(r) { return r.json(); })",
176
+ " .then(function(data) {",
177
+ " var profiles = data.profiles || [];",
178
+ " if(profiles.length === 0) { showToast('No user profiles to export.', 'error'); return; }",
179
+ " var listDiv = document.getElementById('export-profile-list');",
180
+ " var html = '';",
181
+ " for(var i = 0; i < profiles.length; i++) {",
182
+ " var p = profiles[i];",
183
+ " var domainCount = p.domains ? p.domains.length : 0;",
184
+ " var channelCount = p.channelCount || 0;",
185
+ " var providerName = '';",
186
+ " for(var di = 0; di < (p.domains || []).length; di++) {",
187
+ " if(p.domains[di].provider) { providerName = p.domains[di].provider; break; }",
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(' &middot; ') + '</div>';",
198
+ " html += '</div></label>';",
199
+ " }",
200
+ " listDiv.innerHTML = html;",
201
+ " document.getElementById('export-select-all').checked = true;",
202
+ " document.getElementById('export-select-all-row').style.display = (profiles.length > 1) ? '' : 'none';",
203
+ " document.getElementById('export-include-channels').checked = false;",
204
+ " document.getElementById('export-modal').style.display = 'flex';",
205
+ " })",
206
+ " .catch(function() { showToast('Failed to load profiles.', 'error'); });",
207
+ " };",
208
+ // Toggle all export profile checkboxes.
209
+ " window.toggleExportAll = function(master) {",
210
+ " var cbs = document.querySelectorAll('.export-profile-cb');",
211
+ " for(var i = 0; i < cbs.length; i++) cbs[i].checked = master.checked;",
212
+ " };",
213
+ // Close the export modal.
214
+ " window.closeExportModal = function() {",
215
+ " document.getElementById('export-modal').style.display = 'none';",
216
+ " };",
217
+ // Execute the export by building the download URL from the selected profiles.
218
+ " window.executeExport = function() {",
219
+ " var cbs = document.querySelectorAll('.export-profile-cb:checked');",
220
+ " if(cbs.length === 0) { showToast('Select at least one profile.', 'error'); return; }",
221
+ " var keys = [];",
222
+ " for(var i = 0; i < cbs.length; i++) keys.push(cbs[i].value);",
223
+ " var url = '/config/profiles/export?profile=' + encodeURIComponent(keys.join(',')) + '&domains=1';",
224
+ " if(document.getElementById('export-include-channels').checked) url += '&channels=1';",
225
+ " closeExportModal();",
226
+ " window.location.href = url;",
227
+ " };",
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
+ // Auto-generate a provider tag from a provider name: lowercase, strip non-alphanumeric.
244
+ " function autoProviderTag(name) {",
245
+ " return name.toLowerCase().replace(/[^a-z0-9]/g, '');",
246
+ " }",
247
+ // HTML-escape user input for safe innerHTML insertion in the review step.
248
+ " function esc(str) {",
249
+ " var d = document.createElement('div');",
250
+ " d.appendChild(document.createTextNode(str || ''));",
251
+ " return d.innerHTML;",
252
+ " }",
253
+ // Pluralization helper for UI text: returns "1 domain", "2 domains", etc.
254
+ " function plural(n, word) {",
255
+ " return n + ' ' + word + (n === 1 ? '' : 's');",
256
+ " }",
257
+ // Render the current wizard step into the wizard-content area.
258
+ " function renderWizardStep() {",
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
+ " if(!content) return;",
266
+ " errDiv.style.display = 'none';",
267
+ " errDiv.textContent = '';",
268
+ // Update step indicator. Steps up to highestStep are clickable for navigation.
269
+ " var steps = document.querySelectorAll('.wizard-step');",
270
+ " for(var i = 0; i < steps.length; i++) {",
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';",
283
+ " var html = '';",
284
+ " var s = wizardState;",
285
+ // Step 1: Base Profile.
286
+ " if(s.currentStep === 1) {",
287
+ " html += '<div class=\"field-group\">';",
288
+ " html += '<label for=\"wizard-profile-name\">Profile Name</label>';",
289
+ " var nameReadonly = s.editMode ? ' readonly style=\"opacity: 0.6; cursor: not-allowed;\"' : '';",
290
+ " html += '<input type=\"text\" id=\"wizard-profile-name\" value=\"' + s.profileName.replace(/\"/g, '&quot;') +",
291
+ " '\" placeholder=\"e.g., myProvider\"' + nameReadonly + '>';",
292
+ " html += '<div class=\"field-hint\">' + (s.editMode ? 'Profile name cannot be changed.' :",
293
+ " 'Unique identifier. Letters, numbers, and hyphens only.') + '</div>';",
294
+ " html += '</div>';",
295
+ " html += '<label>Base Profile</label>';",
296
+ " var profiles = window.__wizardProfiles || {};",
297
+ " var cats = [",
298
+ " { key: 'api', title: 'Fullscreen API',",
299
+ " desc: 'For single-channel sites that require JavaScript\\'s requestFullscreen() API instead of keyboard shortcuts.' },",
300
+ " { key: 'keyboard', title: 'Keyboard Fullscreen',",
301
+ " desc: 'For single-channel sites that use the \\'f\\' key to toggle fullscreen mode.' },",
302
+ " { key: 'multiChannel', title: 'Multi-Channel',",
303
+ " desc: 'For sites that host multiple live channels on a single page.' },",
304
+ " { key: 'special', title: 'Special',",
305
+ " desc: 'For non-standard use cases like static pages without video.' },",
306
+ " { key: 'custom', title: 'Custom', desc: '' }",
307
+ " ];",
308
+ " for(var ci = 0; ci < cats.length; ci++) {",
309
+ " var cat = cats[ci];",
310
+ " var items = (profiles[cat.key] || []).filter(function(p) { return !s.editMode || p.name !== s.editKey; });",
311
+ " if(items.length === 0) continue;",
312
+ " html += '<div class=\"wizard-profile-category\">';",
313
+ " html += '<h4>' + cat.title + '</h4>';",
314
+ " if(cat.desc) html += '<div class=\"wizard-category-desc\">' + esc(cat.desc) + '</div>';",
315
+ " for(var pi = 0; pi < items.length; pi++) {",
316
+ " var p = items[pi];",
317
+ " var checked = (s.baseProfile === p.name) ? ' checked' : '';",
318
+ " html += '<label class=\"wizard-profile-option\">';",
319
+ " html += '<input type=\"radio\" name=\"baseProfile\" value=\"' + p.name + '\"' + checked + '>';",
320
+ " var sumTag = p.summary ? ' <span class=\"profile-option-summary\">(' + esc(p.summary) + ')</span>' : '';",
321
+ " html += '<div><div class=\"profile-option-name\">' + esc(p.name) + sumTag + '</div>';",
322
+ " html += '<div class=\"profile-option-desc\">' + esc(p.description) + '</div></div>';",
323
+ " html += '</label>';",
324
+ " }",
325
+ " html += '</div>';",
326
+ " }",
327
+ " }",
328
+ // Step 2: Channel Selection Strategy. Strategies and their fields are rendered from the server-side registry (window.__wizardStrategies).
329
+ " if(s.currentStep === 2) {",
330
+ " html += '<label>Channel Selection Strategy</label>';",
331
+ " var strategies = window.__wizardStrategies;",
332
+ " for(var si = 0; si < strategies.length; si++) {",
333
+ " var st = strategies[si];",
334
+ " var checked = (s.strategy === st.id) ? ' checked' : '';",
335
+ " html += '<label class=\"wizard-strategy-option\">';",
336
+ " html += '<input type=\"radio\" name=\"strategy\" value=\"' + st.id + '\"' + checked + '>';",
337
+ " html += '<div><div class=\"wizard-strategy-label\">' + st.name + '</div>';",
338
+ " html += '<div class=\"wizard-strategy-desc\">' + st.description + '</div></div>';",
339
+ " html += '</label>';",
340
+ " }",
341
+ " var currentStrat = strategies.find(function(st) { return st.id === s.strategy; });",
342
+ " if(currentStrat && currentStrat.fields.length > 0) {",
343
+ " html += '<hr style=\"border: none; border-top: 1px solid var(--border-default); margin: 16px 0;\">';",
344
+ " for(var fi = 0; fi < currentStrat.fields.length; fi++) {",
345
+ " var f = currentStrat.fields[fi];",
346
+ " if(f.type === 'text') {",
347
+ " var val = (s.strategyFields[f.id] || '').replace(/\"/g, '&quot;');",
348
+ " var optLabel = f.required ? '' : ' <span style=\"color: var(--text-muted);\">(optional)</span>';",
349
+ " html += '<div class=\"field-group\">';",
350
+ " html += '<label for=\"wizard-sf-' + f.id + '\">' + f.label + optLabel + '</label>';",
351
+ " html += '<input type=\"text\" id=\"wizard-sf-' + f.id + '\" data-field=\"' + f.id + '\" value=\"' + val + '\"';",
352
+ " if(f.placeholder) html += ' placeholder=\"' + f.placeholder.replace(/\"/g, '&quot;') + '\"';",
353
+ " html += '>';",
354
+ " if(f.hint) html += '<div class=\"field-hint\">' + f.hint + '</div>';",
355
+ " html += '</div>';",
356
+ " } else if(f.type === 'boolean') {",
357
+ " var boolChecked = s.strategyFields[f.id] ? ' checked' : '';",
358
+ " html += '<label class=\"wizard-flag\">';",
359
+ " html += '<input type=\"checkbox\" data-field=\"' + f.id + '\"' + boolChecked + '>';",
360
+ " html += '<div><div class=\"wizard-flag-label\">' + f.label + '</div>';",
361
+ " if(f.hint) html += '<div class=\"wizard-flag-desc\">' + f.hint + '</div>';",
362
+ " html += '</div></label>';",
363
+ " }",
364
+ " }",
365
+ " }",
366
+ " }",
367
+ // Step 3: Override Flags. Flags are rendered from the server-side registry (window.__wizardFlags).
368
+ " if(s.currentStep === 3) {",
369
+ " html += '<label>Profile Flags</label>';",
370
+ " html += '<div class=\"field-hint\" style=\"margin-bottom: 12px;\">These override the base profile. ' +",
371
+ " 'Only change what differs from ' + s.baseProfile + '.</div>';",
372
+ " var wizFlags = window.__wizardFlags;",
373
+ " for(var fi = 0; fi < wizFlags.length; fi++) {",
374
+ " var f = wizFlags[fi];",
375
+ " var checked = s.flags[f.id] ? ' checked' : '';",
376
+ " html += '<label class=\"wizard-flag\">';",
377
+ " html += '<input type=\"checkbox\" name=\"flag-' + f.id + '\"' + checked + '>';",
378
+ " html += '<div><div class=\"wizard-flag-label\">' + f.label + '</div>';",
379
+ " html += '<div class=\"wizard-flag-desc\">' + f.description + '</div></div>';",
380
+ " html += '</label>';",
381
+ " }",
382
+ " 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>';",
384
+ " html += '<input type=\"text\" id=\"wizard-hide-selector\" value=\"' + s.hideSelector.replace(/\"/g, '&quot;') +",
385
+ " '\" placeholder=\"e.g., .overlay, .ad-banner\">';",
386
+ " html += '<div class=\"field-hint\">Overlay elements to hide during capture.</div>';",
387
+ " html += '</div>';",
388
+ " }",
389
+ // Step 4: Domain Mapping.
390
+ " if(s.currentStep === 4) {",
391
+ " html += '<label>Domain Mapping</label>';",
392
+ " html += '<div class=\"field-hint\" style=\"margin-bottom: 12px;\">Map a streaming site domain to this profile.</div>';",
393
+ " for(var di = 0; di < s.domains.length; di++) {",
394
+ " var d = s.domains[di];",
395
+ " html += '<div class=\"wizard-domain-row\" data-domain-idx=\"' + di + '\">';",
396
+ " html += '<div class=\"field-group\">';",
397
+ " html += '<label>Domain</label>';",
398
+ " html += '<input type=\"text\" class=\"domain-input\" value=\"' + d.domain.replace(/\"/g, '&quot;') + '\" placeholder=\"e.g., watch.sling.com\">';",
399
+ " html += '</div>';",
400
+ " html += '<div class=\"field-group\">';",
401
+ " html += '<label>Provider Name</label>';",
402
+ " html += '<input type=\"text\" class=\"provider-input\" value=\"' + d.provider.replace(/\"/g, '&quot;') + '\" placeholder=\"e.g., Sling TV\">';",
403
+ " html += '</div>';",
404
+ " html += '<div class=\"field-group\">';",
405
+ " html += '<label>Provider Tag <span style=\"color: var(--text-muted);\">(auto-filled)</span></label>';",
406
+ " html += '<input type=\"text\" class=\"provider-tag-input\" value=\"' + d.providerTag.replace(/\"/g, '&quot;') +",
407
+ " '\" placeholder=\"auto-generated\">';",
408
+ " html += '<div class=\"field-hint\">Identifier for the provider filter. Auto-generated from provider name.</div>';",
409
+ " html += '</div>';",
410
+ " html += '</div>';",
411
+ " }",
412
+ " html += '<button type=\"button\" class=\"wizard-add-domain\" onclick=\"addWizardDomain()\">+ Add another domain</button>';",
413
+ " }",
414
+ // Step 5: Review and Save.
415
+ " if(s.currentStep === 5) {",
416
+ " html += '<label>Review</label>';",
417
+ " html += '<table class=\"wizard-review-table\">';",
418
+ " html += '<tr><td>Profile</td><td>' + esc(s.profileName) + '</td></tr>';",
419
+ " html += '<tr><td>Extends</td><td>' + esc(s.baseProfile) + '</td></tr>';",
420
+ " html += '<tr><td>Strategy</td><td>' + esc(s.strategy) + '</td></tr>';",
421
+ " var domainList = s.domains.filter(function(d) { return d.domain; }).map(function(d) { return esc(d.domain); }).join(', ');",
422
+ " html += '<tr><td>Domain(s)</td><td>' + (domainList || 'none') + '</td></tr>';",
423
+ " var providerList = s.domains.filter(function(d) { return d.provider; }).map(function(d) { return esc(d.provider); }).join(', ');",
424
+ " html += '<tr><td>Provider</td><td>' + (providerList || 'none') + '</td></tr>';",
425
+ " if(s.strategy !== 'none') {",
426
+ " var reviewStrat = window.__wizardStrategies.find(function(st) { return st.id === s.strategy; });",
427
+ " if(reviewStrat) {",
428
+ " for(var ri = 0; ri < reviewStrat.fields.length; ri++) {",
429
+ " var rf = reviewStrat.fields[ri];",
430
+ " var rv = s.strategyFields[rf.id];",
431
+ " if(rf.type === 'text' && rv) {",
432
+ " html += '<tr><td>' + esc(rf.label.replace(/ \\(CSS\\)/, '')) + '</td><td><code>' + esc(rv) + '</code></td></tr>';",
433
+ " } else if(rf.type === 'boolean' && rv) {",
434
+ " html += '<tr><td>' + esc(rf.label) + '</td><td>Yes</td></tr>';",
435
+ " }",
436
+ " }",
437
+ " }",
438
+ " }",
439
+ " var flagNames = Object.keys(s.flags).filter(function(k) { return s.flags[k]; });",
440
+ " if(flagNames.length > 0) {",
441
+ " html += '<tr><td>Flags</td><td>' + esc(flagNames.join(', ')) + '</td></tr>';",
442
+ " }",
443
+ " if(s.hideSelector) html += '<tr><td>Hide Selector</td><td><code>' + esc(s.hideSelector) + '</code></td></tr>';",
444
+ " html += '</table>';",
445
+ " 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>';",
447
+ " html += '<textarea id=\"wizard-description\" placeholder=\"Optional description for documentation.\">' + esc(s.description) + '</textarea>';",
448
+ " html += '</div>';",
449
+ " }",
450
+ " content.innerHTML = html;",
451
+ // Attach live event handlers after rendering.
452
+ " attachWizardHandlers();",
453
+ " }",
454
+ // Attach event handlers to dynamically rendered wizard inputs.
455
+ " function attachWizardHandlers() {",
456
+ " var s = wizardState;",
457
+ // Step 1: profile name and base profile radio.
458
+ " var nameInput = document.getElementById('wizard-profile-name');",
459
+ " if(nameInput) nameInput.addEventListener('input', function() { s.profileName = this.value; });",
460
+ " var radios = document.querySelectorAll('input[name=\"baseProfile\"]');",
461
+ " for(var i = 0; i < radios.length; i++) {",
462
+ " radios[i].addEventListener('change', function() { s.baseProfile = this.value; });",
463
+ " }",
464
+ // Step 2: strategy radio and strategy field inputs. Text and boolean fields are bound by data-field attribute.
465
+ " var stratRadios = document.querySelectorAll('input[name=\"strategy\"]');",
466
+ " for(var i = 0; i < stratRadios.length; i++) {",
467
+ " stratRadios[i].addEventListener('change', function() { s.strategy = this.value; renderWizardStep(); });",
468
+ " }",
469
+ " var sfTextInputs = document.querySelectorAll('#wizard-content input[type=\"text\"][data-field]');",
470
+ " for(var i = 0; i < sfTextInputs.length; i++) {",
471
+ " sfTextInputs[i].addEventListener('input', function() { s.strategyFields[this.getAttribute('data-field')] = this.value; });",
472
+ " }",
473
+ " var sfCheckInputs = document.querySelectorAll('#wizard-content input[type=\"checkbox\"][data-field]');",
474
+ " for(var i = 0; i < sfCheckInputs.length; i++) {",
475
+ " sfCheckInputs[i].addEventListener('change', function() {",
476
+ " if(this.checked) { s.strategyFields[this.getAttribute('data-field')] = true; }",
477
+ " else { delete s.strategyFields[this.getAttribute('data-field')]; }",
478
+ " });",
479
+ " }",
480
+ // Step 3: flag checkboxes and hide selector.
481
+ " var flagCbs = document.querySelectorAll('input[name^=\"flag-\"]');",
482
+ " for(var i = 0; i < flagCbs.length; i++) {",
483
+ " flagCbs[i].addEventListener('change', function() {",
484
+ " var flagName = this.name.replace('flag-', '');",
485
+ " if(this.checked) { s.flags[flagName] = true; } else { delete s.flags[flagName]; }",
486
+ " });",
487
+ " }",
488
+ " var hideInput = document.getElementById('wizard-hide-selector');",
489
+ " if(hideInput) hideInput.addEventListener('input', function() { s.hideSelector = this.value; });",
490
+ // Step 4: domain inputs with auto-fill for provider tag.
491
+ " var domainRows = document.querySelectorAll('.wizard-domain-row');",
492
+ " for(var i = 0; i < domainRows.length; i++) {",
493
+ " (function(idx) {",
494
+ " var row = domainRows[idx];",
495
+ " var domInput = row.querySelector('.domain-input');",
496
+ " var provInput = row.querySelector('.provider-input');",
497
+ " var tagInput = row.querySelector('.provider-tag-input');",
498
+ " if(domInput) domInput.addEventListener('input', function() { s.domains[idx].domain = this.value; });",
499
+ " if(provInput) provInput.addEventListener('input', function() {",
500
+ " s.domains[idx].provider = this.value;",
501
+ " var tag = autoProviderTag(this.value);",
502
+ " s.domains[idx].providerTag = tag;",
503
+ " if(tagInput) tagInput.value = tag;",
504
+ " });",
505
+ " if(tagInput) tagInput.addEventListener('input', function() { s.domains[idx].providerTag = this.value; });",
506
+ " })(i);",
507
+ " }",
508
+ // Step 5: description textarea.
509
+ " var descInput = document.getElementById('wizard-description');",
510
+ " if(descInput) descInput.addEventListener('input', function() { s.description = this.value; });",
511
+ " }",
512
+ // Open the wizard modal and reset state.
513
+ " window.openWizard = function() {",
514
+ " wizardState = {",
515
+ " currentStep: 1, editKey: null, editMode: false, highestStep: 1, profileName: '', baseProfile: '', strategy: 'none',",
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) {",
537
+ " if(!s.editMode) {",
538
+ " if(!s.profileName.trim()) return 'Profile name is required.';",
539
+ " if(!/^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(s.profileName))",
540
+ " return 'Must start with a letter, contain only letters, numbers, and hyphens, and not end with a hyphen.';",
541
+ " if(s.profileName.length > 50) return 'Profile name must be 50 characters or less.';",
542
+ " }",
543
+ " if(!s.baseProfile) return 'Select a base profile.';",
544
+ " }",
545
+ " if(s.currentStep === 2) {",
546
+ " var valStrat = window.__wizardStrategies.find(function(st) { return st.id === s.strategy; });",
547
+ " if(valStrat) {",
548
+ " for(var vi = 0; vi < valStrat.fields.length; vi++) {",
549
+ " var vf = valStrat.fields[vi];",
550
+ " if(vf.required && (vf.type === 'text') && !(s.strategyFields[vf.id] || '').trim()) {",
551
+ " return vf.label.replace(/ \\(CSS\\)/, '') + ' is required for the ' + valStrat.name + ' strategy.';",
552
+ " }",
553
+ " }",
554
+ " }",
555
+ " }",
556
+ " if(s.currentStep === 4) {",
557
+ " var hasDomain = s.domains.some(function(d) { return d.domain.trim(); });",
558
+ " if(!hasDomain) return 'At least one domain is required.';",
559
+ " }",
560
+ " return '';",
561
+ " }",
562
+ // Navigate to next step.
563
+ " window.wizardNext = function() {",
564
+ " var err = validateStep();",
565
+ " var errDiv = document.getElementById('wizard-error');",
566
+ " if(err) {",
567
+ " errDiv.textContent = err;",
568
+ " errDiv.style.display = 'block';",
569
+ " return;",
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
+ " }",
583
+ " };",
584
+ // Navigate to a specific step by clicking the step indicator. Going backward is always allowed for visited steps. Going forward requires validation of the
585
+ // current step and can only reach steps that have been visited before (up to highestStep).
586
+ " function wizardGoToStep(targetStep) {",
587
+ " if(targetStep === wizardState.currentStep) return;",
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();",
600
+ " }",
601
+ // Save the profile to the server.
602
+ " window.saveProfile = function(andTest) {",
603
+ " var s = wizardState;",
604
+ " var profile = { extends: s.baseProfile };",
605
+ " if(s.strategy !== 'none') {",
606
+ " var cs = { strategy: s.strategy };",
607
+ " var saveStrat = window.__wizardStrategies.find(function(st) { return st.id === s.strategy; });",
608
+ " if(saveStrat) {",
609
+ " for(var si = 0; si < saveStrat.fields.length; si++) {",
610
+ " var sf = saveStrat.fields[si];",
611
+ " var sfVal = s.strategyFields[sf.id];",
612
+ " if(sf.type === 'text' && sfVal) { cs[sf.id] = sfVal; }",
613
+ " else if(sf.type === 'boolean' && sfVal) { cs[sf.id] = true; }",
614
+ " }",
615
+ " }",
616
+ " profile.channelSelection = cs;",
617
+ " }",
618
+ " var flagKeys = Object.keys(s.flags).filter(function(k) { return s.flags[k]; });",
619
+ " for(var i = 0; i < flagKeys.length; i++) { profile[flagKeys[i]] = true; }",
620
+ " if(s.hideSelector) profile.hideSelector = s.hideSelector;",
621
+ " if(s.description) profile.description = s.description;",
622
+ " var domains = {};",
623
+ " for(var i = 0; i < s.domains.length; i++) {",
624
+ " var d = s.domains[i];",
625
+ " if(d.domain.trim()) {",
626
+ " domains[d.domain.trim()] = { profile: s.profileName };",
627
+ " if(d.provider.trim()) domains[d.domain.trim()].provider = d.provider.trim();",
628
+ " if(d.providerTag.trim()) domains[d.domain.trim()].providerTag = d.providerTag.trim();",
629
+ " }",
630
+ " }",
631
+ " var body = { domains: domains, key: s.profileName, profile: profile };",
632
+ " fetch('/config/profiles', {",
633
+ " method: 'POST',",
634
+ " headers: { 'Content-Type': 'application/json' },",
635
+ " body: JSON.stringify(body)",
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';",
656
+ " });",
657
+ " };",
658
+ // Test flow: open a browser to a URL with the profile applied and allow selector checking.
659
+ " var testSelectors = {};",
660
+ // Start a profile test by opening the browser to a URL.
661
+ " window.startProfileTest = function(url) {",
662
+ " var s = wizardState;",
663
+ " testSelectors = {};",
664
+ " if(s.strategy !== 'none' && s.strategyFields.matchSelector) testSelectors.matchSelector = s.strategyFields.matchSelector;",
665
+ " if(s.strategyFields.playSelector) testSelectors.playSelector = s.strategyFields.playSelector;",
666
+ " if(s.hideSelector) testSelectors.hideSelector = s.hideSelector;",
667
+ " var testUrl = url.indexOf('://') === -1 ? 'https://' + url : url;",
668
+ " fetch('/config/profiles/test', {",
669
+ " method: 'POST',",
670
+ " headers: { 'Content-Type': 'application/json' },",
671
+ " body: JSON.stringify({ url: testUrl })",
672
+ " })",
673
+ " .then(function(r) { return r.json(); })",
674
+ " .then(function(data) {",
675
+ " if(data.success) {",
676
+ " document.getElementById('test-modal').style.display = 'flex';",
677
+ " document.getElementById('test-selector-results').style.display = 'none';",
678
+ " document.getElementById('test-check-btn').style.display = ",
679
+ " Object.keys(testSelectors).length > 0 ? '' : 'none';",
680
+ " } else {",
681
+ " showToast('Failed to start test: ' + (data.error || 'Unknown error.'), 'error');",
682
+ " }",
683
+ " })",
684
+ " .catch(function() { showToast('Failed to start test.', 'error'); });",
685
+ " };",
686
+ // Check CSS selectors against the live page.
687
+ " window.checkSelectors = function() {",
688
+ " if(Object.keys(testSelectors).length === 0) return;",
689
+ " fetch('/config/profiles/test/check', {",
690
+ " method: 'POST',",
691
+ " headers: { 'Content-Type': 'application/json' },",
692
+ " body: JSON.stringify({ selectors: testSelectors })",
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';",
720
+ " });",
721
+ " };",
722
+ // End the test flow and close the test dialog.
723
+ " window.endProfileTest = function() {",
724
+ " fetch('/config/profiles/test/done', { method: 'POST' })",
725
+ " .then(function() {",
726
+ " document.getElementById('test-modal').style.display = 'none';",
727
+ " location.reload();",
728
+ " })",
729
+ " .catch(function() {",
730
+ " document.getElementById('test-modal').style.display = 'none';",
731
+ " });",
732
+ " };",
733
+ // Initialize channels subtab on load: hash > localStorage > default.
734
+ " var initialSubtab = window.initialChannelsHashSubtab;",
735
+ " if(!initialSubtab) {",
736
+ " try { initialSubtab = localStorage.getItem('prismcast-channels-subtab'); } catch(e) {}",
737
+ " }",
738
+ " if(initialSubtab && document.querySelector('.channels-subtab-btn[data-channels-subtab=\"' + initialSubtab + '\"]')) {",
739
+ " switchChannelsSubtab(initialSubtab, false);",
740
+ " }",
741
+ "})();",
742
+ "</script>"
743
+ ].join("\n");
744
+ }
745
+ //# sourceMappingURL=channels.js.map