mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-10 05:27:03 +03:00
Add channel skip profiles
This commit is contained in:
@@ -1,30 +1,160 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
import { Category } from "../../types";
|
||||
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
|
||||
import { Category, CategorySelection, CategorySkipOption } from "../../types";
|
||||
import { CategorySkipOptionsComponent, ExtraOptionComponent, ToggleOption } from "./CategorySkipOptionsComponent";
|
||||
import { SelectOptionComponent } from "./SelectOptionComponent";
|
||||
import Config, { ConfigurationID, CustomConfiguration } from "../../config";
|
||||
import { generateUserID } from "../../../maze-utils/src/setup";
|
||||
|
||||
export interface CategoryChooserProps {
|
||||
let forceUpdateSkipProfilesTimeout: NodeJS.Timeout | null = null;
|
||||
let forceUpdateSkipProfileIDsTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
}
|
||||
export function CategoryChooserComponent() {
|
||||
const [configurations, setConfigurations] = React.useState(Config.local!.skipProfiles);
|
||||
const [selectedConfigurationID, setSelectedConfigurationID] = React.useState<ConfigurationID | null>(null);
|
||||
const [channelListText, setChannelListText] = React.useState("");
|
||||
|
||||
export interface CategoryChooserState {
|
||||
const [configurationName, setConfigurationName] = React.useState("");
|
||||
const [selections, setSelections] = React.useState<CategorySelection[]>([]);
|
||||
|
||||
}
|
||||
React.useEffect(() => {
|
||||
setConfigurationName(getConfigurationValue(selectedConfigurationID, "name", ""));
|
||||
|
||||
class CategoryChooserComponent extends React.Component<CategoryChooserProps, CategoryChooserState> {
|
||||
updateChannelList(setChannelListText, selectedConfigurationID!);
|
||||
setSelections(getConfigurationValue<CategorySelection[]>(selectedConfigurationID, "categorySelections"));
|
||||
}, [selectedConfigurationID]);
|
||||
|
||||
constructor(props: CategoryChooserProps) {
|
||||
super(props);
|
||||
|
||||
// Setup state
|
||||
this.state = {
|
||||
|
||||
const createNewConfig = () => {
|
||||
let newID = generateUserID().substring(0, 5);
|
||||
while (Config.local.skipProfiles[newID]) {
|
||||
newID = generateUserID().substring(0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
const newConfiguration: CustomConfiguration = {
|
||||
name: `${chrome.i18n.getMessage("NewConfiguration")} ${Object.keys(Config.local.skipProfiles).length}`,
|
||||
categorySelections: [],
|
||||
showAutogeneratedChapters: null,
|
||||
autoSkipOnMusicVideos: null,
|
||||
skipNonMusicOnlyOnYoutubeMusic: null,
|
||||
muteSegments: null,
|
||||
fullVideoSegments: null,
|
||||
manualSkipOnFullVideo: null,
|
||||
minDuration: null
|
||||
};
|
||||
|
||||
Config.local!.skipProfiles[newID] = newConfiguration;
|
||||
forceUpdateConfigurations();
|
||||
setConfigurations(Config.local!.skipProfiles);
|
||||
setSelectedConfigurationID(newID as ConfigurationID);
|
||||
|
||||
updateChannelList(setChannelListText, newID as ConfigurationID);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
if (window.location.hash === "#newProfile") {
|
||||
createNewConfig();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="categoryChooserTopRow">
|
||||
<SelectOptionComponent
|
||||
id="channelProfiles"
|
||||
onChange={(value) => {
|
||||
if (value === "null") value = null;
|
||||
|
||||
setSelectedConfigurationID(value as ConfigurationID);
|
||||
updateChannelList(setChannelListText, value as ConfigurationID);
|
||||
}}
|
||||
value={selectedConfigurationID!}
|
||||
options={[{
|
||||
value: "null",
|
||||
label: chrome.i18n.getMessage("DefaultConfiguration")
|
||||
}].concat(Object.entries(configurations).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: value.name
|
||||
})))}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="option-button trigger-button"
|
||||
onClick={() => createNewConfig()}>
|
||||
{chrome.i18n.getMessage("NewConfiguration")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
selectedConfigurationID &&
|
||||
<div className="configurationInfo">
|
||||
<input
|
||||
type="text"
|
||||
id="configurationName"
|
||||
value={configurationName}
|
||||
placeholder={chrome.i18n.getMessage("ConfigurationName")}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
getConfig(selectedConfigurationID)!.name = newName;
|
||||
setConfigurationName(newName);
|
||||
|
||||
forceUpdateConfigurations();
|
||||
setConfigurations(Config.local!.skipProfiles);
|
||||
}}/>
|
||||
|
||||
<div>
|
||||
{chrome.i18n.getMessage("ChannelListInstructionsSB")}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
className="option-text-box"
|
||||
rows={10}
|
||||
value={channelListText}
|
||||
onChange={(e) => {
|
||||
const newText = e.target.value;
|
||||
setChannelListText(newText);
|
||||
|
||||
const channels = newText.split("\n").map((channel) => channel.trim()).filter((channel) => channel !== "");
|
||||
if (channels.length > 0) {
|
||||
for (const [channelID, id] of Object.entries(Config.local!.channelSkipProfileIDs)) {
|
||||
if (id === selectedConfigurationID) {
|
||||
if (!channels.includes(channelID)) {
|
||||
delete Config.local!.channelSkipProfileIDs[channelID];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const channel of channels) {
|
||||
Config.local!.channelSkipProfileIDs[channel] = selectedConfigurationID;
|
||||
}
|
||||
}
|
||||
|
||||
forceUpdateConfigurationIDs();
|
||||
}}/>
|
||||
|
||||
<div
|
||||
className="option-button trigger-button"
|
||||
onClick={() => {
|
||||
if (confirm(chrome.i18n.getMessage("areYouSureDeleteConfig"))) {
|
||||
delete Config.local.skipProfiles[selectedConfigurationID];
|
||||
forceUpdateConfigurations();
|
||||
|
||||
for (const [channelID, id] of Object.entries(Config.local.channelSkipProfileIDs)) {
|
||||
if (id === selectedConfigurationID) {
|
||||
delete Config.local.channelSkipProfileIDs[channelID];
|
||||
}
|
||||
}
|
||||
forceUpdateConfigurationIDs();
|
||||
|
||||
setConfigurations(Config.local!.skipProfiles);
|
||||
const newID = Object.keys(Config.local!.skipProfiles)[0] as ConfigurationID;
|
||||
setSelectedConfigurationID(newID);
|
||||
}
|
||||
}}>
|
||||
{chrome.i18n.getMessage("DeleteConfiguration")}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<table id="categoryChooserTable"
|
||||
className="categoryChooserTable">
|
||||
<tbody>
|
||||
@@ -51,25 +181,170 @@ class CategoryChooserComponent extends React.Component<CategoryChooserProps, Cat
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
{this.getCategorySkipOptions()}
|
||||
<CategorySkipOptions
|
||||
selectedConfigurationID={selectedConfigurationID}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ExtraOptionsComponent
|
||||
selectedConfigurationID={selectedConfigurationID!}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySkipOptions({ selectedConfigurationID, selections, setSelections}: { selectedConfigurationID: ConfigurationID | null;
|
||||
selections: CategorySelection[]; setSelections: (s: CategorySelection[]) => void; }): JSX.Element {
|
||||
const elements: JSX.Element[] = [];
|
||||
const defaultSkipOption = selectedConfigurationID === null ? CategorySkipOption.Disabled : CategorySkipOption.FallbackToDefault;
|
||||
|
||||
for (const category of CompileConfig.categoryList) {
|
||||
elements.push(
|
||||
<CategorySkipOptionsComponent
|
||||
category={category as Category}
|
||||
selection={selections.find(selection => selection.name === category)?.option ?? defaultSkipOption}
|
||||
updateSelection={(option: CategorySkipOption) => {
|
||||
const existingSelection = selections.find(selection => selection.name === category);
|
||||
const deletingSelection = (option === CategorySkipOption.Disabled && selectedConfigurationID === null)
|
||||
|| (option === CategorySkipOption.FallbackToDefault && selectedConfigurationID !== null);
|
||||
if (existingSelection) {
|
||||
existingSelection.option = option;
|
||||
|
||||
if (deletingSelection) {
|
||||
selections.splice(selections.indexOf(existingSelection), 1);
|
||||
}
|
||||
} else if (!deletingSelection) {
|
||||
selections.push({
|
||||
name: category as Category,
|
||||
option: option
|
||||
});
|
||||
}
|
||||
|
||||
// Clone so React notices the change
|
||||
selections = [...selections];
|
||||
|
||||
updateConfigurationValue(selectedConfigurationID, "categorySelections", selections, setSelections);
|
||||
}}
|
||||
isDefaultConfig={selectedConfigurationID === null}
|
||||
selectedConfigurationID={selectedConfigurationID}
|
||||
key={category}>
|
||||
</CategorySkipOptionsComponent>
|
||||
);
|
||||
}
|
||||
|
||||
getCategorySkipOptions(): JSX.Element[] {
|
||||
const elements: JSX.Element[] = [];
|
||||
return <>
|
||||
{elements}
|
||||
</>;
|
||||
}
|
||||
|
||||
for (const category of CompileConfig.categoryList) {
|
||||
elements.push(
|
||||
<CategorySkipOptionsComponent category={category as Category}
|
||||
key={category}>
|
||||
</CategorySkipOptionsComponent>
|
||||
);
|
||||
function forceUpdateConfigurations() {
|
||||
if (forceUpdateSkipProfilesTimeout) {
|
||||
clearTimeout(forceUpdateSkipProfilesTimeout);
|
||||
}
|
||||
|
||||
forceUpdateSkipProfilesTimeout = setTimeout(() => {
|
||||
Config.forceLocalUpdate("skipProfiles");
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function forceUpdateConfigurationIDs() {
|
||||
if (forceUpdateSkipProfileIDsTimeout) {
|
||||
clearTimeout(forceUpdateSkipProfileIDsTimeout);
|
||||
}
|
||||
|
||||
forceUpdateSkipProfileIDsTimeout = setTimeout(() => {
|
||||
Config.forceLocalUpdate("channelSkipProfileIDs");
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function updateChannelList(setChannelListText: (value: string) => void, selectedConfigurationID: ConfigurationID) {
|
||||
setChannelListText(Object.entries(Config.local!.channelSkipProfileIDs)
|
||||
.filter(([, id]) => id === selectedConfigurationID)
|
||||
.map(([channelID]) => channelID).join("\n"))
|
||||
}
|
||||
|
||||
function getConfig(selectedConfigurationID: ConfigurationID | null) {
|
||||
return selectedConfigurationID ? Config.local!.skipProfiles[selectedConfigurationID] : null;
|
||||
}
|
||||
|
||||
export function getConfigurationValue<T>(selectedConfigurationID: ConfigurationID | null, option: string, defaultValue?: T): T {
|
||||
if (selectedConfigurationID === null) {
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return Config.config[option];
|
||||
}
|
||||
|
||||
return elements;
|
||||
} else {
|
||||
return getConfig(selectedConfigurationID)[option];
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryChooserComponent;
|
||||
export function updateConfigurationValue(selectedConfigurationID: ConfigurationID | null, option: string, value: unknown, setFunction?: (value: unknown) => void) {
|
||||
if (selectedConfigurationID === null) {
|
||||
Config.config[option] = value;
|
||||
} else {
|
||||
const config = getConfig(selectedConfigurationID);
|
||||
if (value !== null) {
|
||||
config[option] = value;
|
||||
} else {
|
||||
delete config[option];
|
||||
}
|
||||
|
||||
forceUpdateConfigurations();
|
||||
}
|
||||
|
||||
if (setFunction) setFunction(value);
|
||||
}
|
||||
|
||||
function ExtraOptionsComponent(props: {selectedConfigurationID: ConfigurationID}): JSX.Element {
|
||||
const options: ToggleOption[][] = [[{
|
||||
configKey: "muteSegments",
|
||||
label: chrome.i18n.getMessage("muteSegments"),
|
||||
type: "toggle"
|
||||
}], [{
|
||||
configKey: "fullVideoSegments",
|
||||
label: chrome.i18n.getMessage("fullVideoSegments"),
|
||||
type: "toggle"
|
||||
}, {
|
||||
configKey: "fullVideoLabelsOnThumbnails",
|
||||
label: chrome.i18n.getMessage("fullVideoLabelsOnThumbnails"),
|
||||
type: "toggle",
|
||||
dontShowOnCustomConfigs: true
|
||||
}, {
|
||||
configKey: "manualSkipOnFullVideo",
|
||||
label: chrome.i18n.getMessage("enableManualSkipOnFullVideo"),
|
||||
description: chrome.i18n.getMessage("whatManualSkipOnFullVideo"),
|
||||
type: "toggle"
|
||||
}], [{
|
||||
configKey: "minDuration",
|
||||
label: chrome.i18n.getMessage("minDuration"),
|
||||
description: chrome.i18n.getMessage("minDurationDescription"),
|
||||
type: "number"
|
||||
}]];
|
||||
|
||||
const result: JSX.Element[] = [];
|
||||
|
||||
for (const optionGroup of options) {
|
||||
const groupResult: JSX.Element[] = [];
|
||||
for (const option of optionGroup) {
|
||||
groupResult.push(
|
||||
<ExtraOptionComponent
|
||||
option={option}
|
||||
selectedConfigurationID={props.selectedConfigurationID}
|
||||
key={option.configKey}/>
|
||||
);
|
||||
}
|
||||
|
||||
result.push(
|
||||
<div className="extraOptionGroup" key={optionGroup.map(o => o.configKey).join("-")}>
|
||||
{groupResult}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (<>
|
||||
{result}
|
||||
</>);
|
||||
}
|
||||
@@ -1,256 +1,295 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Config from "../../config"
|
||||
import Config, { ConfigurationID } from "../../config"
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
import { Category, CategorySkipOption } from "../../types";
|
||||
|
||||
import { getCategorySuffix } from "../../utils/categoryUtils";
|
||||
import ToggleOptionComponent from "./ToggleOptionComponent";
|
||||
import { ToggleOptionComponent } from "./ToggleOptionComponent";
|
||||
import { getConfigurationValue, updateConfigurationValue } from "./CategoryChooserComponent";
|
||||
import { NumberInputOptionComponent } from "./NumberInputOptionComponent";
|
||||
|
||||
export interface CategorySkipOptionsProps {
|
||||
category: Category;
|
||||
selection: CategorySkipOption;
|
||||
updateSelection(selection: CategorySkipOption): void;
|
||||
isDefaultConfig: boolean;
|
||||
selectedConfigurationID: ConfigurationID;
|
||||
defaultColor?: string;
|
||||
defaultPreviewColor?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface CategorySkipOptionsState {
|
||||
color: string;
|
||||
previewColor: string;
|
||||
}
|
||||
|
||||
export interface ToggleOption {
|
||||
configKey: string;
|
||||
label: string;
|
||||
type: "toggle" | "number";
|
||||
description?: string;
|
||||
dontDisable?: boolean;
|
||||
dontShowOnCustomConfigs?: boolean;
|
||||
}
|
||||
|
||||
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
|
||||
setBarColorTimeout: NodeJS.Timeout;
|
||||
export function CategorySkipOptionsComponent(props: CategorySkipOptionsProps): React.ReactElement {
|
||||
const [color, setColor] = React.useState(props.defaultColor || Config.config.barTypes[props.category]?.color);
|
||||
const [previewColor, setPreviewColor] = React.useState(props.defaultPreviewColor || Config.config.barTypes["preview-" + props.category]?.color);
|
||||
|
||||
constructor(props: CategorySkipOptionsProps) {
|
||||
super(props);
|
||||
const selectedOption = React.useMemo(() => {
|
||||
switch (props.selection) {
|
||||
case CategorySkipOption.ShowOverlay:
|
||||
return "showOverlay";
|
||||
case CategorySkipOption.ManualSkip:
|
||||
return "manualSkip";
|
||||
case CategorySkipOption.AutoSkip:
|
||||
return "autoSkip";
|
||||
case CategorySkipOption.FallbackToDefault:
|
||||
return "fallbackToDefault";
|
||||
default:
|
||||
return "disable";
|
||||
}
|
||||
}, [props.selection]);
|
||||
|
||||
// Setup state
|
||||
this.state = {
|
||||
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
|
||||
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color
|
||||
};
|
||||
}
|
||||
const setBarColorTimeout = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
render(): React.ReactElement {
|
||||
let defaultOption = "disable";
|
||||
// Set the default opton properly
|
||||
for (const categorySelection of Config.config.categorySelections) {
|
||||
if (categorySelection.name === this.props.category) {
|
||||
switch (categorySelection.option) {
|
||||
case CategorySkipOption.ShowOverlay:
|
||||
defaultOption = "showOverlay";
|
||||
break;
|
||||
case CategorySkipOption.ManualSkip:
|
||||
defaultOption = "manualSkip";
|
||||
break;
|
||||
case CategorySkipOption.AutoSkip:
|
||||
defaultOption = "autoSkip";
|
||||
break;
|
||||
return (
|
||||
<>
|
||||
<tr id={props.category + "OptionsRow"}
|
||||
className={`categoryTableElement`} >
|
||||
<td id={props.category + "OptionName"}
|
||||
className="categoryTableLabel">
|
||||
{chrome.i18n.getMessage("category_" + props.category)}
|
||||
</td>
|
||||
|
||||
<td id={props.category + "SkipOption"}
|
||||
className="skipOption">
|
||||
<select
|
||||
className="optionsSelector"
|
||||
value={selectedOption}
|
||||
onChange={(e) => skipOptionSelected(e, props.category, props.updateSelection)}>
|
||||
{getCategorySkipOptions(props.category, props.isDefaultConfig)}
|
||||
</select>
|
||||
</td>
|
||||
|
||||
{props.category !== "chapter" &&
|
||||
<td id={props.category + "ColorOption"}
|
||||
className="colorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
disabled={!props.isDefaultConfig}
|
||||
onChange={(event) => {
|
||||
if (setBarColorTimeout.current) {
|
||||
clearTimeout(setBarColorTimeout.current);
|
||||
}
|
||||
|
||||
setColor(event.currentTarget.value);
|
||||
Config.config.barTypes[props.category].color = event.currentTarget.value;
|
||||
|
||||
// Make listener get called
|
||||
setBarColorTimeout.current = setTimeout(() => {
|
||||
Config.config.barTypes = Config.config.barTypes;
|
||||
}, 50);
|
||||
}}
|
||||
value={color} />
|
||||
</td>
|
||||
}
|
||||
|
||||
break;
|
||||
{!["chapter", "exclusive_access"].includes(props.category) &&
|
||||
<td id={props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
disabled={!props.isDefaultConfig}
|
||||
onChange={(event) => {
|
||||
if (setBarColorTimeout.current) {
|
||||
clearTimeout(setBarColorTimeout.current);
|
||||
}
|
||||
|
||||
setPreviewColor(event.currentTarget.value);
|
||||
Config.config.barTypes["preview-" + props.category].color = event.currentTarget.value;
|
||||
|
||||
// Make listener get called
|
||||
setBarColorTimeout.current = setTimeout(() => {
|
||||
Config.config.barTypes = Config.config.barTypes;
|
||||
}, 50);
|
||||
}}
|
||||
value={previewColor} />
|
||||
</td>
|
||||
}
|
||||
|
||||
</tr>
|
||||
|
||||
<tr id={props.category + "DescriptionRow"}
|
||||
className={`small-description categoryTableDescription`}>
|
||||
<td
|
||||
colSpan={2}>
|
||||
{chrome.i18n.getMessage("category_" + props.category + "_description")}
|
||||
{' '}
|
||||
<a href={CompileConfig.wikiLinks[props.category]} target="_blank" rel="noreferrer">
|
||||
{`${chrome.i18n.getMessage("LearnMore")}`}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<ExtraOptionComponents
|
||||
category={props.category}
|
||||
selectedConfigurationID={props.selectedConfigurationID}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>,
|
||||
category: Category, updateSelection: (selection: CategorySkipOption) => void): void {
|
||||
let option: CategorySkipOption;
|
||||
switch (event.target.value) {
|
||||
case "fallbackToDefault":
|
||||
option = CategorySkipOption.FallbackToDefault;
|
||||
break;
|
||||
case "disable":
|
||||
option = CategorySkipOption.Disabled;
|
||||
break;
|
||||
case "showOverlay":
|
||||
option = CategorySkipOption.ShowOverlay;
|
||||
break;
|
||||
case "manualSkip":
|
||||
option = CategorySkipOption.ManualSkip;
|
||||
break;
|
||||
case "autoSkip":
|
||||
option = CategorySkipOption.AutoSkip;
|
||||
|
||||
if (category === "filler" && !Config.config.isVip) {
|
||||
if (!confirm(chrome.i18n.getMessage("FillerWarning"))) {
|
||||
event.target.value = "disable";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr id={this.props.category + "OptionsRow"}
|
||||
className={`categoryTableElement`} >
|
||||
<td id={this.props.category + "OptionName"}
|
||||
className="categoryTableLabel">
|
||||
{chrome.i18n.getMessage("category_" + this.props.category)}
|
||||
</td>
|
||||
break;
|
||||
}
|
||||
|
||||
<td id={this.props.category + "SkipOption"}
|
||||
className="skipOption">
|
||||
<select
|
||||
className="optionsSelector"
|
||||
defaultValue={defaultOption}
|
||||
onChange={this.skipOptionSelected.bind(this)}>
|
||||
{this.getCategorySkipOptions()}
|
||||
</select>
|
||||
</td>
|
||||
updateSelection(option);
|
||||
}
|
||||
|
||||
{this.props.category !== "chapter" &&
|
||||
<td id={this.props.category + "ColorOption"}
|
||||
className="colorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
onChange={(event) => this.setColorState(event, false)}
|
||||
value={this.state.color} />
|
||||
</td>
|
||||
}
|
||||
function getCategorySkipOptions(category: Category, isDefaultConfig: boolean): JSX.Element[] {
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
{!["chapter", "exclusive_access"].includes(this.props.category) &&
|
||||
<td id={this.props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
onChange={(event) => this.setColorState(event, true)}
|
||||
value={this.state.previewColor} />
|
||||
</td>
|
||||
}
|
||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
if (category === "chapter") optionNames = ["disable", "showOverlay"]
|
||||
else if (category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
|
||||
</tr>
|
||||
if (!isDefaultConfig) {
|
||||
optionNames = ["fallbackToDefault"].concat(optionNames);
|
||||
}
|
||||
|
||||
<tr id={this.props.category + "DescriptionRow"}
|
||||
className={`small-description categoryTableDescription`}>
|
||||
<td
|
||||
colSpan={2}>
|
||||
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
|
||||
{' '}
|
||||
<a href={CompileConfig.wikiLinks[this.props.category]} target="_blank" rel="noreferrer">
|
||||
{`${chrome.i18n.getMessage("LearnMore")}`}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.getExtraOptionComponents(this.props.category)}
|
||||
|
||||
</>
|
||||
for (const optionName of optionNames) {
|
||||
elements.push(
|
||||
<option key={optionName} value={optionName}>
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategorySuffix(category)
|
||||
: optionName) || chrome.i18n.getMessage(optionName)}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
let option: CategorySkipOption;
|
||||
|
||||
switch (event.target.value) {
|
||||
case "disable":
|
||||
Config.config.categorySelections = Config.config.categorySelections.filter(
|
||||
categorySelection => categorySelection.name !== this.props.category);
|
||||
return;
|
||||
case "showOverlay":
|
||||
option = CategorySkipOption.ShowOverlay;
|
||||
|
||||
break;
|
||||
case "manualSkip":
|
||||
option = CategorySkipOption.ManualSkip;
|
||||
|
||||
break;
|
||||
case "autoSkip":
|
||||
option = CategorySkipOption.AutoSkip;
|
||||
|
||||
if (this.props.category === "filler" && !Config.config.isVip) {
|
||||
if (!confirm(chrome.i18n.getMessage("FillerWarning"))) {
|
||||
event.target.value = "disable";
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const existingSelection = Config.config.categorySelections.find(selection => selection.name === this.props.category);
|
||||
if (existingSelection) {
|
||||
existingSelection.option = option;
|
||||
} else {
|
||||
Config.config.categorySelections.push({
|
||||
name: this.props.category,
|
||||
option: option
|
||||
});
|
||||
}
|
||||
|
||||
Config.forceSyncUpdate("categorySelections");
|
||||
}
|
||||
|
||||
getCategorySkipOptions(): JSX.Element[] {
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"]
|
||||
else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
|
||||
for (const optionName of optionNames) {
|
||||
elements.push(
|
||||
<option key={optionName} value={optionName}>
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategorySuffix(this.props.category)
|
||||
: optionName)}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
setColorState(event: React.FormEvent<HTMLInputElement>, preview: boolean): void {
|
||||
clearTimeout(this.setBarColorTimeout);
|
||||
|
||||
if (preview) {
|
||||
this.setState({
|
||||
previewColor: event.currentTarget.value
|
||||
});
|
||||
|
||||
Config.config.barTypes["preview-" + this.props.category].color = event.currentTarget.value;
|
||||
|
||||
} else {
|
||||
this.setState({
|
||||
color: event.currentTarget.value
|
||||
});
|
||||
|
||||
Config.config.barTypes[this.props.category].color = event.currentTarget.value;
|
||||
}
|
||||
|
||||
// Make listener get called
|
||||
this.setBarColorTimeout = setTimeout(() => {
|
||||
Config.config.barTypes = Config.config.barTypes;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
getExtraOptionComponents(category: string): JSX.Element[] {
|
||||
const result = [];
|
||||
for (const option of this.getExtraOptions(category)) {
|
||||
result.push(
|
||||
<tr key={option.configKey}>
|
||||
<td id={`${category}_${option.configKey}`} className="categoryExtraOptions">
|
||||
<ToggleOptionComponent
|
||||
configKey={option.configKey}
|
||||
label={option.label}
|
||||
style={{width: "inherit"}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getExtraOptions(category: string): ToggleOption[] {
|
||||
switch (category) {
|
||||
case "chapter":
|
||||
return [{
|
||||
configKey: "renderSegmentsAsChapters",
|
||||
label: chrome.i18n.getMessage("renderAsChapters"),
|
||||
dontDisable: true
|
||||
}, {
|
||||
configKey: "showSegmentNameInChapterBar",
|
||||
label: chrome.i18n.getMessage("showSegmentNameInChapterBar"),
|
||||
dontDisable: true
|
||||
}, {
|
||||
configKey: "showAutogeneratedChapters",
|
||||
label: chrome.i18n.getMessage("showAutogeneratedChapters"),
|
||||
dontDisable: true
|
||||
}];
|
||||
case "music_offtopic":
|
||||
return [{
|
||||
configKey: "autoSkipOnMusicVideos",
|
||||
label: chrome.i18n.getMessage("autoSkipOnMusicVideos"),
|
||||
}, {
|
||||
configKey: "skipNonMusicOnlyOnYoutubeMusic",
|
||||
label: chrome.i18n.getMessage("skipNonMusicOnlyOnYoutubeMusic"),
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
export default CategorySkipOptionsComponent;
|
||||
|
||||
function ExtraOptionComponents(props: {category: string; selectedConfigurationID: ConfigurationID}): JSX.Element {
|
||||
const result = [];
|
||||
for (const option of getExtraOptions(props.category)) {
|
||||
result.push(
|
||||
<ExtraOptionComponent
|
||||
key={option.configKey}
|
||||
option={option}
|
||||
selectedConfigurationID={props.selectedConfigurationID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{result}
|
||||
</>);
|
||||
}
|
||||
|
||||
export function ExtraOptionComponent({option, selectedConfigurationID}: {option: ToggleOption; selectedConfigurationID: ConfigurationID}): JSX.Element {
|
||||
const [value, setValue] = React.useState(getConfigurationValue(selectedConfigurationID, option.configKey));
|
||||
React.useEffect(() => {
|
||||
setValue(getConfigurationValue(selectedConfigurationID, option.configKey));
|
||||
}, [selectedConfigurationID]);
|
||||
|
||||
return (
|
||||
<tr key={option.configKey} className={`${option.dontShowOnCustomConfigs && selectedConfigurationID !== null ? "hidden" : ""}`}>
|
||||
<td id={`${option.configKey}`} className="categoryExtraOptions">
|
||||
{
|
||||
option.type === "toggle" ?
|
||||
<ToggleOptionComponent
|
||||
checked={value ?? Config.config[option.configKey]}
|
||||
partiallyHidden={value === null}
|
||||
showResetButton={value !== null && selectedConfigurationID !== null}
|
||||
onChange={(checked) => {
|
||||
updateConfigurationValue(selectedConfigurationID, option.configKey, checked, setValue);
|
||||
}}
|
||||
onReset={() => {
|
||||
updateConfigurationValue(selectedConfigurationID, option.configKey, null, setValue);
|
||||
}}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
style={{width: "inherit"}}
|
||||
/>
|
||||
:
|
||||
<NumberInputOptionComponent
|
||||
value={value ?? Config.config[option.configKey]}
|
||||
partiallyHidden={value === null}
|
||||
showResetButton={value !== null && selectedConfigurationID !== null}
|
||||
onChange={(value) => {
|
||||
updateConfigurationValue(selectedConfigurationID, option.configKey, value, setValue);
|
||||
}}
|
||||
onReset={() => {
|
||||
updateConfigurationValue(selectedConfigurationID, option.configKey, null, setValue);
|
||||
}}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
style={{width: "inherit"}}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function getExtraOptions(category: string): ToggleOption[] {
|
||||
switch (category) {
|
||||
case "chapter":
|
||||
return [{
|
||||
configKey: "renderSegmentsAsChapters",
|
||||
label: chrome.i18n.getMessage("renderAsChapters"),
|
||||
type: "toggle",
|
||||
dontDisable: true,
|
||||
dontShowOnCustomConfigs: true
|
||||
}, {
|
||||
configKey: "showSegmentNameInChapterBar",
|
||||
label: chrome.i18n.getMessage("showSegmentNameInChapterBar"),
|
||||
type: "toggle",
|
||||
dontDisable: true,
|
||||
dontShowOnCustomConfigs: true
|
||||
}, {
|
||||
configKey: "showAutogeneratedChapters",
|
||||
label: chrome.i18n.getMessage("showAutogeneratedChapters"),
|
||||
type: "toggle",
|
||||
dontDisable: true
|
||||
}];
|
||||
case "music_offtopic":
|
||||
return [{
|
||||
configKey: "autoSkipOnMusicVideos",
|
||||
label: chrome.i18n.getMessage("autoSkipOnMusicVideos"),
|
||||
type: "toggle"
|
||||
}, {
|
||||
configKey: "skipNonMusicOnlyOnYoutubeMusic",
|
||||
label: chrome.i18n.getMessage("skipNonMusicOnlyOnYoutubeMusic"),
|
||||
type: "toggle"
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
52
src/components/options/NumberInputOptionComponent.tsx
Normal file
52
src/components/options/NumberInputOptionComponent.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from "react";
|
||||
import ResetIcon from "../../svg-icons/resetIcon";
|
||||
|
||||
export interface NumberInputOptionProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
partiallyHidden?: boolean;
|
||||
showResetButton?: boolean;
|
||||
onReset?(): void;
|
||||
}
|
||||
|
||||
export function NumberInputOptionComponent(props: NumberInputOptionProps): React.ReactElement {
|
||||
return (
|
||||
<div className={`sb-number-option ${props.disabled ? "disabled" : ""} ${props.partiallyHidden ? "partiallyHidden" : ""}`}>
|
||||
<div style={props.style}>
|
||||
<label className="number-container">
|
||||
<span className="optionLabel">
|
||||
{props.label}
|
||||
</span>
|
||||
<input id={props.label}
|
||||
className="sb-number-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={props.value}
|
||||
disabled={props.disabled}
|
||||
onChange={(e) => props.onChange(Number(e.target.value))}/>
|
||||
</label>
|
||||
|
||||
{
|
||||
props.showResetButton &&
|
||||
<span className="reset-button sb-switch-label" title={chrome.i18n.getMessage("fallbackToDefault")} onClick={() => {
|
||||
props.onReset?.();
|
||||
}}>
|
||||
<ResetIcon/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
props.description &&
|
||||
<div className="small-description">
|
||||
{props.description}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
src/components/options/SelectOptionComponent.tsx
Normal file
60
src/components/options/SelectOptionComponent.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from "react";
|
||||
import ResetIcon from "../../svg-icons/resetIcon";
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SelectOptionComponentProps {
|
||||
id: string;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
label?: string;
|
||||
title?: string;
|
||||
options: SelectOption[];
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
showResetButton?: boolean;
|
||||
onReset?: () => void;
|
||||
applyFormattingToOptions?: boolean;
|
||||
}
|
||||
|
||||
export const SelectOptionComponent = (props: SelectOptionComponentProps) => {
|
||||
return (
|
||||
<div className={`sb-optionContainer ${props.className ?? ""}`} style={props.style}>
|
||||
{
|
||||
props.label &&
|
||||
<label className="sb-optionLabel" htmlFor={props.id}>
|
||||
{props.label}
|
||||
</label>
|
||||
}
|
||||
<select id={props.id}
|
||||
className="sb-selector-element optionsSelector"
|
||||
value={props.value}
|
||||
title={props.title}
|
||||
onChange={(e) => {
|
||||
props.onChange(e.target.value);
|
||||
}}>
|
||||
{getOptions(props.options)}
|
||||
</select>
|
||||
|
||||
{
|
||||
props.showResetButton &&
|
||||
<div className="reset-button" onClick={() => {
|
||||
props.onReset?.();
|
||||
}}>
|
||||
<ResetIcon/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getOptions(options: SelectOption[]): React.ReactNode[] {
|
||||
return options.map((option) => {
|
||||
return (
|
||||
<option value={option.value} key={option.value}>{option.label}</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,57 +1,50 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Config from "../../config";
|
||||
import ResetIcon from "../../svg-icons/resetIcon";
|
||||
|
||||
export interface ToggleOptionProps {
|
||||
configKey: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
checked: boolean | null;
|
||||
onChange(checked: boolean): void;
|
||||
partiallyHidden?: boolean;
|
||||
showResetButton?: boolean;
|
||||
onReset?(): void;
|
||||
}
|
||||
|
||||
export interface ToggleOptionState {
|
||||
enabled: boolean;
|
||||
}
|
||||
export function ToggleOptionComponent(props: ToggleOptionProps): React.ReactElement {
|
||||
return (
|
||||
<div className={`sb-toggle-option ${props.disabled ? "disabled" : ""} ${props.partiallyHidden ? "partiallyHidden" : ""}`}>
|
||||
<div className="switch-container" style={props.style}>
|
||||
<label className="switch">
|
||||
<input id={props.label}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
onChange={(e) => props.onChange(e.target.checked)}/>
|
||||
<span className="slider round"></span>
|
||||
</label>
|
||||
<label className="switch-label" htmlFor={props.label}>
|
||||
{props.label}
|
||||
</label>
|
||||
|
||||
class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOptionState> {
|
||||
|
||||
constructor(props: ToggleOptionProps) {
|
||||
super(props);
|
||||
|
||||
// Setup state
|
||||
this.state = {
|
||||
enabled: Config.config[props.configKey]
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
<div className={`sb-toggle-option ${this.props.disabled ? "disabled" : ""}`}>
|
||||
<div className="switch-container" style={this.props.style}>
|
||||
<label className="switch">
|
||||
<input id={this.props.configKey}
|
||||
type="checkbox"
|
||||
checked={this.state.enabled}
|
||||
disabled={this.props.disabled}
|
||||
onChange={(e) => this.clicked(e)}/>
|
||||
<span className="slider round"></span>
|
||||
</label>
|
||||
<label className="switch-label" htmlFor={this.props.configKey}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
{
|
||||
props.showResetButton &&
|
||||
<div className="reset-button sb-switch-label" title={chrome.i18n.getMessage("fallbackToDefault")} onClick={() => {
|
||||
props.onReset?.();
|
||||
}}>
|
||||
<ResetIcon/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
clicked(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
Config.config[this.props.configKey] = event.target.checked;
|
||||
|
||||
this.setState({
|
||||
enabled: event.target.checked
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ToggleOptionComponent;
|
||||
{
|
||||
props.description &&
|
||||
<div className="small-description">
|
||||
{props.description}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user