diff options
-rw-r--r-- | manifest/manifest.json | 1 | ||||
-rw-r--r-- | public/_locales/en/messages.json | 14 | ||||
-rw-r--r-- | src/config.ts | 4 | ||||
-rw-r--r-- | src/content.ts | 66 | ||||
-rw-r--r-- | src/render/GenericNotice.tsx | 111 | ||||
-rw-r--r-- | src/render/SkipNotice.tsx | 24 | ||||
-rw-r--r-- | src/render/SubmissionNotice.tsx | 20 | ||||
-rw-r--r-- | src/utils.ts | 23 |
8 files changed, 224 insertions, 39 deletions
diff --git a/manifest/manifest.json b/manifest/manifest.json index 7b920dd3..4e8105dd 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -1,6 +1,7 @@ { "name": "__MSG_fullName__", "short_name": "SponsorBlock", + "version": "2.1.0.2", "version": "2.1.1", "default_locale": "en", "description": "__MSG_Description__", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 1ad9b1f2..1e3bc3c8 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -654,7 +654,17 @@ "categoryUpdate2": { "message": "Open the options to skip intros, outros, merch, etc." }, - "help": { - "message": "Help" + "experimentUnlistedTitle": { + "message": "Help prevent this from disappearing" + }, + "experimentUnlistedText": { + "message": "This video is detected as unlisted and uploaded before 2017\nOld unlisted videos are being set to private next month\nWe are collecting *public* videos to back up\nWould you like anonymously to send this video to us?\nhttps://support.google.com/youtube/answer/9230970" + }, + "experiementOptOut": { + "message": "Opt-out of all future experiments", + "description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private." + }, + "hideForever": { + "message": "Hide forever" } } diff --git a/src/config.ts b/src/config.ts index 6877400a..75392f98 100644 --- a/src/config.ts +++ b/src/config.ts @@ -38,6 +38,8 @@ interface SBConfig { testingServer: boolean, refetchWhenNotFound: boolean, ytInfoPermissionGranted: boolean, + askAboutUnlistedVideos: boolean, + allowExpirements: boolean, // What categories should be skipped categorySelections: CategorySelection[], @@ -174,6 +176,8 @@ const Config: SBObject = { testingServer: false, refetchWhenNotFound: true, ytInfoPermissionGranted: false, + askAboutUnlistedVideos: true, + allowExpirements: true, categorySelections: [{ name: "sponsor", diff --git a/src/content.ts b/src/content.ts index d6c47c83..be48503b 100644 --- a/src/content.ts +++ b/src/content.ts @@ -13,6 +13,7 @@ import SkipNotice from "./render/SkipNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SubmissionNotice from "./render/SubmissionNotice"; import { Message, MessageResponse } from "./messageTypes"; +import GenericNotice from "./render/GenericNotice"; // Hack to get the CSS loaded on permission-based sites (Invidious) utils.wait(() => Config.config !== null, 5000, 10).then(addCSS); @@ -272,6 +273,9 @@ async function videoIDChange(id) { // Update whitelist data when the video data is loaded whitelistCheck(); + // Temporary expirement + unlistedCheck(); + //setup the preview bar if (previewBar === null) { if (onMobileYouTube) { @@ -392,7 +396,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: return; } - if (video.paused) return; + if (!video || video.paused) return; if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ return; @@ -865,6 +869,66 @@ async function whitelistCheck() { if (Config.config.forceChannelCheck && sponsorTimes?.length > 0) startSkipScheduleCheckingForStartSponsors(); } +async function unlistedCheck() { + if (!Config.config.allowExpirements || !Config.config.askAboutUnlistedVideos) return; + + try { + await utils.wait(() => !!videoInfo && !!document.getElementById("info-text") + && !!document.querySelector(".ytd-video-primary-info-renderer > .badge > yt-icon > svg"), 6000, 1000); + + const isUnlisted = document.querySelector(".ytd-video-primary-info-renderer > .badge > yt-icon > svg > g > path") + ?.getAttribute("d")?.includes("M3.9 12c0-1.71 1.39-3.1 3.1-3.1h"); // Icon of unlisted badge + const yearMatches = document.querySelector("#info-text > #info-strings > yt-formatted-string") + ?.innerHTML?.match(/20[0-9]{2}/); + const year = yearMatches ? parseInt(yearMatches[0]) : -1; + const isOld = !isNaN(year) && year < 2017 && year > 2004; + const views = parseInt(videoInfo?.videoDetails?.viewCount); + const isHighViews = views > 15000; + + if (isUnlisted && isOld && isHighViews) { + // Ask if they want to submit this videoID + const notice = new GenericNotice(skipNoticeContentContainer, "unlistedWarning", { + title: chrome.i18n.getMessage("experimentUnlistedTitle"), + textBoxes: chrome.i18n.getMessage("experimentUnlistedText").split("\n"), + buttons: [ + { + name: chrome.i18n.getMessage("experiementOptOut"), + listener: () => { + Config.config.allowExpirements = false; + + notice.close(); + } + }, + { + name: chrome.i18n.getMessage("hideForever"), + listener: () => { + Config.config.askAboutUnlistedVideos = false; + + notice.close(); + } + }, + { + name: "Submit", + listener: () => { + utils.asyncRequestToServer("POST", "/api/unlistedVideo", { + videoID: sponsorVideoID, + year, + views, + channelID: channelIDInfo.status === ChannelIDStatus.Found ? channelIDInfo.id : undefined + }); + + notice.close(); + } + } + ] + }); + } + + } catch (e) { + return; + } +} + /** * Returns info about the next upcoming sponsor skip */ diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx new file mode 100644 index 00000000..08570eab --- /dev/null +++ b/src/render/GenericNotice.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import NoticeComponent from "../components/NoticeComponent"; + +import Utils from "../utils"; +const utils = new Utils(); + +import { ContentContainer } from "../types"; +import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent"; + +export interface ButtonListener { + name: string, + listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void +} + +export interface NoticeOptions { + title: string, + textBoxes?: string[], + buttons?: ButtonListener[], + fadeIn?: boolean, + timed?: boolean +} + +export default class GenericNotice { + // Contains functions and variables from the content script needed by the skip notice + contentContainer: ContentContainer; + + noticeElement: HTMLDivElement; + noticeRef: React.MutableRefObject<NoticeComponent>; + + constructor(contentContainer: ContentContainer, idSuffix: string, options: NoticeOptions) { + this.noticeRef = React.createRef(); + + this.contentContainer = contentContainer; + + const referenceNode = utils.findReferenceNode(); + + this.noticeElement = document.createElement("div"); + this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix; + + referenceNode.prepend(this.noticeElement); + + ReactDOM.render( + <NoticeComponent + noticeTitle={options.title} + idSuffix={idSuffix} + fadeIn={options.fadeIn ?? true} + timed={options.timed ?? true} + ref={this.noticeRef} + closeListener={() => this.close()} > + + {this.getMessageBox(idSuffix, options.textBoxes)} + + <tr id={"sponsorSkipNoticeSpacer" + idSuffix} + className="sponsorBlockSpacer"> + </tr> + + <div className="sponsorSkipNoticeRightSection" + style={{position: "relative"}}> + + {this.getButtons(options.buttons)} + </div> + </NoticeComponent>, + this.noticeElement + ); + } + + getMessageBox(idSuffix: string, textBoxes: string[]): JSX.Element[] { + if (textBoxes) { + const result = []; + for (let i = 0; i < textBoxes.length; i++) { + result.push( + <NoticeTextSelectionComponent idSuffix={idSuffix} + key={i} + text={textBoxes[i]} /> + ) + } + + return result; + } else { + return null; + } + } + + getButtons(buttons?: ButtonListener[]): JSX.Element[] { + if (buttons) { + const result: JSX.Element[] = []; + + for (const button of buttons) { + result.push( + <button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton" + key={button.name} + onClick={(e) => button.listener(e)}> + + {button.name} + </button> + ) + } + + return result; + } else { + return null; + } + } + + close(): void { + ReactDOM.unmountComponentAtNode(this.noticeElement); + + this.noticeElement.remove(); + } +}
\ No newline at end of file diff --git a/src/render/SkipNotice.tsx b/src/render/SkipNotice.tsx index 426b1460..58600034 100644 --- a/src/render/SkipNotice.tsx +++ b/src/render/SkipNotice.tsx @@ -1,6 +1,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; +import Utils from "../utils"; +const utils = new Utils(); + import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent"; import { SponsorTime, ContentContainer } from "../types"; @@ -21,26 +24,7 @@ class SkipNotice { this.autoSkip = autoSkip; this.contentContainer = contentContainer; - //get reference node - let referenceNode = document.getElementById("player-container-id") - ?? document.getElementById("movie_player") - ?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music - ?? document.querySelector("#player-container .video-js") // Invidious - ?? document.querySelector(".main-video-section > .video-container"); // Cloudtube - if (referenceNode == null) { - //for embeds - const player = document.getElementById("player"); - referenceNode = player.firstChild as HTMLElement; - let index = 1; - - //find the child that is the video player (sometimes it is not the first) - while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) { - referenceNode = player.children[index] as HTMLElement; - - index++; - } - } - + const referenceNode = utils.findReferenceNode(); const amountOfPreviousNotices = document.getElementsByClassName("sponsorSkipNotice").length; //this is the suffix added at the end of every id diff --git a/src/render/SubmissionNotice.tsx b/src/render/SubmissionNotice.tsx index 8c267854..5f646063 100644 --- a/src/render/SubmissionNotice.tsx +++ b/src/render/SubmissionNotice.tsx @@ -1,6 +1,9 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; +import Utils from "../utils"; +const utils = new Utils(); + import SubmissionNoticeComponent from "../components/SubmissionNoticeComponent"; import { ContentContainer } from "../types"; @@ -20,22 +23,7 @@ class SubmissionNotice { this.contentContainer = contentContainer; this.callback = callback; - //get reference node - let referenceNode = document.getElementById("player-container-id") - || document.getElementById("movie_player") || document.querySelector("#player-container .video-js"); - if (referenceNode == null) { - //for embeds - const player = document.getElementById("player"); - referenceNode = player.firstChild as HTMLElement; - let index = 1; - - //find the child that is the video player (sometimes it is not the first) - while (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed")) { - referenceNode = player.children[index] as HTMLElement; - - index++; - } - } + const referenceNode = utils.findReferenceNode(); this.noticeElement = document.createElement("div"); this.noticeElement.id = "submissionNoticeContainer"; diff --git a/src/utils.ts b/src/utils.ts index fd5684f4..4aa21dbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -366,6 +366,29 @@ export default class Utils { }); } + findReferenceNode(): HTMLElement { + let referenceNode = document.getElementById("player-container-id") + ?? document.getElementById("movie_player") + ?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music + ?? document.querySelector("#player-container .video-js") // Invidious + ?? document.querySelector(".main-video-section > .video-container"); // Cloudtube + if (referenceNode == null) { + //for embeds + const player = document.getElementById("player"); + referenceNode = player.firstChild as HTMLElement; + let index = 1; + + //find the child that is the video player (sometimes it is not the first) + while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) { + referenceNode = player.children[index] as HTMLElement; + + index++; + } + } + + return referenceNode; + } + getFormattedTime(seconds: number, precise?: boolean): string { const hours = Math.floor(seconds / 60 / 60); const minutes = Math.floor(seconds / 60) % 60; |