import * as React from "react"; import { YourWorkComponent } from "./YourWorkComponent"; import { isSafari } from "../../maze-utils/src/config"; import { showDonationLink } from "../utils/configUtils"; import Config, { ConfigurationID, generateDebugDetails } from "../config"; import { IsInfoFoundMessageResponse, LogResponse, Message, MessageResponse, PopupMessage } from "../messageTypes"; import { AnimationUtils } from "../../maze-utils/src/animationUtils"; import { SegmentListComponent } from "./SegmentListComponent"; import { ActionType, SegmentUUID, SponsorSourceType, SponsorTime } from "../types"; import { SegmentSubmissionComponent } from "./SegmentSubmissionComponent"; import { copyToClipboardPopup } from "./popupUtils"; import { getSkipProfileID, getSkipProfileIDForChannel, getSkipProfileIDForTab, getSkipProfileIDForTime, getSkipProfileIDForVideo, setCurrentTabSkipProfile } from "../utils/skipProfiles"; import { SelectOptionComponent } from "../components/options/SelectOptionComponent"; import * as Video from "../../maze-utils/src/video"; export enum LoadingStatus { Loading, SegmentsFound, NoSegmentsFound, ConnectionError, JSError, StillLoading, NoVideo } export interface LoadingData { status: LoadingStatus; code?: number; error?: Error | string; } type SkipProfileAction = "forJustThisVideo" | "forThisChannel" | "forThisTab" | "forAnHour" | null; interface SkipProfileOption { name: SkipProfileAction; active: () => boolean; } interface SegmentsLoadedProps { setStatus: (status: LoadingData) => void; setVideoID: (videoID: string | null) => void; setCurrentTime: (time: number) => void; setSegments: (segments: SponsorTime[]) => void; setLoopedChapter: (loopedChapter: SegmentUUID | null) => void; } interface LoadSegmentsProps extends SegmentsLoadedProps { updating: boolean; } interface SkipProfileRadioButtonsProps { selected: SkipProfileAction; setSelected: (s: SkipProfileAction, updateConfig: boolean) => void; disabled: boolean; configID: ConfigurationID | null; videoID: string; } interface SkipOptionActionComponentProps { selected: boolean; setSelected: (s: boolean) => void; highlighted: boolean; disabled: boolean; overridden: boolean; label: string; } let loadRetryCount = 0; export const PopupComponent = () => { const [status, setStatus] = React.useState({ status: LoadingStatus.Loading }); const [extensionEnabled, setExtensionEnabled] = React.useState(!Config.config!.disableSkipping); const [showForceChannelCheckWarning, setShowForceChannelCheckWarning] = React.useState(false); const [showNoticeButton, setShowNoticeButton] = React.useState(Config.config!.dontShowNotice); const [currentTime, setCurrentTime] = React.useState(0); const [segments, setSegments] = React.useState([]); const [loopedChapter, setLoopedChapter] = React.useState(null); const [videoID, setVideoID] = React.useState(null); React.useEffect(() => { loadSegments({ updating: false, setStatus, setVideoID, setCurrentTime, setSegments, setLoopedChapter }); setupComPort({ setStatus, setVideoID, setCurrentTime, setSegments, setLoopedChapter }); forwardClickEvents(sendMessage); }, []); return (
{ window !== window.top && } { Config.config!.testingServer &&
{ chrome.runtime.sendMessage({ "message": "openConfig", "hash": "advanced" }); }}> {chrome.i18n.getMessage("betaServerWarning")}
}

SponsorBlock

{getVideoStatusText(status)}

{/* Toggle Box */}
{ videoID && }
{ showForceChannelCheckWarning && { chrome.runtime.sendMessage({ "message": "openConfig", "hash": "behavior" }); }}> {chrome.i18n.getMessage("forceChannelCheckPopup")} } { !Config.config.cleanPopup && !Config.config.hideSegmentCreationInPopup && } {/* Your Work box */} { !Config.config.cleanPopup && } {/* Footer */} { !Config.config.cleanPopup && } { showNoticeButton && }
); }; function getVideoStatusText(status: LoadingData): string { switch (status.status) { case LoadingStatus.Loading: return chrome.i18n.getMessage("Loading"); case LoadingStatus.SegmentsFound: return chrome.i18n.getMessage("sponsorFound"); case LoadingStatus.NoSegmentsFound: return chrome.i18n.getMessage("sponsor404"); case LoadingStatus.ConnectionError: return `${chrome.i18n.getMessage("connectionError")} ${chrome.i18n.getMessage("errorCode").replace("{code}", `${status.code}`)}`; case LoadingStatus.JSError: return `${chrome.i18n.getMessage("connectionError")} ${status.error}`; case LoadingStatus.StillLoading: return chrome.i18n.getMessage("segmentsStillLoading"); case LoadingStatus.NoVideo: return chrome.i18n.getMessage("noVideoID"); } } async function loadSegments(props: LoadSegmentsProps): Promise { const response = await sendMessage({ message: "isInfoFound", updating: props.updating }) as IsInfoFoundMessageResponse; if (response && response.videoID) { segmentsLoaded(response, props); } else { // Handle error if it exists chrome.runtime.lastError; props.setStatus({ status: LoadingStatus.NoVideo, }); if (!props.updating) { loadRetryCount++; if (loadRetryCount < 6) { setTimeout(() => loadSegments(props), 100 * loadRetryCount); } } } } function segmentsLoaded(response: IsInfoFoundMessageResponse, props: SegmentsLoadedProps): void { if (response.found) { props.setStatus({ status: LoadingStatus.SegmentsFound }); } else if (typeof response.status !== "number") { props.setStatus({ status: LoadingStatus.JSError, error: response.status, }) } else if (response.status === 404 || response.status === 200) { props.setStatus({ status: LoadingStatus.NoSegmentsFound }); } else if (response.status) { props.setStatus({ status: LoadingStatus.ConnectionError, code: response.status }); } else { props.setStatus({ status: LoadingStatus.StillLoading }); } props.setVideoID(response.videoID); Video.setVideoID(response.videoID as Video.VideoID); props.setCurrentTime(response.time); Video.setChanelIDInfo(response.channelID, response.channelAuthor); setCurrentTabSkipProfile(response.currentTabSkipProfileID); props.setSegments((response.sponsorTimes || []) .filter((segment) => segment.source === SponsorSourceType.Server) .sort((a, b) => b.segment[1] - a.segment[1]) .sort((a, b) => a.segment[0] - b.segment[0]) .sort((a, b) => a.actionType === ActionType.Full ? -1 : b.actionType === ActionType.Full ? 1 : 0)); props.setLoopedChapter(response.loopedChapter); } function sendMessage(request: Message): Promise { return new Promise((resolve) => { if (chrome.tabs) { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => chrome.tabs.sendMessage(tabs[0].id, request, resolve)); } else { chrome.runtime.sendMessage({ message: "tabs", data: request }, resolve); } }); } function setupComPort(props: SegmentsLoadedProps): void { const port = chrome.runtime.connect({ name: "popup" }); port.onDisconnect.addListener(() => setupComPort(props)); port.onMessage.addListener((msg) => onMessage(props, msg)); } function onMessage(props: SegmentsLoadedProps, msg: PopupMessage) { switch (msg.message) { case "time": props.setCurrentTime(msg.time); break; case "infoUpdated": segmentsLoaded(msg, props); break; case "videoChanged": props.setStatus({ status: LoadingStatus.StillLoading }); props.setVideoID(msg.videoID); Video.setVideoID(msg.videoID as Video.VideoID); Video.setChanelIDInfo(msg.channelID, msg.channelAuthor); props.setSegments([]); break; } } function forwardClickEvents(sendMessage: (request: Message) => Promise): void { if (window !== window.top) { document.addEventListener("keydown", (e) => { const target = e.target as HTMLElement; if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || e.key === "ArrowUp" || e.key === "ArrowDown") { return; } if (e.key === " ") { // No scrolling e.preventDefault(); } sendMessage({ message: "keydown", key: e.key, keyCode: e.keyCode, code: e.code, which: e.which, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey }); }); } } // Copy over styles from parent window window.addEventListener("message", async (e): Promise => { if (e.source !== window.parent) return; if (e.origin.endsWith(".youtube.com") && e.data && e.data?.type === "style") { const style = document.createElement("style"); style.textContent = e.data.css; document.head.appendChild(style); } }); function SkipProfileButton(props: {videoID: string; setShowForceChannelCheckWarning: (v: boolean) => void}): JSX.Element { const [menuOpen, setMenuOpen] = React.useState(false); const channelSkipProfileSet = getSkipProfileIDForChannel() !== null; const skipProfileSet = getSkipProfileID() !== null; React.useEffect(() => { setMenuOpen(false); }, [props.videoID]); return ( <> { props.videoID && } ); } const skipProfileOptions: SkipProfileOption[] = [{ name: "forAnHour", active: () => getSkipProfileIDForTime() !== null }, { name: "forThisTab", active: () => getSkipProfileIDForTab() !== null }, { name: "forJustThisVideo", active: () => getSkipProfileIDForVideo() !== null }, { name: "forThisChannel", active: () => getSkipProfileIDForChannel() !== null }]; function SkipProfileMenu(props: {open: boolean; videoID: string}): JSX.Element { const [configID, setConfigID] = React.useState(null); const [selectedSkipProfileAction, setSelectedSkipProfileAction] = React.useState(null); const [allSkipProfiles, setAllSkipProfiles] = React.useState(Object.entries(Config.local!.skipProfiles)); React.useEffect(() => { if (props.open) { const channelInfo = Video.getChannelIDInfo(); if (!channelInfo) { if (Video.isOnYTTV()) { alert(chrome.i18n.getMessage("yttvNoChannelWhitelist")); } else { alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753"); } } } setConfigID(getSkipProfileID()); }, [props.open, props.videoID]); React.useEffect(() => { Config.configLocalListeners.push(() => { setAllSkipProfiles(Object.entries(Config.local!.skipProfiles)); }); }, []); return (
{ if (value === "new") { chrome.runtime.sendMessage({ message: "openConfig", hash: "newProfile" }); return; } const configID = value === "null" ? null : value as ConfigurationID; setConfigID(configID); if (configID === null) { setSelectedSkipProfileAction(null); } if (selectedSkipProfileAction) { updateSkipProfileSetting(selectedSkipProfileAction, configID); if (configID === null) { for (const option of skipProfileOptions) { if (option.name !== selectedSkipProfileAction && option.active()) { updateSkipProfileSetting(option.name, null); } } } } }} value={configID ?? "null"} options={[{ value: "null", label: chrome.i18n.getMessage("DefaultConfiguration") }].concat(allSkipProfiles.map(([key, value]) => ({ value: key, label: value.name }))).concat([{ value: "new", label: chrome.i18n.getMessage("CreateNewConfiguration") }])} /> { if (updateConfig) { if (s === null) { updateSkipProfileSetting(selectedSkipProfileAction, null); } else { updateSkipProfileSetting(s, configID); } } else if (s !== null) { setConfigID(getSkipProfileID()); } setSelectedSkipProfileAction(s); }} disabled={configID === null} configID={configID} videoID={props.videoID} />
); } function SkipProfileRadioButtons(props: SkipProfileRadioButtonsProps): JSX.Element { const result: JSX.Element[] = []; React.useEffect(() => { if (props.configID === null) { props.setSelected(null, false); } else { for (const option of skipProfileOptions) { if (option.active()) { if (props.selected !== option.name) { props.setSelected(option.name, false); } return; } } } }, [props.configID, props.videoID, props.selected]); let alreadySelected = false; for (const option of skipProfileOptions) { const highlighted = option.active() && props.selected !== option.name; const overridden = !highlighted && alreadySelected; result.push( { props.setSelected(s ? option.name : null, true); }}/> ); if (props.selected === option.name) { alreadySelected = true; } } return
{result}
} function SkipOptionActionComponent(props: SkipOptionActionComponentProps): JSX.Element { let title = ""; if (props.selected) { title = chrome.i18n.getMessage("clickToNotApplyThisProfile"); } else if ((props.highlighted && !props.disabled) || props.overridden) { title = chrome.i18n.getMessage("skipProfileBeingOverriddenByHigherPriority"); } else if (!props.highlighted && !props.disabled) { title = chrome.i18n.getMessage("clickToApplyThisProfile"); } else if (props.disabled) { title = chrome.i18n.getMessage("selectASkipProfileFirst"); } return (
{ // Need to uncheck or disable a higher priority option first if (!props.disabled && !props.highlighted) { props.setSelected(!props.selected); } }}> {props.label}
); } function updateSkipProfileSetting(action: SkipProfileAction, configID: ConfigurationID | null) { switch (action) { case "forAnHour": Config.local!.skipProfileTemp = configID ? { time: Date.now(), configID } : null; break; case "forThisTab": setCurrentTabSkipProfile(configID); sendMessage({ message: "setCurrentTabSkipProfile", configID }); break; case "forJustThisVideo": if (configID) { Config.local!.channelSkipProfileIDs[Video.getVideoID()!] = configID; } else { delete Config.local!.channelSkipProfileIDs[Video.getVideoID()!]; } Config.forceLocalUpdate("channelSkipProfileIDs"); break; case "forThisChannel": { const channelInfo = Video.getChannelIDInfo(); if (configID) { Config.local!.channelSkipProfileIDs[channelInfo.id] = configID; delete Config.local!.channelSkipProfileIDs[channelInfo.author]; } else { delete Config.local!.channelSkipProfileIDs[channelInfo.id]; delete Config.local!.channelSkipProfileIDs[channelInfo.author]; } Config.forceLocalUpdate("channelSkipProfileIDs"); break; } } }