@oxymoron.13/tizentube-s95b 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -42
- package/S95B-CHANGES.txt +10 -4
- package/UPSTREAM-README.md +42 -0
- package/dist/userScript.js +2 -2
- package/mods/config.js +117 -0
- package/mods/domrect-polyfill.js +104 -0
- package/mods/features/adblock.js +584 -0
- package/mods/features/autoFrameRate.js +42 -0
- package/mods/features/enableFeatures.js +19 -0
- package/mods/features/moreSubtitles.js +378 -0
- package/mods/features/pictureInPicture.js +198 -0
- package/mods/features/preferredVideoQuality.js +101 -0
- package/mods/features/s95bCodec.js +45 -0
- package/mods/features/s95bI18n.js +28 -0
- package/mods/features/s95bMenu.js +142 -0
- package/mods/features/sponsorblock.js +437 -0
- package/mods/features/standaloneUserscript.js +107 -0
- package/mods/features/updater.js +119 -0
- package/mods/features/userAgentSpoofing.js +73 -0
- package/mods/features/videoQueuing.js +64 -0
- package/mods/package-lock.json +2321 -0
- package/mods/package.json +33 -0
- package/mods/resolveCommand.js +376 -0
- package/mods/rollup.config.js +41 -0
- package/mods/spatial-navigation-polyfill.js +1756 -0
- package/mods/tiny-sha256.js +99 -0
- package/mods/translations/i18nResources.js +11 -0
- package/mods/translations/index.js +18 -0
- package/mods/translations/language-names.js +981 -0
- package/mods/translations/resources/ar.json +308 -0
- package/mods/translations/resources/az.json +308 -0
- package/mods/translations/resources/bs.json +308 -0
- package/mods/translations/resources/cs.json +308 -0
- package/mods/translations/resources/de.json +308 -0
- package/mods/translations/resources/dk.json +308 -0
- package/mods/translations/resources/el.json +308 -0
- package/mods/translations/resources/en.json +308 -0
- package/mods/translations/resources/es-419.json +308 -0
- package/mods/translations/resources/es.json +308 -0
- package/mods/translations/resources/fi-FI.json +308 -0
- package/mods/translations/resources/fr.json +311 -0
- package/mods/translations/resources/hr.json +308 -0
- package/mods/translations/resources/hu.json +308 -0
- package/mods/translations/resources/id.json +308 -0
- package/mods/translations/resources/it.json +308 -0
- package/mods/translations/resources/ja.json +308 -0
- package/mods/translations/resources/lt.json +308 -0
- package/mods/translations/resources/lv.json +308 -0
- package/mods/translations/resources/pl.json +308 -0
- package/mods/translations/resources/pt-BR.json +308 -0
- package/mods/translations/resources/pt-PT.json +308 -0
- package/mods/translations/resources/ro.json +310 -0
- package/mods/translations/resources/ru.json +308 -0
- package/mods/translations/resources/sr-Cyrl.json +308 -0
- package/mods/translations/resources/sr-Latn.json +308 -0
- package/mods/translations/resources/tlh.json +308 -0
- package/mods/translations/resources/tr.json +308 -0
- package/mods/translations/resources/uk.json +308 -0
- package/mods/translations/resources/vi.json +309 -0
- package/mods/translations/resources/zh-CN.json +308 -0
- package/mods/translations/resources/zh-TW.json +308 -0
- package/mods/translations/update-translations.js +55 -0
- package/mods/ui/chapters.js +137 -0
- package/mods/ui/clock.js +71 -0
- package/mods/ui/customCommandExecution.js +49 -0
- package/mods/ui/customGuideAction.js +119 -0
- package/mods/ui/customUI.js +192 -0
- package/mods/ui/customYTSettings.js +29 -0
- package/mods/ui/disableWhosWatching.js +48 -0
- package/mods/ui/settings.js +1024 -0
- package/mods/ui/sidebarModification.js +179 -0
- package/mods/ui/speedUI.js +119 -0
- package/mods/ui/theme.js +26 -0
- package/mods/ui/ui.css +157 -0
- package/mods/ui/ui.js +261 -0
- package/mods/ui/ytUI.js +510 -0
- package/mods/userScript.js +28 -0
- package/mods/utils/ASTParser.js +67 -0
- package/mods/utils/innerTubeCalls.js +138 -0
- package/package.json +11 -5
- package/service/package-lock.json +4193 -0
- package/service/package.json +29 -0
- package/service/rollup.config.js +47 -0
- package/service/service.js +118 -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();
|