From 3b89ef0487ad001a39b4b59513fcfec9c455103a Mon Sep 17 00:00:00 2001 From: gosha305 Date: Sun, 13 Apr 2025 21:35:22 +0200 Subject: [PATCH] Added the possibility to loop a chapter --- public/icons/loop.svg | 4 +++ public/icons/looped.svg | 4 +++ src/content.ts | 61 +++++++++++++++++++++++++++++++++-------- src/messageTypes.ts | 17 ++++++++++-- src/popup.ts | 40 ++++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 public/icons/loop.svg create mode 100644 public/icons/looped.svg diff --git a/public/icons/loop.svg b/public/icons/loop.svg new file mode 100644 index 00000000..c2a05cd1 --- /dev/null +++ b/public/icons/loop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/looped.svg b/public/icons/looped.svg new file mode 100644 index 00000000..62ddd5be --- /dev/null +++ b/public/icons/looped.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/content.ts b/src/content.ts index 2b9588b1..95e90d4d 100644 --- a/src/content.ts +++ b/src/content.ts @@ -76,6 +76,7 @@ let sponsorTimes: SponsorTime[] = []; let existingChaptersImported = false; let importingChaptersWaitingForFocus = false; let importingChaptersWaiting = false; +let loopedChapter :SponsorTime = null; // List of open skip notices const skipNotices: SkipNotice[] = []; let upcomingNotice: UpcomingNotice | null = null; @@ -301,6 +302,20 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo case "copyToClipboard": navigator.clipboard.writeText(request.text); break; + case "loopChapter": + if (!request.UUID){ + loopedChapter = null; + break; + } + loopedChapter = {...utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID)}; + loopedChapter.actionType = ActionType.Skip; + loopedChapter.segment = [loopedChapter.segment[1], loopedChapter.segment[0]]; + break; + case "getLoopedChapter": + sendResponse({ + UUID: loopedChapter?.UUID, + }); + break; case "importSegments": { const importedSegments = importTimes(request.data, getVideoDuration()); let addedSegments = false; @@ -397,6 +412,7 @@ function resetValues() { sponsorTimes = []; existingChaptersImported = false; sponsorSkipped = []; + loopedChapter = null; lastResponseStatus = 0; shownSegmentFailedToFetchWarning = false; @@ -694,7 +710,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current for (const segment of skipInfo.array) { if (shouldAutoSkip(segment) && segment.segment[0] >= skipTime[0] && segment.segment[1] <= skipTime[1] - && segment.segment[0] === segment.scheduledTime) { // Don't include artifical scheduled segments (end times for mutes) + && segment.segment[0] === segment.scheduledTime) { // Don't include artificial scheduled segments (end times for mutes) skippingSegments.push(segment); } } @@ -711,7 +727,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current forceVideoTime ||= Math.max(getCurrentTime(), getVirtualTime()); if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) { - if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) { + if (forceVideoTime >= skipTime[0] - skipBuffer && (forceVideoTime < skipTime[1] || skipTime[1] < skipTime[0])) { skipToTime({ v: getVideo(), skipTime, @@ -719,7 +735,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current openNotice: skipInfo.openNotice }); - // These are segments that start at the exact same time but need seperate notices + // These are segments that start at the exact same time but need separate notices for (const extra of skipInfo.extraIndexes) { const extraSkip = skipInfo.array[extra]; if (shouldSkip(extraSkip)) { @@ -752,7 +768,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current } // Don't pretend to be earlier than we are, could result in loops - if (forcedSkipTime !== null && forceVideoTime > forcedSkipTime) { + if (forcedSkipTime !== null && forceVideoTime > forcedSkipTime && skipTime[1] > skipTime[0]) { forcedSkipTime = forceVideoTime; } @@ -867,7 +883,8 @@ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boole const recordedVideoID = videoID || getVideoID(); if (currentVideoID !== recordedVideoID || (sponsorTime && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment[0] === sponsorTime.segment[0] && time.segment[1] === sponsorTime.segment[1])) - && !sponsorTimesSubmitting.some((time) => time.segment[0] === sponsorTime.segment[0] && time.segment[1] === sponsorTime.segment[1]))) { + && !sponsorTimesSubmitting.some((time) => time.segment[0] === sponsorTime.segment[0] && time.segment[1] === sponsorTime.segment[1]) + && (!isLoopedChapter(sponsorTime)))) { // Something has really gone wrong console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be."); console.error("[SponsorBlock] VideoID recorded: " + recordedVideoID + ". Actual VideoID: " + currentVideoID); @@ -1537,7 +1554,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool array: submittedArray, index: minSponsorTimeIndex, endIndex: endTimeIndex, - extraIndexes, // Segments at same time that need seperate notices + extraIndexes, // Segments at same time that need separate notices openNotice: true }; } else { @@ -1572,8 +1589,16 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH return index; } - // Default to the normal endTime - let latestEndTimeIndex = index; + let latestEndTimeIndex; + // Default to looped chapter if its end would have been skipped + if (loopedChapter + && (loopedChapter.segment[0] > sponsorTimes[index].segment[0] + && loopedChapter.segment[0] <= sponsorTimes[index]?.segment[1])){ + latestEndTimeIndex = sponsorTimes.length - 1; + } else { + // or the normal end time otherwise + latestEndTimeIndex = index; + } for (let i = 0; i < sponsorTimes?.length; i++) { const currentSegment = sponsorTimes[i].segment; @@ -1616,7 +1641,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: const shouldIncludeTime = (segment: ScheduledTime ) => (minimum === undefined || ((includeNonIntersectingSegments && segment.scheduledTime >= minimum) || (includeIntersectingSegments && segment.scheduledTime < minimum - && segment.segment[1] > minimum && shouldSkip(segment)))) // Only include intersecting skippable segments + && ((segment.segment[1] > minimum && shouldSkip(segment)) // Only include intersecting skippable segments + || isLoopedChapter(segment))))) && (!hideHiddenSponsors || segment.hidden === SponsorHideType.Visible) && segment.segment.length === 2 && segment.actionType !== ActionType.Poi @@ -1638,6 +1664,12 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: } }); + if (loopedChapter){ + possibleTimes.push({ + ...loopedChapter, + scheduledTime: loopedChapter.segment[0]}) + } + for (let i = 0; i < possibleTimes.length; i++) { if (shouldIncludeTime(possibleTimes[i])) { scheduledTimes.push(possibleTimes[i].scheduledTime); @@ -1895,7 +1927,8 @@ function shouldAutoSkip(segment: SponsorTime): boolean { && (utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic") && segment.actionType === ActionType.Skip) - || sponsorTimesSubmitting.some((s) => s.segment === segment.segment)); + || sponsorTimesSubmitting.some((s) => s.segment === segment.segment)) + || isLoopedChapter(segment); } function shouldSkip(segment: SponsorTime): boolean { @@ -1903,7 +1936,13 @@ function shouldSkip(segment: SponsorTime): boolean { && segment.source !== SponsorSourceType.YouTube && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic") - && segment.actionType === ActionType.Skip); + && segment.actionType === ActionType.Skip) + || isLoopedChapter(segment); +} + +function isLoopedChapter(segment: SponsorTime) :boolean{ + return !!segment && !!loopedChapter && segment.actionType === ActionType.Skip && segment.segment[1] != undefined + && segment.segment[0] === loopedChapter.segment[0] && segment.segment[1] === loopedChapter.segment[1]; } /** Creates any missing buttons on the YouTube player if possible. */ diff --git a/src/messageTypes.ts b/src/messageTypes.ts index 5241348d..e9920d7e 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -18,7 +18,8 @@ interface DefaultMessage { | "submitTimes" | "refreshSegments" | "closePopup" - | "getLogs"; + | "getLogs" + | "getLoopedChapter"; } interface BoolValueMessage { @@ -58,6 +59,11 @@ interface ImportSegmentsMessage { data: string; } +interface LoopChapterMessage { + message: "loopChapter"; + UUID: SegmentUUID; +} + interface KeyDownMessage { message: "keydown"; key: string; @@ -70,7 +76,7 @@ interface KeyDownMessage { metaKey: boolean; } -export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage); +export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage | LoopChapterMessage); export interface IsInfoFoundMessageResponse { found: boolean; @@ -97,6 +103,10 @@ export interface IsChannelWhitelistedResponse { value: boolean; } +export interface LoopedChapterResponse { + UUID: SegmentUUID; +} + export type MessageResponse = IsInfoFoundMessageResponse | GetVideoIdResponse @@ -107,7 +117,8 @@ export type MessageResponse = | VoteResponse | ImportSegmentsResponse | RefreshSegmentsResponse - | LogResponse; + | LogResponse + | LoopedChapterResponse; export interface VoteResponse { successType: number; diff --git a/src/popup.ts b/src/popup.ts index 41d9fa9f..1b74644c 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -13,6 +13,7 @@ import { IsChannelWhitelistedResponse, IsInfoFoundMessageResponse, LogResponse, + LoopedChapterResponse, Message, MessageResponse, PopupMessage, @@ -539,7 +540,7 @@ async function runThePopup(messageListener?: MessageListener): Promise { } //display the video times from the array at the top, in a different section - function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) { + async function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) { let currentSegmentTab = segmentTab; if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter && segment.source !== SponsorSourceType.YouTube)) { PageElements.issueReporterTabs.classList.add("hidden"); @@ -554,6 +555,9 @@ async function runThePopup(messageListener?: MessageListener): Promise { } } + const response = await sendTabMessageAsync({message : "getLoopedChapter"}) as LoopedChapterResponse + const loopedChapter = response.UUID; + // Sort list by start time const downloadedTimes = sponsorTimes .filter((segment) => { @@ -728,6 +732,37 @@ async function runThePopup(messageListener?: MessageListener): Promise { }) }); + const loopButton = document.createElement("input"); + const loopButtonIcon = document.createElement("img"); + loopButtonIcon.src = loopedChapter === UUID ? chrome.runtime.getURL("icons/looped.svg") : chrome.runtime.getURL("icons/loop.svg"); + loopButtonIcon.className = "loopButtonIcon"; + loopButton.type = "checkbox"; + loopButton.checked = loopedChapter === UUID; + loopButton.id = "loopChapterButtonContainer" + UUID; + loopButton.className = "loopButton hidden"; + + loopButton.addEventListener("click", () => { + const stopAnimation = AnimationUtils.applyLoadingAnimation(loopButtonIcon, 0.4); + stopAnimation(); + if (loopButton.checked) { + document.querySelectorAll(".loopButton").forEach((e :HTMLInputElement) => e.checked = false); + document.querySelectorAll(".loopButtonIcon").forEach((e :HTMLImageElement) => e.src = chrome.runtime.getURL("icons/loop.svg")); + sendTabMessage({message: "loopChapter", UUID : UUID}) + loopButton.checked = true; + } else { + sendTabMessage({message: "loopChapter", UUID : null}) + loopButton.checked = false; + } + loopButtonIcon.src = loopButton.checked ? chrome.runtime.getURL("icons/looped.svg") : chrome.runtime.getURL("icons/loop.svg"); + loopButtonLabel.title = loopButton.checked ? chrome.i18n.getMessage("unloopChapter") : chrome.i18n.getMessage("loopChapter"); + }); + const loopButtonLabel = document.createElement("label"); + loopButtonLabel.setAttribute("for", loopButton.id); + loopButtonLabel.className = "voteButton"; + loopButtonLabel.title = loopedChapter === UUID ? chrome.i18n.getMessage("unloopChapter") : chrome.i18n.getMessage("loopChapter"); + loopButtonLabel.appendChild(loopButtonIcon); + loopButtonLabel.appendChild(loopButton); + const skipButton = document.createElement("img"); skipButton.id = "sponsorTimesSkipButtonContainer" + UUID; skipButton.className = "voteButton"; @@ -743,6 +778,9 @@ async function runThePopup(messageListener?: MessageListener): Promise { voteButtonsContainer.appendChild(upvoteButton); voteButtonsContainer.appendChild(downvoteButton); voteButtonsContainer.appendChild(uuidButton); + if (downloadedTimes[i].actionType === ActionType.Chapter) { + voteButtonsContainer.appendChild(loopButtonLabel); + } if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute || downloadedTimes[i].actionType === ActionType.Poi && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {