Added better UI for nested chapters

This commit is contained in:
gosha305
2025-09-14 14:24:55 +02:00
parent 06ead877b4
commit 233da8c449
2 changed files with 259 additions and 150 deletions

View File

@@ -197,9 +197,7 @@
* <details> 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 <summary>)
*/

View File

@@ -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 (
<div id="issueReporterContainer">
<div id="issueReporterTabs" className={props.segments && props.segments.find(s => s.actionType === ActionType.Chapter) ? "" : "hidden"}>
@@ -73,14 +107,14 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
sendMessage: props.sendMessage
})}>
{
props.segments.map((segment) => (
segmentsWithNesting.map((segment) => (
<SegmentListItem
key={segment.UUID}
videoID={props.videoID}
segment={segment}
currentTime={props.currentTime}
isVip={isVip}
startingLooped={props.loopedChapter === segment.UUID}
loopedChapter={props.loopedChapter} // UUID instead of boolean so it can be passed down to nested chapters
tabFilter={tabFilter}
sendMessage={props.sendMessage}
@@ -98,19 +132,19 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
);
};
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<MessageResponse>;
}) {
const [voteMessage, setVoteMessage] = React.useState<string | null>(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,6 +158,8 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
}
return (
<div className={"segmentWrapper " + (!tabFilter(segment) ? "hidden" : "")}>
{
<details data-uuid={segment.UUID}
onDoubleClick={() => skipSegment({
segment,
@@ -135,7 +171,8 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
sendMessage
});
}}
className={"votingButtons " + (!tabFilter(segment) ? "hidden" : "")}>
className={"votingButtons"}
>
<summary className={"segmentSummary " + (
currentTime >= segment.segment[0] ? (
currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed"
@@ -280,9 +317,60 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
</div>
</div>
</details>
}
{
segment.innerChapters
&& <InnerChapterList
chapters={segment.innerChapters}
videoID={videoID}
currentTime={currentTime}
isVip={isVip}
loopedChapter={loopedChapter}
tabFilter={tabFilter}
sendMessage={sendMessage}
/>
}
</div>
);
}
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<MessageResponse>;
}) {
return <details className="innerChapterList" open>
<summary
onClick={(e) => {
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")}
</summary>
<div className="innerChaptersContainer">
{
chapters.map((chapter) => {
return <SegmentListItem
key={chapter.UUID}
videoID={videoID}
segment={chapter}
currentTime={currentTime}
isVip={isVip}
loopedChapter={loopedChapter}
tabFilter={tabFilter}
sendMessage={sendMessage}
/>
})
}
</div>
</details>
}
async function vote(props: {
type: number;
UUID: SegmentUUID;