diff --git a/public/popup.css b/public/popup.css index a3400503..31b68092 100644 --- a/public/popup.css +++ b/public/popup.css @@ -7,14 +7,26 @@ } /* - * IDs on container element (when inserted in page), element, - * element and main container + * Container when popup displayed in-page */ #sponsorBlockPopupContainer { + position: relative; margin-bottom: 16px; } +/* + * Disable fixed popup width when displayed in-page + */ + +#sponsorBlockPopupContainer #sponsorBlockPopupBody { + width: auto; +} + +/* + * Main containers + */ + #sponsorBlockPopupHTML { color-scheme: dark; } @@ -33,14 +45,6 @@ text-align: center; } -/* - * Disable fixed popup width when displayed inline - */ - -#sponsorBlockPopupContainer #sponsorBlockPopupBody { - width: auto; -} - /* * Disable transition on all elements until the extension has loaded */ @@ -49,6 +53,25 @@ transition: none !important; } +/* + * Close popup button when displayed in-page + */ + +.sbCloseButton { + background: transparent; + border: 0; + padding: 8px; + cursor: pointer; + position: absolute; + top: 5px; + right: 5px; + opacity: 0.5; +} + +.sbCloseButton:hover { + opacity: 1; +} + /* * Header logo */ diff --git a/src/content.ts b/src/content.ts index e133abe7..5987050b 100644 --- a/src/content.ts +++ b/src/content.ts @@ -34,7 +34,7 @@ let sponsorVideoID: VideoID = null; const skipNotices: SkipNotice[] = []; let activeSkipKeybindElement: ToggleSkippable = null; -// JSON video info +// JSON video info let videoInfo: VideoInfo = null; // The channel this video is about let channelIDInfo: ChannelIDInfo; @@ -221,8 +221,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo /** * Called when the config is updated - * - * @param {String} changes + * + * @param {String} changes */ function contentConfigUpdateListener(changes: StorageChangesObject) { for (const key in changes) { @@ -301,7 +301,7 @@ async function videoIDChange(id) { // If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up if (Config.config.checkForUnlistedVideos) { - const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled." + const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled." + " Due to a change in how segment fetching works, this setting is not needed anymore as it cannot leak your video ID to the server." + " It instead sends just the first 4 characters of a longer hash of the videoID to the server, and filters through a subset of the database." + " More info about this implementation can be found here: https://github.com/ajayyy/SponsorBlockServer/issues/25" @@ -327,13 +327,13 @@ async function videoIDChange(id) { let controlsContainer = null; utils.wait(() => { - controlsContainer = document.getElementById("player-control-container") + controlsContainer = document.getElementById("player-control-container") return controlsContainer !== null }).then(() => { - observer.observe(document.getElementById("player-control-container"), { - attributes: true, - childList: true, - subtree: true + observer.observe(document.getElementById("player-control-container"), { + attributes: true, + childList: true, + subtree: true }); }).catch(); } else { @@ -456,7 +456,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: if (!video || video.paused) return; if (currentTime === undefined || currentTime === null) { - const virtualTime = lastKnownVideoTime.videoTime ? + const virtualTime = lastKnownVideoTime.videoTime ? (performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null; if (!isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6){ currentTime = virtualTime; @@ -516,13 +516,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: if (forceVideoTime >= skipTime[0] && forceVideoTime < skipTime[1]) { skipToTime({ - v: video, - skipTime, - skippingSegments, + v: video, + skipTime, + skippingSegments, openNotice: skipInfo.openNotice }); - if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip + if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip || currentSkip.actionType === ActionType.Mute) { forcedSkipTime = skipTime[0] + 0.001; } else { @@ -573,8 +573,8 @@ function inMuteSegment(currentTime: number): boolean { */ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean { const currentVideoID = getYouTubeVideoID(document); - if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime - && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) + if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime + && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) && !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) { // Something has really gone wrong console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be."); @@ -592,13 +592,13 @@ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boole function setupVideoMutationListener() { const videoContainer = document.querySelector(".html5-video-container"); if (!videoContainer || videoMutationObserver !== null || onInvidious) return; - + videoMutationObserver = new MutationObserver(refreshVideoAttachments); - videoMutationObserver.observe(videoContainer, { - attributes: true, - childList: true, - subtree: true + videoMutationObserver.observe(videoContainer, { + attributes: true, + childList: true, + subtree: true }); } @@ -647,19 +647,19 @@ function setupVideoListeners() { // If already segments loaded before video, retry to skip starting segments if (sponsorTimes) startSkipScheduleCheckingForStartSponsors(); } - + // Check if an ad is playing updateAdFlag(); - + // Make sure it doesn't get double called with the playing event if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); lastCheckVideoTime = video.currentTime; - + startSponsorSchedule(); } - + }); video.addEventListener('playing', () => { updateVirtualTime(); @@ -669,7 +669,7 @@ function setupVideoListeners() { || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); lastCheckVideoTime = video.currentTime; - + startSponsorSchedule(); } }); @@ -680,7 +680,7 @@ function setupVideoListeners() { lastCheckVideoTime = video.currentTime; updateVirtualTime(); - + startSponsorSchedule(); } }); @@ -696,12 +696,12 @@ function setupVideoListeners() { videoTime: null, preciseTime: null } - + cancelSponsorSchedule(); }; video.addEventListener('pause', paused); video.addEventListener('waiting', paused); - + startSponsorSchedule(); } } @@ -717,10 +717,10 @@ function setupSkipButtonControlBar() { if (!skipButtonControlBar) { skipButtonControlBar = new SkipButtonControlBar({ skip: (segment) => skipToTime({ - v: video, - skipTime: segment.segment, - skippingSegments: [segment], - openNotice: true, + v: video, + skipTime: segment.segment, + skippingSegments: [segment], + openNotice: true, forceAutoSkip: true }), onMobileYouTube @@ -759,7 +759,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4) as VideoID & HashedValue; const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { categories, - actionTypes: getEnabledActionTypes(), + actionTypes: getEnabledActionTypes(), userAgent: `${chrome.runtime.id}`, ...extraRequestData }); @@ -768,7 +768,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { const recievedSegments: SponsorTime[] = JSON.parse(response.responseText) ?.filter((video) => video.videoID === id) ?.map((video) => video.segments)[0]; - if (!recievedSegments || !recievedSegments.length) { + if (!recievedSegments || !recievedSegments.length) { // return if no video found retryFetch(); return; @@ -834,7 +834,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { } else if (response?.status === 404) { retryFetch(); } - + lookupVipInformation(id); } @@ -910,8 +910,8 @@ function retryFetch(): void { } /** - * 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 + * * Ex. When segments are first loaded */ function startSkipScheduleCheckingForStartSponsors() { @@ -922,7 +922,7 @@ function startSkipScheduleCheckingForStartSponsors() { let found = false; let startingSegment: SponsorTime = null; for (const time of sponsorTimes) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; startingSegment = time; @@ -932,7 +932,7 @@ function startSkipScheduleCheckingForStartSponsors() { } if (!found) { for (const time of sponsorTimesSubmitting) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; startingSegment = time; @@ -951,8 +951,8 @@ function startSkipScheduleCheckingForStartSponsors() { if (skipOption !== CategorySkipOption.ShowOverlay) { skipToTime({ v: video, - skipTime: time.segment, - skippingSegments: [time], + skipTime: time.segment, + skippingSegments: [time], openNotice: true, unskipTime: video.currentTime }); @@ -975,7 +975,7 @@ function startSkipScheduleCheckingForStartSponsors() { /** * Get the video info for the current tab from YouTube - * + * * TODO: Replace */ async function getVideoInfo(): Promise { @@ -1023,9 +1023,9 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean { //Attempt to parse url let urlObject: URL = null; - try { + try { urlObject = new URL(url); - } catch (e) { + } catch (e) { console.error("[SB] Unable to parse URL: " + url); return false; } @@ -1143,7 +1143,7 @@ async function whitelistCheck() { } //see if this is a whitelisted channel - if (whitelistedChannels != undefined && + if (whitelistedChannels != undefined && channelIDInfo.status === ChannelIDStatus.Found && whitelistedChannels.includes(channelIDInfo.id)) { channelWhitelisted = true; } @@ -1155,24 +1155,24 @@ async function whitelistCheck() { /** * Returns info about the next upcoming sponsor skip */ -function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): +function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} { - const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = + const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true); const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime)); const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex); - const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = + const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false); const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime)); const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex); - if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || + if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || sponsorStartTimes[minSponsorTimeIndex] < unsubmittedSponsorStartTimes[minUnsubmittedSponsorTimeIndex]) { return { array: submittedArray, @@ -1192,20 +1192,20 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool /** * This returns index if the skip option is not AutoSkip - * + * * Finds the last endTime that occurs in a segment that the given * segment skips into that is part of an AutoSkip category. - * - * Used to find where a segment should truely skip to if there are intersecting submissions due to + * + * Used to find where a segment should truely skip to if there are intersecting submissions due to * them having different categories. - * - * @param sponsorTimes + * + * @param sponsorTimes * @param index Index of the given sponsor - * @param hideHiddenSponsors + * @param hideHiddenSponsors */ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideHiddenSponsors = true): number { // Only combine segments for AutoSkip - if (index == -1 || + if (index == -1 || !shouldAutoSkip(sponsorTimes[index]) || sponsorTimes[index].actionType !== ActionType.Skip) { return index; @@ -1218,7 +1218,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH const currentSegment = sponsorTimes[i].segment; const latestEndTime = sponsorTimes[latestEndTimeIndex].segment[1]; - if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime + if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) && shouldAutoSkip(sponsorTimes[i]) && sponsorTimes[i].actionType === ActionType.Skip) { @@ -1238,11 +1238,11 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH /** * Gets just the start times from a sponsor times array. * Optionally specify a minimum - * - * @param sponsorTimes + * + * @param sponsorTimes * @param minimum * @param hideHiddenSponsors - * @param includeIntersectingSegments If true, it will include segments that start before + * @param includeIntersectingSegments If true, it will include segments that start before * the current time, but end after */ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean, @@ -1271,14 +1271,14 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: for (let i = 0; i < possibleTimes.length; i++) { if ((minimum === undefined || ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum) - || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum))) + || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum))) && (!onlySkippableSponsors || shouldSkip(possibleTimes[i])) && (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible) && possibleTimes[i].actionType !== ActionType.Poi) { scheduledTimes.push(possibleTimes[i].scheduledTime); includedTimes.push(possibleTimes[i]); - } + } } return { includedTimes, scheduledTimes }; @@ -1286,8 +1286,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: /** * Skip to exact time in a video and autoskips - * - * @param time + * + * @param time */ function previewTime(time: number, unpause = true) { video.currentTime = time; @@ -1312,7 +1312,7 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: Config.config.skipCount = Config.config.skipCount + 1; counted = true; } - + if (fullSkip) utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); } } @@ -1325,7 +1325,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u // There will only be one submission if it is manual skip const autoSkip: boolean = forceAutoSkip || shouldAutoSkip(skippingSegments[0]); - if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) + if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) && v.currentTime !== skipTime[1]) { switch(skippingSegments[0].actionType) { case ActionType.Poi: @@ -1366,8 +1366,8 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u }) } - if (!autoSkip - && skippingSegments.length === 1 + if (!autoSkip + && skippingSegments.length === 1 && skippingSegments[0].actionType === ActionType.Poi) { skipButtonControlBar.enable(skippingSegments[0]); if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false); @@ -1400,7 +1400,7 @@ function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null) { //add a tiny bit of time to make sure it is not skipped again video.currentTime = unskipTime ?? segment.segment[0] + 0.001; } - + } function reskipSponsorTime(segment: SponsorTime) { @@ -1411,7 +1411,7 @@ function reskipSponsorTime(segment: SponsorTime) { const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0); const segmentDuration = segment.segment[1] - segment.segment[0]; const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; - + video.currentTime = segment.segment[1]; sendTelemetryAndCount([segment], skippedTime, fullSkip); startSponsorSchedule(true, segment.segment[1], false); @@ -1463,8 +1463,8 @@ function shouldAutoSkip(segment: SponsorTime): boolean { } function shouldSkip(segment: SponsorTime): boolean { - return (segment.actionType !== ActionType.Full - && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) + return (segment.actionType !== ActionType.Full + && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")); } @@ -1482,10 +1482,10 @@ async function createButtons(): Promise { createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker.svg"); const controlsContainer = getControls(); - if (Config.config.autoHideInfoButton && !onInvidious && controlsContainer + if (Config.config.autoHideInfoButton && !onInvidious && controlsContainer && playerButtons["info"]?.button && !controlsWithEventListeners.includes(controlsContainer)) { controlsWithEventListeners.push(controlsContainer); - + AnimationUtils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer); } } @@ -1580,7 +1580,7 @@ function startOrEndTimingNewSegment() { const existingSegment = getIncompleteSegment(); const existingTime = existingSegment.segment[0]; const currentTime = roundedTime; - + // Swap timestamps if the user put the segment end before the start existingSegment.segment = [Math.min(existingTime, currentTime), Math.max(existingTime, currentTime)]; } @@ -1677,17 +1677,19 @@ function openInfoMenu() { popup.innerHTML = htmlData; //close button - const closeButton = document.createElement("div"); - closeButton.innerText = chrome.i18n.getMessage("closePopup"); - closeButton.classList.add("smallLink"); - closeButton.setAttribute("align", "center"); + const closeButton = document.createElement("button"); + const closeButtonIcon = document.createElement("img"); + closeButtonIcon.src = chrome.extension.getURL("icons/close.png"); + closeButtonIcon.width = 16; + closeButtonIcon.height = 16; + closeButton.appendChild(closeButtonIcon); + closeButton.setAttribute("title", chrome.i18n.getMessage("closePopup")); + closeButton.classList.add("sbCloseButton"); closeButton.addEventListener("click", closeInfoMenu); - // Theme based color - closeButton.style.color = "var(--yt-spec-text-primary)"; //add the close button popup.prepend(closeButton); - + const parentNodes = document.querySelectorAll("#secondary"); let parentNode = null; for (let i = 0; i < parentNodes.length; i++) { @@ -1798,7 +1800,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo } else { skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText)) } - + skipNotice.resetVoteButtonInfo.bind(skipNotice)(); } } @@ -1824,7 +1826,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): // Count this as a skip Config.config.minutesSaved = Config.config.minutesSaved + factor * (sponsorTimes[sponsorIndex].segment[1] - sponsorTimes[sponsorIndex].segment[0]) / 60; - + Config.config.skipCount = Config.config.skipCount + factor; } @@ -1886,8 +1888,8 @@ function submitSponsorTimes() { submissionNotice.close(); submissionNotice = null; return; - } - + } + if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) { submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage); } @@ -1923,9 +1925,9 @@ async function sendSubmitMessage() { for (let i = 0; i < sponsorTimesSubmitting.length; i++) { const duration = sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0]; if (duration > 0 && duration < Config.config.minDuration) { - const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + + const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + getSegmentsMessage(sponsorTimesSubmitting); - + if(!confirm(confirmShort)) return; } } @@ -2030,10 +2032,10 @@ function hotkeyListener(e: KeyboardEvent): void { || document.activeElement?.id?.toLowerCase()?.includes("editable")) return; const key: Keybind = { - key: e.key, - code: e.code, - alt: e.altKey, - ctrl: e.ctrlKey, + key: e.key, + code: e.code, + alt: e.altKey, + ctrl: e.ctrlKey, shift: e.shiftKey }; @@ -2094,7 +2096,7 @@ function sendRequestToCustomServer(type, fullAddress, callback) { xmlhttp.onreadystatechange = function () { callback(xmlhttp, false); }; - + xmlhttp.onerror = function() { callback(xmlhttp, true); }; @@ -2138,7 +2140,7 @@ function showTimeWithoutSkips(skippedDuration: number): void { display.appendChild(duration); } - + const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration) duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")"; @@ -2146,7 +2148,7 @@ function showTimeWithoutSkips(skippedDuration: number): void { function checkForPreloadedSegment() { if (loadedPreloadedSegment) return; - + loadedPreloadedSegment = true; const hashParams = getHashParams(); @@ -2174,4 +2176,4 @@ function checkForPreloadedSegment() { Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); } -} \ No newline at end of file +}