Compare commits

..

21 Commits
5.0 ... 5.0.1

Author SHA1 Message Date
Ajay
c06b7857f8 Move to controls to make info button visible in selenium test 2022-09-05 00:32:04 -04:00
Ajay
e798cfdfe3 clarify what needs to be translated 2022-09-05 00:18:28 -04:00
Ajay
0e76342b04 fix typo 2022-09-05 00:17:44 -04:00
Ajay
d91e38fec9 bump version 2022-09-05 00:16:37 -04:00
Ajay
3316072f5d Fix votes appearing for unsubmitted segments 2022-09-05 00:14:23 -04:00
Ajay
4c568212ac Hide custom chapter bar while generating 2022-09-05 00:03:57 -04:00
Ajay
eaa119f152 Make sure original chapter bar that is used is always the right one 2022-09-05 00:01:11 -04:00
Ajay
e7deabe8d9 Properly handle hover previews for chapters and clear old unused ones 2022-09-04 23:57:10 -04:00
Ajay
6d47700ebd Safer document script 2022-09-04 23:14:15 -04:00
Ajay
93c616de23 Prevent some event bubbling issues 2022-09-04 22:04:48 -04:00
Ajay
ee25b41d7e Don't carry over incorrect/harmful vote menu between videos 2022-09-04 21:58:56 -04:00
Ajay
00f134029a Prevent creating multiple chapter vote containers 2022-09-04 21:52:14 -04:00
Ajay
00d625013b Add option to manual skip when a full video segment exists 2022-09-03 23:16:18 -04:00
Ajay
e81ff66dd3 Fix chapter -> full -> chapter not saving times 2022-09-03 21:28:02 -04:00
Ajay
97af12416e Fix copy tooltip 2022-09-03 01:58:22 -04:00
Ajay
bf191dab92 fix react errors about using inherit 2022-09-03 01:09:16 -04:00
Ajay
f8c61b7848 Don't use video before it is set 2022-09-03 00:36:28 -04:00
Ajay
5b136f2da8 Fix crashes on invidious 2022-09-03 00:32:20 -04:00
Ajay
8b80b33810 Don't show empty chapter bar for youtube chapters in popup 2022-09-03 00:27:18 -04:00
Ajay
e3c36ae6e2 Fix the freezing on firefox due to hover preview text 2022-09-03 00:22:03 -04:00
Ajay
533b15f44b Add support for hours in import segments 2022-09-02 21:42:36 -04:00
17 changed files with 140 additions and 51 deletions

View File

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

View File

@@ -463,6 +463,12 @@
"minDurationDescription": {
"message": "Segments shorter than the set value will not be skipped or show in the player."
},
"enableManualSkipOnFullVideo": {
"message": "Use manual skip when a full video label exists"
},
"whatManualSkipOnFullVideo": {
"message": "For people who want to watch the video uninterrupted if it is fully sponsored or self promotion."
},
"skipNoticeDuration": {
"message": "Skip notice duration (seconds):"
},
@@ -1134,7 +1140,7 @@
},
"cantAfford": {
"message": "If you can't afford to purchase a license, click {here} to see if you are eligible for a discount",
"description": "Keep the curly braces"
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Sign in with Patreon"

View File

@@ -126,6 +126,10 @@ div:hover > .sponsorBlockChapterBar {
vertical-align: top;
}
.playerButton.hidden {
display: none !important;
}
/* Removes auto width from being a ytp-player-button */
.sbPlayerDownvote {
width: auto !important;

View File

@@ -98,6 +98,20 @@
<div class="small-description">__MSG_minDurationDescription__</div>
</div>
<div data-type="toggle" data-sync="manualSkipOnFullVideo">
<div class="switch-container">
<label class="switch">
<input id="manualSkipOnFullVideo" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="manualSkipOnFullVideo">
__MSG_enableManualSkipOnFullVideo__
</label>
</div>
<div class="small-description">__MSG_whatManualSkipOnFullVideo__</div>
</div>
<div data-type="toggle" data-sync="forceChannelCheck">
<div class="switch-container">

View File

@@ -32,6 +32,11 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
}
render(): React.ReactElement {
if (this.tooltip && !this.state.show) {
this.tooltip.close();
this.tooltip = null;
}
return (
<>
{/* Upvote Button */}
@@ -42,7 +47,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
onClick={(e) => this.vote(e, 1)}>
<ThumbsUpSvg className="playerButtonImage"
fill={Config.config.colorPalette.white}
width={"inherit"} height={"inherit"} />
width={null} height={null} />
</button>
{/* Downvote Button */}
@@ -92,8 +97,8 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
<ThumbsDownSvg
className="playerButtonImage"
fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
width={"inherit"}
height={"inherit"} />
width={null}
height={null} />
</button>
</>
);

View File

@@ -41,7 +41,10 @@ class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
for (const option of this.props.options) {
result.push(
<div className="sbSelectorOption"
onClick={() => this.props.onChange(option.label)}
onClick={(e) => {
e.stopPropagation();
this.props.onChange(option.label);
}}
key={option.label}>
{option.label}
</div>

View File

@@ -237,6 +237,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
ref={this.descriptionOptionRef}
type="text"
value={this.state.description}
onContextMenu={(e) => e.stopPropagation()}
onChange={(e) => this.descriptionUpdate(e.target.value)}
onFocus={() => this.setState({chapterNameSelectorOpen: true})}>
</input>
@@ -477,7 +478,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
this.previousSkipType = ActionType.Full;
} else if ((category === "chooseACategory" || (CompileConfig.categorySupport[category]?.includes(ActionType.Skip)
} else if ((category === "chooseACategory" || ((CompileConfig.categorySupport[category]?.includes(ActionType.Skip)
|| CompileConfig.categorySupport[category]?.includes(ActionType.Chapter))
&& ![ActionType.Poi, ActionType.Full].includes(this.getNextActionType(category, actionType))))
&& this.previousSkipType !== ActionType.Skip) {
if (this.timesBeforeChanging[0]) {

View File

@@ -25,6 +25,7 @@ interface SBConfig {
disableSkipping: boolean,
muteSegments: boolean,
fullVideoSegments: boolean,
manualSkipOnFullVideo: boolean,
trackViewCount: boolean,
trackViewCountInPrivate: boolean,
trackDownvotes: boolean,
@@ -159,6 +160,7 @@ const Config: SBObject = {
disableSkipping: false,
muteSegments: true,
fullVideoSegments: true,
manualSkipOnFullVideo: false,
trackViewCount: true,
trackViewCountInPrivate: true,
trackDownvotes: true,

View File

@@ -1287,7 +1287,7 @@ function updatePreviewBar(): void {
});
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
updateActiveSegment(video.currentTime);
if (video) updateActiveSegment(video.currentTime);
if (Config.config.showTimeWithSkips) {
const skippedDuration = utils.getTimestampsDuration(previewBarSegments
@@ -1690,9 +1690,10 @@ function createButton(baseID: string, title: string, callback: () => void, image
}
function shouldAutoSkip(segment: SponsorTime): boolean {
return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
&& (utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType !== ActionType.Poi);
&& segment.actionType !== ActionType.Poi));
}
function shouldSkip(segment: SponsorTime): boolean {
@@ -2275,7 +2276,8 @@ function addPageListeners(): void {
// inject into document
const docScript = document.createElement("script");
docScript.src = chrome.runtime.getURL("js/document.js");
(document.head || document.documentElement).appendChild(docScript);
// Not injected on invidious
(document.head || document.documentElement)?.appendChild(docScript);
document.addEventListener("yt-navigate-start", resetValues);
document.addEventListener("yt-navigate-finish", refreshListners);

View File

@@ -54,27 +54,40 @@ document.addEventListener("yt-navigate-finish", navigateFinishSend);
function navigationParser(event: CustomEvent): StartMessage {
const pageType: PageType = event.detail.pageType;
const result: StartMessage = { type: "navigation", pageType, videoID: null };
if (pageType === "shorts" || pageType === "watch") {
const endpoint = event.detail.endpoint
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
}
if (pageType) {
const result: StartMessage = { type: "navigation", pageType, videoID: null };
if (pageType === "shorts" || pageType === "watch") {
const endpoint = event.detail.endpoint
if (!endpoint) return null;
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
}
return result;
return result;
} else {
return null;
}
}
function navigationStartSend(event: CustomEvent): void {
sendMessage(navigationParser(event) as StartMessage);
const message = navigationParser(event) as StartMessage;
if (message) {
sendMessage(message);
}
}
function navigateFinishSend(event: CustomEvent): void {
sendVideoData(); // arrived at new video, send video data
const videoDetails = event.detail?.response?.playerResponse?.videoDetails;
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
if (videoDetails) {
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
}
}
function sendVideoData(): void {
if (!playerClient) return;
const { video_id, isLive, isPremiere } = playerClient.getVideoData();
sendMessage({ type: "data", videoID: video_id, isLive, isPremiere } as VideoData);
const videoData = playerClient.getVideoData();
if (videoData) {
sendMessage({ type: "data", videoID: videoData.video_id, isLive: videoData.isLive, isPremiere: videoData.isPremiere } as VideoData);
}
}

View File

@@ -11,6 +11,7 @@ import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceT
import { partition } from "../utils/arrayUtils";
import { shortCategoryName } from "../utils/categoryUtils";
import { GenericUtils } from "../utils/genericUtils";
import { findValidElement } from "../utils/pageUtils";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003;
@@ -39,6 +40,7 @@ class PreviewBar {
parent: HTMLElement;
onMobileYouTube: boolean;
onInvidious: boolean;
progressBar: HTMLElement;
segments: PreviewBarSegment[] = [];
existingChapters: PreviewBarSegment[] = [];
@@ -62,6 +64,7 @@ class PreviewBar {
this.onInvidious = onInvidious;
this.chapterVote = chapterVote;
this.updatePageElements();
this.createElement(parent);
this.createChapterMutationObservers();
@@ -105,7 +108,7 @@ class PreviewBar {
if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return;
// If the mutation observed is only for our tooltip text, ignore
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
if (mutations.some((mutation) => (mutation.target as HTMLElement).classList.contains("sponsorCategoryTooltip"))) {
return;
}
@@ -208,9 +211,9 @@ class PreviewBar {
this.segments = segments ?? [];
this.videoDuration = videoDuration ?? 0;
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
this.updatePageElements();
// Sometimes video duration is inaccurate, pull from accessibility info
const ariaDuration = parseInt(progressBar?.getAttribute('aria-valuemax')) ?? 0;
const ariaDuration = parseInt(this.progressBar?.getAttribute('aria-valuemax')) ?? 0;
if (ariaDuration && Math.abs(ariaDuration - this.videoDuration) > 3) {
this.videoDuration = ariaDuration;
}
@@ -218,13 +221,21 @@ class PreviewBar {
this.update();
}
private updatePageElements(): void {
const allProgressBars = document.querySelectorAll('.ytp-progress-bar') as NodeListOf<HTMLElement>;
this.progressBar = findValidElement(allProgressBars) ?? allProgressBars?.[0];
this.originalChapterBar = this.progressBar.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
}
private update(): void {
this.clear();
if (!this.segments) return;
this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0])
if (this.originalChapterBar) {
this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0]);
}
const sortedSegments = this.segments.sort(({ segment: a }, { segment: b }) => {
// Sort longer segments before short segments to make shorter segments render later
@@ -239,11 +250,13 @@ class PreviewBar {
this.createChaptersBar(this.segments.sort((a, b) => a.segment[0] - b.segment[0]));
const chapterChevron = this.getChapterChevron();
if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
if (chapterChevron) {
if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
&& segment.source === SponsorSourceType.YouTube)) {
chapterChevron.style.removeProperty("display");
} else {
chapterChevron.style.display = "none";
chapterChevron.style.removeProperty("display");
} else {
chapterChevron.style.display = "none";
}
}
}
@@ -274,8 +287,7 @@ class PreviewBar {
}
createChaptersBar(segments: PreviewBarSegment[]): void {
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
if (!progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) return;
if (!this.progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) return;
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|| (!Config.config.renderSegmentsAsChapters
@@ -298,17 +310,19 @@ class PreviewBar {
// Create it from cloning
let createFromScratch = false;
if (!this.customChaptersBar) {
if (!this.customChaptersBar || !this.progressBar.contains(this.customChaptersBar)) {
// Clear anything remaining
document.querySelectorAll(".sponsorBlockChapterBar").forEach((element) => element.remove());
createFromScratch = true;
this.customChaptersBar = this.originalChapterBar.cloneNode(true) as HTMLElement;
this.customChaptersBar.classList.add("sponsorBlockChapterBar");
}
this.customChaptersBar.style.removeProperty("display");
this.customChaptersBar.style.display = "none";
const originalSections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container");
const originalSection = originalSections[0];
this.customChaptersBar = this.customChaptersBar;
// For switching to a video with less chapters
if (originalSections.length > chaptersToRender.length) {
for (let i = originalSections.length - 1; i >= chaptersToRender.length; i--) {
@@ -332,16 +346,17 @@ class PreviewBar {
// Hide old bar
this.originalChapterBar.style.display = "none";
this.customChaptersBar.style.removeProperty("display");
if (createFromScratch) {
if (this.container?.parentElement === progressBar) {
progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
if (this.container?.parentElement === this.progressBar) {
this.progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
} else {
progressBar.prepend(this.customChaptersBar);
this.progressBar.prepend(this.customChaptersBar);
}
}
this.updateChapterAllMutation(this.originalChapterBar, progressBar, true);
this.updateChapterAllMutation(this.originalChapterBar, this.progressBar, true);
}
createChapterRenderGroups(segments: PreviewBarSegment[]): ChapterGroup[] {
@@ -455,9 +470,7 @@ class PreviewBar {
}
private createChapterMutationObservers(): void {
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
if (!progressBar || !chapterBar) return;
if (!this.progressBar || !this.originalChapterBar) return;
const attributeObserver = new MutationObserver((mutations) => {
const changes: Record<string, HTMLElement> = {};
@@ -469,10 +482,10 @@ class PreviewBar {
}
}
this.updateChapterMutation(changes, progressBar);
this.updateChapterMutation(changes, this.progressBar);
});
attributeObserver.observe(chapterBar, {
attributeObserver.observe(this.originalChapterBar, {
subtree: true,
attributes: true,
attributeFilter: ["style", "class"]
@@ -486,11 +499,11 @@ class PreviewBar {
}
}
this.updateChapterMutation(changes, progressBar);
this.updateChapterMutation(changes, this.progressBar);
});
// Only direct children, no subtree
childListObserver.observe(chapterBar, {
childListObserver.observe(this.originalChapterBar, {
childList: true
});
}
@@ -665,6 +678,11 @@ class PreviewBar {
const chapterVoteContainer = this.chapterVote.getContainer();
if (chosenSegment.source === SponsorSourceType.Server) {
if (!chapterButton.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
}
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
}
@@ -676,6 +694,7 @@ class PreviewBar {
} else {
// Hide chapters menu again
chaptersContainer.style.display = "none";
this.chapterVote.setVisibility(false);
}
}
}

View File

@@ -498,7 +498,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//display the video times from the array at the top, in a different section
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
let currentSegmentTab = segmentTab;
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter && segment.source !== SponsorSourceType.YouTube)) {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Segments;
} else {

View File

@@ -37,6 +37,7 @@ export class ChapterVote {
setVisibility(show: boolean): void {
const newState = {
show,
...(!show ? { segment: null } : {})
};
if (this.ref.current) {

View File

@@ -70,7 +70,7 @@ export default class GenericNotice {
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
className="sponsorTimeMessagesRow"
style={{maxHeight: (this.contentContainer().v.offsetHeight - 200) + "px"}}>
style={{maxHeight: this.contentContainer ? (this.contentContainer().v.offsetHeight - 200) + "px" : null}}>
<td style={{width: "100%"}}>
{this.getMessageBoxes(this.idSuffix, options.textBoxes)}
</td>

View File

@@ -35,7 +35,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const lines = data.split("\n");
const result: SponsorTime[] = [];
for (const line of lines) {
const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
if (startTime) {

View File

@@ -238,4 +238,20 @@ describe("Import segments", () => {
category: "chapter" as Category
}]);
});
it ("22. 2:04:22 some name", () => {
const input = ` 22. 2:04:22 some name
23. 2:04:22.23 some other name`;
const result = importTimes(input, 8000);
expect(result).toMatchObject([{
segment: [7462, 7462.23],
description: "some name",
category: "chapter" as Category
}, {
segment: [7462.23, 8000],
description: "some other name",
category: "chapter" as Category
}]);
});
});

View File

@@ -202,6 +202,8 @@ async function muteSkipSegment(driver: WebDriver, startTime: number, endTime: nu
async function toggleWhitelist(driver: WebDriver): Promise<void> {
const popupButton = await driver.findElement(By.id("infoButton"));
const rightControls = await driver.findElement(By.css(".ytp-right-controls"));
await driver.actions().move({ origin: rightControls }).perform();
if ((await popupButton.getCssValue("display")) !== "none") {
await driver.actions().move({ origin: popupButton }).perform();
await popupButton.click();