diff options
27 files changed, 254 insertions, 240 deletions
diff --git a/LICENSE-APPSTORE.txt b/LICENSE-APPSTORE.txt index 5df878ab..414a3de7 100644 --- a/LICENSE-APPSTORE.txt +++ b/LICENSE-APPSTORE.txt @@ -1,12 +1,12 @@ The developers are aware that the terms of service that apply to apps distributed via Apple's App Store services and similar app stores may conflict with rights granted under the SponsorBlock license, the GNU General -Public License, version 3 or (at your option) any later version. The -copyright holders of the SponsorBlock project do not wish this conflict -to prevent the otherwise-compliant distribution of derived apps via -the App Store and similar app stores. Therefore, we have committed not to pursue any license +Public License, version 3. The copyright holders of the SponsorBlock +project do not wish this conflict to prevent the otherwise-compliant +distribution of derived apps via the App Store and similar app stores. +Therefore, we have committed not to pursue any license violation that results solely from the conflict between the GNU GPLv3 -or any later version and the Apple App Store terms of service or similar app stores. In +and the Apple App Store terms of service or similar app stores. In other words, as long as you comply with the GPL in all other respects, including its requirements to provide users with source code and the text of the license, we will not object to your distribution of the @@ -75,4 +75,4 @@ Icons made by: ### License -This project is licensed under GNU LGPL v3 or any later version +This project is licensed under GNU GPL v3 or any later version diff --git a/manifest/manifest.json b/manifest/manifest.json index ed81f4c3..9ad1f384 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_fullName__", "short_name": "SponsorBlock", - "version": "5.4.28", + "version": "5.5.7", "default_locale": "en", "description": "__MSG_Description__", "homepage_url": "https://sponsor.ajay.app", diff --git a/maze-utils b/maze-utils -Subproject 27010ba86e475f6225ef3ae8b3b8fb72f2ef2bb +Subproject 8c0385deb54414bf5436e4a1a59e1a87f3a5f41 diff --git a/public/_locales b/public/_locales -Subproject 7f2d4e63dc53facfeed96aae1086c2bc3329b51 +Subproject 3f17e350861638b1c58da3cf2381ec681dd9ed7 diff --git a/public/content.css b/public/content.css index 285ba11d..7dd3c33f 100644 --- a/public/content.css +++ b/public/content.css @@ -780,6 +780,18 @@ input::-webkit-inner-spin-button { line-height: 1.5em; } +/* Description on right layout */ +#title > #categoryPillParent { + font-size: 2rem; + font-weight: bold; + display: flex; + justify-content: center; + line-height: 2.8rem; +} +#title > #categoryPillParent > #categoryPill.cbPillOpen { + margin-bottom: 5px; +} + #categoryPillParent { height: fit-content; margin-top: auto; diff --git a/public/options/options.html b/public/options/options.html index 01a3b345..146b7956 100644 --- a/public/options/options.html +++ b/public/options/options.html @@ -448,6 +448,11 @@ <div class="inline"></div> </div> + <div data-type="keybind-change" data-sync="previewKeybind"> + <label class="optionLabel">__MSG_setPreviewKeybind__:</label> + <div class="inline"></div> + </div> + <div data-type="keybind-change" data-sync="actuallySubmitKeybind"> <label class="optionLabel">__MSG_setSubmitKeybind__:</label> <div class="inline"></div> diff --git a/src/background.ts b/src/background.ts index 99df62db..23914f46 100644 --- a/src/background.ts +++ b/src/background.ts @@ -123,7 +123,7 @@ chrome.runtime.onInstalled.addListener(function () { // If there is no userID, then it is the first install. if (!userID && !Config.local.alreadyInstalled){ //open up the install page - chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")}); + chrome.tabs.create({url: chrome.runtime.getURL("/help/index.html")}); //generate a userID const newUserID = generateUserID(); @@ -137,7 +137,7 @@ chrome.runtime.onInstalled.addListener(function () { if (Config.config.supportInvidious) { if (!(await utils.containsInvidiousPermission())) { - chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")}); + chrome.tabs.create({url: chrome.runtime.getURL("/permissions/index.html")}); } } }, 1500); @@ -160,8 +160,8 @@ async function registerFirefoxContentScript(options: Registration) { ids: [options.id] }).catch(() => []); - if (existingRegistrations.length > 0 - && existingRegistrations[0].matches.every((match) => options.matches.includes(match))) { + if (existingRegistrations && existingRegistrations.length > 0 + && options.matches.every((match) => existingRegistrations[0].matches.includes(match))) { // No need to register another script, already registered return; } @@ -222,27 +222,35 @@ async function submitVote(type: number, UUID: string, category: string) { const typeSection = (type !== undefined) ? "&type=" + type : "&category=" + category; - //publish this vote - const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&userID=" + userID + typeSection); - - if (response.ok) { - return { - successType: 1, - responseText: await response.text() - }; - } else if (response.status == 405) { - //duplicate vote - return { - successType: 0, - statusCode: response.status, - responseText: await response.text() - }; - } else { - //error while connect + try { + const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&userID=" + userID + typeSection); + + if (response.ok) { + return { + successType: 1, + responseText: await response.text() + }; + } else if (response.status == 405) { + //duplicate vote + return { + successType: 0, + statusCode: response.status, + responseText: await response.text() + }; + } else { + //error while connect + return { + successType: -1, + statusCode: response.status, + responseText: await response.text() + }; + } + } catch (e) { + console.error(e); return { successType: -1, - statusCode: response.status, - responseText: await response.text() + statusCode: -1, + responseText: "" }; } } diff --git a/src/components/CategoryPillComponent.tsx b/src/components/CategoryPillComponent.tsx index 1d39a400..e4c5a3ed 100644 --- a/src/components/CategoryPillComponent.tsx +++ b/src/components/CategoryPillComponent.tsx @@ -6,7 +6,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg"; import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils"; import { VoteResponse } from "../messageTypes"; -import { AnimationUtils } from "../utils/animationUtils"; +import { AnimationUtils } from "../../maze-utils/src/animationUtils"; import { Tooltip } from "../render/Tooltip"; import { getErrorMessage } from "../../maze-utils/src/formating"; @@ -23,12 +23,14 @@ export interface CategoryPillState { } class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryPillState> { - + mainRef: React.MutableRefObject<HTMLSpanElement>; tooltip?: Tooltip; constructor(props: CategoryPillProps) { super(props); + this.mainRef = React.createRef(); + this.state = { segment: null, show: false, @@ -43,17 +45,21 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP color: this.getTextColor(), } + // To be able to remove the margin from the parent + this.mainRef?.current?.parentElement?.classList?.toggle("cbPillOpen", this.state.show); + return ( <span style={style} className={"sponsorBlockCategoryPill" + (!this.props.showTextByDefault ? " sbPillNoText" : "")} aria-label={this.getTitleText()} onClick={(e) => this.toggleOpen(e)} onMouseEnter={() => this.openTooltip()} - onMouseLeave={() => this.closeTooltip()}> + onMouseLeave={() => this.closeTooltip()} + ref={this.mainRef}> <span className="sponsorBlockCategoryPillTitleSection"> <img className="sponsorSkipLogo sponsorSkipObject" - src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}> + src={chrome.runtime.getURL("icons/IconSponsorBlocker256px.png")}> </img> { @@ -86,7 +92,7 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP )} {/* Close Button */} - <img src={chrome.extension.getURL("icons/close.png")} + <img src={chrome.runtime.getURL("icons/close.png")} className="categoryPillClose" onClick={() => { this.setState({ show: false }); diff --git a/src/components/ChapterVoteComponent.tsx b/src/components/ChapterVoteComponent.tsx index d50878a6..677a966d 100644 --- a/src/components/ChapterVoteComponent.tsx +++ b/src/components/ChapterVoteComponent.tsx @@ -6,7 +6,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg"; import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils"; import { VoteResponse } from "../messageTypes"; -import { AnimationUtils } from "../utils/animationUtils"; +import { AnimationUtils } from "../../maze-utils/src/animationUtils"; import { Tooltip } from "../render/Tooltip"; import { getErrorMessage } from "../../maze-utils/src/formating"; diff --git a/src/components/NoticeComponent.tsx b/src/components/NoticeComponent.tsx index e41c3fa7..56a96378 100644 --- a/src/components/NoticeComponent.tsx +++ b/src/components/NoticeComponent.tsx @@ -207,7 +207,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> { {/* Close button */} - <img src={chrome.extension.getURL("icons/close.png")} + <img src={chrome.runtime.getURL("icons/close.png")} className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton" + (this.props.biggerCloseButton ? " biggerCloseButton" : "")} onClick={() => this.close()}> diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index 81f43753..0564293a 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -8,6 +8,7 @@ import SelectorComponent, { SelectorOption } from "./SelectorComponent"; import { DEFAULT_CATEGORY } from "../utils/categoryUtils"; import { getFormattedTime, getFormattedTimeToSeconds } from "../../maze-utils/src/formating"; import { asyncRequestToServer } from "../utils/requests"; +import { defaultPreviewTime } from "../utils/constants"; export interface SponsorTimeEditProps { index: number; @@ -33,7 +34,7 @@ export interface SponsorTimeEditState { chapterNameSelectorHovering: boolean; } -const categoryNamesGrams: string[] = [].concat(...CompileConfig.categoryList.filter((name) => name !== "chapter") +const categoryNamesGrams: string[] = [].concat(...CompileConfig.categoryList.filter((name) => !["chapter", "intro"].includes(name)) .map((name) => chrome.i18n.getMessage("category_" + name).split(/\/|\s|-/))); class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> { @@ -80,13 +81,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo componentDidMount(): void { // Prevent inputs from triggering key events - document.getElementById("sponsorTimeEditContainer" + this.idSuffix).addEventListener('keydown', function (event) { - event.stopPropagation(); + document.getElementById("sponsorTimeEditContainer" + this.idSuffix).addEventListener('keydown', (e) => { + e.stopPropagation(); }); // Prevent scrolling while changing times - document.getElementById("sponsorTimesContainer" + this.idSuffix).addEventListener('wheel', function (event) { - event.preventDefault(); + document.getElementById("sponsorTimesContainer" + this.idSuffix).addEventListener('wheel', (e) => { + if (this.state.editing) { + e.preventDefault(); + } }, {passive: false}); // Add as a config listener @@ -220,7 +223,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo target="_blank" rel="noreferrer"> <img id={"sponsorTimeCategoriesHelpButton" + this.idSuffix} className="helpButton" - src={chrome.extension.getURL("icons/help.svg")} + src={chrome.runtime.getURL("icons/help.svg")} title={chrome.i18n.getMessage("categoryGuidelines")} /> </a> </div> @@ -671,7 +674,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo previewTime(ctrlPressed = false, shiftPressed = false, skipToEndTime = false): void { const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting; const index = this.props.index; - let seekTime = 2; + let seekTime = defaultPreviewTime; if (ctrlPressed) seekTime = 0.5; if (shiftPressed) seekTime = 0.25; diff --git a/src/components/SubmissionNoticeComponent.tsx b/src/components/SubmissionNoticeComponent.tsx index 9aa52670..2cf394f1 100644 --- a/src/components/SubmissionNoticeComponent.tsx +++ b/src/components/SubmissionNoticeComponent.tsx @@ -14,7 +14,7 @@ export interface SubmissionNoticeProps { // Contains functions and variables from the content script needed by the skip notice contentContainer: ContentContainer; - callback: () => unknown; + callback: () => Promise<boolean>; closeListener: () => void; } @@ -69,6 +69,13 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S this.videoObserver.observe(this.contentContainer().v, { attributes: true }); + + // Prevent zooming while changing times + document.getElementById("sponsorSkipNoticeMiddleRow" + this.state.idSuffix).addEventListener('wheel', function (event) { + if (event.ctrlKey) { + event.preventDefault(); + } + }, {passive: false}); } componentWillUnmount(): void { @@ -82,13 +89,17 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S if (currentSegmentCount > this.lastSegmentCount) { this.lastSegmentCount = currentSegmentCount; - const scrollElement = this.noticeRef.current.getElement().current.querySelector("#sponsorSkipNoticeMiddleRowSubmissionNotice"); - scrollElement.scrollTo({ - top: scrollElement.scrollHeight + 1000 - }); + this.scrollToBottom(); } } + scrollToBottom() { + const scrollElement = this.noticeRef.current.getElement().current.querySelector("#sponsorSkipNoticeMiddleRowSubmissionNotice"); + scrollElement.scrollTo({ + top: scrollElement.scrollHeight + 1000 + }); + } + render(): React.ReactElement { const sortButton = <img id={"sponsorSkipSortButton" + this.state.idSuffix} @@ -96,7 +107,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S onClick={() => this.sortSegments()} title={chrome.i18n.getMessage("sortSegments")} key="sortButton" - src={chrome.extension.getURL("icons/sort.svg")}> + src={chrome.runtime.getURL("icons/sort.svg")}> </img>; const exportButton = <img id={"sponsorSkipExportButton" + this.state.idSuffix} @@ -104,7 +115,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S onClick={() => this.exportSegments()} title={chrome.i18n.getMessage("exportSegments")} key="exportButton" - src={chrome.extension.getURL("icons/export.svg")}> + src={chrome.runtime.getURL("icons/export.svg")}> </img>; return ( <NoticeComponent noticeTitle={this.state.noticeTitle} @@ -228,9 +239,11 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S } } - this.props.callback(); - - this.cancel(); + this.props.callback().then((success) => { + if (success) { + this.cancel(); + } + }); } sortSegments(): void { diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx index b82b0a52..d9d89cf0 100644 --- a/src/components/options/CategorySkipOptionsComponent.tsx +++ b/src/components/options/CategorySkipOptionsComponent.tsx @@ -158,7 +158,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr }); } - Config.forceLocalUpdate("categorySelections"); + Config.forceSyncUpdate("categorySelections"); } getCategorySkipOptions(): JSX.Element[] { diff --git a/src/components/options/KeybindDialogComponent.tsx b/src/components/options/KeybindDialogComponent.tsx index 2fdd728d..b034d7eb 100644 --- a/src/components/options/KeybindDialogComponent.tsx +++ b/src/components/options/KeybindDialogComponent.tsx @@ -144,6 +144,7 @@ class KeybindDialogComponent extends React.Component<KeybindDialogProps, Keybind if (this.props.option !== "skipKeybind" && this.equals(Config.config['skipKeybind']) || this.props.option !== "submitKeybind" && this.equals(Config.config['submitKeybind']) || this.props.option !== "actuallySubmitKeybind" && this.equals(Config.config['actuallySubmitKeybind']) || + this.props.option !== "previewKeybind" && this.equals(Config.config['previewKeybind']) || this.props.option !== "closeSkipNoticeKeybind" && this.equals(Config.config['closeSkipNoticeKeybind']) || this.props.option !== "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind'])) return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true}; diff --git a/src/config.ts b/src/config.ts index a8a8a682..81f41997 100644 --- a/src/config.ts +++ b/src/config.ts @@ -91,6 +91,7 @@ interface SBConfig { startSponsorKeybind: Keybind; submitKeybind: Keybind; actuallySubmitKeybind: Keybind; + previewKeybind: Keybind; nextChapterKeybind: Keybind; previousChapterKeybind: Keybind; closeSkipNoticeKeybind: Keybind; @@ -347,6 +348,7 @@ const syncDefaults = { startSponsorKeybind: { key: ";" }, submitKeybind: { key: "'" }, actuallySubmitKeybind: { key: "'", ctrl: true }, + previewKeybind: { key: ";", ctrl: true }, nextChapterKeybind: { key: "ArrowRight", ctrl: true }, previousChapterKeybind: { key: "ArrowLeft", ctrl: true }, closeSkipNoticeKeybind: { key: "Backspace" }, diff --git a/src/content.ts b/src/content.ts index 0ea9da11..f89fb369 100644 --- a/src/content.ts +++ b/src/content.ts @@ -26,7 +26,7 @@ import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { getStartTimeFromUrl } from "./utils/urlParser"; import { getControls, getExistingChapters, getHashParams, isPlayingPlaylist, isVisible } from "./utils/pageUtils"; import { CategoryPill } from "./render/CategoryPill"; -import { AnimationUtils } from "./utils/animationUtils"; +import { AnimationUtils } from "../maze-utils/src/animationUtils"; import { GenericUtils } from "./utils/genericUtils"; import { logDebug, logWarn } from "./utils/logger"; import { importTimes } from "./utils/exporter"; @@ -35,7 +35,7 @@ import { openWarningDialog } from "./utils/warnings"; import { isFirefoxOrSafari, waitFor } from "../maze-utils/src"; import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating"; import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube } from "../maze-utils/src/video"; -import { Keybind, StorageChangesObject, isSafari, keybindEquals } from "../maze-utils/src/config"; +import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config"; import { findValidElement } from "../maze-utils/src/dom" import { getHash, HashedValue } from "../maze-utils/src/hash"; import { generateUserID } from "../maze-utils/src/setup"; @@ -47,6 +47,8 @@ import { cleanPage } from "./utils/pageCleaner"; import { addCleanupListener } from "../maze-utils/src/cleanup"; import { hideDeArrowPromotion, tryShowingDeArrowPromotion } from "./dearrowPromotion"; import { asyncRequestToServer } from "./utils/requests"; +import { isMobileControlsOpen } from "./utils/mobileUtils"; +import { defaultPreviewTime } from "./utils/constants"; cleanPage(); @@ -76,6 +78,7 @@ let activeSkipKeybindElement: ToggleSkippable = null; let retryFetchTimeout: NodeJS.Timeout = null; let shownSegmentFailedToFetchWarning = false; let selectedSegment: SegmentUUID | null = null; +let previewedSegment = false; // JSON video info let videoInfo: VideoInfo = null; @@ -255,7 +258,12 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo break; case "refreshSegments": // update video on refresh if videoID invalid - if (!getVideoID()) checkVideoIDChange(); + if (!getVideoID()) { + checkVideoIDChange().then(() => { + // if still no video ID found, return an empty info to the popup + if (!getVideoID()) chrome.runtime.sendMessage({ message: "infoUpdated" }); + }); + } // fetch segments sponsorsLookup(false); @@ -370,6 +378,7 @@ function resetValues() { lastCheckTime = 0; lastCheckVideoTime = -1; retryCount = 0; + previewedSegment = false; sponsorTimes = []; existingChaptersImported = false; @@ -450,23 +459,22 @@ function videoIDChange(): void { } function handleMobileControlsMutations(): void { - if (!chrome.runtime?.id) return; + // Don't update while scrubbing + if (!chrome.runtime?.id + || document.querySelector(".YtProgressBarProgressBarPlayheadDotInDragging")) return; updateVisibilityOfPlayerControlsButton(); skipButtonControlBar?.updateMobileControls(); if (previewBar !== null) { - if (document.body.contains(previewBar.container)) { - const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background"); - - if (progressBarBackground !== null) { - updatePreviewBarPositionMobile(progressBarBackground); - } + if (!previewBar.parent.contains(previewBar.container) && isMobileControlsOpen()) { + previewBar.createElement(); + updatePreviewBar(); return; - } else { - // The container does not exist anymore, remove that old preview bar + } else if (!previewBar.parent.isConnected) { + // The parent does not exist anymore, remove that old preview bar previewBar.remove(); previewBar = null; } @@ -483,14 +491,14 @@ function createPreviewBar(): void { if (previewBar !== null) return; const progressElementOptions = [{ - // For mobile YouTube - selector: ".progress-bar-background", - isVisibleCheck: true - }, { // For new mobile YouTube (#1287) selector: ".progress-bar-line", isVisibleCheck: true }, { + // For newer mobile YouTube (Jan 2024) + selector: ".YtProgressBarProgressBarLine", + isVisibleCheck: true + }, { // For Desktop YouTube selector: ".ytp-progress-bar", isVisibleCheck: true @@ -769,6 +777,7 @@ function getVirtualTime(): number { function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean { const checkFunction = (segment) => segment.actionType === ActionType.Mute + && segment.hidden === SponsorHideType.Visible && segment.segment[0] <= currentTime && (segment.segment[1] > currentTime || (includeOverlap && segment.segment[1] + 0.02 > currentTime)); return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction); @@ -1314,15 +1323,6 @@ function startSkipScheduleCheckingForStartSponsors() { } } -/** - * This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears - */ -function updatePreviewBarPositionMobile(parent: HTMLElement) { - if (document.getElementById("previewbar") === null) { - previewBar.createElement(parent); - } -} - function selectSegment(UUID: SegmentUUID): void { selectedSegment = UUID; updatePreviewBar(); @@ -1589,6 +1589,7 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: * @param time */ function previewTime(time: number, unpause = true) { + previewedSegment = true; getVideo().currentTime = time; // Unpause the video if needed @@ -1615,6 +1616,9 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: } if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); + } else if (!previewedSegment && sponsorTimesSubmitting.some((s) => s.segment === segment.segment)) { + // Count that as a previewed segment + previewedSegment = true; } } } @@ -1775,7 +1779,7 @@ function createButton(baseID: string, title: string, callback: () => void, image newButton.draggable = isDraggable; newButtonImage.id = baseID + "Image"; newButtonImage.className = "playerButtonImage"; - newButtonImage.src = chrome.extension.getURL("icons/" + imageName); + newButtonImage.src = chrome.runtime.getURL("icons/" + imageName); // Append image to button newButton.appendChild(newButtonImage); @@ -1874,10 +1878,10 @@ function updateEditButtonsOnPlayer(): void { if (buttonsEnabled) { if (creatingSegment) { - playerButtons.startSegment.image.src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker.svg"); + playerButtons.startSegment.image.src = chrome.runtime.getURL("icons/PlayerStopIconSponsorBlocker.svg"); playerButtons.startSegment.button.setAttribute("title", chrome.i18n.getMessage("sponsorEnd")); } else { - playerButtons.startSegment.image.src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker.svg"); + playerButtons.startSegment.image.src = chrome.runtime.getURL("icons/PlayerStartIconSponsorBlocker.svg"); playerButtons.startSegment.button.setAttribute("title", chrome.i18n.getMessage("sponsorStart")); } } @@ -1990,6 +1994,9 @@ function updateSponsorTimesSubmitting(getFromConfig = true) { } if (sponsorTimesSubmitting.length > 0) { + // Assume they already previewed a segment + previewedSegment = true; + importExistingChapters(true); } } @@ -2055,7 +2062,7 @@ function openInfoMenu() { } } }); - frame.src = chrome.extension.getURL("popup.html"); + frame.src = chrome.runtime.getURL("popup.html"); popup.appendChild(frame); const elemHasChild = (elements: NodeListOf<HTMLElement>): Element => { @@ -2241,7 +2248,16 @@ function openSubmissionMenu() { if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) { submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage); } +} +function previewRecentSegment() { + if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) { + previewTime(sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment[0] - defaultPreviewTime); + + if (submissionNotice) { + submissionNotice.scrollToBottom(); + } + } } function submitSegments() { @@ -2255,17 +2271,26 @@ function submitSegments() { //send the message to the background js //called after all the checks have been made that it's okay to do so -async function sendSubmitMessage() { +async function sendSubmitMessage(): Promise<boolean> { // check if all segments are full video const onlyFullVideo = sponsorTimesSubmitting.every((segment) => segment.actionType === ActionType.Full); // Block if submitting on a running livestream or premiere if (!onlyFullVideo && (getIsLivePremiere() || isVisible(document.querySelector(".ytp-live-badge")))) { alert(chrome.i18n.getMessage("liveOrPremiere")); - return; + return false; + } + + if (!previewedSegment + && !sponsorTimesSubmitting.every((segment) => + [ActionType.Full, ActionType.Chapter, ActionType.Poi].includes(segment.actionType) + || segment.segment[1] >= getVideo()?.duration + || segment.segment[0] === 0)) { + alert(`${chrome.i18n.getMessage("previewSegmentRequired")} ${keybindToString(Config.config.previewKeybind)}`); + return false; } // Add loading animation - playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg"); + playerButtons.submit.image.src = chrome.runtime.getURL("icons/PlayerUploadIconSponsorBlocker.svg"); const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer()); //check if a sponsor exceeds the duration of the video @@ -2287,7 +2312,7 @@ async function sendSubmitMessage() { const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + getSegmentsMessage(sponsorTimesSubmitting); - if(!confirm(confirmShort)) return; + if(!confirm(confirmShort)) return false; } } } @@ -2337,10 +2362,12 @@ async function sendSubmitMessage() { if (fullVideoSegment) { categoryPill?.setSegment(fullVideoSegment); } + + return true; } else { // Show that the upload failed playerButtons.submit.button.style.animation = "unset"; - playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg"); + playerButtons.submit.image.src = chrome.runtime.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg"); if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a tip from a moderator.")) { openWarningDialog(skipNoticeContentContainer); @@ -2348,6 +2375,8 @@ async function sendSubmitMessage() { alert(getErrorMessage(response.status, response.responseText)); } } + + return false; } //get the message that visually displays the video times @@ -2373,16 +2402,12 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string { } function updateActiveSegment(currentTime: number): void { - const activeSegments = previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime); + previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime); + chrome.runtime.sendMessage({ message: "time", time: currentTime }); - - const chapterSegments = activeSegments?.filter((segment) => segment.actionType === ActionType.Chapter); - if (chapterSegments?.length > 0) { - sendTelemetryAndCount(chapterSegments, 0, true); - } } function nextChapter(): void { @@ -2461,6 +2486,7 @@ function hotkeyListener(e: KeyboardEvent): void { const closeSkipNoticeKey = Config.config.closeSkipNoticeKeybind; const startSponsorKey = Config.config.startSponsorKeybind; const submitKey = Config.config.actuallySubmitKeybind; + const previewKey = Config.config.previewKeybind; const openSubmissionMenuKey = Config.config.submitKeybind; const nextChapterKey = Config.config.nextChapterKeybind; const previousChapterKey = Config.config.previousChapterKeybind; @@ -2492,6 +2518,9 @@ function hotkeyListener(e: KeyboardEvent): void { } else if (keybindEquals(key, openSubmissionMenuKey)) { openSubmissionMenu(); return; + } else if (keybindEquals(key, previewKey)) { + previewRecentSegment(); + return; } else if (keybindEquals(key, nextChapterKey)) { if (sponsorTimes.length > 0) e.stopPropagation(); nextChapter(); @@ -2526,7 +2555,7 @@ function addCSS() { fileref.rel = "stylesheet"; fileref.type = "text/css"; - fileref.href = chrome.extension.getURL(file); + fileref.href = chrome.runtime.getURL(file); head.appendChild(fileref); } diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index 1406f691..5e002fb9 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -11,7 +11,6 @@ import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceT import { partition } from "../utils/arrayUtils"; import { DEFAULT_CATEGORY, shortCategoryName } from "../utils/categoryUtils"; import { normalizeChapterName } from "../utils/exporter"; -import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating"; import { findValidElement } from "../../maze-utils/src/dom"; import { addCleanupListener } from "../../maze-utils/src/cleanup"; @@ -125,34 +124,11 @@ class PreviewBar { mouseOnSeekBar = false; }); - const observer = new MutationObserver((mutations) => { - if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return; + seekBar.addEventListener("mousemove", (e: MouseEvent) => { + if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer || !chrome.runtime?.id) return; - // Only care about mutations to time tooltip - if (!mutations.some((mutation) => (mutation.target as HTMLElement).classList.contains("ytp-tooltip-text"))) { - return; - } - - const tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text"); - let timeInSeconds: number | null = null; - let noYoutubeChapters = false; - - for (const tooltipTextElement of tooltipTextElements) { - if (tooltipTextElement.classList.contains('ytp-tooltip-text-no-title')) noYoutubeChapters = true; - - const tooltipText = tooltipTextElement.textContent; - if (tooltipText === null || tooltipText.length === 0) continue; - - timeInSeconds = getFormattedTimeToSeconds(tooltipText); - - if (timeInSeconds !== null) break; - } - - if (timeInSeconds === null) { - originalTooltip.style.removeProperty("display"); - - return; - } + let noYoutubeChapters = !!tooltipTextWrapper.querySelector(".ytp-tooltip-text.ytp-tooltip-text-no-title"); + const timeInSeconds = this.decimalToTime((e.clientX - seekBar.getBoundingClientRect().x) / seekBar.clientWidth); // Find the segment at that location, using the shortest if multiple found const [normalSegments, chapterSegments] = @@ -198,15 +174,6 @@ class PreviewBar { this.chapterTooltip.style.textAlign = titleTooltip.style.textAlign; } }); - - observer.observe(tooltipTextWrapper, { - childList: true, - subtree: true, - }); - - addCleanupListener(() => { - observer.disconnect(); - }); } private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void { @@ -224,16 +191,12 @@ class PreviewBar { } } - createElement(parent: HTMLElement): void { - this.parent = parent; + createElement(parent?: HTMLElement): void { + if (parent) this.parent = parent; if (this.onMobileYouTube) { - if (parent.classList.contains("progress-bar-background")) { - parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)"; - parent.style.opacity = "1"; - } - this.container.style.transform = "none"; + this.container.style.height = "var(--yt-progress-bar-height)"; } else if (!this.onInvidious) { this.container.classList.add("sbNotInvidious"); } @@ -311,6 +274,7 @@ class PreviewBar { return (b[1] - b[0]) - (a[1] - a[0]); }); for (const segment of sortedSegments) { + if (segment.actionType === ActionType.Chapter) continue; const bar = this.createBar(segment); this.container.appendChild(bar); @@ -350,7 +314,7 @@ class PreviewBar { bar.style.left = this.timeToPercentage(startTime); if (duration > 0) { - bar.style.right = this.timeToPercentage(this.videoDuration - endTime); + bar.style.right = this.timeToRightPercentage(endTime); } if (this.chapterFilter(barSegment) && segment[1] < this.videoDuration) { bar.style.marginRight = `${this.chapterMargin}px`; @@ -923,7 +887,22 @@ class PreviewBar { return `${this.timeToDecimal(time) * 100}%` } + timeToRightPercentage(time: number): string { + return `${(1 - this.timeToDecimal(time)) * 100}%` + } + timeToDecimal(time: number): number { + return this.decimalTimeConverter(time, true); + } + + decimalToTime(decimal: number): number { + return this.decimalTimeConverter(decimal, false); + } + + /** + * Decimal to time or time to decimal + */ + decimalTimeConverter(value: number, isTime: boolean): number { if (this.originalChapterBarBlocks?.length > 1 && this.existingChapters.length === this.originalChapterBarBlocks?.length) { // Parent element to still work when display: none const totalPixels = this.originalChapterBar.parentElement.clientWidth; @@ -933,8 +912,9 @@ class PreviewBar { const chapterElement = this.originalChapterBarBlocks[i]; const widthPixels = parseFloat(chapterElement.style.width.replace("px", "")); - if (time >= this.existingChapters[i].segment[1]) { - const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0; + const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0; + if ((isTime && value >= this.existingChapters[i].segment[1]) + || (!isTime && value >= (pixelOffset + widthPixels + marginPixels) / totalPixels)) { pixelOffset += widthPixels + marginPixels; lastCheckedChapter = i; } else { @@ -948,13 +928,22 @@ class PreviewBar { const latestWidth = parseFloat(this.originalChapterBarBlocks[lastCheckedChapter + 1].style.width.replace("px", "")); const latestChapterDuration = latestChapter.segment[1] - latestChapter.segment[0]; - const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration; - const sizeOfCurrentChapter = latestWidth / totalPixels; - return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter))); + if (isTime) { + const percentageInCurrentChapter = (value - latestChapter.segment[0]) / latestChapterDuration; + const sizeOfCurrentChapter = latestWidth / totalPixels; + return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter))); + } else { + const percentageInCurrentChapter = (value * totalPixels - pixelOffset) / latestWidth; + return Math.max(0, latestChapter.segment[0] + (percentageInCurrentChapter * latestChapterDuration)); + } } } - return Math.min(1, time / this.videoDuration); + if (isTime) { + return Math.min(1, value / this.videoDuration); + } else { + return Math.max(0, value * this.videoDuration); + } } /* diff --git a/src/js-components/skipButtonControlBar.ts b/src/js-components/skipButtonControlBar.ts index b14eed18..b5c18386 100644 --- a/src/js-components/skipButtonControlBar.ts +++ b/src/js-components/skipButtonControlBar.ts @@ -1,8 +1,9 @@ import Config from "../config"; import { SegmentUUID, SponsorTime } from "../types"; import { getSkippingText } from "../utils/categoryUtils"; -import { AnimationUtils } from "../utils/animationUtils"; +import { AnimationUtils } from "../../maze-utils/src/animationUtils"; import { keybindToString } from "../../maze-utils/src/config"; +import { isMobileControlsOpen } from "../utils/mobileUtils"; export interface SkipButtonControlBarProps { skip: (segment: SponsorTime) => void; @@ -183,10 +184,8 @@ export class SkipButtonControlBar { } updateMobileControls(): void { - const overlay = document.getElementById("player-control-overlay"); - - if (overlay && this.enabled) { - if (overlay?.classList?.contains("fadein")) { + if (this.enabled) { + if (isMobileControlsOpen()) { this.showButton(); } else { this.hideButton(); diff --git a/src/popup.ts b/src/popup.ts index e1e5757f..f954f168 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -19,7 +19,7 @@ import { VoteResponse, } from "./messageTypes"; import { showDonationLink } from "./utils/configUtils"; -import { AnimationUtils } from "./utils/animationUtils"; +import { AnimationUtils } from "../maze-utils/src/animationUtils"; import { shortCategoryName } from "./utils/categoryUtils"; import { localizeHtmlPage } from "../maze-utils/src/setup"; import { exportTimes } from "./utils/exporter"; @@ -465,8 +465,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { return; } - //if request is undefined, then the page currently being browsed is not YouTube - if (request != undefined) { + // if request has no field other than message, then the page currently being browsed is not YouTube + if (request.found != undefined) { //remove loading text PageElements.mainControls.style.display = "block"; if (request.onMobileYouTube) PageElements.mainControls.classList.add("hidden"); @@ -490,6 +490,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { PageElements.issueReporterImportExport.classList.remove("hidden"); } + } else { + displayNoVideo(); } //see if whitelist button should be swapped diff --git a/src/render/CategoryPill.tsx b/src/render/CategoryPill.tsx index 57730af9..20923fba 100644 --- a/src/render/CategoryPill.tsx +++ b/src/render/CategoryPill.tsx @@ -43,9 +43,15 @@ export class CategoryPill { } private async attachToPageInternal(): Promise<void> { - const referenceNode = + let referenceNode = await waitFor(() => getYouTubeTitleNode()); + // Experimental YouTube layout with description on right + const isOnDescriptionOnRightLayout = document.querySelector("#title #description"); + if (isOnDescriptionOnRightLayout) { + referenceNode = referenceNode.parentElement; + } + if (referenceNode && !referenceNode.contains(this.container)) { if (!this.container) { this.container = document.createElement('span'); @@ -91,7 +97,9 @@ export class CategoryPill { parent.appendChild(this.container); referenceNode.prepend(parent); - referenceNode.style.display = "flex"; + if (!isOnDescriptionOnRightLayout) { + referenceNode.style.display = "flex"; + } } } diff --git a/src/render/RectangleTooltip.tsx b/src/render/RectangleTooltip.tsx index 1b357fa8..d325688a 100644 --- a/src/render/RectangleTooltip.tsx +++ b/src/render/RectangleTooltip.tsx @@ -59,7 +59,7 @@ export class RectangleTooltip { className="sponsorBlockRectangleTooltip" > <div> <img className="sponsorSkipLogo sponsorSkipObject" - src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}> + src={chrome.runtime.getURL("icons/IconSponsorBlocker256px.png")}> </img> <span className="sponsorSkipObject"> {this.text + (props.link ? ". " : "")} diff --git a/src/render/SubmissionNotice.tsx b/src/render/SubmissionNotice.tsx index 671dde6b..c0159cc0 100644 --- a/src/render/SubmissionNotice.tsx +++ b/src/render/SubmissionNotice.tsx @@ -11,7 +11,7 @@ class SubmissionNotice { // Contains functions and variables from the content script needed by the skip notice contentContainer: () => unknown; - callback: () => unknown; + callback: () => Promise<boolean>; noticeRef: React.MutableRefObject<SubmissionNoticeComponent>; @@ -19,7 +19,7 @@ class SubmissionNotice { root: Root; - constructor(contentContainer: ContentContainer, callback: () => unknown) { + constructor(contentContainer: ContentContainer, callback: () => Promise<boolean>) { this.noticeRef = React.createRef(); this.contentContainer = contentContainer; @@ -56,6 +56,10 @@ class SubmissionNotice { submit(): void { this.noticeRef.current?.submit?.(); } + + scrollToBottom(): void { + this.noticeRef.current?.scrollToBottom?.(); + } } export default SubmissionNotice;
\ No newline at end of file diff --git a/src/utils/animationUtils.ts b/src/utils/animationUtils.ts deleted file mode 100644 index 08a59ce0..00000000 --- a/src/utils/animationUtils.ts +++ /dev/null @@ -1,78 +0,0 @@ - /** - * Starts a spinning animation and returns a function to be called when it should be stopped - * The callback will be called when the animation is finished - * It waits until a full rotation is complete - */ -function applyLoadingAnimation(element: HTMLElement, time: number, callback?: () => void): () => Promise<void> { - element.style.animation = `rotate ${time}s 0s infinite`; - - return async () => new Promise((resolve) => { - // Make the animation finite - element.style.animation = `rotate ${time}s`; - - // When the animation is over, hide the button - const animationEndListener = () => { - if (callback) callback(); - - element.style.animation = "none"; - - element.removeEventListener("animationend", animationEndListener); - - resolve(); - }; - - element.addEventListener("animationend", animationEndListener); - }); -} - -function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void; show: () => void } { - if (enabled) element.classList.add("autoHiding"); - element.classList.add("sbhidden"); - element.classList.add("animationDone"); - if (!rightSlide) element.classList.add("autoHideLeft"); - - let mouseEntered = false; - - return { - hide: () => { - mouseEntered = false; - if (element.classList.contains("autoHiding")) { - element.classList.add("sbhidden"); - } - }, - show: () => { - mouseEntered = true; - element.classList.remove("animationDone"); - - // Wait for next event loop - setTimeout(() => { - if (mouseEntered) element.classList.remove("sbhidden") - }, 10); - } - }; -} - -function setupAutoHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): void { - const { hide, show } = this.setupCustomHideAnimation(element, container, enabled, rightSlide); - - container.addEventListener("mouseleave", () => hide()); - container.addEventListener("mouseenter", () => show()); -} - -function enableAutoHideAnimation(element: Element): void { - element.classList.add("autoHiding"); - element.classList.add("sbhidden"); -} - -function disableAutoHideAnimation(element: Element): void { - element.classList.remove("autoHiding"); - element.classList.remove("sbhidden"); -} - -export const AnimationUtils = { - applyLoadingAnimation, - setupAutoHideAnimation, - setupCustomHideAnimation, - enableAutoHideAnimation, - disableAutoHideAnimation -};
\ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index dd44676a..afceb710 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -157,4 +157,6 @@ export function getGuidelineInfo(category: Category): TextBox[] { text: chrome.i18n.getMessage(`generic_guideline2`) }]; } -}
\ No newline at end of file +} + +export const defaultPreviewTime = 2;
\ No newline at end of file diff --git a/src/utils/mobileUtils.ts b/src/utils/mobileUtils.ts new file mode 100644 index 00000000..3cff18f7 --- /dev/null +++ b/src/utils/mobileUtils.ts @@ -0,0 +1,9 @@ +export function isMobileControlsOpen(): boolean { + const overlay = document.getElementById("player-control-overlay"); + + if (overlay) { + return !!overlay?.classList?.contains("fadein"); + } + + return false; +}
\ No newline at end of file diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index e1a3e66c..189681c6 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -160,7 +160,7 @@ module.exports = env => { if (path.match(/(\/|\\)_locales(\/|\\).+/)) { const parsed = JSON.parse(content.toString()); if (env.browser.toLowerCase() === "safari") { - parsed.fullName.message = parsed.fullName.message.match(/^.+(?= -)/)?.[0] || parsed.fullName.message; + parsed.fullName.message = parsed.fullName.message.match(/^.+(?= [-–])/)?.[0] || parsed.fullName.message; if (parsed.fullName.message.length > 50) { parsed.fullName.message = parsed.fullName.message.slice(0, 47) + "..."; } |