@oxymoron.13/tizentube-s95b 0.2.2 → 0.3.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 (87) hide show
  1. package/README.md +51 -42
  2. package/S95B-CHANGES.txt +12 -4
  3. package/UPSTREAM-README.md +42 -0
  4. package/dist/service.js +3 -0
  5. package/dist/userScript.js +2 -2
  6. package/mods/config.js +117 -0
  7. package/mods/domrect-polyfill.js +104 -0
  8. package/mods/features/adblock.js +584 -0
  9. package/mods/features/autoFrameRate.js +42 -0
  10. package/mods/features/enableFeatures.js +19 -0
  11. package/mods/features/moreSubtitles.js +378 -0
  12. package/mods/features/pictureInPicture.js +198 -0
  13. package/mods/features/preferredVideoQuality.js +101 -0
  14. package/mods/features/s95bCodec.js +45 -0
  15. package/mods/features/s95bI18n.js +28 -0
  16. package/mods/features/s95bMenu.js +137 -0
  17. package/mods/features/s95bOptions.js +30 -0
  18. package/mods/features/s95bPlayback.js +29 -0
  19. package/mods/features/sponsorblock.js +437 -0
  20. package/mods/features/standaloneUserscript.js +107 -0
  21. package/mods/features/updater.js +119 -0
  22. package/mods/features/userAgentSpoofing.js +73 -0
  23. package/mods/features/videoQueuing.js +64 -0
  24. package/mods/package-lock.json +2321 -0
  25. package/mods/package.json +33 -0
  26. package/mods/resolveCommand.js +381 -0
  27. package/mods/rollup.config.js +41 -0
  28. package/mods/spatial-navigation-polyfill.js +1756 -0
  29. package/mods/tiny-sha256.js +99 -0
  30. package/mods/translations/i18nResources.js +11 -0
  31. package/mods/translations/index.js +18 -0
  32. package/mods/translations/language-names.js +981 -0
  33. package/mods/translations/resources/ar.json +308 -0
  34. package/mods/translations/resources/az.json +308 -0
  35. package/mods/translations/resources/bs.json +308 -0
  36. package/mods/translations/resources/cs.json +308 -0
  37. package/mods/translations/resources/de.json +308 -0
  38. package/mods/translations/resources/dk.json +308 -0
  39. package/mods/translations/resources/el.json +308 -0
  40. package/mods/translations/resources/en.json +308 -0
  41. package/mods/translations/resources/es-419.json +308 -0
  42. package/mods/translations/resources/es.json +308 -0
  43. package/mods/translations/resources/fi-FI.json +308 -0
  44. package/mods/translations/resources/fr.json +311 -0
  45. package/mods/translations/resources/hr.json +308 -0
  46. package/mods/translations/resources/hu.json +308 -0
  47. package/mods/translations/resources/id.json +308 -0
  48. package/mods/translations/resources/it.json +308 -0
  49. package/mods/translations/resources/ja.json +308 -0
  50. package/mods/translations/resources/lt.json +308 -0
  51. package/mods/translations/resources/lv.json +308 -0
  52. package/mods/translations/resources/pl.json +308 -0
  53. package/mods/translations/resources/pt-BR.json +308 -0
  54. package/mods/translations/resources/pt-PT.json +308 -0
  55. package/mods/translations/resources/ro.json +310 -0
  56. package/mods/translations/resources/ru.json +308 -0
  57. package/mods/translations/resources/sr-Cyrl.json +308 -0
  58. package/mods/translations/resources/sr-Latn.json +308 -0
  59. package/mods/translations/resources/tlh.json +308 -0
  60. package/mods/translations/resources/tr.json +308 -0
  61. package/mods/translations/resources/uk.json +308 -0
  62. package/mods/translations/resources/vi.json +309 -0
  63. package/mods/translations/resources/zh-CN.json +308 -0
  64. package/mods/translations/resources/zh-TW.json +308 -0
  65. package/mods/translations/update-translations.js +55 -0
  66. package/mods/ui/chapters.js +137 -0
  67. package/mods/ui/clock.js +71 -0
  68. package/mods/ui/customCommandExecution.js +49 -0
  69. package/mods/ui/customGuideAction.js +119 -0
  70. package/mods/ui/customUI.js +192 -0
  71. package/mods/ui/customYTSettings.js +29 -0
  72. package/mods/ui/disableWhosWatching.js +48 -0
  73. package/mods/ui/settings.js +1024 -0
  74. package/mods/ui/sidebarModification.js +179 -0
  75. package/mods/ui/speedUI.js +119 -0
  76. package/mods/ui/theme.js +26 -0
  77. package/mods/ui/ui.css +157 -0
  78. package/mods/ui/ui.js +261 -0
  79. package/mods/ui/ytUI.js +510 -0
  80. package/mods/userScript.js +28 -0
  81. package/mods/utils/ASTParser.js +67 -0
  82. package/mods/utils/innerTubeCalls.js +138 -0
  83. package/package.json +11 -5
  84. package/service/package-lock.json +4193 -0
  85. package/service/package.json +29 -0
  86. package/service/rollup.config.js +47 -0
  87. package/service/service.js +121 -0
@@ -0,0 +1,378 @@
1
+ // TizenTube Subtitle Localization Mod
2
+ // Automatically adds user's local language to subtitle auto-translate menu if not present
3
+
4
+ import { configRead } from "../config.js";
5
+ import languages from "../translations/language-names.js";
6
+
7
+ const LANGUAGE_CODES = [
8
+ "af", "sq", "am", "ar", "hy", "as", "az", "eu", "be", "bn", "bs", "bg",
9
+ "my", "ca", "zh-CN", "zh-TW", "zh-HK", "hr", "cs", "da", "nl", "en", "et",
10
+ "fil", "fi", "fr", "gl", "ka", "de", "el", "gu", "he", "hi", "hu", "is",
11
+ "id", "ga", "it", "ja", "kn", "kk", "km", "ko", "ky", "lo", "lv", "lt",
12
+ "mk", "ms", "ml", "mt", "mr", "mn", "ne", "no", "or", "fa", "pl", "pt",
13
+ "pa", "ro", "ru", "sr", "si", "sk", "sl", "es", "sw", "sv", "ta", "te",
14
+ "th", "tr", "uk", "ur", "uz", "vi", "cy", "yi", "yo", "zu"
15
+ ];
16
+
17
+ // Return an object mapping language code -> localized language name.
18
+ export function getComprehensiveLanguageList() {
19
+ try {
20
+ const map = {};
21
+ LANGUAGE_CODES.forEach((code) => {
22
+ if (code.includes("-")) {
23
+ const [lang, region] = code.split("-");
24
+ const languageName = languages.language.standard.long[lang] || code;
25
+ const regionName = languages.region.long[region] || region;
26
+ map[code] = `${languageName} (${regionName})`;
27
+ } else {
28
+ const name = languages.language.standard.long[code] || code;
29
+ map[code] = name;
30
+ }
31
+ });
32
+ return map;
33
+ } catch (e) {
34
+ const fallback = {};
35
+ LANGUAGE_CODES.forEach((c) => (fallback[c] = c));
36
+ return fallback;
37
+ }
38
+ }
39
+
40
+ // Infer the most likely language for a given ISO 3166-1 alpha-2 country code using Intl.Locale.
41
+ // Returns { code, name } or null if unknown.
42
+ export function getCountryLanguage(countryCode) {
43
+ if (!countryCode) return null;
44
+ try {
45
+ const region = String(countryCode).toUpperCase();
46
+
47
+ const zhRegionMap = { CN: "zh-CN", TW: "zh-TW", HK: "zh-HK", SG: "zh-CN" };
48
+ if (zhRegionMap[region]) {
49
+ const code = zhRegionMap[region];
50
+ const name = languages.language.standard.long[code] || code;
51
+ return { code, name };
52
+ }
53
+
54
+ const base = new Intl.Locale("und", { region });
55
+ const maximized = base.maximize ? base.maximize() : base;
56
+ const lang = maximized.language || "en";
57
+
58
+ const name = languages.language.standard.long[lang] || lang;
59
+
60
+ return { code: lang, name };
61
+ } catch (e) {
62
+ console.warn("TizenTube Subtitle Localization: Could not infer language for country", countryCode, e);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ let isPatched = false;
68
+
69
+ // Function to get user's country code
70
+ function getUserCountryCode() {
71
+ try {
72
+ // Always use window.yt.config_.GL as primary source
73
+ if (window.yt && window.yt.config_ && window.yt.config_.GL) {
74
+ return window.yt.config_.GL;
75
+ }
76
+
77
+ console.warn(
78
+ "TizenTube Subtitle Localization: Could not determine user country code"
79
+ );
80
+ return null;
81
+ } catch (error) {
82
+ console.error(
83
+ "TizenTube Subtitle Localization: Error getting country code:",
84
+ error
85
+ );
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // Function to check if language already exists in the menu
91
+ function languageExistsInMenu(items, languageCode, languageName) {
92
+ return items.some((item) => {
93
+ if (
94
+ item.compactLinkRenderer &&
95
+ item.compactLinkRenderer.serviceEndpoint
96
+ ) {
97
+ const commands =
98
+ item.compactLinkRenderer.serviceEndpoint.commandExecutorCommand
99
+ ?.commands;
100
+ if (
101
+ commands &&
102
+ commands[0] &&
103
+ commands[0].selectSubtitlesTrackCommand
104
+ ) {
105
+ const translationLang =
106
+ commands[0].selectSubtitlesTrackCommand.translationLanguage;
107
+ return (
108
+ translationLang &&
109
+ (translationLang.languageCode === languageCode ||
110
+ translationLang.languageName === languageName)
111
+ );
112
+ }
113
+ }
114
+ return false;
115
+ });
116
+ }
117
+
118
+ // Function to create a language option
119
+ function createLanguageOption(languageCode, languageName) {
120
+ return {
121
+ compactLinkRenderer: {
122
+ title: { simpleText: languageName },
123
+ serviceEndpoint: {
124
+ commandExecutorCommand: {
125
+ commands: [
126
+ {
127
+ selectSubtitlesTrackCommand: {
128
+ translationLanguage: {
129
+ languageCode,
130
+ languageName,
131
+ },
132
+ },
133
+ },
134
+ {
135
+ openClientOverlayAction: {
136
+ type: "CLIENT_OVERLAY_TYPE_CAPTIONS_LANGUAGE",
137
+ updateAction: true,
138
+ },
139
+ },
140
+ {
141
+ signalAction: { signal: "POPUP_BACK" },
142
+ },
143
+ ],
144
+ },
145
+ },
146
+ secondaryIcon: { iconType: "RADIO_BUTTON_UNCHECKED" },
147
+ },
148
+ };
149
+ }
150
+
151
+ // Function to get languages already present in menu
152
+ function getExistingLanguages(items) {
153
+ const existingLanguages = new Set();
154
+
155
+ items.forEach((item) => {
156
+ if (
157
+ item.compactLinkRenderer &&
158
+ item.compactLinkRenderer.serviceEndpoint
159
+ ) {
160
+ const commands =
161
+ item.compactLinkRenderer.serviceEndpoint.commandExecutorCommand
162
+ ?.commands;
163
+ if (
164
+ commands &&
165
+ commands[0] &&
166
+ commands[0].selectSubtitlesTrackCommand
167
+ ) {
168
+ const translationLang =
169
+ commands[0].selectSubtitlesTrackCommand.translationLanguage;
170
+ if (translationLang) {
171
+ existingLanguages.add(translationLang.languageCode);
172
+ existingLanguages.add(translationLang.languageName);
173
+ }
174
+ }
175
+ }
176
+ });
177
+
178
+ return existingLanguages;
179
+ }
180
+
181
+ // Function to create section title
182
+ function createSectionTitle(title) {
183
+ return {
184
+ overlayMessageRenderer: {
185
+ title: { simpleText: "" },
186
+ subtitle: { simpleText: title },
187
+ style: "OVERLAY_MESSAGE_STYLE_SUBSECTION_TITLE",
188
+ },
189
+ };
190
+ }
191
+
192
+ // Main function to patch the subtitle menu
193
+ function patchSubtitleMenu() {
194
+ if (isPatched) return;
195
+
196
+ const player = document.querySelector('.html5-video-player');
197
+ if (!player) return setTimeout(patchSubtitleMenu, 250);
198
+
199
+ // Always patch if possible - settings will be checked dynamically
200
+ if (!window._yttv) return setTimeout(patchSubtitleMenu, 250);
201
+ const yttvInstance = Object.values(window._yttv).find(
202
+ (obj) =>
203
+ obj &&
204
+ obj.instance &&
205
+ typeof obj.instance.resolveCommand === "function"
206
+ );
207
+
208
+ if (
209
+ !yttvInstance ||
210
+ yttvInstance.instance.resolveCommand.isPatchedBySubtitleLocalization
211
+ ) {
212
+ if (!yttvInstance) {
213
+ console.error(
214
+ "TizenTube Subtitle Localization: Could not find resolveCommand instance."
215
+ );
216
+ } else {
217
+ console.log("TizenTube Subtitle Localization: Already patched.");
218
+ }
219
+ return;
220
+ }
221
+
222
+ const originalResolveCommand = yttvInstance.instance.resolveCommand;
223
+
224
+ yttvInstance.instance.resolveCommand = function (cmd, _) {
225
+ // Identify the correct command using its uniqueId
226
+ if (
227
+ cmd?.openPopupAction?.uniqueId ===
228
+ "CLIENT_OVERLAY_TYPE_CAPTIONS_AUTO_TRANSLATE"
229
+ ) {
230
+ // Check current settings dynamically each time menu opens
231
+ const showUserLanguage = configRead("enableShowUserLanguage");
232
+ const showOtherLanguages = configRead("enableShowOtherLanguages");
233
+
234
+ // If neither feature is enabled, don't modify the menu
235
+ if (!showUserLanguage && !showOtherLanguages) {
236
+ return originalResolveCommand.apply(this, arguments);
237
+ }
238
+
239
+ const items =
240
+ cmd.openPopupAction.popup.overlaySectionRenderer.overlay
241
+ .overlayTwoPanelRenderer.actionPanel.overlayPanelRenderer
242
+ .content.overlayPanelItemListRenderer.items;
243
+
244
+ // Get existing languages
245
+ const existingLanguages = getExistingLanguages(items);
246
+
247
+ // Add user's local language if enabled
248
+ if (showUserLanguage) {
249
+ const userCountryCode = getUserCountryCode();
250
+ const userLanguage = getCountryLanguage(userCountryCode);
251
+
252
+ if (userLanguage) {
253
+ // Check if the user's language already exists
254
+ if (
255
+ !languageExistsInMenu(items, userLanguage.code, userLanguage.name)
256
+ ) {
257
+ console.log(
258
+ `%c[TizenTube Subtitle Localization] Adding user's local language: ${userLanguage.name} (${userLanguage.code})`,
259
+ "background: #2196F3; color: #ffffff; font-size: 14px; font-weight: bold;"
260
+ );
261
+
262
+ const userLanguageOption = createLanguageOption(
263
+ userLanguage.code,
264
+ userLanguage.name
265
+ );
266
+
267
+ // Find the "Recommended languages" section and insert after it
268
+ const recommendedIndex = items.findIndex(
269
+ (item) =>
270
+ item.overlayMessageRenderer?.subtitle
271
+ ?.simpleText === "Recommended languages"
272
+ );
273
+
274
+ if (recommendedIndex > -1) {
275
+ // Insert user's language as the first recommendation
276
+ items.splice(
277
+ recommendedIndex + 1,
278
+ 0,
279
+ userLanguageOption
280
+ );
281
+ // Update existing languages set
282
+ existingLanguages.add(userLanguage.code);
283
+ existingLanguages.add(userLanguage.name);
284
+ } else {
285
+ // Find "Other languages" section and insert before it
286
+ const otherLanguagesIndex = items.findIndex(
287
+ (item) =>
288
+ item.overlayMessageRenderer?.subtitle
289
+ ?.simpleText === "Other languages"
290
+ );
291
+
292
+ if (otherLanguagesIndex > -1) {
293
+ items.splice(
294
+ otherLanguagesIndex,
295
+ 0,
296
+ userLanguageOption
297
+ );
298
+ } else {
299
+ // As a fallback, add it at the beginning
300
+ items.unshift(userLanguageOption);
301
+ }
302
+ // Update existing languages set
303
+ existingLanguages.add(userLanguage.code);
304
+ existingLanguages.add(userLanguage.name);
305
+ }
306
+ } else {
307
+ console.log(
308
+ `%c[TizenTube Subtitle Localization] User's language ${userLanguage.name} already exists in menu`,
309
+ "background: #4CAF50; color: #ffffff; font-size: 12px;"
310
+ );
311
+ }
312
+ } else {
313
+ console.warn(
314
+ `TizenTube Subtitle Localization: No language mapping found for country code: ${userCountryCode}`
315
+ );
316
+ }
317
+ }
318
+
319
+ // Create "Tizen Languages" section with all missing languages if enabled
320
+ if (showOtherLanguages) {
321
+ const missingLanguages = Object.entries(getComprehensiveLanguageList())
322
+ .filter(([code, name]) => !existingLanguages.has(code) && !existingLanguages.has(name))
323
+ .sort(([, a], [, b]) => a.localeCompare(b));
324
+
325
+ if (missingLanguages.length > 0) {
326
+ console.log(
327
+ `%c[TizenTube Subtitle Localization] Adding "Tizen Languages" section with ${missingLanguages.length} additional languages`,
328
+ "background: #FF9800; color: #ffffff; font-size: 12px;"
329
+ );
330
+
331
+ // Add section title
332
+ items.push(createSectionTitle("Other Languages"));
333
+
334
+ // Add all missing languages
335
+ missingLanguages.forEach(([code, name]) => {
336
+ items.push(createLanguageOption(code, name));
337
+ });
338
+
339
+ console.log(
340
+ `%c[TizenTube Subtitle Localization] Added "Tizen Languages" section`,
341
+ "background: #FF9800; color: #ffffff; font-size: 12px;"
342
+ );
343
+ } else {
344
+ console.log(
345
+ `%c[TizenTube Subtitle Localization] All languages already present in menu`,
346
+ "background: #4CAF50; color: #ffffff; font-size: 12px;"
347
+ );
348
+ }
349
+ }
350
+ }
351
+
352
+ // Let the original function run with our modified 'cmd' object
353
+ return originalResolveCommand.apply(this, arguments);
354
+ };
355
+
356
+ yttvInstance.instance.resolveCommand.isPatchedBySubtitleLocalization = true;
357
+ console.log("TizenTube Subtitle Localization: Patch successful!");
358
+ isPatched = true;
359
+ }
360
+
361
+ // Wait for the YouTube TV app to be ready
362
+ const interval = setInterval(() => {
363
+ if (window._yttv && Object.keys(window._yttv).length > 0) {
364
+ patchSubtitleMenu();
365
+ clearInterval(interval);
366
+ }
367
+ }, 1000);
368
+
369
+ // Also try to patch when DOM is loaded
370
+ if (document.readyState === "loading") {
371
+ document.addEventListener("DOMContentLoaded", patchSubtitleMenu);
372
+ } else {
373
+ patchSubtitleMenu();
374
+ }
375
+
376
+ console.log(
377
+ "TizenTube Subtitle Localization: Module loaded, waiting for YouTube TV..."
378
+ );
@@ -0,0 +1,198 @@
1
+ // Picture in Picture Mode for TizenTube
2
+
3
+ import resolveCommand from "../resolveCommand.js";
4
+
5
+ window.isPipPlaying = false;
6
+ let PlayerService = null;
7
+
8
+ function pipLoad() {
9
+ const mappings = Object.values(window._yttv).find(a => a && a.mappings);
10
+ PlayerService = mappings.get('PlayerService');
11
+ const PlaybackPreviewService = mappings.get('PlaybackPreviewService');
12
+ const PlaybackPreviewServiceStart = PlaybackPreviewService.start;
13
+ const PlaybackPreviewServiceStop = PlaybackPreviewService.stop;
14
+
15
+
16
+ PlaybackPreviewService.start = function (...args) {
17
+ if (window.isPipPlaying) return;
18
+ return PlaybackPreviewServiceStart.apply(this, args);
19
+ }
20
+
21
+ PlaybackPreviewService.stop = function (...args) {
22
+ if (window.isPipPlaying) return;
23
+ return PlaybackPreviewServiceStop.apply(this, args);
24
+ }
25
+ }
26
+
27
+ if (document.readyState === 'complete') {
28
+ pipLoad();
29
+ } else window.addEventListener('load', pipLoad);
30
+
31
+ function enablePip() {
32
+ if (!PlayerService) return;
33
+ const timestamp = Math.floor(document.querySelector('video').currentTime);
34
+ const videoElement = document.querySelector('video');
35
+
36
+ const ytlrPlayer = document.querySelector('ytlr-player');
37
+ const ytlrPlayerContainer = document.querySelector('ytlr-player-container');
38
+
39
+ const observer = new MutationObserver((mutations) => {
40
+ mutations.forEach((mutation) => {
41
+ if (mutation.attributeName === 'class') {
42
+ if (!ytlrPlayer.classList.contains('ytLrPlayerEnabled')) {
43
+ function setStyles() {
44
+ ytlrPlayerContainer.style.zIndex = '10';
45
+ ytlrPlayer.style.display = 'block';
46
+ ytlrPlayer.style.backgroundColor = 'rgba(0,0,0,0)';
47
+ }
48
+
49
+ setStyles();
50
+ setTimeout(setStyles, 500);
51
+
52
+ function onPipEnter() {
53
+ videoElement.style.removeProperty('inset');
54
+ const pipWidth = window.innerWidth / 3.5;
55
+ const pipHeight = window.innerHeight / 3.5;
56
+ videoElement.style.width = `${pipWidth}px`;
57
+ videoElement.style.height = `${pipHeight}px`;
58
+ videoElement.style.top = '68vh';
59
+ videoElement.style.left = '68vw';
60
+
61
+ window.isPipPlaying = true;
62
+ videoElement.removeEventListener('play', onPipEnter);
63
+ }
64
+
65
+ videoElement.addEventListener('play', onPipEnter);
66
+ observer.disconnect();
67
+
68
+ setTimeout(() => {
69
+ PlayerService.loadedPlaybackConfig.watchEndpoint.startTimeSeconds = timestamp;
70
+ PlayerService.loadVideo(PlayerService.loadedPlaybackConfig);
71
+ }, 1000);
72
+ }
73
+ }
74
+ });
75
+ });
76
+
77
+ observer.observe(ytlrPlayer, { attributes: true });
78
+
79
+ // Exit from the current video player
80
+ resolveCommand({
81
+ signalAction: {
82
+ signal: "HISTORY_BACK"
83
+ }
84
+ });
85
+ }
86
+
87
+ function pipToFullscreen() {
88
+ const { clickTrackingParams, commandMetadata, watchEndpoint } = PlayerService.loadedPlaybackConfig;
89
+ watchEndpoint.startTimeSeconds = Math.floor(document.querySelector('video').currentTime);
90
+ const command = {
91
+ clickTrackingParams,
92
+ commandMetadata,
93
+ watchEndpoint
94
+ };
95
+ resolveCommand(command);
96
+ window.isPipPlaying = false;
97
+ };
98
+
99
+ const originalClasses = {
100
+ ytlrSearchVoice: {
101
+ length: 0,
102
+ classes: []
103
+ },
104
+ ytlrSearchVoiceMicButton: {
105
+ length: 0,
106
+ classes: []
107
+ }
108
+ }
109
+
110
+ const observerPipEnter = new MutationObserver(() => {
111
+ if (!window.isPipPlaying) return;
112
+ const searchBar = document.querySelector('ytlr-search-bar');
113
+ if (searchBar) {
114
+ const pipButtonExists = document.querySelector('#tt-pip-button');
115
+ if (!pipButtonExists) {
116
+ const voiceButton = searchBar.querySelector('ytlr-search-voice');
117
+ if (voiceButton) {
118
+ const iconClassNames = Object.values(window._yttv).find(a => a instanceof Map && a.has("CLEAR_COOKIES"));
119
+ const iconClassToBeRemoved = iconClassNames.get('MICROPHONE_ON');
120
+ const iconClearCookiesClass = iconClassNames.get('CLEAR_COOKIES');
121
+ const pipButton = document.createElement('ytlr-search-voice');
122
+ for (let i = 0; i < voiceButton.classList.length; i++) {
123
+ if (originalClasses.ytlrSearchVoice.length === 0) {
124
+ originalClasses.ytlrSearchVoice.length = voiceButton.classList.length;
125
+ }
126
+
127
+ if (originalClasses.ytlrSearchVoice.length !== voiceButton.classList.length) {
128
+ for (const className of originalClasses.ytlrSearchVoice.classes) {
129
+ pipButton.classList.add(className);
130
+ }
131
+ break;
132
+ }
133
+
134
+ if (!originalClasses.ytlrSearchVoice.classes.includes(voiceButton.classList[i]))
135
+ originalClasses.ytlrSearchVoice.classes.push(voiceButton.classList[i]);
136
+
137
+ pipButton.classList.add(voiceButton.classList[i]);
138
+
139
+ }
140
+ pipButton.style.left = '10.25em';
141
+ pipButton.id = 'tt-pip-button';
142
+ const pipButtonMicButton = document.createElement('ytlr-search-voice-mic-button');
143
+ for (let i = 0; i < voiceButton.children[0].classList.length; i++) {
144
+ if (originalClasses.ytlrSearchVoiceMicButton.length === 0) {
145
+ originalClasses.ytlrSearchVoiceMicButton.length = voiceButton.children[0].classList.length;
146
+ }
147
+
148
+ if (originalClasses.ytlrSearchVoiceMicButton.length !== voiceButton.children[0].classList.length) {
149
+ for (const className of originalClasses.ytlrSearchVoiceMicButton.classes) {
150
+ pipButtonMicButton.classList.add(className);
151
+ }
152
+ break;
153
+ }
154
+
155
+ if (!originalClasses.ytlrSearchVoiceMicButton.classes.includes(voiceButton.children[0].classList[i]))
156
+ originalClasses.ytlrSearchVoiceMicButton.classes.push(voiceButton.children[0].classList[i]);
157
+
158
+ pipButtonMicButton.classList.add(voiceButton.children[0].classList[i]);
159
+ }
160
+ const pipIcon = document.createElement('yt-icon');
161
+ for (let i = 0; i < voiceButton.children[0].children[0].classList.length; i++) {
162
+ pipIcon.classList.add(voiceButton.children[0].children[0].classList[i]);
163
+ }
164
+ pipIcon.classList.remove(iconClassToBeRemoved);
165
+ pipIcon.classList.add(iconClearCookiesClass);
166
+
167
+ pipButtonMicButton.appendChild(pipIcon);
168
+ pipButton.appendChild(pipButtonMicButton);
169
+ searchBar.appendChild(pipButton);
170
+ } else {
171
+ const pipButton = document.createElement('ytlr-search-voice');
172
+ pipButton.style.left = '10.25em';
173
+ pipButton.id = 'tt-pip-button';
174
+ pipButton.setAttribute('idomkey', 'ytLrSearchBarSearchVoice');
175
+ pipButton.setAttribute('tabindex', '0');
176
+ pipButton.classList.add('ytLrSearchVoiceHost', 'ytLrSearchBarSearchVoice');
177
+ const pipButtonMicButton = document.createElement('ytlr-search-voice-mic-button');
178
+ pipButtonMicButton.setAttribute('hybridnavfocusable', 'true');
179
+ pipButtonMicButton.setAttribute('tabindex', '-1');
180
+ pipButtonMicButton.classList.add('ytLrSearchVoiceMicButtonHost', 'zylon-ve');
181
+ const pipIcon = document.createElement('yt-icon');
182
+ pipIcon.setAttribute('tabindex', '-1');
183
+ pipIcon.classList.add('ytContribIconTvArrowLeft', 'ytContribIconHost', 'ytLrSearchVoiceMicButtonIcon');
184
+
185
+ pipButtonMicButton.appendChild(pipIcon);
186
+ pipButton.appendChild(pipButtonMicButton);
187
+ searchBar.appendChild(pipButton);
188
+ }
189
+ }
190
+ }
191
+ });
192
+
193
+ observerPipEnter.observe(document.body, { childList: true, subtree: true });
194
+
195
+ export {
196
+ enablePip,
197
+ pipToFullscreen
198
+ }
@@ -0,0 +1,101 @@
1
+ import { configRead, configChangeEmitter } from "../config.js";
2
+
3
+ const SELECTORS = {
4
+ PLAYER: '.html5-video-player',
5
+ };
6
+
7
+ const EVENTS = {
8
+ YT_STATE_CHANGE: 'onStateChange',
9
+ CONFIG_CHANGE: 'configChange',
10
+ };
11
+
12
+ const CONFIG_KEYS = {
13
+ QUALITY: 'preferredVideoQuality',
14
+ };
15
+
16
+ class PreferredQualityHandler {
17
+ #player = null;
18
+ #attachTimeout = null;
19
+ #lastVideoId = null;
20
+ #hasAppliedQuality = false;
21
+
22
+ constructor() {
23
+ this.init();
24
+ }
25
+
26
+ init() {
27
+ this.#pollForPlayer();
28
+ this.#setupConfigListener();
29
+ }
30
+
31
+ #pollForPlayer() {
32
+ clearTimeout(this.#attachTimeout);
33
+
34
+ const playerElement = document.querySelector(SELECTORS.PLAYER);
35
+
36
+ if (!playerElement) {
37
+ this.#attachTimeout = setTimeout(() => this.#pollForPlayer(), 100);
38
+ return;
39
+ }
40
+
41
+ this.#player = playerElement;
42
+
43
+ this.#player.addEventListener(EVENTS.YT_STATE_CHANGE, this.#handleStateChange);
44
+
45
+ this.#handleStateChange();
46
+ }
47
+
48
+ #setupConfigListener() {
49
+ configChangeEmitter.addEventListener(EVENTS.CONFIG_CHANGE, (ev) => {
50
+ if (ev.detail?.key === CONFIG_KEYS.QUALITY) {
51
+ this.#applyQuality();
52
+ }
53
+ });
54
+ }
55
+
56
+ #handleStateChange = () => {
57
+ const state = this.#player?.getPlayerStateObject?.();
58
+ const videoData = this.#player?.getVideoData?.();
59
+ const videoId = videoData?.video_id;
60
+
61
+ if (videoId !== this.#lastVideoId) {
62
+ this.#lastVideoId = videoId;
63
+ this.#hasAppliedQuality = false;
64
+ }
65
+
66
+ const isShorts = Object.values(this.#player.getVideoStats()).find(a => a && a === 'shortspage');
67
+ if (state?.isPlaying && !this.#hasAppliedQuality && !isShorts) {
68
+ this.#applyQuality();
69
+ this.#hasAppliedQuality = true;
70
+ }
71
+ };
72
+
73
+ #applyQuality() {
74
+ const preferredQuality = configRead(CONFIG_KEYS.QUALITY);
75
+ if (!preferredQuality || preferredQuality === 'auto' || !this.#player) return;
76
+
77
+ try {
78
+ const quality = this.#determineQuality(preferredQuality);
79
+
80
+ if (quality) {
81
+ this.#player.setPlaybackQualityRange(quality, quality)
82
+ }
83
+ } catch (e) {
84
+ console.warn('[PreferredQuality] Failed to apply quality:', e);
85
+ }
86
+ }
87
+
88
+ #determineQuality(preference) {
89
+ const availableQualities = this.#player.getAvailableQualityData();
90
+ if (!availableQualities?.length) return 'highres';
91
+
92
+ const getQualityValue = (label) => parseInt(label, 10) || 0;
93
+ const targetValue = getQualityValue(preference);
94
+
95
+ const match = availableQualities.find(q => getQualityValue(q.qualityLabel) === targetValue);
96
+
97
+ return match ? match.quality : 'highres';
98
+ }
99
+ }
100
+
101
+ window.preferredVideoQualityHandler = new PreferredQualityHandler();