Added the possibility to loop a chapter

This commit is contained in:
gosha305
2025-04-13 21:35:22 +02:00
parent 7904913fdb
commit 3b89ef0487
5 changed files with 111 additions and 15 deletions

4
public/icons/loop.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-8 4-8 7h2s1-5 6-5c1.66 0 3.14.69 4.22 1.78L14 10h6V4l-2.35 2.35z"/>
<path d="M5.85 17.65C7.3 19.1 9.29 20 11.5 20c4.42 0 8-4 8-7h-2s-1 5-6 5c-1.66 0-3.14-.69-4.22-1.78L9.5 14h-6v6z"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

4
public/icons/looped.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#80fff6">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-8 4-8 7h2s1-5 6-5c1.66 0 3.14.69 4.22 1.78L14 10h6V4l-2.35 2.35z"/>
<path d="M5.85 17.65C7.3 19.1 9.29 20 11.5 20c4.42 0 8-4 8-7h-2s-1 5-6 5c-1.66 0-3.14-.69-4.22-1.78L9.5 14h-6v6z"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -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. */

View File

@@ -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;

View File

@@ -13,6 +13,7 @@ import {
IsChannelWhitelistedResponse,
IsInfoFoundMessageResponse,
LogResponse,
LoopedChapterResponse,
Message,
MessageResponse,
PopupMessage,
@@ -539,7 +540,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
//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<void> {
}
}
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<void> {
})
});
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<void> {
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)) {