diff options
-rw-r--r-- | public/options/options.html | 12 | ||||
-rw-r--r-- | src/components/UpcomingNoticeComponent.tsx | 99 | ||||
-rw-r--r-- | src/config.ts | 2 | ||||
-rw-r--r-- | src/content.ts | 39 | ||||
-rw-r--r-- | src/render/UpcomingNotice.tsx | 54 | ||||
-rw-r--r-- | src/types.ts | 2 | ||||
-rw-r--r-- | src/utils/categoryUtils.ts | 10 |
7 files changed, 216 insertions, 2 deletions
diff --git a/public/options/options.html b/public/options/options.html index b9258f33..e63b9837 100644 --- a/public/options/options.html +++ b/public/options/options.html @@ -209,6 +209,18 @@ <div class="small-description">__MSG_skipNoticeDurationDescription__</div> </div> + <div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowUpcomingNotice"> + <div class="switch-container"> + <label class="switch"> + <input id="dontShowUpcomingNotice" type="checkbox" checked> + <span class="slider round"></span> + </label> + <label class="switch-label" for="dontShowUpcomingNotice"> + __MSG_showUpcomingNotice__ + </label> + </div> + </div> + <div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowNotice"> <div class="switch-container"> <label class="switch"> diff --git a/src/components/UpcomingNoticeComponent.tsx b/src/components/UpcomingNoticeComponent.tsx new file mode 100644 index 00000000..71eb2d47 --- /dev/null +++ b/src/components/UpcomingNoticeComponent.tsx @@ -0,0 +1,99 @@ +import * as React from "react"; +import { ContentContainer, NoticeVisbilityMode, SponsorTime } from "../types"; +import NoticeComponent from "./NoticeComponent"; +import Config from "../config"; +import { getUpcomingText } from "../utils/categoryUtils"; + +export interface UpcomingNoticeProps { + segments: SponsorTime[]; + + autoSkip: boolean; + timeUntilSegment: number; + contentContainer: ContentContainer; + + closeListener: () => void; + showKeybindHint?: boolean; +} + +class UpcomingNoticeComponent extends React.Component<UpcomingNoticeProps> { + noticeTitle: string; + segments: SponsorTime[]; + autoSkip: boolean; + contentContainer: ContentContainer; + + amountOfPreviousNotices: number; + timeUntilSegment: number; + + idSuffix: string; + + noticeRef: React.MutableRefObject<NoticeComponent>; + + configListener: () => void; + + constructor(props: UpcomingNoticeProps) { + super(props); + this.noticeRef = React.createRef(); + + this.segments = props.segments; + this.autoSkip = props.autoSkip; + this.contentContainer = props.contentContainer; + this.timeUntilSegment = props.timeUntilSegment; + + const previousUpcomingNotices = document.getElementsByClassName("sponsorSkipNoticeParent"); + this.amountOfPreviousNotices = previousUpcomingNotices.length; + + if (this.segments.length > 1) { + this.segments.sort((a, b) => a.segment[0] - b.segment[0]); + } + + // This is the suffix added at the end of every id + for (const segment of this.segments) { + this.idSuffix += segment.UUID; + } + this.idSuffix += this.amountOfPreviousNotices; + + this.noticeTitle = getUpcomingText(this.segments); + } + + render(): React.ReactElement { + const noticeStyle: React.CSSProperties = { }; + if (this.contentContainer().onMobileYouTube) { + noticeStyle.bottom = "4em"; + noticeStyle.transform = "scale(0.8) translate(10%, 10%)"; + } + + return ( + <NoticeComponent + noticeTitle={this.noticeTitle} + amountOfPreviousNotices={this.amountOfPreviousNotices} + idSuffix={this.idSuffix} + fadeIn + startFaded={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAll + || (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)} + timed + maxCountdownTime={() => Math.round(this.timeUntilSegment / 1000)} + style={noticeStyle} + biggerCloseButton={this.contentContainer().onMobileYouTube} + ref={this.noticeRef} + closeListener={() => this.closeListener()} + logoFill={Config.config.barTypes[this.segments[0].category].color} + limitWidth + dontPauseCountdown /> + ) + } + + closeListener(): void { + this.clearConfigListener(); + + this.props.closeListener(); + } + + clearConfigListener(): void { + if (this.configListener) { + Config.configSyncListeners.splice(Config.configSyncListeners.indexOf(this.configListener), 1); + this.configListener = null; + } + } +} + +export default UpcomingNoticeComponent;
\ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 3fbe23ba..0aff6355 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ interface SBConfig { trackDownvotes: boolean; trackDownvotesInPrivate: boolean; dontShowNotice: boolean; + dontShowUpcomingNotice: boolean; noticeVisibilityMode: NoticeVisbilityMode; hideVideoPlayerControls: boolean; hideInfoButtonPlayerControls: boolean; @@ -293,6 +294,7 @@ const syncDefaults = { trackDownvotes: true, trackDownvotesInPrivate: false, dontShowNotice: false, + dontShowUpcomingNotice: false, noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip, hideVideoPlayerControls: false, hideInfoButtonPlayerControls: false, diff --git a/src/content.ts b/src/content.ts index 262b51c9..5a9af1d3 100644 --- a/src/content.ts +++ b/src/content.ts @@ -20,6 +20,7 @@ import Utils from "./utils"; import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar"; import SkipNotice from "./render/SkipNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; +import UpcomingNotice from "./render/UpcomingNotice"; import SubmissionNotice from "./render/SubmissionNotice"; import { Message, MessageResponse, VoteResponse } from "./messageTypes"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; @@ -77,6 +78,7 @@ let importingChaptersWaiting = false; let triedImportingChapters = false; // List of open skip notices const skipNotices: SkipNotice[] = []; +const upcomingNotices: UpcomingNotice[] = []; let activeSkipKeybindElement: ToggleSkippable = null; let retryFetchTimeout: NodeJS.Timeout = null; let shownSegmentFailedToFetchWarning = false; @@ -107,6 +109,7 @@ const lastNextChapterKeybind = { let currentSkipSchedule: NodeJS.Timeout = null; let currentSkipInterval: NodeJS.Timeout = null; let currentVirtualTimeInterval: NodeJS.Timeout = null; +let currentUpcomingSchedule: NodeJS.Timeout = null; /** Has the sponsor been skipped */ let sponsorSkipped: boolean[] = []; @@ -177,6 +180,7 @@ const skipNoticeContentContainer: ContentContainer = () => ({ sponsorTimes, sponsorTimesSubmitting, skipNotices, + upcomingNotices, sponsorVideoID: getVideoID(), reskipSponsorTime, updatePreviewBar, @@ -417,6 +421,10 @@ function resetValues() { skipNotices.pop()?.close(); } + for (let i = 0; i < upcomingNotices.length; i++) { + upcomingNotices.pop()?.close(); + } + hideDeArrowPromotion(); } @@ -601,6 +609,11 @@ function cancelSponsorSchedule(): void { clearInterval(currentSkipInterval); currentSkipInterval = null; } + + if (currentUpcomingSchedule !== null) { + clearTimeout(currentUpcomingSchedule); + currentUpcomingSchedule = null; + } } /** @@ -782,7 +795,17 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current const offset = (isFirefoxOrSafari() && !isSafari() ? 600 : 150); // Schedule for right before to be more precise than normal timeout - currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - offset)); + const offsetDelayTime = Math.max(0, delayTime - offset); + currentSkipSchedule = setTimeout(skippingFunction, offsetDelayTime); + + // Show the notice only if the segment hasn't already started + if (!Config.config.dontShowUpcomingNotice && getCurrentTime() < skippingSegments[0].segment[0]) { + const maxPopupTime = 3000; + const timeUntilPopup = Math.max(0, offsetDelayTime - maxPopupTime); + const popupTime = offsetDelayTime - timeUntilPopup; + const autoSkip = shouldAutoSkip(skippingSegments[0]) + currentUpcomingSchedule = setTimeout(createUpcomingNotice, timeUntilPopup, skippingSegments, popupTime, autoSkip); + } } } } @@ -1784,6 +1807,19 @@ function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, un activeSkipKeybindElement = newSkipNotice; } +function createUpcomingNotice(skippingSegments: SponsorTime[], timeLeft: number, autoSkip: boolean) { + for (const upcomingNotice of upcomingNotices) { + if (skippingSegments.length === upcomingNotice.segments.length + && skippingSegments.every((segment) => upcomingNotice.segments.some((s) => s.UUID === segment.UUID))) { + // Upcoming notice already exists + return; + } + } + + const newUpcomingNotice = new UpcomingNotice(skippingSegments, skipNoticeContentContainer, timeLeft, autoSkip); + upcomingNotices.push(newUpcomingNotice); +} + function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false) { if (segment.actionType === ActionType.Mute) { getVideo().muted = false; @@ -2561,6 +2597,7 @@ function hotkeyListener(e: KeyboardEvent): void { } else if (keybindEquals(key, closeSkipNoticeKey)) { for (let i = 0; i < skipNotices.length; i++) { skipNotices.pop().close(); + upcomingNotices.pop().close(); } return; diff --git a/src/render/UpcomingNotice.tsx b/src/render/UpcomingNotice.tsx new file mode 100644 index 00000000..8c0aeffe --- /dev/null +++ b/src/render/UpcomingNotice.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { createRoot, Root } from "react-dom/client"; +import { ContentContainer, SponsorTime } from "../types"; +import UpcomingNoticeComponent from "../components/UpcomingNoticeComponent"; + +import Utils from "../utils"; +const utils = new Utils(); + +class UpcomingNotice { + segments: SponsorTime[]; + // Contains functions and variables from the content script needed by the skip notice + contentContainer: ContentContainer; + + noticeElement: HTMLDivElement; + + upcomingNoticeRef: React.MutableRefObject<UpcomingNoticeComponent>; + root: Root; + + constructor(segments: SponsorTime[], contentContainer: ContentContainer, timeLeft: number, autoSkip: boolean) { + this.upcomingNoticeRef = React.createRef(); + + this.segments = segments; + this.contentContainer = contentContainer; + + const referenceNode = utils.findReferenceNode(); + + this.noticeElement = document.createElement("div"); + this.noticeElement.className = "sponsorSkipNoticeContainer"; + + referenceNode.prepend(this.noticeElement); + + this.root = createRoot(this.noticeElement); + this.root.render( + <UpcomingNoticeComponent + segments={segments} + autoSkip={autoSkip} + contentContainer={contentContainer} + timeUntilSegment={timeLeft} + ref={this.upcomingNoticeRef} + closeListener={() => this.close()} /> + ); + } + + close(): void { + this.root.unmount(); + + this.noticeElement.remove(); + + const upcomingNotices = this.contentContainer().upcomingNotices; + upcomingNotices.splice(upcomingNotices.indexOf(this), 1); + } +} + +export default UpcomingNotice;
\ No newline at end of file diff --git a/src/types.ts b/src/types.ts index fdaee7b8..ed43fb97 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import SubmissionNotice from "./render/SubmissionNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SkipNotice from "./render/SkipNotice"; +import UpcomingNotice from "./render/UpcomingNotice"; export interface ContentContainer { (): { @@ -10,6 +11,7 @@ export interface ContentContainer { sponsorTimes: SponsorTime[]; sponsorTimesSubmitting: SponsorTime[]; skipNotices: SkipNotice[]; + upcomingNotices: UpcomingNotice[]; sponsorVideoID; reskipSponsorTime: (segment: SponsorTime, forceSeek?: boolean) => void; updatePreviewBar: () => void; diff --git a/src/utils/categoryUtils.ts b/src/utils/categoryUtils.ts index d93c2c7b..38f4b8e9 100644 --- a/src/utils/categoryUtils.ts +++ b/src/utils/categoryUtils.ts @@ -36,6 +36,15 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str } } +export function getUpcomingText(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 = "upcoming"; + return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); +} + + export function getCategorySuffix(category: Category): string { if (category.startsWith("poi_")) { return "_POI"; @@ -51,5 +60,4 @@ export function getCategorySuffix(category: Category): string { export function shortCategoryName(categoryName: string): string { return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName); } - export const DEFAULT_CATEGORY = "chooseACategory";
\ No newline at end of file |