Compare commits

...

63 Commits

Author SHA1 Message Date
Ajay
48e8abb247 bump version 2025-03-09 14:28:08 -04:00
Ajay
a10f95f900 update translations 2025-03-09 14:28:03 -04:00
Ajay
70b415b67a Fix skipping on /live/ links
Fixes #2219
2025-03-09 03:15:18 -04:00
Ajay
c22a763da6 Fix issue with unfinished chapter submissions causing the seek bar to look wrong 2025-03-08 16:10:33 -05:00
Ajay
abbab887c6 More fixes for skiping accuracy with bluetooth headphones
Raise threshold virtual time is allowed to adjust by
2025-03-08 04:59:16 -05:00
Ajay
169350a5bf Fix extension context lost errors 2025-03-08 04:05:29 -05:00
Ajay
a4bb17f935 Fix skipping accuracy with bluetooth headphones on firefox 2025-03-08 03:56:19 -05:00
Ajay
1fa886690a Add back clarified comment 2025-03-06 21:22:30 -05:00
Ajay Ramachandran
5540728fc6 Merge pull request #2187 from tech234a/master
Implement support for YouTube TV (tv.youtube.com)
2025-03-06 03:16:36 -05:00
Ajay
9ab235c017 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/tech234a/2187 2025-03-06 03:14:22 -05:00
Ajay
e5cc0587e2 update submodules 2025-03-06 03:12:59 -05:00
Ajay
e8b3753a17 Remove confusing comment 2025-03-06 03:10:21 -05:00
Ajay Ramachandran
b0984e9bbc Merge pull request #2203 from gosha305/master
Added keybinds for voting on segments
2025-03-05 01:49:53 -05:00
Ajay
1a3964e238 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/gosha305/2203 2025-03-05 01:45:52 -05:00
Ajay
5903d1a023 Fixes for vote notice 2025-03-05 01:45:08 -05:00
Ajay
8b0b670ea8 update translation 2025-03-05 01:44:01 -05:00
Ajay
9ef948670c bump version 2025-03-04 21:08:39 -05:00
gosha305
5a7e16b51c Fixed chapter skip notices not working + fixed typo
Fixed chapter skip notices not working + fixed typo

Fixed chapter skip notices not working + fixed typo

Fixed chapter skip notices not working + fixed typo

A skip notice is now shown upon voting if one isn't already on screen

minor fixes

fixed commit error
2025-03-04 03:12:05 +01:00
Ajay
be98dc7b73 update translations 2025-03-02 23:41:58 -05:00
Ajay
3174eda29a bump version 2025-03-02 23:40:30 -05:00
Ajay
6a4bb50006 Fix document script player client fetching 2025-03-02 23:34:50 -05:00
gosha305
66876283b4 A skip notice is now shown upon voting if one isn't already on screen
A skip notice is now shown upon voting if one isn't already on screen

minor fixes
2025-02-20 21:00:14 +01:00
gosha305
75dc6a1980 Added limit to how far back the segment voted on can be 2025-02-12 20:15:32 +01:00
Ajay
2823e51b1d Allow importing placeholder chapters without start times 2025-02-11 20:28:10 -05:00
gosha305
c9c02e91ff Added keybinds to vote on the last segment
Added keybinds to vote on the last segment

fixed Upvote/Downvote keybind message variable names to match others in that options section
2025-02-11 23:16:01 +01:00
Ajay
19efcb5a98 Handle video being null in skip notice
Resolves #2201
2025-02-02 22:17:51 -05:00
Ajay
5c7e3b5cb3 bump version 2025-01-23 05:19:10 -05:00
Ajay
c3cbc44d28 Fix hiding segments not working on videos with YouTube chapters
Fixes #2194
2025-01-23 05:18:49 -05:00
Ajay
6965279d36 bump version 2025-01-22 14:17:15 -05:00
Ajay
eb61ad76c1 Add extension name to storage error message 2025-01-22 14:17:04 -05:00
Ajay
7a1fa601da bump version 2025-01-21 23:43:11 -05:00
Ajay
945ac5aad3 Fix local storage error appearing unnecessarily
Also adds more details to the log message
2025-01-21 23:42:58 -05:00
Ajay
617f0e25e8 bump version 2025-01-20 02:28:35 -05:00
Ajay
ac52a1cb1d update translations 2025-01-20 02:28:23 -05:00
Ajay
2cefb9822b Make copy button in popup use full segment ID 2025-01-18 03:13:40 -05:00
Ajay
b81de96277 Add support for partial segment IDs 2025-01-18 03:10:14 -05:00
Ajay
236c95d2f6 Only do pre-fetch on hover if there are segments near the start of the video 2025-01-18 00:25:33 -05:00
Ajay
0f75953ad1 bump version 2025-01-17 04:35:52 -05:00
Ajay
0f4c1c50cb Add delay to fetch on hover to make it less easy to trigger 2025-01-17 04:33:56 -05:00
Ajay
9e4939e2e3 Fix options page layout issue 2025-01-14 15:08:01 -05:00
tech234a
f4dc98112a Fix tests 2025-01-13 00:57:12 -05:00
tech234a
138e57b720 Use a specific error message channel whitelisting on YTTV 2025-01-13 00:47:23 -05:00
tech234a
de1d6bd76a Implement support for YouTube TV (tv.youtube.com) 2025-01-12 23:01:33 -05:00
Ajay
4a217300a2 bump version 2025-01-12 20:03:33 -05:00
Ajay
f4822fa587 update translations 2025-01-12 20:03:21 -05:00
Ajay
e4d6fdbd97 New filler wiki page 2025-01-11 15:33:04 -05:00
Ajay
1bd72e4db1 filler is gone🦀, long live tangents 2025-01-11 15:24:23 -05:00
Ajay
307eadc2da Fix compilation error 2025-01-10 13:45:30 -05:00
Ajay
99014dd113 Handle when local storage gets reset
Might fix the age old issue where the entire extension randomly becomes corrupted
2025-01-10 03:42:36 -05:00
Ajay
89b7cb0230 Fix audio glitch when chapter boundary passes
Fixes #1596
2025-01-10 03:29:04 -05:00
Ajay
e3da6c8643 bump version 2024-12-25 23:36:48 -05:00
Ajay
136f5e7d61 Fix preview bar on mobile for videos without chapters 2024-12-25 23:36:33 -05:00
Ajay
16f24978ff Fix offset with chapters on hover preview 2024-12-24 15:20:16 -05:00
Ajay
091d67e6b3 bump version 2024-12-24 14:34:49 -05:00
Ajay
fcf9bdc68e Fix linting issues 2024-12-24 14:34:19 -05:00
Ajay
af73aabbae Fix errors 2024-12-24 14:33:28 -05:00
Ajay
778b433ad4 Remove unused auto refetch segments option 2024-12-24 14:31:17 -05:00
Ajay
dc9c08f1fa Fix chapter titles still showing on hover after they've been removed from the seek bar 2024-12-24 14:29:07 -05:00
Ajay
4efa8b464e Fix capitalization 2024-12-14 18:30:13 -05:00
Ajay
e7d0180e15 Change options page to sentence case 2024-12-14 16:36:02 -05:00
Ajay
0fa24dbae4 bump version 2024-12-13 03:10:54 -05:00
Ajay
975b876a74 Fix preview bar on mobile
Fixes #2172
2024-12-13 03:10:17 -05:00
Ajay
557e2ea2df Fix full size manual skip notices showing "unskip" button instead of "skip"
Fixes #2160
2024-12-11 00:50:55 -05:00
28 changed files with 444 additions and 159 deletions

View File

@@ -24,7 +24,7 @@
"intro": "https://wiki.sponsor.ajay.app/w/Intermission/Intro_Animation", "intro": "https://wiki.sponsor.ajay.app/w/Intermission/Intro_Animation",
"outro": "https://wiki.sponsor.ajay.app/w/Endcards/Credits", "outro": "https://wiki.sponsor.ajay.app/w/Endcards/Credits",
"preview": "https://wiki.sponsor.ajay.app/w/Preview/Recap", "preview": "https://wiki.sponsor.ajay.app/w/Preview/Recap",
"filler": "https://wiki.sponsor.ajay.app/w/Filler_Tangent", "filler": "https://wiki.sponsor.ajay.app/w/Tangents/Jokes",
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section", "music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight", "poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines", "guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines",

View File

@@ -1,7 +1,7 @@
{ {
"name": "__MSG_fullName__", "name": "__MSG_fullName__",
"short_name": "SponsorBlock", "short_name": "SponsorBlock",
"version": "5.10.2", "version": "5.11.8",
"default_locale": "en", "default_locale": "en",
"description": "__MSG_Description__", "description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app", "homepage_url": "https://sponsor.ajay.app",

View File

@@ -31,6 +31,15 @@
transition: transform .1s cubic-bezier(0,0,0.2,1); transition: transform .1s cubic-bezier(0,0,0.2,1);
} }
/* Prevent bar from covering highlights on YTTV */
#previewbar.sponsorblock-yttv-container {
z-index: unset;
}
ytu-time-bar.ytu-storyboard {
text-align: center;
}
/* May 2024 hover preview */ /* May 2024 hover preview */
.YtPlayerProgressBarProgressBar #previewbar { .YtPlayerProgressBarProgressBar #previewbar {
transform: none; transform: none;
@@ -67,6 +76,11 @@ div:hover > #previewbar.sbNotInvidious {
min-width: 1px; min-width: 1px;
} }
.previewbar-yttv {
height: 10px;
top: 14px;
}
.previewbar.requiredSegment { .previewbar.requiredSegment {
transform: scaleY(3); transform: scaleY(3);
} }
@@ -88,7 +102,7 @@ div:hover > #previewbar.sbNotInvidious {
display: none !important; display: none !important;
} }
.ytp-tooltip.sponsorCategoryTooltipVisible { .ytp-tooltip.sponsorCategoryTooltipVisible:not(.sponsorTooltipHasYTChapters) {
transform: translateY(-1em) !important; transform: translateY(-1em) !important;
} }
@@ -184,6 +198,16 @@ div:hover > .sponsorBlockChapterBar {
padding-right: 3.6px; padding-right: 3.6px;
} }
.sbButtonYTTV {
padding-left: 5px !important;
}
/* YTTV only */
.ytu-player-controls > .skipButtonControlBarContainer > div {
padding-left: 5px;
align-content: center;
}
.autoHiding { .autoHiding {
overflow: visible !important; overflow: visible !important;
} }

View File

@@ -373,6 +373,7 @@ input[type='number'] {
display: inline-block; display: inline-block;
width: 40px; width: 40px;
height: 24px; height: 24px;
min-width: 40px;
} }
.switch input { .switch input {

View File

@@ -154,20 +154,6 @@
<div class="small-description">__MSG_whatForceChannelCheck__</div> <div class="small-description">__MSG_whatForceChannelCheck__</div>
</div> </div>
<div data-type="toggle" data-sync="refetchWhenNotFound">
<div class="switch-container">
<label class="switch">
<input id="refetchWhenNotFound" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="refetchWhenNotFound">
__MSG_enableRefetchWhenNotFound__
</label>
</div>
<div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
</div>
<div data-type="toggle" data-sync="showCategoryWithoutPermission"> <div data-type="toggle" data-sync="showCategoryWithoutPermission">
<div class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">
@@ -486,6 +472,16 @@
<div class="inline"></div> <div class="inline"></div>
</div> </div>
<div data-type="keybind-change" data-sync="upvoteKeybind">
<label class="optionLabel">__MSG_setUpvoteKeybind__:</label>
<div class="inline"></div>
</div>
<div data-type="keybind-change" data-sync="downvoteKeybind">
<label class="optionLabel">__MSG_setDownvoteKeybind__:</label>
<div class="inline"></div>
</div>
</div> </div>
<div id="import" class="option-group hidden"> <div id="import" class="option-group hidden">

View File

@@ -43,7 +43,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
chrome.tabs.create({url: chrome.runtime.getURL(request.url)}); chrome.tabs.create({url: chrome.runtime.getURL(request.url)});
return false; return false;
case "submitVote": case "submitVote":
submitVote(request.type, request.UUID, request.category).then(callback); submitVote(request.type, request.UUID, request.category, request.videoID).then(callback);
//this allows the callback to be called later //this allows the callback to be called later
return true; return true;
@@ -214,7 +214,7 @@ async function unregisterFirefoxContentScript(id: string) {
} }
} }
async function submitVote(type: number, UUID: string, category: string) { async function submitVote(type: number, UUID: string, category: string, videoID: string) {
let userID = Config.config.userID; let userID = Config.config.userID;
if (userID == undefined || userID === "undefined") { if (userID == undefined || userID === "undefined") {
@@ -226,7 +226,7 @@ async function submitVote(type: number, UUID: string, category: string) {
const typeSection = (type !== undefined) ? "&type=" + type : "&category=" + category; const typeSection = (type !== undefined) ? "&type=" + type : "&category=" + category;
try { try {
const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&userID=" + userID + typeSection); const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&videoID=" + videoID + "&userID=" + userID + typeSection);
if (response.ok) { if (response.ok) {
return { return {

View File

@@ -44,7 +44,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
<> <>
{/* Upvote Button */} {/* Upvote Button */}
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"} <button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden" : "")} className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
draggable="false" draggable="false"
title={chrome.i18n.getMessage("upvoteButtonInfo")} title={chrome.i18n.getMessage("upvoteButtonInfo")}
onClick={(e) => this.vote(e, 1)}> onClick={(e) => this.vote(e, 1)}>
@@ -55,7 +55,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
{/* Downvote Button */} {/* Downvote Button */}
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"} <button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden" : "")} className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
draggable="false" draggable="false"
title={chrome.i18n.getMessage("reportButtonInfo")} title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={(e) => { onClick={(e) => {

View File

@@ -1,12 +1,12 @@
import * as React from "react"; import * as React from "react";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Config from "../config" import Config from "../config"
import { Category, ContentContainer, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types"; import { Category, ContentContainer, SponsorTime, NoticeVisibilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import NoticeComponent from "./NoticeComponent"; import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils"; import Utils from "../utils";
const utils = new Utils(); const utils = new Utils();
import { getSkippingText, getUpcomingText } from "../utils/categoryUtils"; import { getSkippingText, getUpcomingText, getVoteText } from "../utils/categoryUtils";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg"; import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
@@ -29,6 +29,7 @@ export interface SkipNoticeProps {
autoSkip: boolean; autoSkip: boolean;
startReskip?: boolean; startReskip?: boolean;
upcomingNotice?: boolean; upcomingNotice?: boolean;
voteNotice?: boolean;
// Contains functions and variables from the content script needed by the skip notice // Contains functions and variables from the content script needed by the skip notice
contentContainer: ContentContainer; contentContainer: ContentContainer;
@@ -102,7 +103,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.autoSkip = props.autoSkip; this.autoSkip = props.autoSkip;
this.contentContainer = props.contentContainer; this.contentContainer = props.contentContainer;
const noticeTitle = !this.props.upcomingNotice ? getSkippingText(this.segments, this.props.autoSkip) : getUpcomingText(this.segments); const noticeTitle = this.props.voteNotice ? getVoteText(this.segments) : !this.props.upcomingNotice ? getSkippingText(this.segments, this.props.autoSkip) : getUpcomingText(this.segments);
const previousSkipNotices = document.querySelectorAll(".sponsorSkipNoticeParent:not(.sponsorSkipUpcomingNotice)"); const previousSkipNotices = document.querySelectorAll(".sponsorSkipNoticeParent:not(.sponsorSkipUpcomingNotice)");
this.amountOfPreviousNotices = previousSkipNotices.length; this.amountOfPreviousNotices = previousSkipNotices.length;
@@ -186,8 +187,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
idSuffix={this.idSuffix} idSuffix={this.idSuffix}
fadeIn={this.props.fadeIn} fadeIn={this.props.fadeIn}
fadeOut={!this.props.upcomingNotice} fadeOut={!this.props.upcomingNotice}
startFaded={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAll startFaded={Config.config.noticeVisibilityMode >= NoticeVisibilityMode.FadedForAll
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)} || (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.FadedForAutoSkip && this.autoSkip)}
timed={true} timed={true}
maxCountdownTime={this.state.maxCountdownTime} maxCountdownTime={this.state.maxCountdownTime}
style={noticeStyle} style={noticeStyle}
@@ -242,6 +243,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
</div> </div>
{/* Copy and Downvote Button */} {/* Copy and Downvote Button */}
{
!this.props.voteNotice &&
<div id={"sponsorTimesDownvoteButtonsContainerCopyDownvote" + this.idSuffix} <div id={"sponsorTimesDownvoteButtonsContainerCopyDownvote" + this.idSuffix}
className="voteButton" className="voteButton"
style={{marginLeft: "5px"}} style={{marginLeft: "5px"}}
@@ -251,6 +254,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|| this.state.choosingCategory === true || this.state.choosingCategory === true
? this.selectedColor : this.unselectedColor} /> ? this.selectedColor : this.unselectedColor} />
</div> </div>
}
</td> </td>
: :
@@ -278,11 +282,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
{/* Unskip/Skip Button */} {/* Unskip/Skip Button */}
{!this.props.smaller || this.segments[0].actionType === ActionType.Mute {!this.props.voteNotice && (!this.props.smaller || this.segments[0].actionType === ActionType.Mute)
? this.getSkipButton(1) : null} ? this.getSkipButton(1) : null}
{/* Never show button */} {/* Never show button */}
{!this.autoSkip || this.props.startReskip ? "" : {!this.autoSkip || this.props.startReskip || this.props.voteNotice ? "" :
<td className="sponsorSkipNoticeRightSection" <td className="sponsorSkipNoticeRightSection"
key={1}> key={1}>
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton" <button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
@@ -374,7 +378,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style.minWidth = "100px"; style.minWidth = "100px";
} }
const showSkipButton = (buttonIndex !== 0 || this.props.smaller || this.segments[0].actionType === ActionType.Mute) && !this.props.upcomingNotice; const showSkipButton = (buttonIndex !== 0 || this.props.smaller || !this.props.voteNotice || this.segments[0].actionType === ActionType.Mute) && !this.props.upcomingNotice;
return ( return (
<span className="sponsorSkipNoticeUnskipSection" style={{ visibility: !showSkipButton ? "hidden" : null }}> <span className="sponsorSkipNoticeUnskipSection" style={{ visibility: !showSkipButton ? "hidden" : null }}>
@@ -624,14 +628,17 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
unskip(buttonIndex: number, index: number, forceSeek: boolean): void { unskip(buttonIndex: number, index: number, forceSeek: boolean): void {
this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime, forceSeek); this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime, forceSeek, this.props.voteNotice);
this.unskippedMode(buttonIndex, index, SkipButtonState.Redo); this.unskippedMode(buttonIndex, index, this.segments[0].actionType === ActionType.Poi ? SkipButtonState.Undo : SkipButtonState.Redo);
} }
reskip(buttonIndex: number, index: number, forceSeek: boolean): void { reskip(buttonIndex: number, index: number, forceSeek: boolean): void {
this.contentContainer().reskipSponsorTime(this.segments[index], forceSeek); this.contentContainer().reskipSponsorTime(this.segments[index], forceSeek);
this.reskippedMode(buttonIndex);
}
reskippedMode(buttonIndex: number): void {
const skipButtonStates = this.state.skipButtonStates; const skipButtonStates = this.state.skipButtonStates;
skipButtonStates[buttonIndex] = SkipButtonState.Undo; skipButtonStates[buttonIndex] = SkipButtonState.Undo;
@@ -661,7 +668,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
getUnskippedModeInfo(buttonIndex: number, index: number, skipButtonState: SkipButtonState): SkipNoticeState { getUnskippedModeInfo(buttonIndex: number, index: number, skipButtonState: SkipButtonState): SkipNoticeState {
const changeCountdown = this.segments[index].actionType !== ActionType.Poi; const changeCountdown = !this.props.voteNotice && this.segments[index].actionType !== ActionType.Poi;
const maxCountdownTime = changeCountdown ? const maxCountdownTime = changeCountdown ?
this.getFullDurationCountdown(index) : this.state.maxCountdownTime; this.getFullDurationCountdown(index) : this.state.maxCountdownTime;
@@ -669,7 +676,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
const skipButtonStates = this.state.skipButtonStates; const skipButtonStates = this.state.skipButtonStates;
const skipButtonCallbacks = this.state.skipButtonCallbacks; const skipButtonCallbacks = this.state.skipButtonCallbacks;
if (buttonIndex === null) { if (buttonIndex === null) {
for (let i = 0; i < this.segments.length; i++) { for (let i = 0; i < skipButtonStates.length; i++) {
skipButtonStates[i] = skipButtonState; skipButtonStates[i] = skipButtonState;
skipButtonCallbacks[i] = this.reskip.bind(this); skipButtonCallbacks[i] = this.reskip.bind(this);
} }
@@ -697,7 +704,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
getFullDurationCountdown(index: number): () => number { getFullDurationCountdown(index: number): () => number {
return () => { return () => {
const sponsorTime = this.segments[index]; const sponsorTime = this.segments[index];
const duration = Math.round((sponsorTime.segment[1] - getCurrentTime()) * (1 / getVideo().playbackRate)); const duration = Math.round((sponsorTime.segment[1] - getCurrentTime()) * (1 / (getVideo()?.playbackRate ?? 1)));
return Math.max(duration, Config.config.skipNoticeDuration); return Math.max(duration, Config.config.skipNoticeDuration);
}; };

View File

@@ -146,7 +146,9 @@ class KeybindDialogComponent extends React.Component<KeybindDialogProps, Keybind
this.props.option !== "actuallySubmitKeybind" && this.equals(Config.config['actuallySubmitKeybind']) || this.props.option !== "actuallySubmitKeybind" && this.equals(Config.config['actuallySubmitKeybind']) ||
this.props.option !== "previewKeybind" && this.equals(Config.config['previewKeybind']) || this.props.option !== "previewKeybind" && this.equals(Config.config['previewKeybind']) ||
this.props.option !== "closeSkipNoticeKeybind" && this.equals(Config.config['closeSkipNoticeKeybind']) || this.props.option !== "closeSkipNoticeKeybind" && this.equals(Config.config['closeSkipNoticeKeybind']) ||
this.props.option !== "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind'])) this.props.option !== "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind']) ||
this.props.option !== "downvoteKeybind" && this.equals(Config.config['downvoteKeybind']) ||
this.props.option !== "upvoteKeybind" && this.equals(Config.config['upvoteKeybind']))
return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true}; return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true};
return null; return null;

View File

@@ -1,6 +1,6 @@
import * as CompileConfig from "../config.json"; import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json"; import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types"; import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config"; import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
import { HashedValue } from "../maze-utils/src/hash"; import { HashedValue } from "../maze-utils/src/hash";
@@ -32,7 +32,7 @@ interface SBConfig {
trackDownvotesInPrivate: boolean; trackDownvotesInPrivate: boolean;
dontShowNotice: boolean; dontShowNotice: boolean;
showUpcomingNotice: boolean; showUpcomingNotice: boolean;
noticeVisibilityMode: NoticeVisbilityMode; noticeVisibilityMode: NoticeVisibilityMode;
hideVideoPlayerControls: boolean; hideVideoPlayerControls: boolean;
hideInfoButtonPlayerControls: boolean; hideInfoButtonPlayerControls: boolean;
hideDeleteButtonPlayerControls: boolean; hideDeleteButtonPlayerControls: boolean;
@@ -48,7 +48,6 @@ interface SBConfig {
audioNotificationOnSkip: boolean; audioNotificationOnSkip: boolean;
checkForUnlistedVideos: boolean; checkForUnlistedVideos: boolean;
testingServer: boolean; testingServer: boolean;
refetchWhenNotFound: boolean;
ytInfoPermissionGranted: boolean; ytInfoPermissionGranted: boolean;
allowExpirements: boolean; allowExpirements: boolean;
showDonationLink: boolean; showDonationLink: boolean;
@@ -98,6 +97,8 @@ interface SBConfig {
nextChapterKeybind: Keybind; nextChapterKeybind: Keybind;
previousChapterKeybind: Keybind; previousChapterKeybind: Keybind;
closeSkipNoticeKeybind: Keybind; closeSkipNoticeKeybind: Keybind;
upvoteKeybind: Keybind;
downvoteKeybind: Keybind;
// What categories should be skipped // What categories should be skipped
categorySelections: CategorySelection[]; categorySelections: CategorySelection[];
@@ -296,7 +297,7 @@ const syncDefaults = {
trackDownvotesInPrivate: false, trackDownvotesInPrivate: false,
dontShowNotice: false, dontShowNotice: false,
showUpcomingNotice: false, showUpcomingNotice: false,
noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip, noticeVisibilityMode: NoticeVisibilityMode.FadedForAutoSkip,
hideVideoPlayerControls: false, hideVideoPlayerControls: false,
hideInfoButtonPlayerControls: false, hideInfoButtonPlayerControls: false,
hideDeleteButtonPlayerControls: false, hideDeleteButtonPlayerControls: false,
@@ -312,7 +313,6 @@ const syncDefaults = {
audioNotificationOnSkip: false, audioNotificationOnSkip: false,
checkForUnlistedVideos: false, checkForUnlistedVideos: false,
testingServer: false, testingServer: false,
refetchWhenNotFound: true,
ytInfoPermissionGranted: false, ytInfoPermissionGranted: false,
allowExpirements: true, allowExpirements: true,
showDonationLink: true, showDonationLink: true,
@@ -358,6 +358,8 @@ const syncDefaults = {
nextChapterKeybind: { key: "ArrowRight", ctrl: true }, nextChapterKeybind: { key: "ArrowRight", ctrl: true },
previousChapterKeybind: { key: "ArrowLeft", ctrl: true }, previousChapterKeybind: { key: "ArrowLeft", ctrl: true },
closeSkipNoticeKeybind: { key: "Backspace" }, closeSkipNoticeKeybind: { key: "Backspace" },
downvoteKeybind: { key: "h", shift: true },
upvoteKeybind: { key: "g", shift: true },
categorySelections: [{ categorySelections: [{
name: "sponsor" as Category, name: "sponsor" as Category,
@@ -468,7 +470,11 @@ const syncDefaults = {
"preview-filler": { "preview-filler": {
color: "#2E0066", color: "#2E0066",
opacity: "0.7" opacity: "0.7"
} },
"chapter": {
color: "#fff",
opacity: "0"
},
} }
}; };

View File

@@ -35,7 +35,7 @@ import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings"; import { openWarningDialog } from "./utils/warnings";
import { isFirefoxOrSafari, waitFor } from "../maze-utils/src"; import { isFirefoxOrSafari, waitFor } from "../maze-utils/src";
import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating"; import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating";
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"; 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";
import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config"; import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config";
import { findValidElement } from "../maze-utils/src/dom" import { findValidElement } from "../maze-utils/src/dom"
import { getHash, HashedValue } from "../maze-utils/src/hash"; import { getHash, HashedValue } from "../maze-utils/src/hash";
@@ -80,7 +80,6 @@ let importingChaptersWaiting = false;
const skipNotices: SkipNotice[] = []; const skipNotices: SkipNotice[] = [];
let upcomingNotice: UpcomingNotice | null = null; let upcomingNotice: UpcomingNotice | null = null;
let activeSkipKeybindElement: ToggleSkippable = null; let activeSkipKeybindElement: ToggleSkippable = null;
let retryFetchTimeout: NodeJS.Timeout = null;
let shownSegmentFailedToFetchWarning = false; let shownSegmentFailedToFetchWarning = false;
let selectedSegment: SegmentUUID | null = null; let selectedSegment: SegmentUUID | null = null;
let previewedSegment = false; let previewedSegment = false;
@@ -173,7 +172,6 @@ let popupInitialised = false;
let submissionNotice: SubmissionNotice = null; let submissionNotice: SubmissionNotice = null;
let lastResponseStatus: number; let lastResponseStatus: number;
let retryCount = 0;
// Contains all of the functions and variables needed by the skip notice // Contains all of the functions and variables needed by the skip notice
const skipNoticeContentContainer: ContentContainer = () => ({ const skipNoticeContentContainer: ContentContainer = () => ({
@@ -242,7 +240,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
break; break;
case "getChannelID": case "getChannelID":
sendResponse({ sendResponse({
channelID: getChannelIDInfo().id channelID: getChannelIDInfo().id,
isYTTV: (document.location.host === "tv.youtube.com")
}); });
break; break;
@@ -308,7 +307,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
for (const segment of importedSegments) { for (const segment of importedSegments) {
if (!sponsorTimesSubmitting.some( if (!sponsorTimesSubmitting.some(
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1 (s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)) { && Math.abs(s.segment[1] - segment.segment[1]) < 1
&& s.description === segment.description)) {
const hasChaptersPermission = (Config.config.showCategoryWithoutPermission const hasChaptersPermission = (Config.config.showCategoryWithoutPermission
|| Config.config.permissions["chapter"]); || Config.config.permissions["chapter"]);
if (segment.category === "chapter" && (!utils.getCategorySelection("chapter") || !hasChaptersPermission)) { if (segment.category === "chapter" && (!utils.getCategorySelection("chapter") || !hasChaptersPermission)) {
@@ -391,7 +391,6 @@ if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
function resetValues() { function resetValues() {
lastCheckTime = 0; lastCheckTime = 0;
lastCheckVideoTime = -1; lastCheckVideoTime = -1;
retryCount = 0;
previewedSegment = false; previewedSegment = false;
firstPlay = true; firstPlay = true;
@@ -519,7 +518,7 @@ function handleMobileControlsMutations(): void {
function getPreviewBarAttachElement(): HTMLElement | null { function getPreviewBarAttachElement(): HTMLElement | null {
const progressElementOptions = [{ const progressElementOptions = [{
// For newer mobile YouTube (Sept 2024) // For newer mobile YouTube (Sept 2024)
selector: ".YtProgressBarLineHost, .YtChapteredProgressBarHost", selector: ".ytChapteredProgressBarHost, .ytProgressBarLineHost, .YtProgressBarLineHost, .YtChapteredProgressBarHost",
isVisibleCheck: true isVisibleCheck: true
}, { }, {
// For newer mobile YouTube (May 2024) // For newer mobile YouTube (May 2024)
@@ -555,6 +554,10 @@ function getPreviewBarAttachElement(): HTMLElement | null {
}, { }, {
// For Vorapis v3 // For Vorapis v3
selector: ".ytp-progress-bar-container > .html5-progress-bar > .ytp-progress-list" selector: ".ytp-progress-bar-container > .html5-progress-bar > .ytp-progress-list"
}, {
// For YTTV
selector: ".yssi-slider > div.ytu-ss-timeline-container",
isVisibleCheck: false
} }
]; ];
@@ -580,7 +583,7 @@ function createPreviewBar(): void {
if (el) { if (el) {
const chapterVote = new ChapterVote(voteAsync); const chapterVote = new ChapterVote(voteAsync);
previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), chapterVote, () => importExistingChapters(true)); previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), isOnYTTV(), chapterVote, () => importExistingChapters(true));
updatePreviewBar(); updatePreviewBar();
} }
@@ -760,7 +763,8 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
skippingFunction(currentTime); skippingFunction(currentTime);
} else { } else {
let delayTime = timeUntilSponsor * 1000 * (1 / getVideo().playbackRate); let delayTime = timeUntilSponsor * 1000 * (1 / getVideo().playbackRate);
if (delayTime < (isFirefoxOrSafari() && !isSafari() ? 750 : 300)) { if (delayTime < (isFirefoxOrSafari() && !isSafari() ? 750 : 300)
&& shouldAutoSkip(skippingSegments[0])) {
let forceStartIntervalTime: number | null = null; let forceStartIntervalTime: number | null = null;
if (isFirefoxOrSafari() && !isSafari() && delayTime > 300) { if (isFirefoxOrSafari() && !isSafari() && delayTime > 300) {
forceStartIntervalTime = await waitForNextTimeChange(); forceStartIntervalTime = await waitForNextTimeChange();
@@ -768,7 +772,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
// Use interval instead of timeout near the end to combat imprecise video time // Use interval instead of timeout near the end to combat imprecise video time
const startIntervalTime = forceStartIntervalTime || performance.now(); const startIntervalTime = forceStartIntervalTime || performance.now();
const startVideoTime = Math.max(currentTime, getCurrentTime()); const startVideoTime = Math.max(currentTime, getVirtualTime());
delayTime = (skipTime?.[0] - startVideoTime) * 1000 * (1 / getVideo().playbackRate); delayTime = (skipTime?.[0] - startVideoTime) * 1000 * (1 / getVideo().playbackRate);
let startWaitingForReportedTimeToChange = true; let startWaitingForReportedTimeToChange = true;
@@ -787,7 +791,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
} }
const intervalDuration = performance.now() - startIntervalTime; const intervalDuration = performance.now() - startIntervalTime;
if (intervalDuration + skipBuffer * 1000 >= delayTime || getCurrentTime() >= skipTime[0]) { if (intervalDuration + skipBuffer * 1000 >= delayTime || getVirtualTime() + skipBuffer >= skipTime[0]) {
clearInterval(currentSkipInterval); clearInterval(currentSkipInterval);
if (!isFirefoxOrSafari() && !getVideo().muted && !inMuteSegment(getCurrentTime(), true)) { if (!isFirefoxOrSafari() && !getVideo().muted && !inMuteSegment(getCurrentTime(), true)) {
// Workaround for more accurate skipping on Chromium // Workaround for more accurate skipping on Chromium
@@ -795,7 +799,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
getVideo().muted = false; getVideo().muted = false;
} }
skippingFunction(Math.max(getCurrentTime(), startVideoTime + getVideo().playbackRate * Math.max(delayTime, intervalDuration) / 1000)); skippingFunction(Math.max(getVirtualTime(), startVideoTime + getVideo().playbackRate * Math.max(delayTime, intervalDuration) / 1000));
} }
}, 0); }, 0);
} else { } else {
@@ -838,7 +842,7 @@ function getVirtualTime(): number {
(performance.now() - lastKnownVideoTime.preciseTime) * (getVideo()?.playbackRate || 1) / 1000 + lastKnownVideoTime.videoTime : null); (performance.now() - lastKnownVideoTime.preciseTime) * (getVideo()?.playbackRate || 1) / 1000 + lastKnownVideoTime.videoTime : null);
if (Config.config.useVirtualTime && !isSafari() && virtualTime if (Config.config.useVirtualTime && !isSafari() && virtualTime
&& Math.abs(virtualTime - getCurrentTime()) < 0.2 && getCurrentTime() !== 0) { && virtualTime > getCurrentTime() && virtualTime - getCurrentTime() < 0.8 && getCurrentTime() !== 0) {
return Math.max(virtualTime, getCurrentTime()); return Math.max(virtualTime, getCurrentTime());
} else { } else {
return getCurrentTime(); return getCurrentTime();
@@ -1148,7 +1152,8 @@ function setupSkipButtonControlBar() {
forceAutoSkip: true forceAutoSkip: true
}), }),
selectSegment, selectSegment,
onMobileYouTube: isOnMobileYouTube() onMobileYouTube: isOnMobileYouTube(),
onYTTV: isOnYTTV(),
}); });
} }
@@ -1241,11 +1246,7 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
if (!isNaN(getVideoDuration())) { if (!isNaN(getVideoDuration())) {
updatePreviewBar(); updatePreviewBar();
} }
} else {
retryFetch(404);
} }
} else {
retryFetch(lastResponseStatus);
} }
importExistingChapters(true); importExistingChapters(true);
@@ -1312,27 +1313,6 @@ async function lockedCategoriesLookup(): Promise<void> {
} }
} }
function retryFetch(errorCode: number): void {
sponsorDataFound = false;
if (!Config.config.refetchWhenNotFound) return;
if (retryFetchTimeout) clearTimeout(retryFetchTimeout);
if ((errorCode !== 404 && retryCount > 1) || (errorCode !== 404 && retryCount > 10)) {
// Too many errors (50x), give up
return;
}
retryCount++;
const delay = errorCode === 404 ? (30000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
retryFetchTimeout = setTimeout(() => {
if (getVideoID() && sponsorTimes?.length === 0
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
// sponsorsLookup();
}
}, delay);
}
/** /**
* Only should be used when it is okay to skip a sponsor when in the middle of it * Only should be used when it is okay to skip a sponsor when in the middle of it
* *
@@ -1427,13 +1407,14 @@ function updatePreviewBar(): void {
showLarger: segment.actionType === ActionType.Poi, showLarger: segment.actionType === ActionType.Poi,
description: segment.description, description: segment.description,
source: segment.source, source: segment.source,
requiredSegment: requiredSegment && (segment.UUID === requiredSegment || segment.UUID?.startsWith(requiredSegment)), requiredSegment: requiredSegment && (segment.UUID === requiredSegment || segment.UUID?.startsWith(requiredSegment) || requiredSegment.startsWith(segment.UUID)),
selectedSegment: selectedSegment && segment.UUID === selectedSegment selectedSegment: selectedSegment && segment.UUID === selectedSegment
}); });
}); });
} }
sponsorTimesSubmitting.forEach((segment) => { sponsorTimesSubmitting.forEach((segment) => {
if (segment.actionType !== ActionType.Chapter || segment.segment.length > 1) {
previewBarSegments.push({ previewBarSegments.push({
segment: segment.segment as [number, number], segment: segment.segment as [number, number],
category: segment.category, category: segment.category,
@@ -1443,6 +1424,7 @@ function updatePreviewBar(): void {
description: segment.description, description: segment.description,
source: segment.source source: segment.source
}); });
}
}); });
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), getVideoDuration()) previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), getVideoDuration())
@@ -1705,7 +1687,7 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped:
counted = true; counted = true;
} }
if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID + "&videoID=" + getVideoID());
} }
} }
} }
@@ -1805,7 +1787,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
if (autoSkip || isSubmittingSegment) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true); if (autoSkip || isSubmittingSegment) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
} }
function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, unskipTime: number, startReskip: boolean) { function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, unskipTime: number, startReskip: boolean, voteNotice = false) {
for (const skipNotice of skipNotices) { for (const skipNotice of skipNotices) {
if (skippingSegments.length === skipNotice.segments.length if (skippingSegments.length === skipNotice.segments.length
&& skippingSegments.every((segment) => skipNotice.segments.some((s) => s.UUID === segment.UUID))) { && skippingSegments.every((segment) => skipNotice.segments.some((s) => s.UUID === segment.UUID))) {
@@ -1819,7 +1801,7 @@ function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, un
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, () => { const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, () => {
upcomingNotice?.close(); upcomingNotice?.close();
upcomingNotice = null; upcomingNotice = null;
}, unskipTime, startReskip, upcomingNoticeShown); }, unskipTime, startReskip, upcomingNoticeShown, voteNotice);
if (isOnMobileYouTube() || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false); if (isOnMobileYouTube() || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
skipNotices.push(newSkipNotice); skipNotices.push(newSkipNotice);
@@ -1838,13 +1820,13 @@ function createUpcomingNotice(skippingSegments: SponsorTime[], timeLeft: number,
upcomingNotice = new UpcomingNotice(skippingSegments, skipNoticeContentContainer, timeLeft / 1000, autoSkip); upcomingNotice = new UpcomingNotice(skippingSegments, skipNoticeContentContainer, timeLeft / 1000, autoSkip);
} }
function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false) { function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false, voteNotice = false) {
if (segment.actionType === ActionType.Mute) { if (segment.actionType === ActionType.Mute) {
getVideo().muted = false; getVideo().muted = false;
videoMuted = false; videoMuted = false;
} }
if (forceSeek || segment.actionType === ActionType.Skip) { if (forceSeek || segment.actionType === ActionType.Skip || voteNotice) {
//add a tiny bit of time to make sure it is not skipped again //add a tiny bit of time to make sure it is not skipped again
setCurrentTime(unskipTime ?? segment.segment[0] + 0.001); setCurrentTime(unskipTime ?? segment.segment[0] + 0.001);
} }
@@ -1876,6 +1858,10 @@ function createButton(baseID: string, title: string, callback: () => void, image
newButton.id = baseID + "Button"; newButton.id = baseID + "Button";
newButton.classList.add("playerButton"); newButton.classList.add("playerButton");
newButton.classList.add("ytp-button"); newButton.classList.add("ytp-button");
if (isOnYTTV()) {
// Some style needs to be set here, but the numbers don't matter
newButton.setAttribute("style", "width: 40px; height: 40px");
}
newButton.setAttribute("title", chrome.i18n.getMessage(title)); newButton.setAttribute("title", chrome.i18n.getMessage(title));
newButton.addEventListener("click", () => { newButton.addEventListener("click", () => {
callback(); callback();
@@ -1950,7 +1936,7 @@ async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
updateEditButtonsOnPlayer(); updateEditButtonsOnPlayer();
// Don't show the info button on embeds // Don't show the info button on embeds
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious() if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious() || isOnYTTV()
|| document.getElementById("sponsorBlockPopupContainer") != null) { || document.getElementById("sponsorBlockPopupContainer") != null) {
playerButtons.info.button.style.display = "none"; playerButtons.info.button.style.display = "none";
} else { } else {
@@ -2017,6 +2003,11 @@ function getRealCurrentTime(): number {
} }
function startOrEndTimingNewSegment() { function startOrEndTimingNewSegment() {
if (isOnYTTV() && getIsLivePremiere()) {
alert(chrome.i18n.getMessage("yttvLiveContentWarning"));
return;
}
verifyCurrentTime(); verifyCurrentTime();
const roundedTime = Math.round((getRealCurrentTime() + Number.EPSILON) * 1000) / 1000; const roundedTime = Math.round((getRealCurrentTime() + Number.EPSILON) * 1000) / 1000;
if (!isSegmentCreationInProgress()) { if (!isSegmentCreationInProgress()) {
@@ -2295,7 +2286,8 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
message: "submitVote", message: "submitVote",
type: type, type: type,
UUID: UUID, UUID: UUID,
category: category category: category,
videoID: getVideoID()
}, (response) => { }, (response) => {
if (response.successType === 1) { if (response.successType === 1) {
// Change the sponsor locally // Change the sponsor locally
@@ -2511,6 +2503,8 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
} }
function updateActiveSegment(currentTime: number): void { function updateActiveSegment(currentTime: number): void {
if (!chrome.runtime?.id) return;
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime); previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
@@ -2558,6 +2552,23 @@ function previousChapter(): void {
} }
} }
async function handleKeybindVote(type: number): Promise<void>{
let lastSkipNotice = skipNotices[0]?.skipNoticeRef.current;
lastSkipNotice?.onMouseEnter();
if (!lastSkipNotice) {
const lastSegment = [...sponsorTimes].reverse()?.find((s) => s.source == SponsorSourceType.Server && (s.segment[0] <= getCurrentTime() && getCurrentTime() - (s.segment[1] || s.segment[0]) <= Config.config.skipNoticeDuration));
if (!lastSegment) return;
createSkipNotice([lastSegment], shouldAutoSkip(lastSegment), lastSegment?.segment[0] + 0.001,false, true);
lastSkipNotice = await skipNotices[0].waitForSkipNoticeRef();
lastSkipNotice?.reskippedMode(0);
}
vote(type,lastSkipNotice?.segments[0]?.UUID, undefined, lastSkipNotice);
return;
}
function addHotkeyListener(): void { function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener); document.addEventListener("keydown", hotkeyListener);
@@ -2601,6 +2612,8 @@ function hotkeyListener(e: KeyboardEvent): void {
const openSubmissionMenuKey = Config.config.submitKeybind; const openSubmissionMenuKey = Config.config.submitKeybind;
const nextChapterKey = Config.config.nextChapterKeybind; const nextChapterKey = Config.config.nextChapterKeybind;
const previousChapterKey = Config.config.previousChapterKeybind; const previousChapterKey = Config.config.previousChapterKeybind;
const upvoteKey = Config.config.upvoteKeybind;
const downvoteKey = Config.config.downvoteKeybind;
if (keybindEquals(key, skipKey)) { if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement) { if (activeSkipKeybindElement) {
@@ -2644,6 +2657,12 @@ function hotkeyListener(e: KeyboardEvent): void {
if (sponsorTimes.length > 0) e.stopPropagation(); if (sponsorTimes.length > 0) e.stopPropagation();
previousChapter(); previousChapter();
return; return;
} else if (keybindEquals(key, upvoteKey)) {
handleKeybindVote(1);
return;
} else if (keybindEquals(key, downvoteKey)) {
handleKeybindVote(0);
return;
} }
} }
@@ -2694,7 +2713,8 @@ function showTimeWithoutSkips(skippedDuration: number): void {
// YouTube player time display // YouTube player time display
const selector = const selector =
isOnInvidious() ? ".vjs-duration" : isOnInvidious() ? ".vjs-duration" :
isOnMobileYouTube() ? ".YtwPlayerTimeDisplayContent" : isOnYTTV() ? ".ypl-full-controls .ypmcs-control .time-info-bar" :
isOnMobileYouTube() ? ".ytwPlayerTimeDisplayContent" :
".ytp-time-display.notranslate .ytp-time-wrapper"; ".ytp-time-display.notranslate .ytp-time-wrapper";
const display = document.querySelector(selector); const display = document.querySelector(selector);
if (!display) return; if (!display) return;

View File

@@ -15,6 +15,7 @@ import { findValidElement } from "../../maze-utils/src/dom";
import { addCleanupListener } from "../../maze-utils/src/cleanup"; import { addCleanupListener } from "../../maze-utils/src/cleanup";
import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils"; import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils";
import { isVorapisInstalled } from "../utils/compatibility"; import { isVorapisInstalled } from "../utils/compatibility";
import { isOnYTTV } from "../../maze-utils/src/video";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003; const MIN_CHAPTER_SIZE = 0.003;
@@ -41,6 +42,12 @@ class PreviewBar {
categoryTooltip?: HTMLDivElement; categoryTooltip?: HTMLDivElement;
categoryTooltipContainer?: HTMLElement; categoryTooltipContainer?: HTMLElement;
chapterTooltip?: HTMLDivElement; chapterTooltip?: HTMLDivElement;
// ScrubTooltips for YTTV only
categoryScrubTooltip?: HTMLDivElement;
categoryScrubTooltipContainer?: HTMLElement;
chapterScrubTooltip?: HTMLDivElement;
lastSmallestSegment: Record<string, { lastSmallestSegment: Record<string, {
index: number; index: number;
segment: PreviewBarSegment; segment: PreviewBarSegment;
@@ -49,9 +56,11 @@ class PreviewBar {
parent: HTMLElement; parent: HTMLElement;
onMobileYouTube: boolean; onMobileYouTube: boolean;
onInvidious: boolean; onInvidious: boolean;
onYTTV: boolean;
progressBar: HTMLElement; progressBar: HTMLElement;
segments: PreviewBarSegment[] = []; segments: PreviewBarSegment[] = [];
hasYouTubeChapters = false;
existingChapters: PreviewBarSegment[] = []; existingChapters: PreviewBarSegment[] = [];
videoDuration = 0; videoDuration = 0;
updateExistingChapters: () => void; updateExistingChapters: () => void;
@@ -69,14 +78,19 @@ class PreviewBar {
unfilteredChapterGroups: ChapterGroup[]; unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[]; chapterGroups: ChapterGroup[];
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) { constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, onYTTV: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
if (test) return; if (test) return;
this.container = document.createElement('ul'); this.container = document.createElement('ul');
this.container.id = 'previewbar'; this.container.id = 'previewbar';
if (onYTTV) {
this.container.classList.add("sponsorblock-yttv-container");
}
this.parent = parent; this.parent = parent;
this.onMobileYouTube = onMobileYouTube; this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious; this.onInvidious = onInvidious;
this.onYTTV = onYTTV;
this.chapterVote = chapterVote; this.chapterVote = chapterVote;
this.updateExistingChapters = updateExistingChapters; this.updateExistingChapters = updateExistingChapters;
@@ -96,26 +110,49 @@ class PreviewBar {
// Create label placeholder // Create label placeholder
this.categoryTooltip = document.createElement("div"); this.categoryTooltip = document.createElement("div");
if (isOnYTTV()) {
this.categoryTooltip.className = "sponsorCategoryTooltip";
} else {
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip"; this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
}
this.chapterTooltip = document.createElement("div"); this.chapterTooltip = document.createElement("div");
if (isOnYTTV()) {
this.chapterTooltip.className = "sponsorCategoryTooltip";
} else {
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip"; this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
}
// global chaper tooltip or duration tooltip if (isOnYTTV()) {
// YT, Vorapis, unknown this.categoryScrubTooltip = document.createElement("div");
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info"); this.categoryScrubTooltip.className = "sponsorCategoryTooltip";
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip)") as HTMLElement; this.chapterScrubTooltip = document.createElement("div");
this.chapterScrubTooltip.className = "sponsorCategoryTooltip";
}
// global chapter tooltip or duration tooltip
// YT, Vorapis, unknown, YTTV
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");
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip), .current-time:not(.sponsorCategoryTooltip)") as HTMLElement;
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return; if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init // Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
this.categoryTooltipContainer = tooltipTextWrapper.parentElement; this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
// YT, Vorapis // YT, Vorapis, YTTV
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text") as HTMLElement; const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text, .current-time") as HTMLElement;
if (!this.categoryTooltipContainer || !titleTooltip) return; if (!this.categoryTooltipContainer || !titleTooltip) return;
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling); tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling); tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
const seekBar = document.querySelector(".ytp-progress-bar-container"); if (isOnYTTV()) {
const scrubTooltipTextWrapper = document.querySelector(".yssi-slider .ysl-filmstrip-lens .time-info-bar")
if (!this.categoryTooltipContainer) return;
scrubTooltipTextWrapper.appendChild(this.categoryScrubTooltip);
scrubTooltipTextWrapper.appendChild(this.chapterScrubTooltip);
}
const seekBar = (document.querySelector(".ytp-progress-bar-container, .ypcs-scrub-slider-slot.ytu-player-controls"));
if (!seekBar) return; if (!seekBar) return;
let mouseOnSeekBar = false; let mouseOnSeekBar = false;
@@ -136,7 +173,7 @@ class PreviewBar {
// Find the segment at that location, using the shortest if multiple found // Find the segment at that location, using the shortest if multiple found
const [normalSegments, chapterSegments] = const [normalSegments, chapterSegments] =
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube), partition(this.segments,
(segment) => segment.actionType !== ActionType.Chapter); (segment) => segment.actionType !== ActionType.Chapter);
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments, "normal"); let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments, "normal");
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments, "chapter"); let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments, "chapter");
@@ -145,19 +182,44 @@ class PreviewBar {
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment)); secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
} }
const hasAYouTubeChapterRemoved = this.hasYouTubeChapters
|| (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters());
if (hasAYouTubeChapterRemoved) {
// Hide original tooltip if some chapter has been filtered out
originalTooltip.style.display = "none";
noYoutubeChapters = true;
originalTooltip.classList.add("sponsorTooltipHasYTChapters");
} else {
originalTooltip.classList.remove("sponsorTooltipHasYTChapters");
}
if (mainSegment === null && secondarySegment === null) { if (mainSegment === null && secondarySegment === null) {
if (!hasAYouTubeChapterRemoved) {
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS); this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
originalTooltip.style.removeProperty("display"); originalTooltip.style.removeProperty("display");
}
if (this.onYTTV) {
this.setTooltipTitle(mainSegment, this.categoryTooltip);
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
}
} else { } else {
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS); this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
if (mainSegment !== null && secondarySegment !== null) { if (mainSegment !== null && secondarySegment !== null) {
this.categoryTooltipContainer.classList.add("sponsorTwoTooltips"); this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
originalTooltip.classList.remove("sponsorTooltipHasYTChapters");
} else { } else {
this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips"); this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips");
} }
this.setTooltipTitle(mainSegment, this.categoryTooltip); this.setTooltipTitle(mainSegment, this.categoryTooltip);
this.setTooltipTitle(secondarySegment, this.chapterTooltip); this.setTooltipTitle(secondarySegment, this.chapterTooltip);
if (this.onYTTV) {
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
}
if (isVorapisInstalled()) { if (isVorapisInstalled()) {
const tooltipParent = tooltipTextWrapper.parentElement!; const tooltipParent = tooltipTextWrapper.parentElement!;
@@ -172,16 +234,16 @@ class PreviewBar {
originalTooltip.style.removeProperty("display"); originalTooltip.style.removeProperty("display");
} }
// Used to prevent overlapping
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
// To prevent offset issue // To prevent offset issue
this.categoryTooltip.style.right = titleTooltip.style.right; this.categoryTooltip.style.right = titleTooltip.style.right;
this.chapterTooltip.style.right = titleTooltip.style.right; this.chapterTooltip.style.right = titleTooltip.style.right;
this.categoryTooltip.style.textAlign = titleTooltip.style.textAlign; this.categoryTooltip.style.textAlign = titleTooltip.style.textAlign;
this.chapterTooltip.style.textAlign = titleTooltip.style.textAlign; this.chapterTooltip.style.textAlign = titleTooltip.style.textAlign;
} }
// Used to prevent overlapping
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
}); });
} }
@@ -210,8 +272,13 @@ class PreviewBar {
} }
// On the seek bar // On the seek bar
if (this.onYTTV) {
// order of sibling elements matters on YTTV
this.parent.insertBefore(this.container, this.parent.firstChild.nextSibling.nextSibling);
} else {
this.parent.prepend(this.container); this.parent.prepend(this.container);
} }
}
clear(): void { clear(): void {
while (this.container.firstChild) { while (this.container.firstChild) {
@@ -232,6 +299,7 @@ class PreviewBar {
set(segments: PreviewBarSegment[], videoDuration: number): void { set(segments: PreviewBarSegment[], videoDuration: number): void {
this.segments = segments ?? []; this.segments = segments ?? [];
this.videoDuration = videoDuration ?? 0; this.videoDuration = videoDuration ?? 0;
this.hasYouTubeChapters = segments.some((segment) => segment.source === SponsorSourceType.YouTube);
// Remove unnecessary original chapters if submitted replacements exist // Remove unnecessary original chapters if submitted replacements exist
for (const chapter of this.segments.filter((s) => s.actionType === ActionType.Chapter && s.source === SponsorSourceType.Server)) { for (const chapter of this.segments.filter((s) => s.actionType === ActionType.Chapter && s.source === SponsorSourceType.Server)) {
@@ -345,6 +413,10 @@ class PreviewBar {
bar.style.marginRight = `${this.chapterMargin}px`; bar.style.marginRight = `${this.chapterMargin}px`;
} }
if (this.onYTTV) {
bar.classList.add("previewbar-yttv");
}
return bar; return bar;
} }
@@ -851,8 +923,10 @@ class PreviewBar {
})[0]; })[0];
const chapterButton = this.getChapterButton(chaptersContainer); const chapterButton = this.getChapterButton(chaptersContainer);
if (chapterButton) {
chapterButton.classList.remove("ytp-chapter-container-disabled"); chapterButton.classList.remove("ytp-chapter-container-disabled");
chapterButton.disabled = false; chapterButton.disabled = false;
}
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement; const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.style.display = "none"; chapterTitle.style.display = "none";
@@ -861,6 +935,9 @@ class PreviewBar {
const elem = document.createElement("div"); const elem = document.createElement("div");
chapterTitle.parentElement.insertBefore(elem, chapterTitle); chapterTitle.parentElement.insertBefore(elem, chapterTitle);
elem.classList.add("sponsorChapterText"); elem.classList.add("sponsorChapterText");
if (document.location.host === "tv.youtube.com") {
elem.style.lineHeight = "initial";
}
return elem; return elem;
})()) as HTMLDivElement; })()) as HTMLDivElement;
chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category); chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
@@ -873,7 +950,15 @@ class PreviewBar {
if (chosenSegment.source === SponsorSourceType.Server) { if (chosenSegment.source === SponsorSourceType.Server) {
const chapterVoteContainer = this.chapterVote.getContainer(); const chapterVoteContainer = this.chapterVote.getContainer();
if (!chapterButton.contains(chapterVoteContainer)) { if (document.location.host === "tv.youtube.com") {
if (!chaptersContainer.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
}
chaptersContainer.appendChild(chapterVoteContainer);
}
} else if (!chapterButton.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote"); const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) { if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove()); oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
@@ -912,6 +997,18 @@ class PreviewBar {
} }
private getChaptersContainer(): HTMLElement { private getChaptersContainer(): HTMLElement {
if (document.location.host === "tv.youtube.com") {
if (!document.querySelector(".ytp-chapter-container")) {
const dest = document.querySelector(".ypcs-control-buttons-left");
if (!dest) return null;
const sbChapterContainer = document.createElement("div");
sbChapterContainer.className = "ytp-chapter-container";
const sbChapterTitleContent = document.createElement("div");
sbChapterTitleContent.className = "ytp-chapter-title-content";
sbChapterContainer.appendChild(sbChapterTitleContent);
dest.appendChild(sbChapterContainer);
}
}
return document.querySelector(".ytp-chapter-container") as HTMLElement; return document.querySelector(".ytp-chapter-container") as HTMLElement;
} }

View File

@@ -9,6 +9,7 @@ export interface SkipButtonControlBarProps {
skip: (segment: SponsorTime) => void; skip: (segment: SponsorTime) => void;
selectSegment: (UUID: SegmentUUID) => void; selectSegment: (UUID: SegmentUUID) => void;
onMobileYouTube: boolean; onMobileYouTube: boolean;
onYTTV: boolean;
} }
export class SkipButtonControlBar { export class SkipButtonControlBar {
@@ -21,6 +22,7 @@ export class SkipButtonControlBar {
showKeybindHint = true; showKeybindHint = true;
onMobileYouTube: boolean; onMobileYouTube: boolean;
onYTTV: boolean;
enabled = false; enabled = false;
@@ -40,6 +42,7 @@ export class SkipButtonControlBar {
constructor(props: SkipButtonControlBarProps) { constructor(props: SkipButtonControlBarProps) {
this.skip = props.skip; this.skip = props.skip;
this.onMobileYouTube = props.onMobileYouTube; this.onMobileYouTube = props.onMobileYouTube;
this.onYTTV = props.onYTTV;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.classList.add("skipButtonControlBarContainer"); this.container.classList.add("skipButtonControlBarContainer");
@@ -50,6 +53,10 @@ export class SkipButtonControlBar {
this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg"); this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg");
this.skipIcon.classList.add("ytp-button"); this.skipIcon.classList.add("ytp-button");
this.skipIcon.id = "sbSkipIconControlBarImage"; this.skipIcon.id = "sbSkipIconControlBarImage";
if (this.onYTTV) {
this.skipIcon.style.width = "24px";
this.skipIcon.style.height = "24px";
}
this.textContainer = document.createElement("div"); this.textContainer = document.createElement("div");
@@ -84,7 +91,7 @@ export class SkipButtonControlBar {
this.chapterText = document.querySelector(".ytp-chapter-container"); this.chapterText = document.querySelector(".ytp-chapter-container");
if (mountingContainer && !mountingContainer.contains(this.container)) { if (mountingContainer && !mountingContainer.contains(this.container)) {
if (this.onMobileYouTube) { if (this.onMobileYouTube || this.onYTTV) {
mountingContainer.appendChild(this.container); mountingContainer.appendChild(this.container);
} else { } else {
mountingContainer.insertBefore(this.container, this.chapterText); mountingContainer.insertBefore(this.container, this.chapterText);
@@ -101,8 +108,10 @@ export class SkipButtonControlBar {
} }
private getMountingContainer(): HTMLElement { private getMountingContainer(): HTMLElement {
if (!this.onMobileYouTube) { if (!this.onMobileYouTube && !this.onYTTV) {
return document.querySelector(".ytp-left-controls"); return document.querySelector(".ytp-left-controls");
} else if (this.onYTTV) {
return document.querySelector(".ypcs-control-buttons-left");
} else { } else {
return document.getElementById("player-container-id"); return document.getElementById("player-container-id");
} }

View File

@@ -86,6 +86,7 @@ interface GetVideoIdResponse {
export interface GetChannelIDResponse { export interface GetChannelIDResponse {
channelID: string; channelID: string;
isYTTV: boolean;
} }
export interface SponsorStartResponse { export interface SponsorStartResponse {

View File

@@ -681,9 +681,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
uuidButton.className = "voteButton"; uuidButton.className = "voteButton";
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg"); uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
uuidButton.title = chrome.i18n.getMessage("copySegmentID"); uuidButton.title = chrome.i18n.getMessage("copySegmentID");
uuidButton.addEventListener("click", () => { uuidButton.addEventListener("click", async () => {
copyToClipboard(UUID);
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3); const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
if (UUID.length > 60) {
copyToClipboard(UUID);
} else {
const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", {
UUID: UUID,
videoID: currentVideoID
});
if (segmentIDData.ok && segmentIDData.responseText) {
copyToClipboard(segmentIDData.responseText);
}
}
stopAnimation(); stopAnimation();
}); });
@@ -917,7 +930,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//get the channel url //get the channel url
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse; const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
if (!response.channelID) { if (!response.channelID) {
if (response.isYTTV) {
alert(chrome.i18n.getMessage("yttvNoChannelWhitelist"));
} else {
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753"); alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
}
return; return;
} }

View File

@@ -20,6 +20,10 @@ export class ChapterVote {
this.container.id = "chapterVote"; this.container.id = "chapterVote";
this.container.style.height = "100%"; this.container.style.height = "100%";
if (document.location.host === "tv.youtube.com") {
this.container.style.lineHeight = "initial";
}
this.root = createRoot(this.container); this.root = createRoot(this.container);
this.root.render(<ChapterVoteComponent ref={this.ref} vote={vote} />); this.root.render(<ChapterVoteComponent ref={this.ref} vote={vote} />);
} }

View File

@@ -5,7 +5,7 @@ import Utils from "../utils";
const utils = new Utils(); const utils = new Utils();
import SkipNoticeComponent from "../components/SkipNoticeComponent"; import SkipNoticeComponent from "../components/SkipNoticeComponent";
import { SponsorTime, ContentContainer, NoticeVisbilityMode } from "../types"; import { SponsorTime, ContentContainer, NoticeVisibilityMode } from "../types";
import Config from "../config"; import Config from "../config";
import { SkipNoticeAction } from "../utils/noticeUtils"; import { SkipNoticeAction } from "../utils/noticeUtils";
@@ -20,7 +20,7 @@ class SkipNotice {
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>; skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
root: Root; root: Root;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, componentDidMount: () => void, unskipTime: number = null, startReskip = false, upcomingNoticeShown: boolean) { constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, componentDidMount: () => void, unskipTime: number = null, startReskip = false, upcomingNoticeShown: boolean, voteNotice = false) {
this.skipNoticeRef = React.createRef(); this.skipNoticeRef = React.createRef();
this.segments = segments; this.segments = segments;
@@ -42,18 +42,18 @@ class SkipNotice {
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix; this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
referenceNode.prepend(this.noticeElement); referenceNode.prepend(this.noticeElement);
this.root = createRoot(this.noticeElement); this.root = createRoot(this.noticeElement);
this.root.render( this.root.render(
<SkipNoticeComponent segments={segments} <SkipNoticeComponent segments={segments}
autoSkip={autoSkip} autoSkip={autoSkip}
startReskip={startReskip} startReskip={startReskip}
voteNotice={voteNotice}
contentContainer={contentContainer} contentContainer={contentContainer}
ref={this.skipNoticeRef} ref={this.skipNoticeRef}
closeListener={() => this.close()} closeListener={() => this.close()}
smaller={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAll smaller={!voteNotice && (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAll
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAutoSkip && autoSkip)} || (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAutoSkip && autoSkip))}
fadeIn={!upcomingNoticeShown} fadeIn={!upcomingNoticeShown && !voteNotice}
unskipTime={unskipTime} unskipTime={unskipTime}
componentDidMount={componentDidMount} /> componentDidMount={componentDidMount} />
); );
@@ -81,6 +81,26 @@ class SkipNotice {
unmutedListener(time: number): void { unmutedListener(time: number): void {
this.skipNoticeRef?.current?.unmutedListener(time); this.skipNoticeRef?.current?.unmutedListener(time);
} }
async waitForSkipNoticeRef(): Promise<SkipNoticeComponent> {
const waitForRef = () => new Promise<SkipNoticeComponent>((resolve) => {
const observer = new MutationObserver(() => {
if (this.skipNoticeRef.current) {
observer.disconnect();
resolve(this.skipNoticeRef.current);
}
});
observer.observe(document.getElementsByClassName("sponsorSkipNoticeContainer")[0], { childList: true, subtree: true});
if (this.skipNoticeRef.current) {
observer.disconnect();
resolve(this.skipNoticeRef.current);
}
});
return this.skipNoticeRef?.current || await waitForRef();
}
} }
export default SkipNotice; export default SkipNotice;

View File

@@ -6,7 +6,7 @@ export interface ContentContainer {
(): { (): {
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void; vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void;
dontShowNoticeAgain: () => void; dontShowNoticeAgain: () => void;
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void; unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean, voteNotice?: boolean) => void;
sponsorTimes: SponsorTime[]; sponsorTimes: SponsorTime[];
sponsorTimesSubmitting: SponsorTime[]; sponsorTimesSubmitting: SponsorTime[];
skipNotices: SkipNotice[]; skipNotices: SkipNotice[];
@@ -219,7 +219,7 @@ export interface ToggleSkippable {
setShowKeybindHint: (show: boolean) => void; setShowKeybindHint: (show: boolean) => void;
} }
export enum NoticeVisbilityMode { export enum NoticeVisibilityMode {
FullSize = 0, FullSize = 0,
MiniForAutoSkip = 1, MiniForAutoSkip = 1,
MiniForAll = 2, MiniForAll = 2,

View File

@@ -5,6 +5,7 @@ import { getHash, HashedValue } from "../maze-utils/src/hash";
import { waitFor } from "../maze-utils/src"; import { waitFor } from "../maze-utils/src";
import { findValidElementFromSelector } from "../maze-utils/src/dom"; import { findValidElementFromSelector } from "../maze-utils/src/dom";
import { isSafari } from "../maze-utils/src/config"; import { isSafari } from "../maze-utils/src/config";
import { asyncRequestToServer } from "./utils/requests";
export default class Utils { export default class Utils {
@@ -198,7 +199,7 @@ export default class Utils {
getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number { getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number {
for (let i = 0; i < sponsorTimes.length; i++) { for (let i = 0; i < sponsorTimes.length; i++) {
if (sponsorTimes[i].UUID === UUID) { if (sponsorTimes[i].UUID && (sponsorTimes[i].UUID.startsWith(UUID) || UUID.startsWith(sponsorTimes[i].UUID))) {
return i; return i;
} }
} }
@@ -248,6 +249,7 @@ export default class Utils {
".main-video-section > .video-container", // Cloudtube ".main-video-section > .video-container", // Cloudtube
".shaka-video-container", // Piped ".shaka-video-container", // Piped
"#player-container.ytk-player", // YT Kids "#player-container.ytk-player", // YT Kids
"#id-tv-container" // YTTV
]; ];
let referenceNode = findValidElementFromSelector(selectors) let referenceNode = findValidElementFromSelector(selectors)
@@ -282,6 +284,17 @@ export default class Utils {
if ((chrome.extension.inIncognitoContext && !Config.config.trackDownvotesInPrivate) if ((chrome.extension.inIncognitoContext && !Config.config.trackDownvotesInPrivate)
|| !Config.config.trackDownvotes) return; || !Config.config.trackDownvotes) return;
if (segmentUUID.length < 60) {
const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", {
UUID: segmentUUID,
videoID
});
if (segmentIDData.ok && segmentIDData.responseText) {
segmentUUID = segmentIDData.responseText;
}
}
const hashedVideoID = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue; const hashedVideoID = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const UUIDHash = await getHash(segmentUUID, 1); const UUIDHash = await getHash(segmentUUID, 1);
@@ -308,6 +321,7 @@ export default class Utils {
allDownvotes[hashedVideoID] = currentVideoData; allDownvotes[hashedVideoID] = currentVideoData;
} }
console.log(allDownvotes)
const entries = Object.entries(allDownvotes); const entries = Object.entries(allDownvotes);
if (entries.length > 10000) { if (entries.length > 10000) {

View File

@@ -44,6 +44,14 @@ export function getUpcomingText(segments: SponsorTime[]): string {
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
} }
export function getVoteText(segments: SponsorTime[]): string {
const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments"
: "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category);
const messageId = "voted_on";
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
}
export function getCategorySuffix(category: Category): string { export function getCategorySuffix(category: Category): string {
if (category.startsWith("poi_")) { if (category.startsWith("poi_")) {

View File

@@ -34,9 +34,12 @@ function exportTime(segment: SponsorTime): string {
export function importTimes(data: string, videoDuration: number): SponsorTime[] { export function importTimes(data: string, videoDuration: number): SponsorTime[] {
const lines = data.split("\n"); const lines = data.split("\n");
const timeRegex = /(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g;
const anyLineHasTime = lines.some((line) => timeRegex.test(line));
const result: SponsorTime[] = []; const result: SponsorTime[] = [];
for (const line of lines) { for (const line of lines) {
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g); const match = line.match(timeRegex);
if (match) { if (match) {
const startTime = getFormattedTimeToSeconds(match[0]); const startTime = getFormattedTimeToSeconds(match[0]);
if (startTime !== null) { if (startTime !== null) {
@@ -71,6 +74,18 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
result.push(segment); result.push(segment);
} }
} else if (!anyLineHasTime) {
// Adding chapters with placeholder times
const segment: SponsorTime = {
segment: [0, 0],
category: "chapter" as Category,
actionType: ActionType.Chapter,
description: line,
source: SponsorSourceType.Local,
UUID: generateUserID() as SegmentUUID
};
result.push(segment);
} }
} }

View File

@@ -13,7 +13,9 @@ export function getControls(): HTMLElement {
// Piped shaka player // Piped shaka player
".shaka-bottom-controls", ".shaka-bottom-controls",
// Vorapis v3 // Vorapis v3
".html5-player-chrome" ".html5-player-chrome",
// tv.youtube.com
".ypcs-control-buttons-right"
]; ];
for (const controlsSelector of controlsSelectors) { for (const controlsSelector of controlsSelectors) {

View File

@@ -50,9 +50,11 @@ async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse>
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue; const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const hasDownvotedSegments = !!Config.local.downvotedSegments[hashPrefix];
const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories: CompileConfig.categoryList, categories: CompileConfig.categoryList,
actionTypes: ActionTypes, actionTypes: ActionTypes,
trimUUIDs: hasDownvotedSegments ? null : 5,
...extraRequestData ...extraRequestData
}, { }, {
"X-CLIENT-NAME": `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}` "X-CLIENT-NAME": `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}`

View File

@@ -1,6 +1,6 @@
import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video"; import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video";
import Config from "../config"; import Config from "../config";
import { getVideoLabel } from "./videoLabels"; import { getHasStartSegment, getVideoLabel } from "./videoLabels";
import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement"; import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
import { VideoID } from "../types"; import { VideoID } from "../types";
import { getSegmentsForVideo } from "./segmentData"; import { getSegmentsForVideo } from "./segmentData";
@@ -58,10 +58,27 @@ function thumbnailHoverListener(e: MouseEvent) {
if (!thumbnail) return; if (!thumbnail) return;
// Pre-fetch data for this video // Pre-fetch data for this video
let fetched = false;
const preFetch = async () => {
fetched = true;
const videoID = extractVideoID(thumbnail); const videoID = extractVideoID(thumbnail);
if (videoID) { if (videoID && await getHasStartSegment(videoID)) {
void getSegmentsForVideo(videoID, false); void getSegmentsForVideo(videoID, false);
} }
};
const timeout = setTimeout(preFetch, 100);
const onMouseDown = () => {
clearTimeout(timeout);
if (!fetched) {
preFetch();
}
};
e.target.addEventListener("mousedown", onMouseDown, { once: true });
e.target.addEventListener("mouseleave", () => {
clearTimeout(timeout);
e.target.removeEventListener("mousedown", onMouseDown);
}, { once: true });
} }
function getLink(thumbnail: HTMLImageElement): HTMLAnchorElement | null { function getLink(thumbnail: HTMLImageElement): HTMLAnchorElement | null {

View File

@@ -6,9 +6,14 @@ import { asyncRequestToServer } from "./requests";
const utils = new Utils(); const utils = new Utils();
interface VideoLabelsCacheData {
category: Category;
hasStartSegment: boolean;
}
export interface LabelCacheEntry { export interface LabelCacheEntry {
timestamp: number; timestamp: number;
videos: Record<VideoID, Category>; videos: Record<VideoID, VideoLabelsCacheData>;
} }
const labelCache: Record<string, LabelCacheEntry> = {}; const labelCache: Record<string, LabelCacheEntry> = {};
@@ -21,7 +26,7 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
return cachedEntry; return cachedEntry;
} }
const response = await asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}`); const response = await asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}?hasStartSegment=true`);
if (response.status !== 200) { if (response.status !== 200) {
// No video labels or server down // No video labels or server down
labelCache[hashPrefix] = { labelCache[hashPrefix] = {
@@ -36,7 +41,10 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
const newEntry: LabelCacheEntry = { const newEntry: LabelCacheEntry = {
timestamp: Date.now(), timestamp: Date.now(),
videos: Object.fromEntries(data.map(video => [video.videoID, video.segments[0].category])), videos: Object.fromEntries(data.map(video => [video.videoID, {
category: video.segments[0]?.category,
hasStartSegment: video.hasStartSegment
}])),
}; };
labelCache[hashPrefix] = newEntry; labelCache[hashPrefix] = newEntry;
@@ -55,11 +63,11 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
} }
export async function getVideoLabel(videoID: VideoID): Promise<Category | null> { export async function getVideoLabel(videoID: VideoID): Promise<Category | null> {
const prefix = (await getHash(videoID, 1)).slice(0, 3); const prefix = (await getHash(videoID, 1)).slice(0, 4);
const result = await getLabelHashBlock(prefix); const result = await getLabelHashBlock(prefix);
if (result) { if (result) {
const category = result.videos[videoID]; const category = result.videos[videoID]?.category;
if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) { if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) {
return category; return category;
} else { } else {
@@ -69,3 +77,14 @@ export async function getVideoLabel(videoID: VideoID): Promise<Category | null>
return null; return null;
} }
export async function getHasStartSegment(videoID: VideoID): Promise<boolean | null> {
const prefix = (await getHash(videoID, 1)).slice(0, 4);
const result = await getLabelHashBlock(prefix);
if (result) {
return result?.videos[videoID]?.hasStartSegment ?? false;
}
return null;
}

View File

@@ -1,9 +1,13 @@
/**
* @jest-environment jsdom
*/
import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar"; import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
describe("createChapterRenderGroups", () => { describe("createChapterRenderGroups", () => {
let previewBar: PreviewBar; let previewBar: PreviewBar;
beforeEach(() => { beforeEach(() => {
previewBar = new PreviewBar(null, null, null, null, null, true); previewBar = new PreviewBar(null, null, null, null, null, null, true);
}) })
it("Two unrelated times", () => { it("Two unrelated times", () => {