mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-06 19:47:04 +03:00
Add chapter name autocomplete
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
|
:root {
|
||||||
|
--skip-notice-right: 10px;
|
||||||
|
--skip-notice-padding: 5px;
|
||||||
|
--skip-notice-margin: 5px;
|
||||||
|
--skip-notice-border-horizontal: 5px;
|
||||||
|
--skip-notice-border-vertical: 10px;
|
||||||
|
--sb-dark-red-outline: rgb(130,0,0,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -112,8 +121,8 @@ div:hover > .sponsorBlockChapterBar {
|
|||||||
.sponsorSkipObject {
|
.sponsorSkipObject {
|
||||||
font-family: Roboto, Arial, Helvetica, sans-serif;
|
font-family: Roboto, Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
margin-left: 2px;
|
margin-left: var(--skip-notice-margin);
|
||||||
margin-right: 2px;
|
margin-right: var(--skip-notice-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipLogo {
|
.sponsorSkipLogo {
|
||||||
@@ -144,23 +153,23 @@ div:hover > .sponsorBlockChapterBar {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
right: 10px;
|
right: var(--skip-notice-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipNoticeParent {
|
.sponsorSkipNoticeParent {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
right: 10px;
|
right: var(--skip-notice-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipNoticeParent, .sponsorSkipNotice {
|
.sponsorSkipNoticeParent, .sponsorSkipNotice {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
|
|
||||||
border-spacing: 5px 10px;
|
border-spacing: var(--skip-notice-border-horizontal) var(--skip-notice-border-vertical);
|
||||||
padding-left: 5px;
|
padding-left: var(--skip-notice-padding);
|
||||||
padding-right: 5px;
|
padding-right: var(--skip-notice-padding);
|
||||||
|
|
||||||
border-collapse: unset;
|
border-collapse: unset;
|
||||||
}
|
}
|
||||||
@@ -505,12 +514,51 @@ input::-webkit-inner-spin-button {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
background-color: rgba(28, 28, 28, 0.9);
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
border-color: rgb(130,0,0,0.9);
|
border-color: var(--sb-dark-red-outline);
|
||||||
color: white;
|
color: white;
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Start SelectorComponent */
|
||||||
|
|
||||||
|
.sbSelector {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: calc(100% - var(--skip-notice-right) - var(--skip-notice-padding) * 2 - var(--skip-notice-margin) * 2 - var(--skip-notice-border-horizontal) * 2);
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorBackground {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
margin: auto;
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorOption {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgb(43, 43, 43);
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorOption:hover {
|
||||||
|
background-color: #3a0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* End SelectorComponent */
|
||||||
|
|
||||||
.helpButton {
|
.helpButton {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
55
src/components/SelectorComponent.tsx
Normal file
55
src/components/SelectorComponent.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface SelectorOption {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorProps {
|
||||||
|
id: string;
|
||||||
|
options: SelectorOption[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
|
||||||
|
|
||||||
|
constructor(props: SelectorProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div id={this.props.id}
|
||||||
|
className="sbSelector">
|
||||||
|
<div className="sbSelectorBackground">
|
||||||
|
{this.getOptions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): React.ReactElement[] {
|
||||||
|
const result: React.ReactElement[] = [];
|
||||||
|
for (const option of this.props.options) {
|
||||||
|
result.push(
|
||||||
|
<div className="sbSelectorOption"
|
||||||
|
onClick={() => this.props.onChange(option.label)}
|
||||||
|
key={option.label}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectorComponent;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as CompileConfig from "../../config.json";
|
import * as CompileConfig from "../../config.json";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import { ActionType, ActionTypes, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
|
import { ActionType, ActionTypes, Category, CategoryActionType, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||||
import Utils from "../utils";
|
import Utils from "../utils";
|
||||||
import { getCategoryActionType } from "../utils/categoryUtils";
|
import { getCategoryActionType } from "../utils/categoryUtils";
|
||||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||||
import { RectangleTooltip } from "../render/RectangleTooltip";
|
import { RectangleTooltip } from "../render/RectangleTooltip";
|
||||||
|
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
|
||||||
|
|
||||||
|
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
@@ -25,6 +26,9 @@ export interface SponsorTimeEditState {
|
|||||||
editing: boolean;
|
editing: boolean;
|
||||||
sponsorTimeEdits: [string, string];
|
sponsorTimeEdits: [string, string];
|
||||||
selectedCategory: Category;
|
selectedCategory: Category;
|
||||||
|
description: string;
|
||||||
|
suggestedNames: SelectorOption[];
|
||||||
|
chapterNameSelectorOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CATEGORY = "chooseACategory";
|
const DEFAULT_CATEGORY = "chooseACategory";
|
||||||
@@ -42,6 +46,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
previousSkipType: CategoryActionType;
|
previousSkipType: CategoryActionType;
|
||||||
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
|
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
|
||||||
|
|
||||||
|
// For description auto-complete
|
||||||
|
fetchingSuggestions: boolean;
|
||||||
|
|
||||||
constructor(props: SponsorTimeEditProps) {
|
constructor(props: SponsorTimeEditProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -52,10 +59,14 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
this.idSuffix = this.props.idSuffix;
|
this.idSuffix = this.props.idSuffix;
|
||||||
this.previousSkipType = CategoryActionType.Skippable;
|
this.previousSkipType = CategoryActionType.Skippable;
|
||||||
|
|
||||||
|
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||||
this.state = {
|
this.state = {
|
||||||
editing: false,
|
editing: false,
|
||||||
sponsorTimeEdits: [null, null],
|
sponsorTimeEdits: [null, null],
|
||||||
selectedCategory: DEFAULT_CATEGORY as Category
|
selectedCategory: DEFAULT_CATEGORY as Category,
|
||||||
|
description: sponsorTime.description || "",
|
||||||
|
suggestedNames: [],
|
||||||
|
chapterNameSelectorOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,14 +217,22 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
{/* Chapter Name */}
|
{/* Chapter Name */}
|
||||||
{sponsorTime.actionType === ActionType.Chapter ? (
|
{sponsorTime.actionType === ActionType.Chapter ? (
|
||||||
<div style={{position: "relative"}}>
|
<div onMouseLeave={() => this.setState({chapterNameSelectorOpen: false})}>
|
||||||
<input id={"chapterName" + this.idSuffix}
|
<input id={"chapterName" + this.idSuffix}
|
||||||
className="sponsorTimeEdit"
|
className="sponsorTimeEdit"
|
||||||
ref={this.descriptionOptionRef}
|
ref={this.descriptionOptionRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={sponsorTime.description || ""}
|
value={this.state.description}
|
||||||
onChange={() => this.saveEditTimes()}>
|
onChange={(e) => this.descriptionUpdate(e.target.value)}
|
||||||
|
onFocus={() => this.setState({chapterNameSelectorOpen: true})}>
|
||||||
</input>
|
</input>
|
||||||
|
{this.state.chapterNameSelectorOpen && this.state.description &&
|
||||||
|
<SelectorComponent
|
||||||
|
id={"chapterNameSelector" + this.idSuffix}
|
||||||
|
options={this.state.suggestedNames}
|
||||||
|
onChange={(v) => this.descriptionUpdate(v)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
): ""}
|
): ""}
|
||||||
|
|
||||||
@@ -520,6 +539,41 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descriptionUpdate(description: string): void {
|
||||||
|
this.setState({
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.fetchingSuggestions) {
|
||||||
|
this.fetchSuggestions(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveEditTimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSuggestions(description: string): Promise<void> {
|
||||||
|
if (this.props.contentContainer().channelIDInfo.status !== ChannelIDStatus.Found) return;
|
||||||
|
|
||||||
|
this.fetchingSuggestions = true;
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/chapterNames", {
|
||||||
|
description,
|
||||||
|
channelID: this.props.contentContainer().channelIDInfo.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
try {
|
||||||
|
const names = JSON.parse(result.responseText) as {description: string}[];
|
||||||
|
this.setState({
|
||||||
|
suggestedNames: names.map(n => ({
|
||||||
|
label: n.description
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (e) {} //eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchingSuggestions = false;
|
||||||
|
}
|
||||||
|
|
||||||
configUpdate(): void {
|
configUpdate(): void {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({
|
|||||||
previewTime,
|
previewTime,
|
||||||
videoInfo,
|
videoInfo,
|
||||||
getRealCurrentTime: getRealCurrentTime,
|
getRealCurrentTime: getRealCurrentTime,
|
||||||
lockedCategories
|
lockedCategories,
|
||||||
|
channelIDInfo
|
||||||
});
|
});
|
||||||
|
|
||||||
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export interface ContentContainer {
|
|||||||
previewTime: (time: number, unpause?: boolean) => void,
|
previewTime: (time: number, unpause?: boolean) => void,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
getRealCurrentTime: () => number,
|
getRealCurrentTime: () => number,
|
||||||
lockedCategories: string[]
|
lockedCategories: string[],
|
||||||
|
channelIDInfo: ChannelIDInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user