From 233da8c449016a887b3b3a348f96f736a79d151c Mon Sep 17 00:00:00 2001 From: gosha305 Date: Sun, 14 Sep 2025 14:24:55 +0200 Subject: [PATCH] Added better UI for nested chapters --- public/popup.css | 25 +- src/popup/SegmentListComponent.tsx | 384 ++++++++++++++++++----------- 2 files changed, 259 insertions(+), 150 deletions(-) diff --git a/public/popup.css b/public/popup.css index aa3fd108..c82f69a4 100644 --- a/public/popup.css +++ b/public/popup.css @@ -197,9 +197,7 @@ *
wrapper around each segment */ .votingButtons { - font-family: Arial, Helvetica, sans-serif; border-radius: 8px; - margin: 4px 16px; } .votingButtons[open] { padding-bottom: 5px; @@ -208,6 +206,29 @@ background-color: var(--sb-grey-bg-color); } +/* + * Nested chapters + */ +.innerChapterList { + border-radius: 8px; +} + +.innerChapterList > summary { + font-weight: bold; + padding: 4px; + cursor: pointer; +} + +.segmentWrapper:has(> .innerChapterList > summary:hover) { + background-color: var(--sb-grey-bg-color); +} + +.segmentWrapper{ + font-family: Arial, Helvetica, sans-serif; + border-radius: 8px; + margin: 4px 16px; +} + /* * Individual segments summaries (clickable ) */ diff --git a/src/popup/SegmentListComponent.tsx b/src/popup/SegmentListComponent.tsx index f415cf25..a2acf007 100644 --- a/src/popup/SegmentListComponent.tsx +++ b/src/popup/SegmentListComponent.tsx @@ -27,6 +27,10 @@ enum SegmentListTab { Chapter } +interface segmentWithNesting extends SponsorTime { + innerChapters?: (segmentWithNesting|SponsorTime)[]; +} + export const SegmentListComponent = (props: SegmentListComponentProps) => { const [tab, setTab] = React.useState(SegmentListTab.Segments); const [isVip, setIsVip] = React.useState(Config.config?.isVip ?? false); @@ -53,6 +57,36 @@ 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)] + + 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); + } + } + props.segments.forEach((seg) => {nestChapters(segmentsWithNesting, {...seg}, true)}) + return (
s.actionType === ActionType.Chapter) ? "" : "hidden"}> @@ -73,14 +107,14 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => { sendMessage: props.sendMessage })}> { - props.segments.map((segment) => ( + segmentsWithNesting.map((segment) => ( { ); }; -function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped, tabFilter, sendMessage }: { - segment: SponsorTime; +function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: { + segment: segmentWithNesting; videoID: VideoID; currentTime: number; isVip: boolean; - startingLooped: boolean; + loopedChapter: SegmentUUID; tabFilter: (segment: SponsorTime) => boolean; sendMessage: (request: Message) => Promise; }) { const [voteMessage, setVoteMessage] = React.useState(null); const [hidden, setHidden] = React.useState(segment.hidden || SponsorHideType.Visible); - const [isLooped, setIsLooped] = React.useState(startingLooped); + const [isLooped, setIsLooped] = React.useState(loopedChapter === segment.UUID); let extraInfo = ""; if (segment.hidden === SponsorHideType.Downvoted) { @@ -124,165 +158,219 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped, } return ( -
skipSegment({ - segment, - sendMessage - })} - onMouseEnter={() => { - selectSegment({ +
+ { +
skipSegment({ segment, sendMessage - }); - }} - className={"votingButtons " + (!tabFilter(segment) ? "hidden" : "")}> - = segment.segment[0] ? ( - currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed" - ) : "" - )}> -
- { - segment.actionType !== ActionType.Chapter && - - } - {(segment.description || shortCategoryName(segment.category)) + extraInfo} -
- -
- { - segment.actionType === ActionType.Full ? chrome.i18n.getMessage("full") : - (getFormattedTime(segment.segment[0], true) + - (segment.actionType !== ActionType.Poi - ? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segment.segment[1], true) - : "")) - } -
-
- -
- { - vote({ - type: 1, - UUID: segment.UUID, - setVoteMessage: setVoteMessage, + })} + onMouseEnter={() => { + selectSegment({ + segment, sendMessage }); - }}/> - { - vote({ - type: 0, - UUID: segment.UUID, - setVoteMessage: setVoteMessage, - sendMessage - }); - }}/> - { - const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.3); - - if (segment.UUID.length > 60) { - copyToClipboardPopup(segment.UUID, sendMessage); - } else { - const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", { - UUID: segment.UUID, - videoID: videoID - }); - - if (segmentIDData.ok && segmentIDData.responseText) { - copyToClipboardPopup(segmentIDData.responseText, sendMessage); - } + }} + className={"votingButtons"} + > + = segment.segment[0] ? ( + currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed" + ) : "" + )}> +
+ { + segment.actionType !== ActionType.Chapter && + } + {(segment.description || shortCategoryName(segment.category)) + extraInfo} +
- stopAnimation(); - }}/> - { - segment.actionType === ActionType.Chapter && +
+ { + segment.actionType === ActionType.Full ? chrome.i18n.getMessage("full") : + (getFormattedTime(segment.segment[0], true) + + (segment.actionType !== ActionType.Poi + ? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segment.segment[1], true) + : "")) + } +
+
+ +
{ - if (isLooped) { - loopChapter({ - segment: null, - element: e.currentTarget, - sendMessage - }); + title="Upvote" + src={chrome.runtime.getURL("icons/thumbs_up.svg")} + onClick={() => { + vote({ + type: 1, + UUID: segment.UUID, + setVoteMessage: setVoteMessage, + sendMessage + }); + }}/> + { + vote({ + type: 0, + UUID: segment.UUID, + setVoteMessage: setVoteMessage, + sendMessage + }); + }}/> + { + const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.3); + + if (segment.UUID.length > 60) { + copyToClipboardPopup(segment.UUID, sendMessage); } else { - loopChapter({ + const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", { + UUID: segment.UUID, + videoID: videoID + }); + + if (segmentIDData.ok && segmentIDData.responseText) { + copyToClipboardPopup(segmentIDData.responseText, sendMessage); + } + } + + stopAnimation(); + }}/> + { + segment.actionType === ActionType.Chapter && + { + if (isLooped) { + loopChapter({ + segment: null, + element: e.currentTarget, + sendMessage + }); + } else { + loopChapter({ + segment, + element: e.currentTarget, + sendMessage + }); + } + + setIsLooped(!isLooped); + }}/> + } + { + (segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute + || segment.actionType === ActionType.Poi + && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) && + { + const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.4); + stopAnimation(); + + if (segment.hidden === SponsorHideType.Hidden) { + segment.hidden = SponsorHideType.Visible; + setHidden(SponsorHideType.Visible); + } else { + segment.hidden = SponsorHideType.Hidden; + setHidden(SponsorHideType.Hidden); + } + + sendMessage({ + message: "hideSegment", + type: segment.hidden, + UUID: segment.UUID + }); + }}/> + } + { + segment.actionType !== ActionType.Full && + { + skipSegment({ segment, element: e.currentTarget, sendMessage }); - } - - setIsLooped(!isLooped); - }}/> - } - { - (segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute - || segment.actionType === ActionType.Poi - && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) && - { - const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.4); - stopAnimation(); - - if (segment.hidden === SponsorHideType.Hidden) { - segment.hidden = SponsorHideType.Visible; - setHidden(SponsorHideType.Visible); - } else { - segment.hidden = SponsorHideType.Hidden; - setHidden(SponsorHideType.Hidden); - } - - sendMessage({ - message: "hideSegment", - type: segment.hidden, - UUID: segment.UUID - }); - }}/> - } - { - segment.actionType !== ActionType.Full && - { - skipSegment({ - segment, - element: e.currentTarget, - sendMessage - }); - }}/> - } -
- -
-
- {voteMessage} + }}/> + }
-
-
+ +
+
+ {voteMessage} +
+
+
+ } + { + segment.innerChapters + && + } +
); } +function InnerChapterList({ chapters, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: { + chapters: (segmentWithNesting)[]; + videoID: VideoID; + currentTime: number; + isVip: boolean; + loopedChapter: SegmentUUID; + + tabFilter: (segment: SponsorTime) => boolean; + sendMessage: (request: Message) => Promise; +}) { + return
+ { + e.currentTarget.firstChild.textContent = (e.currentTarget.parentElement as HTMLDetailsElement).open ? chrome.i18n.getMessage("expandChapters").replace("{0}", String(chapters.length)) : chrome.i18n.getMessage("collapseChapters"); + }}> + {chrome.i18n.getMessage("collapseChapters")} + +
+ { + chapters.map((chapter) => { + return + }) + } +
+
+} + async function vote(props: { type: number; UUID: SegmentUUID;