From 808c3718a8d4850dfc174bd3d049e2632694f894 Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:49:26 +0200 Subject: [PATCH 1/4] Fix segment hiding/unhiding --- src/popup/SegmentListComponent.tsx | 43 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx index 4bf438b6..3e7fee11 100644 --- a/src/popup/SegmentListComponent.tsx +++ b/src/popup/SegmentListComponent.tsx @@ -146,18 +146,27 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, sendMessage: (request: Message) => Promise; }) { const [voteMessage, setVoteMessage] = React.useState(null); - const [hidden, setHidden] = React.useState(segment.hidden || SponsorHideType.Visible); + const [hidden, setHidden] = React.useState(segment.hidden ?? SponsorHideType.Visible); // undefined ?? undefined lol const [isLooped, setIsLooped] = React.useState(loopedChapter === segment.UUID); - let extraInfo = ""; - if (segment.hidden === SponsorHideType.Downvoted) { - // This one is downvoted - extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")"; - } else if (segment.hidden === SponsorHideType.MinimumDuration) { - // This one is too short - extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")"; - } else if (segment.hidden === SponsorHideType.Hidden) { - extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")"; + let extraInfo: string; + switch (hidden) { + case SponsorHideType.Visible: + extraInfo = ""; + break; + case SponsorHideType.Downvoted: + extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")"; + break; + case SponsorHideType.MinimumDuration: + extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")"; + break; + case SponsorHideType.Hidden: + extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")"; + break; + default: + // hidden satisfies never; // need to upgrade TS + console.warn(`[SB] Unhandled variant of SponsorHideType in SegmentListItem: ${hidden}`); + extraInfo = ""; } return ( @@ -279,7 +288,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, { (segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute || segment.actionType === ActionType.Poi - && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) && + && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(hidden)) && From 5043a5fbe20bddfdbdd57237a82879b370f60625 Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:07:51 +0200 Subject: [PATCH 2/4] Sync internal hidden status with segment's value on change --- src/popup/SegmentListComponent.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx index 3e7fee11..c9c426c4 100644 --- a/src/popup/SegmentListComponent.tsx +++ b/src/popup/SegmentListComponent.tsx @@ -149,6 +149,11 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, const [hidden, setHidden] = React.useState(segment.hidden ?? SponsorHideType.Visible); // undefined ?? undefined lol const [isLooped, setIsLooped] = React.useState(loopedChapter === segment.UUID); + // Update internal state if the hidden property of the segment changes + React.useEffect(() => { + setHidden(segment.hidden ?? SponsorHideType.Visible); + }, [segment.hidden]) + let extraInfo: string; switch (hidden) { case SponsorHideType.Visible: From 915fd70274bed0ebeba653bbe26437ea71a884af Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:17:30 +0200 Subject: [PATCH 3/4] Cache chapter nesting results --- src/popup/SegmentListComponent.tsx | 62 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx index c9c426c4..8ea207e3 100644 --- a/src/popup/SegmentListComponent.tsx +++ b/src/popup/SegmentListComponent.tsx @@ -28,8 +28,8 @@ enum SegmentListTab { Chapter } -interface segmentWithNesting extends SponsorTime { - innerChapters?: (segmentWithNesting|SponsorTime)[]; +interface SegmentWithNesting extends SponsorTime { + innerChapters?: (SegmentWithNesting|SponsorTime)[]; } export const SegmentListComponent = (props: SegmentListComponentProps) => { @@ -58,37 +58,41 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => { } }; - const segmentsWithNesting: segmentWithNesting[] = []; - let nbTrailingNonChapters = 0; - function nestChapters(segments: segmentWithNesting[], seg: SponsorTime, topLevel?: boolean) { - if (seg.actionType === ActionType.Chapter && segments.length) { - // trailing non-chapters can only exist at top level - const lastElement = segments[segments.length - (topLevel ? nbTrailingNonChapters + 1 : 1)] + const segmentsWithNesting = React.useMemo(() => { + const result: SegmentWithNesting[] = []; + let nbTrailingNonChapters = 0; + function nestChapters(segments: SegmentWithNesting[], seg: SponsorTime, topLevel?: boolean) { + if (seg.actionType === ActionType.Chapter && segments.length) { + // trailing non-chapters can only exist at top level + const lastElement = segments[segments.length - (topLevel ? nbTrailingNonChapters + 1 : 1)] - if (lastElement.actionType === ActionType.Chapter - && lastElement.segment[0] <= seg.segment[0] - && lastElement.segment[1] >= seg.segment[1]) { - if (lastElement.innerChapters){ - nestChapters(lastElement.innerChapters, seg); + if (lastElement.actionType === ActionType.Chapter + && lastElement.segment[0] <= seg.segment[0] + && lastElement.segment[1] >= seg.segment[1]) { + if (lastElement.innerChapters){ + nestChapters(lastElement.innerChapters, seg); + } else { + lastElement.innerChapters = [seg]; + } } else { - lastElement.innerChapters = [seg]; - } - } else { - if (topLevel) { - nbTrailingNonChapters = 0; - } + if (topLevel) { + nbTrailingNonChapters = 0; + } - segments.push(seg); + segments.push(seg); + } + } else { + if (seg.actionType !== ActionType.Chapter) { + nbTrailingNonChapters++; } - } else { - if (seg.actionType !== ActionType.Chapter) { - nbTrailingNonChapters++; - } - segments.push(seg); + segments.push(seg); + } } - } - props.segments.forEach((seg) => nestChapters(segmentsWithNesting, {...seg}, true)); + props.segments.forEach((seg) => nestChapters(result, {...seg}, true)); + return result; + }, [props.segments]) + return (
@@ -136,7 +140,7 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => { }; function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: { - segment: segmentWithNesting; + segment: SegmentWithNesting; videoID: VideoID; currentTime: number; isVip: boolean; @@ -351,7 +355,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, } function InnerChapterList({ chapters, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: { - chapters: (segmentWithNesting)[]; + chapters: (SegmentWithNesting)[]; videoID: VideoID; currentTime: number; isVip: boolean; From 14d7d79ce7d6f594bfe6fd88964dfc61b9e429de Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Sat, 20 Sep 2025 23:11:50 +0200 Subject: [PATCH 4/4] Rewrite chapter nesting code Friendship ended with recursion, chapter stack is my new friend side effect: fixes popup crash for videos where the first segment is not a chapter --- src/popup/SegmentListComponent.tsx | 58 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx index 8ea207e3..1d6aa396 100644 --- a/src/popup/SegmentListComponent.tsx +++ b/src/popup/SegmentListComponent.tsx @@ -60,36 +60,38 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => { const segmentsWithNesting = React.useMemo(() => { const result: SegmentWithNesting[] = []; - let nbTrailingNonChapters = 0; - function nestChapters(segments: SegmentWithNesting[], seg: SponsorTime, topLevel?: boolean) { - if (seg.actionType === ActionType.Chapter && segments.length) { - // trailing non-chapters can only exist at top level - const lastElement = segments[segments.length - (topLevel ? nbTrailingNonChapters + 1 : 1)] - - if (lastElement.actionType === ActionType.Chapter - && lastElement.segment[0] <= seg.segment[0] - && lastElement.segment[1] >= seg.segment[1]) { - if (lastElement.innerChapters){ - nestChapters(lastElement.innerChapters, seg); - } else { - lastElement.innerChapters = [seg]; - } - } else { - if (topLevel) { - nbTrailingNonChapters = 0; - } - - segments.push(seg); - } - } else { - if (seg.actionType !== ActionType.Chapter) { - nbTrailingNonChapters++; - } - - segments.push(seg); + const chapterStack: SegmentWithNesting[] = []; + for (let seg of props.segments) { + seg = {...seg}; + // non-chapter, do not nest + if (seg.actionType !== ActionType.Chapter) { + result.push(seg); + continue; } + // traverse the stack + while (chapterStack.length !== 0) { + // where's Array.prototype.at() :sob: + const lastChapter = chapterStack[chapterStack.length - 1]; + // we know lastChapter.startTime <= seg.startTime, as content.ts sorts these + // so only compare endTime - if new ends before last, new is nested inside last + if (lastChapter.segment[1] >= seg.segment[1]) { + lastChapter.innerChapters ??= []; + lastChapter.innerChapters.push(seg); + chapterStack.push(seg); + break; + } + // last did not match, pop it off the stack + chapterStack.pop(); + } + // chapter stack not empty = we found a place for the chapter + if (chapterStack.length !== 0) { + continue; + } + // push the chapter to the top-level list and to the stack + result.push(seg); + chapterStack.push(seg); + } - props.segments.forEach((seg) => nestChapters(result, {...seg}, true)); return result; }, [props.segments])