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)) {