Skip to content

Commit 5540728

Browse files
authored
Merge pull request #2187 from tech234a/master
Implement support for YouTube TV (tv.youtube.com)
2 parents b0984e9 + 9ab235c commit 5540728

13 files changed

+172
-28
lines changed

public/_locales

public/content.css

+24
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131
transition: transform .1s cubic-bezier(0,0,0.2,1);
3232
}
3333

34+
/* Prevent bar from covering highlights on YTTV */
35+
#previewbar.sponsorblock-yttv-container {
36+
z-index: unset;
37+
}
38+
39+
ytu-time-bar.ytu-storyboard {
40+
text-align: center;
41+
}
42+
3443
/* May 2024 hover preview */
3544
.YtPlayerProgressBarProgressBar #previewbar {
3645
transform: none;
@@ -67,6 +76,11 @@ div:hover > #previewbar.sbNotInvidious {
6776
min-width: 1px;
6877
}
6978

79+
.previewbar-yttv {
80+
height: 10px;
81+
top: 14px;
82+
}
83+
7084
.previewbar.requiredSegment {
7185
transform: scaleY(3);
7286
}
@@ -184,6 +198,16 @@ div:hover > .sponsorBlockChapterBar {
184198
padding-right: 3.6px;
185199
}
186200

201+
.sbButtonYTTV {
202+
padding-left: 5px !important;
203+
}
204+
205+
/* YTTV only */
206+
.ytu-player-controls > .skipButtonControlBarContainer > div {
207+
padding-left: 5px;
208+
align-content: center;
209+
}
210+
187211
.autoHiding {
188212
overflow: visible !important;
189213
}

src/components/ChapterVoteComponent.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
4444
<>
4545
{/* Upvote Button */}
4646
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
47-
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden" : "")}
47+
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
4848
draggable="false"
4949
title={chrome.i18n.getMessage("upvoteButtonInfo")}
5050
onClick={(e) => this.vote(e, 1)}>
@@ -55,7 +55,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
5555

5656
{/* Downvote Button */}
5757
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
58-
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden" : "")}
58+
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
5959
draggable="false"
6060
title={chrome.i18n.getMessage("reportButtonInfo")}
6161
onClick={(e) => {

src/content.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { ChapterVote } from "./render/ChapterVote";
3535
import { openWarningDialog } from "./utils/warnings";
3636
import { isFirefoxOrSafari, waitFor } from "../maze-utils/src";
3737
import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating";
38-
import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, getVideoDuration, verifyCurrentTime, waitForVideo } from "../maze-utils/src/video";
38+
import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, isOnYTTV, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, getVideoDuration, verifyCurrentTime, waitForVideo } from "../maze-utils/src/video";
3939
import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config";
4040
import { findValidElement } from "../maze-utils/src/dom"
4141
import { getHash, HashedValue } from "../maze-utils/src/hash";
@@ -240,7 +240,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
240240
break;
241241
case "getChannelID":
242242
sendResponse({
243-
channelID: getChannelIDInfo().id
243+
channelID: getChannelIDInfo().id,
244+
isYTTV: (document.location.host === "tv.youtube.com")
244245
});
245246

246247
break;
@@ -553,6 +554,10 @@ function getPreviewBarAttachElement(): HTMLElement | null {
553554
}, {
554555
// For Vorapis v3
555556
selector: ".ytp-progress-bar-container > .html5-progress-bar > .ytp-progress-list"
557+
}, {
558+
// For YTTV
559+
selector: ".yssi-slider > div.ytu-ss-timeline-container",
560+
isVisibleCheck: false
556561
}
557562
];
558563

@@ -578,7 +583,7 @@ function createPreviewBar(): void {
578583

579584
if (el) {
580585
const chapterVote = new ChapterVote(voteAsync);
581-
previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), chapterVote, () => importExistingChapters(true));
586+
previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), isOnYTTV(), chapterVote, () => importExistingChapters(true));
582587

583588
updatePreviewBar();
584589
}
@@ -1147,7 +1152,8 @@ function setupSkipButtonControlBar() {
11471152
forceAutoSkip: true
11481153
}),
11491154
selectSegment,
1150-
onMobileYouTube: isOnMobileYouTube()
1155+
onMobileYouTube: isOnMobileYouTube(),
1156+
onYTTV: isOnYTTV(),
11511157
});
11521158
}
11531159

@@ -1850,6 +1856,9 @@ function createButton(baseID: string, title: string, callback: () => void, image
18501856
newButton.id = baseID + "Button";
18511857
newButton.classList.add("playerButton");
18521858
newButton.classList.add("ytp-button");
1859+
if (isOnYTTV()) {
1860+
newButton.setAttribute("style", "width: 40px; height: 40px");
1861+
}
18531862
newButton.setAttribute("title", chrome.i18n.getMessage(title));
18541863
newButton.addEventListener("click", () => {
18551864
callback();
@@ -1924,7 +1933,7 @@ async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
19241933
updateEditButtonsOnPlayer();
19251934

19261935
// Don't show the info button on embeds
1927-
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious()
1936+
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious() || isOnYTTV()
19281937
|| document.getElementById("sponsorBlockPopupContainer") != null) {
19291938
playerButtons.info.button.style.display = "none";
19301939
} else {
@@ -1991,6 +2000,11 @@ function getRealCurrentTime(): number {
19912000
}
19922001

19932002
function startOrEndTimingNewSegment() {
2003+
if (isOnYTTV() && getIsLivePremiere()) {
2004+
alert(chrome.i18n.getMessage("yttvLiveContentWarning"));
2005+
return;
2006+
}
2007+
19942008
verifyCurrentTime();
19952009
const roundedTime = Math.round((getRealCurrentTime() + Number.EPSILON) * 1000) / 1000;
19962010
if (!isSegmentCreationInProgress()) {
@@ -2694,6 +2708,7 @@ function showTimeWithoutSkips(skippedDuration: number): void {
26942708
// YouTube player time display
26952709
const selector =
26962710
isOnInvidious() ? ".vjs-duration" :
2711+
isOnYTTV() ? ".ypl-full-controls .ypmcs-control .time-info-bar" :
26972712
isOnMobileYouTube() ? ".ytwPlayerTimeDisplayContent" :
26982713
".ytp-time-display.notranslate .ytp-time-wrapper";
26992714
const display = document.querySelector(selector);

src/js-components/previewBar.ts

+94-14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { findValidElement } from "../../maze-utils/src/dom";
1515
import { addCleanupListener } from "../../maze-utils/src/cleanup";
1616
import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils";
1717
import { isVorapisInstalled } from "../utils/compatibility";
18+
import { isOnYTTV } from "../../maze-utils/src/video";
1819

1920
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
2021
const MIN_CHAPTER_SIZE = 0.003;
@@ -41,6 +42,12 @@ class PreviewBar {
4142
categoryTooltip?: HTMLDivElement;
4243
categoryTooltipContainer?: HTMLElement;
4344
chapterTooltip?: HTMLDivElement;
45+
46+
// ScrubTooltips for YTTV only
47+
categoryScrubTooltip?: HTMLDivElement;
48+
categoryScrubTooltipContainer?: HTMLElement;
49+
chapterScrubTooltip?: HTMLDivElement;
50+
4451
lastSmallestSegment: Record<string, {
4552
index: number;
4653
segment: PreviewBarSegment;
@@ -49,6 +56,7 @@ class PreviewBar {
4956
parent: HTMLElement;
5057
onMobileYouTube: boolean;
5158
onInvidious: boolean;
59+
onYTTV: boolean;
5260
progressBar: HTMLElement;
5361

5462
segments: PreviewBarSegment[] = [];
@@ -70,14 +78,19 @@ class PreviewBar {
7078
unfilteredChapterGroups: ChapterGroup[];
7179
chapterGroups: ChapterGroup[];
7280

73-
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
81+
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, onYTTV: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
7482
if (test) return;
7583
this.container = document.createElement('ul');
7684
this.container.id = 'previewbar';
7785

86+
if (onYTTV) {
87+
this.container.classList.add("sponsorblock-yttv-container");
88+
}
89+
7890
this.parent = parent;
7991
this.onMobileYouTube = onMobileYouTube;
8092
this.onInvidious = onInvidious;
93+
this.onYTTV = onYTTV;
8194
this.chapterVote = chapterVote;
8295
this.updateExistingChapters = updateExistingChapters;
8396

@@ -97,26 +110,49 @@ class PreviewBar {
97110

98111
// Create label placeholder
99112
this.categoryTooltip = document.createElement("div");
100-
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
113+
if (isOnYTTV()) {
114+
this.categoryTooltip.className = "sponsorCategoryTooltip";
115+
} else {
116+
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
117+
}
101118
this.chapterTooltip = document.createElement("div");
102-
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
119+
if (isOnYTTV()) {
120+
this.chapterTooltip.className = "sponsorCategoryTooltip";
121+
} else {
122+
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
123+
}
124+
125+
if (isOnYTTV()) {
126+
this.categoryScrubTooltip = document.createElement("div");
127+
this.categoryScrubTooltip.className = "sponsorCategoryTooltip";
128+
this.chapterScrubTooltip = document.createElement("div");
129+
this.chapterScrubTooltip.className = "sponsorCategoryTooltip";
130+
}
103131

104-
// global chaper tooltip or duration tooltip
105-
// YT, Vorapis, unknown
106-
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
107-
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip)") as HTMLElement;
132+
// global chapter tooltip or duration tooltip
133+
// YT, Vorapis, unknown, YTTV
134+
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container, .yssi-slider .ys-seek-details .time-info-bar") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
135+
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip), .current-time:not(.sponsorCategoryTooltip)") as HTMLElement;
108136
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
109137

110138
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
111139
this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
112-
// YT, Vorapis
113-
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text") as HTMLElement;
140+
// YT, Vorapis, YTTV
141+
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text, .current-time") as HTMLElement;
114142
if (!this.categoryTooltipContainer || !titleTooltip) return;
115143

116144
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
117145
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
118146

119-
const seekBar = document.querySelector(".ytp-progress-bar-container");
147+
if (isOnYTTV()) {
148+
const scrubTooltipTextWrapper = document.querySelector(".yssi-slider .ysl-filmstrip-lens .time-info-bar")
149+
if (!this.categoryTooltipContainer) return;
150+
151+
scrubTooltipTextWrapper.appendChild(this.categoryScrubTooltip);
152+
scrubTooltipTextWrapper.appendChild(this.chapterScrubTooltip);
153+
}
154+
155+
const seekBar = (document.querySelector(".ytp-progress-bar-container, .ypcs-scrub-slider-slot.ytu-player-controls"));
120156
if (!seekBar) return;
121157

122158
let mouseOnSeekBar = false;
@@ -163,6 +199,12 @@ class PreviewBar {
163199
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
164200
originalTooltip.style.removeProperty("display");
165201
}
202+
if (this.onYTTV) {
203+
this.setTooltipTitle(mainSegment, this.categoryTooltip);
204+
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
205+
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
206+
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
207+
}
166208
} else {
167209
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
168210
if (mainSegment !== null && secondarySegment !== null) {
@@ -174,6 +216,10 @@ class PreviewBar {
174216

175217
this.setTooltipTitle(mainSegment, this.categoryTooltip);
176218
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
219+
if (this.onYTTV) {
220+
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
221+
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
222+
}
177223

178224
if (isVorapisInstalled()) {
179225
const tooltipParent = tooltipTextWrapper.parentElement!;
@@ -226,7 +272,12 @@ class PreviewBar {
226272
}
227273

228274
// On the seek bar
229-
this.parent.prepend(this.container);
275+
if (this.onYTTV) {
276+
// order of sibling elements matters on YTTV
277+
this.parent.insertBefore(this.container, this.parent.firstChild.nextSibling.nextSibling);
278+
} else {
279+
this.parent.prepend(this.container);
280+
}
230281
}
231282

232283
clear(): void {
@@ -362,6 +413,10 @@ class PreviewBar {
362413
bar.style.marginRight = `${this.chapterMargin}px`;
363414
}
364415

416+
if (this.onYTTV) {
417+
bar.classList.add("previewbar-yttv");
418+
}
419+
365420
return bar;
366421
}
367422

@@ -868,8 +923,10 @@ class PreviewBar {
868923
})[0];
869924

870925
const chapterButton = this.getChapterButton(chaptersContainer);
871-
chapterButton.classList.remove("ytp-chapter-container-disabled");
872-
chapterButton.disabled = false;
926+
if (chapterButton) {
927+
chapterButton.classList.remove("ytp-chapter-container-disabled");
928+
chapterButton.disabled = false;
929+
}
873930

874931
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
875932
chapterTitle.style.display = "none";
@@ -878,6 +935,9 @@ class PreviewBar {
878935
const elem = document.createElement("div");
879936
chapterTitle.parentElement.insertBefore(elem, chapterTitle);
880937
elem.classList.add("sponsorChapterText");
938+
if (document.location.host === "tv.youtube.com") {
939+
elem.style.lineHeight = "initial";
940+
}
881941
return elem;
882942
})()) as HTMLDivElement;
883943
chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
@@ -890,7 +950,15 @@ class PreviewBar {
890950

891951
if (chosenSegment.source === SponsorSourceType.Server) {
892952
const chapterVoteContainer = this.chapterVote.getContainer();
893-
if (!chapterButton.contains(chapterVoteContainer)) {
953+
if (document.location.host === "tv.youtube.com") {
954+
if (!chaptersContainer.contains(chapterVoteContainer)) {
955+
const oldVoteContainers = document.querySelectorAll("#chapterVote");
956+
if (oldVoteContainers.length > 0) {
957+
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
958+
}
959+
chaptersContainer.appendChild(chapterVoteContainer);
960+
}
961+
} else if (!chapterButton.contains(chapterVoteContainer)) {
894962
const oldVoteContainers = document.querySelectorAll("#chapterVote");
895963
if (oldVoteContainers.length > 0) {
896964
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
@@ -929,6 +997,18 @@ class PreviewBar {
929997
}
930998

931999
private getChaptersContainer(): HTMLElement {
1000+
if (document.location.host === "tv.youtube.com") {
1001+
if (!document.querySelector(".ytp-chapter-container")) {
1002+
const dest = document.querySelector(".ypcs-control-buttons-left");
1003+
if (!dest) return null;
1004+
const sbChapterContainer = document.createElement("div");
1005+
sbChapterContainer.className = "ytp-chapter-container";
1006+
const sbChapterTitleContent = document.createElement("div");
1007+
sbChapterTitleContent.className = "ytp-chapter-title-content";
1008+
sbChapterContainer.appendChild(sbChapterTitleContent);
1009+
dest.appendChild(sbChapterContainer);
1010+
}
1011+
}
9321012
return document.querySelector(".ytp-chapter-container") as HTMLElement;
9331013
}
9341014

0 commit comments

Comments
 (0)