mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-12 14:37:23 +03:00
Added better UI for nested chapters
This commit is contained in:
@@ -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>)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user