mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-11 05:57:07 +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,165 +158,219 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, startingLooped,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details data-uuid={segment.UUID}
|
<div className={"segmentWrapper " + (!tabFilter(segment) ? "hidden" : "")}>
|
||||||
onDoubleClick={() => skipSegment({
|
{
|
||||||
segment,
|
<details data-uuid={segment.UUID}
|
||||||
sendMessage
|
onDoubleClick={() => skipSegment({
|
||||||
})}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
selectSegment({
|
|
||||||
segment,
|
segment,
|
||||||
sendMessage
|
sendMessage
|
||||||
});
|
})}
|
||||||
}}
|
onMouseEnter={() => {
|
||||||
className={"votingButtons " + (!tabFilter(segment) ? "hidden" : "")}>
|
selectSegment({
|
||||||
<summary className={"segmentSummary " + (
|
segment,
|
||||||
currentTime >= segment.segment[0] ? (
|
|
||||||
currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed"
|
|
||||||
) : ""
|
|
||||||
)}>
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
segment.actionType !== ActionType.Chapter &&
|
|
||||||
<span className="sponsorTimesCategoryColorCircle dot" style={{ backgroundColor: Config.config.barTypes[segment.category]?.color }}></span>
|
|
||||||
}
|
|
||||||
<span className="summaryLabel">{(segment.description || shortCategoryName(segment.category)) + extraInfo}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ margin: "5px" }}>
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
: ""))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<div className={"sbVoteButtonsContainer " + (voteMessage ? "hidden" : "")}>
|
|
||||||
<img
|
|
||||||
className="voteButton"
|
|
||||||
title="Upvote"
|
|
||||||
src={chrome.runtime.getURL("icons/thumbs_up.svg")}
|
|
||||||
onClick={() => {
|
|
||||||
vote({
|
|
||||||
type: 1,
|
|
||||||
UUID: segment.UUID,
|
|
||||||
setVoteMessage: setVoteMessage,
|
|
||||||
sendMessage
|
sendMessage
|
||||||
});
|
});
|
||||||
}}/>
|
}}
|
||||||
<img
|
className={"votingButtons"}
|
||||||
className="voteButton"
|
>
|
||||||
title="Downvote"
|
<summary className={"segmentSummary " + (
|
||||||
src={segment.locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg")}
|
currentTime >= segment.segment[0] ? (
|
||||||
onClick={() => {
|
currentTime < segment.segment[1] ? "segmentActive" : "segmentPassed"
|
||||||
vote({
|
) : ""
|
||||||
type: 0,
|
)}>
|
||||||
UUID: segment.UUID,
|
<div>
|
||||||
setVoteMessage: setVoteMessage,
|
{
|
||||||
sendMessage
|
segment.actionType !== ActionType.Chapter &&
|
||||||
});
|
<span className="sponsorTimesCategoryColorCircle dot" style={{ backgroundColor: Config.config.barTypes[segment.category]?.color }}></span>
|
||||||
}}/>
|
|
||||||
<img
|
|
||||||
className="voteButton"
|
|
||||||
title="Copy Segment ID"
|
|
||||||
src={chrome.runtime.getURL("icons/clipboard.svg")}
|
|
||||||
onClick={async (e) => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
<span className="summaryLabel">{(segment.description || shortCategoryName(segment.category)) + extraInfo}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
stopAnimation();
|
<div style={{ margin: "5px" }}>
|
||||||
}}/>
|
{
|
||||||
{
|
segment.actionType === ActionType.Full ? chrome.i18n.getMessage("full") :
|
||||||
segment.actionType === ActionType.Chapter &&
|
(getFormattedTime(segment.segment[0], true) +
|
||||||
|
(segment.actionType !== ActionType.Poi
|
||||||
|
? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segment.segment[1], true)
|
||||||
|
: ""))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div className={"sbVoteButtonsContainer " + (voteMessage ? "hidden" : "")}>
|
||||||
<img
|
<img
|
||||||
className="voteButton"
|
className="voteButton"
|
||||||
title={isLooped ? chrome.i18n.getMessage("unloopChapter") : chrome.i18n.getMessage("loopChapter")}
|
title="Upvote"
|
||||||
src={isLooped ? chrome.runtime.getURL("icons/looped.svg") : chrome.runtime.getURL("icons/loop.svg")}
|
src={chrome.runtime.getURL("icons/thumbs_up.svg")}
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
if (isLooped) {
|
vote({
|
||||||
loopChapter({
|
type: 1,
|
||||||
segment: null,
|
UUID: segment.UUID,
|
||||||
element: e.currentTarget,
|
setVoteMessage: setVoteMessage,
|
||||||
sendMessage
|
sendMessage
|
||||||
});
|
});
|
||||||
|
}}/>
|
||||||
|
<img
|
||||||
|
className="voteButton"
|
||||||
|
title="Downvote"
|
||||||
|
src={segment.locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg")}
|
||||||
|
onClick={() => {
|
||||||
|
vote({
|
||||||
|
type: 0,
|
||||||
|
UUID: segment.UUID,
|
||||||
|
setVoteMessage: setVoteMessage,
|
||||||
|
sendMessage
|
||||||
|
});
|
||||||
|
}}/>
|
||||||
|
<img
|
||||||
|
className="voteButton"
|
||||||
|
title="Copy Segment ID"
|
||||||
|
src={chrome.runtime.getURL("icons/clipboard.svg")}
|
||||||
|
onClick={async (e) => {
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.3);
|
||||||
|
|
||||||
|
if (segment.UUID.length > 60) {
|
||||||
|
copyToClipboardPopup(segment.UUID, sendMessage);
|
||||||
} else {
|
} 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 &&
|
||||||
|
<img
|
||||||
|
className="voteButton"
|
||||||
|
title={isLooped ? chrome.i18n.getMessage("unloopChapter") : chrome.i18n.getMessage("loopChapter")}
|
||||||
|
src={isLooped ? chrome.runtime.getURL("icons/looped.svg") : chrome.runtime.getURL("icons/loop.svg")}
|
||||||
|
onClick={(e) => {
|
||||||
|
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)) &&
|
||||||
|
<img
|
||||||
|
className="voteButton"
|
||||||
|
title={chrome.i18n.getMessage("hideSegment")}
|
||||||
|
src={hidden === SponsorHideType.Hidden ? chrome.runtime.getURL("icons/not_visible.svg") : chrome.runtime.getURL("icons/visible.svg")}
|
||||||
|
onClick={(e) => {
|
||||||
|
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 &&
|
||||||
|
<img
|
||||||
|
className="voteButton"
|
||||||
|
title={segment.actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter") : chrome.i18n.getMessage("skipSegment")}
|
||||||
|
src={chrome.runtime.getURL("icons/skip.svg")}
|
||||||
|
onClick={(e) => {
|
||||||
|
skipSegment({
|
||||||
segment,
|
segment,
|
||||||
element: e.currentTarget,
|
element: e.currentTarget,
|
||||||
sendMessage
|
sendMessage
|
||||||
});
|
});
|
||||||
}
|
}}/>
|
||||||
|
}
|
||||||
setIsLooped(!isLooped);
|
|
||||||
}}/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
(segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute
|
|
||||||
|| segment.actionType === ActionType.Poi
|
|
||||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) &&
|
|
||||||
<img
|
|
||||||
className="voteButton"
|
|
||||||
title={chrome.i18n.getMessage("hideSegment")}
|
|
||||||
src={hidden === SponsorHideType.Hidden ? chrome.runtime.getURL("icons/not_visible.svg") : chrome.runtime.getURL("icons/visible.svg")}
|
|
||||||
onClick={(e) => {
|
|
||||||
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 &&
|
|
||||||
<img
|
|
||||||
className="voteButton"
|
|
||||||
title={segment.actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter") : chrome.i18n.getMessage("skipSegment")}
|
|
||||||
src={chrome.runtime.getURL("icons/skip.svg")}
|
|
||||||
onClick={(e) => {
|
|
||||||
skipSegment({
|
|
||||||
segment,
|
|
||||||
element: e.currentTarget,
|
|
||||||
sendMessage
|
|
||||||
});
|
|
||||||
}}/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={"sponsorTimesVoteStatusContainer " + (voteMessage ? "" : "hidden")}>
|
|
||||||
<div className="sponsorTimesThanksForVotingText">
|
|
||||||
{voteMessage}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</details>
|
<div className={"sponsorTimesVoteStatusContainer " + (voteMessage ? "" : "hidden")}>
|
||||||
|
<div className="sponsorTimesThanksForVotingText">
|
||||||
|
{voteMessage}
|
||||||
|
</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: {
|
async function vote(props: {
|
||||||
type: number;
|
type: number;
|
||||||
UUID: SegmentUUID;
|
UUID: SegmentUUID;
|
||||||
|
|||||||
Reference in New Issue
Block a user