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 * <details> wrapper around each segment
*/ */
.votingButtons { .votingButtons {
font-family: Arial, Helvetica, sans-serif;
border-radius: 8px; border-radius: 8px;
margin: 4px 16px;
} }
.votingButtons[open] { .votingButtons[open] {
padding-bottom: 5px; padding-bottom: 5px;
@@ -208,6 +206,29 @@
background-color: var(--sb-grey-bg-color); 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>) * Individual segments summaries (clickable <summary>)
*/ */

View File

@@ -27,6 +27,10 @@ enum SegmentListTab {
Chapter Chapter
} }
interface segmentWithNesting extends SponsorTime {
innerChapters?: (segmentWithNesting|SponsorTime)[];
}
export const SegmentListComponent = (props: SegmentListComponentProps) => { export const SegmentListComponent = (props: SegmentListComponentProps) => {
const [tab, setTab] = React.useState(SegmentListTab.Segments); const [tab, setTab] = React.useState(SegmentListTab.Segments);
const [isVip, setIsVip] = React.useState(Config.config?.isVip ?? false); 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 ( return (
<div id="issueReporterContainer"> <div id="issueReporterContainer">
<div id="issueReporterTabs" className={props.segments && props.segments.find(s => s.actionType === ActionType.Chapter) ? "" : "hidden"}> <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 sendMessage: props.sendMessage
})}> })}>
{ {
props.segments.map((segment) => ( segmentsWithNesting.map((segment) => (
<SegmentListItem <SegmentListItem
key={segment.UUID} key={segment.UUID}
videoID={props.videoID} videoID={props.videoID}
segment={segment} segment={segment}
currentTime={props.currentTime} currentTime={props.currentTime}
isVip={isVip} 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} tabFilter={tabFilter}
sendMessage={props.sendMessage} sendMessage={props.sendMessage}
@@ -98,19 +132,19 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
); );
}; };
function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped, tabFilter, sendMessage }: { function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: {
segment: SponsorTime; segment: segmentWithNesting;
videoID: VideoID; videoID: VideoID;
currentTime: number; currentTime: number;
isVip: boolean; isVip: boolean;
startingLooped: boolean; loopedChapter: SegmentUUID;
tabFilter: (segment: SponsorTime) => boolean; tabFilter: (segment: SponsorTime) => boolean;
sendMessage: (request: Message) => Promise<MessageResponse>; sendMessage: (request: Message) => Promise<MessageResponse>;
}) { }) {
const [voteMessage, setVoteMessage] = React.useState<string | null>(null); const [voteMessage, setVoteMessage] = React.useState<string | null>(null);
const [hidden, setHidden] = React.useState(segment.hidden || SponsorHideType.Visible); 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 = ""; let extraInfo = "";
if (segment.hidden === SponsorHideType.Downvoted) { if (segment.hidden === SponsorHideType.Downvoted) {
@@ -124,6 +158,8 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
} }
return ( return (
<div className={"segmentWrapper " + (!tabFilter(segment) ? "hidden" : "")}>
{
<details data-uuid={segment.UUID} <details data-uuid={segment.UUID}
onDoubleClick={() => skipSegment({ onDoubleClick={() => skipSegment({
segment, segment,
@@ -135,7 +171,8 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
sendMessage sendMessage
}); });
}} }}
className={"votingButtons " + (!tabFilter(segment) ? "hidden" : "")}> className={"votingButtons"}
>
<summary className={"segmentSummary " + ( <summary className={"segmentSummary " + (
currentTime >= segment.segment[0] ? ( currentTime >= segment.segment[0] ? (
currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed" currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed"
@@ -280,9 +317,60 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
</div> </div>
</div> </div>
</details> </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: { async function vote(props: {
type: number; type: number;
UUID: SegmentUUID; UUID: SegmentUUID;