Add voting for chapters

This commit is contained in:
Ajay
2022-08-28 23:38:40 -04:00
parent 42d76cf257
commit 901d6e6c92
12 changed files with 304 additions and 35 deletions

View File

@@ -865,6 +865,13 @@
"downvoteDescription": {
"message": "Incorrect/Wrong Timing"
},
"incorrectVote": {
"message": "Incorrect"
},
"harmfulVote": {
"message": "Harmful",
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
},
"incorrectCategory": {
"message": "Change Category"
},

View File

@@ -122,6 +122,16 @@ div:hover > .sponsorBlockChapterBar {
vertical-align: top;
}
/* Removes auto width from being a ytp-player-button */
.sbPlayerDownvote {
width: auto !important;
}
/* Adds back the padding */
.sbPlayerDownvote svg {
padding-right: 3.6px;
}
.autoHiding {
overflow: visible !important;
}
@@ -696,6 +706,11 @@ input::-webkit-inner-spin-button {
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
}
.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
left: 50%;
right: 50%;
}
.sponsorBlockLockedColor {
color: #ffc83d;
}

View File

@@ -0,0 +1,121 @@
import * as React from "react";
import Config from "../config";
import { Category, SegmentUUID, SponsorTime } from "../types";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { VoteResponse } from "../messageTypes";
import { AnimationUtils } from "../utils/animationUtils";
import { GenericUtils } from "../utils/genericUtils";
import { Tooltip } from "../render/Tooltip";
export interface ChapterVoteProps {
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
}
export interface ChapterVoteState {
segment?: SponsorTime;
show: boolean;
}
class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVoteState> {
tooltip?: Tooltip;
constructor(props: ChapterVoteProps) {
super(props);
this.state = {
segment: null,
show: false
};
}
render(): React.ReactElement {
return (
<>
{/* Upvote Button */}
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "hidden" : "")}
draggable="false"
title={chrome.i18n.getMessage("upvoteButtonInfo")}
onClick={(e) => this.vote(e, 1)}>
<ThumbsUpSvg className="playerButtonImage"
fill={Config.config.colorPalette.white}
width={"inherit"} height={"inherit"} />
</button>
{/* Downvote Button */}
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "hidden" : "")}
draggable="false"
title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={(e) => {
const chapterNode = document.querySelector(".ytp-chapter-container") as HTMLElement;
if (this.tooltip) {
this.tooltip.close();
this.tooltip = null;
} else {
const referenceNode = chapterNode?.parentElement?.parentElement;
if (referenceNode) {
const outerBounding = referenceNode.getBoundingClientRect();
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
this.tooltip = new Tooltip({
referenceNode: chapterNode?.parentElement?.parentElement,
prependElement: chapterNode?.parentElement,
showLogo: false,
showGotIt: false,
bottomOffset: `${outerBounding.height + 25}px`,
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
extraClass: "centeredSBTriangle",
buttons: [
{
name: chrome.i18n.getMessage("incorrectVote"),
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}, {
name: chrome.i18n.getMessage("harmfulVote"),
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}
]
});
}
}
}}>
<ThumbsDownSvg
className="playerButtonImage"
fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
width={"inherit"}
height={"inherit"} />
</button>
</>
);
}
private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise<void> {
event.stopPropagation();
if (this.state.segment) {
const stopAnimation = AnimationUtils.applyLoadingAnimation(element ?? event.currentTarget as HTMLElement, 0.3);
const response = await this.props.vote(type, this.state.segment.UUID);
await stopAnimation();
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
this.setState({
show: type === 1
});
} else if (response.statusCode !== 403) {
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
}
}
}
}
export default ChapterVoteComponent;

View File

@@ -19,6 +19,7 @@ import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { logDebug } from "./utils/logger";
import { importTimes } from "./utils/exporter";
import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings";
// Hack to get the CSS loaded on permission-based sites (Invidious)
@@ -475,7 +476,8 @@ function createPreviewBar(): void {
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
if (el) {
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
const chapterVote = new ChapterVote(voteAsync);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
updatePreviewBar();

View File

@@ -6,6 +6,7 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
'use strict';
import Config from "../config";
import { ChapterVote } from "../render/ChapterVote";
import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
import { partition } from "../utils/arrayUtils";
import { shortCategoryName } from "../utils/categoryUtils";
@@ -45,10 +46,12 @@ class PreviewBar {
// For chapter bar
hoveredSection: HTMLElement;
customChaptersBar: HTMLElement;
chaptersBarSegments: PreviewBarSegment[];
chapterVote: ChapterVote;
originalChapterBar: HTMLElement;
originalChapterBarBlocks: NodeListOf<HTMLElement>;
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, test=false) {
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
if (test) return;
this.container = document.createElement('ul');
this.container.id = 'previewbar';
@@ -56,6 +59,7 @@ class PreviewBar {
this.parent = parent;
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.chapterVote = chapterVote;
this.createElement(parent);
this.createChapterMutationObservers();
@@ -233,7 +237,7 @@ class PreviewBar {
this.createChaptersBar(this.segments.sort((a, b) => a.segment[0] - b.segment[0]));
const chapterChevron = document.querySelector(".ytp-chapter-title-chevron") as HTMLElement;
const chapterChevron = this.getChapterChevron();
if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
&& segment.source === SponsorSourceType.YouTube)) {
chapterChevron.style.removeProperty("display");
@@ -655,6 +659,18 @@ class PreviewBar {
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
const chapterVoteContainer = this.chapterVote.getContainer();
if (chosenSegment.source === SponsorSourceType.Server) {
if (!chapterButton.contains(chapterVoteContainer)) {
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
}
this.chapterVote.setVisibility(true);
this.chapterVote.setSegment(chosenSegment);
} else {
this.chapterVote.setVisibility(false);
}
} else {
// Hide chapters menu again
chaptersContainer.style.display = "none";
@@ -759,6 +775,10 @@ class PreviewBar {
return segment;
}
private getChapterChevron(): HTMLElement {
return document.querySelector(".ytp-chapter-title-chevron");
}
}
export default PreviewBar;

View File

@@ -0,0 +1,63 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
import { VoteResponse } from "../messageTypes";
import { Category, SegmentUUID, SponsorTime } from "../types";
export class ChapterVote {
container: HTMLElement;
ref: React.RefObject<ChapterVoteComponent>;
unsavedState: ChapterVoteState;
mutationObserver?: MutationObserver;
constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>) {
this.ref = React.createRef();
this.container = document.createElement('span');
this.container.id = "chapterVote";
this.container.style.height = "100%";
ReactDOM.render(
<ChapterVoteComponent ref={this.ref} vote={vote} />,
this.container
);
}
getContainer(): HTMLElement {
return this.container;
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.container.remove();
}
setVisibility(show: boolean): void {
const newState = {
show,
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
}
async setSegment(segment: SponsorTime): Promise<void> {
if (this.ref.current?.state?.segment !== segment) {
const newState = {
segment,
show: true
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
}
}
}

View File

@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils";
const utils = new Utils();
import { ContentContainer } from "../types";
import { ButtonListener, ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
export interface TextBox {
icon: string,
text: string

View File

@@ -1,29 +1,37 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { ButtonListener } from "../types";
export interface TooltipProps {
text: string,
link?: string,
referenceNode: HTMLElement,
prependElement?: HTMLElement, // Element to append before
bottomOffset?: string
text?: string;
link?: string;
referenceNode: HTMLElement;
prependElement?: HTMLElement; // Element to append before
bottomOffset?: string;
leftOffset?: string;
rightOffset?: string;
timeout?: number;
opacity?: number;
displayTriangle?: boolean;
extraClass?: string;
showLogo?: boolean;
showGotIt?: boolean;
buttons?: ButtonListener[];
}
export class Tooltip {
text: string;
text?: string;
container: HTMLDivElement;
timer: NodeJS.Timeout;
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
props.leftOffset ??= "inherit";
props.rightOffset ??= "inherit";
props.opacity ??= 0.7;
props.displayTriangle ??= true;
props.extraClass ??= "";
props.showLogo ??= true;
props.showGotIt ??= true;
this.text = props.text;
@@ -45,25 +53,29 @@ export class Tooltip {
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render(
<div style={{bottom: props.bottomOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div>
{props.showLogo ?
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
: null}
<span className="sponsorSkipObject">
{this.text + (props.link ? ". " : "")}
{props.link ?
<a style={{textDecoration: "underline"}}
target="_blank"
rel="noopener noreferrer"
href={props.link}>
{chrome.i18n.getMessage("LearnMore")}
</a>
: null}
</span>
{this.text ?
<span className="sponsorSkipObject">
{this.text + (props.link ? ". " : "")}
{props.link ?
<a style={{textDecoration: "underline"}}
target="_blank"
rel="noopener noreferrer"
href={props.link}>
{chrome.i18n.getMessage("LearnMore")}
</a>
: null}
</span>
: null}
{this.getButtons(props.buttons)}
</div>
{props.showGotIt ?
<button className="sponsorSkipObject sponsorSkipNoticeButton"
@@ -78,6 +90,27 @@ export class Tooltip {
)
}
getButtons(buttons?: ButtonListener[]): JSX.Element[] {
if (buttons) {
const result: JSX.Element[] = [];
for (const button of buttons) {
result.push(
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
key={button.name}
onClick={(e) => button.listener(e)}>
{button.name}
</button>
)
}
return result;
} else {
return null;
}
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.container.remove();

View File

@@ -1,13 +1,17 @@
import * as React from "react";
const thumbsDownSvg = ({
fill = "#ffffff"
fill = "#ffffff",
className = "",
width = "18",
height = "18"
}): JSX.Element => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
width={width}
height={height}
fill={fill}
className={className}
viewBox="0 0 24 24"
>
<path

View File

@@ -1,13 +1,17 @@
import * as React from "react";
const thumbsUpSvg = ({
fill = "#ffffff"
fill = "#ffffff",
className = "",
width = "18",
height = "18"
}): JSX.Element => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill={fill}
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
>
<path

View File

@@ -238,3 +238,8 @@ export type Keybind = {
alt?: boolean,
shift?: boolean
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}

View File

@@ -3,7 +3,7 @@ import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
describe("createChapterRenderGroups", () => {
let previewBar: PreviewBar;
beforeEach(() => {
previewBar = new PreviewBar(null, null, null, true);
previewBar = new PreviewBar(null, null, null, null, true);
})
it("Two unrelated times", () => {