diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 6cdeb506..ab6c4e4c 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -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" }, diff --git a/public/content.css b/public/content.css index 16fdcbec..da79f74d 100644 --- a/public/content.css +++ b/public/content.css @@ -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; } diff --git a/src/components/ChapterVoteComponent.tsx b/src/components/ChapterVoteComponent.tsx new file mode 100644 index 00000000..b1f590a7 --- /dev/null +++ b/src/components/ChapterVoteComponent.tsx @@ -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; +} + +export interface ChapterVoteState { + segment?: SponsorTime; + show: boolean; +} + +class ChapterVoteComponent extends React.Component { + tooltip?: Tooltip; + + constructor(props: ChapterVoteProps) { + super(props); + + this.state = { + segment: null, + show: false + }; + } + + render(): React.ReactElement { + return ( + <> + {/* Upvote Button */} + + + {/* Downvote Button */} + + + ); + } + + private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise { + 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; diff --git a/src/content.ts b/src/content.ts index fc611a89..36cf0784 100644 --- a/src/content.ts +++ b/src/content.ts @@ -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(); diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index 57035e8c..9779913f 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -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; - 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; diff --git a/src/render/ChapterVote.tsx b/src/render/ChapterVote.tsx new file mode 100644 index 00000000..b78a5ce8 --- /dev/null +++ b/src/render/ChapterVote.tsx @@ -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; + + unsavedState: ChapterVoteState; + + mutationObserver?: MutationObserver; + + constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise) { + this.ref = React.createRef(); + + this.container = document.createElement('span'); + this.container.id = "chapterVote"; + this.container.style.height = "100%"; + + ReactDOM.render( + , + 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 { + 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; + } + } + } +} \ No newline at end of file diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx index 639edb86..1b691488 100644 --- a/src/render/GenericNotice.tsx +++ b/src/render/GenericNotice.tsx @@ -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) => void -} - export interface TextBox { icon: string, text: string diff --git a/src/render/Tooltip.tsx b/src/render/Tooltip.tsx index 66e581d5..d59728fe 100644 --- a/src/render/Tooltip.tsx +++ b/src/render/Tooltip.tsx @@ -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( -
+
{props.showLogo ? : null} - - {this.text + (props.link ? ". " : "")} - {props.link ? - - {chrome.i18n.getMessage("LearnMore")} - - : null} - + {this.text ? + + {this.text + (props.link ? ". " : "")} + {props.link ? + + {chrome.i18n.getMessage("LearnMore")} + + : null} + + : null} + + {this.getButtons(props.buttons)}
{props.showGotIt ? + ) + } + + return result; + } else { + return null; + } + } + close(): void { ReactDOM.unmountComponentAtNode(this.container); this.container.remove(); diff --git a/src/svg-icons/thumbs_down_svg.tsx b/src/svg-icons/thumbs_down_svg.tsx index ce61db5a..a1161c10 100644 --- a/src/svg-icons/thumbs_down_svg.tsx +++ b/src/svg-icons/thumbs_down_svg.tsx @@ -1,13 +1,17 @@ import * as React from "react"; const thumbsDownSvg = ({ - fill = "#ffffff" + fill = "#ffffff", + className = "", + width = "18", + height = "18" }): JSX.Element => ( ( ) => void } \ No newline at end of file diff --git a/test/previewBar.test.ts b/test/previewBar.test.ts index b4663355..b599b3a3 100644 --- a/test/previewBar.test.ts +++ b/test/previewBar.test.ts @@ -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", () => {