diff --git a/maze-utils b/maze-utils
index e2f19e21..2c6822ec 160000
--- a/maze-utils
+++ b/maze-utils
@@ -1 +1 @@
-Subproject commit e2f19e21c23342638e26fd642ca9665b5b2ee775
+Subproject commit 2c6822ec85f97a919279742d22cc98426fb752b9
diff --git a/public/_locales b/public/_locales
index c9dcf1ec..0285b879 160000
--- a/public/_locales
+++ b/public/_locales
@@ -1 +1 @@
-Subproject commit c9dcf1ec21c068c7b235fb146bb4922aa5b5706d
+Subproject commit 0285b87951827abe87cd5f7fe4032a1389b0954a
diff --git a/public/options/options.css b/public/options/options.css
index 657fbee2..35ae92af 100644
--- a/public/options/options.css
+++ b/public/options/options.css
@@ -116,14 +116,26 @@ html, body {
color: white;
}
-.option-group > div {
+.option-group > div, .extraOptionGroup {
min-height: 50px;
padding: 15px 0;
- border-bottom: 1px solid var(--border-color);
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
}
+.option-group > div {
+ border-bottom: 1px solid var(--border-color);
+}
+.extraOptionGroup {
+ border-top: 1px solid var(--border-color);
+}
+.extraOptionGroup tr:not(:last-child) {
+ padding-bottom: 15px;
+ display: block;
+}
+#category-type {
+ padding: 0;
+}
-.categoryExtraOptions {
+.categoryChooserTable .categoryExtraOptions {
padding-bottom: 15px;
}
@@ -364,6 +376,11 @@ input[type='number'] {
padding: 4px;
}
+.sb-number-input {
+ margin-left: 4px;
+ margin-right: 4px;
+}
+
.switch-label {
width: inherit;
}
@@ -744,4 +761,40 @@ svg {
.advanced-config-help-message {
margin-bottom: 10px;
transition: none;
+}
+
+.categoryChooserTopRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ margin-bottom: 10px;
+}
+
+.partiallyHidden {
+ opacity: 0.5;
+}
+
+.partiallyHidden:hover {
+ opacity: 1;
+}
+
+.reset-button svg {
+ margin-left: 5px;
+ width: 10px;
+ fill: var(--white);
+
+ cursor: pointer;
+}
+
+.skipProfileMenu {
+ position: absolute;
+}
+
+.configurationInfo > *:not(:last-child) {
+ margin-bottom: 10px;
+}
+
+.configurationInfo .option-text-box {
+ width: 100%;
}
\ No newline at end of file
diff --git a/public/options/options.html b/public/options/options.html
index 0a8f6f44..bd1a7c90 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -80,66 +80,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
__MSG_minDurationDescription__
-
-
-
-
-
-
-
-
-
__MSG_whatManualSkipOnFullVideo__
-
-
diff --git a/public/popup.css b/public/popup.css
index aa3fd108..76b03c13 100644
--- a/public/popup.css
+++ b/public/popup.css
@@ -277,8 +277,8 @@
border-radius: 8px;
background-color: var(--sb-grey-bg-color);
justify-content: space-evenly;
- overflow: hidden;
display: flex;
+ position: relative;
}
.sbControlsMenu-item {
display: flex;
@@ -627,4 +627,62 @@
.sbPopupButton {
width: 16px;
fill: var(--sb-main-fg-color);
+}
+
+#skipProfileMenu {
+ position: absolute;
+ top: 80px;
+ left: 50%;
+
+ background-color: #292828;
+ border-radius: 10px;
+ padding: 10px;
+
+ transform: translateX(-50%);
+}
+
+#skipProfileActions {
+ padding-top: 10px;
+}
+
+.skipOptionAction {
+ transition: border-color 0.2s ease-in-out, background-color 0.2s ease-in-out;
+ font-size: 14px;
+ padding: 5px;
+ margin: 5px;
+
+ background-color: #222;
+ border-radius: 5px;
+
+ cursor: help;
+ user-select: none;
+
+ border-color: transparent;
+ border-width: 2px;
+ border-style: solid;
+}
+.skipOptionAction:not(.highlighted, .disabled):hover {
+ background-color: var(--sb-grey-bg-color);
+}
+.skipOptionAction.selected {
+ border-color: var(--sb-red-bg-color);
+}
+.skipOptionAction.highlighted {
+ border-color: rgb(127, 0, 0);
+}
+.skipOptionAction:not(.highlighted, .disabled) {
+ cursor: pointer;
+}
+.skipOptionAction.disabled {
+ color: #808080
+}
+
+.optionsSelector {
+ background-color: #c00000;
+ color: white;
+
+ border: none;
+ font-size: 14px;
+ padding: 5px;
+ border-radius: 5px;
}
\ No newline at end of file
diff --git a/src/components/options/CategoryChooserComponent.tsx b/src/components/options/CategoryChooserComponent.tsx
index 1da4f641..23cc4dad 100644
--- a/src/components/options/CategoryChooserComponent.tsx
+++ b/src/components/options/CategoryChooserComponent.tsx
@@ -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
(null);
+ const [channelListText, setChannelListText] = React.useState("");
-export interface CategoryChooserState {
+ const [configurationName, setConfigurationName] = React.useState("");
+ const [selections, setSelections] = React.useState([]);
-}
+ React.useEffect(() => {
+ setConfigurationName(getConfigurationValue(selectedConfigurationID, "name", ""));
-class CategoryChooserComponent extends React.Component {
+ updateChannelList(setChannelListText, selectedConfigurationID!);
+ setSelections(getConfigurationValue(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 (
+ <>
+
+
{
+ 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
+ })))}
+ />
+
+ createNewConfig()}>
+ {chrome.i18n.getMessage("NewConfiguration")}
+
+
+
+ {
+ selectedConfigurationID &&
+
+
{
+ const newName = e.target.value;
+ getConfig(selectedConfigurationID)!.name = newName;
+ setConfigurationName(newName);
+
+ forceUpdateConfigurations();
+ setConfigurations(Config.local!.skipProfiles);
+ }}/>
+
+
+ {chrome.i18n.getMessage("ChannelListInstructionsSB")}
+
+
+
+ }
+
@@ -51,25 +181,170 @@ class CategoryChooserComponent extends React.Component
- {this.getCategorySkipOptions()}
+
+
+
+ >
+ );
+}
+
+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(
+ 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}>
+
);
}
- getCategorySkipOptions(): JSX.Element[] {
- const elements: JSX.Element[] = [];
+ return <>
+ {elements}
+ >;
+}
- for (const category of CompileConfig.categoryList) {
- elements.push(
-
-
- );
+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(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;
\ No newline at end of file
+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(
+
+ );
+ }
+
+ result.push(
+ o.configKey).join("-")}>
+ {groupResult}
+
+ );
+ }
+
+ return (<>
+ {result}
+ >);
+}
\ No newline at end of file
diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx
index d865abb2..808b78d6 100644
--- a/src/components/options/CategorySkipOptionsComponent.tsx
+++ b/src/components/options/CategorySkipOptionsComponent.tsx
@@ -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 {
- 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(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 (
+ <>
+
+ |
+ {chrome.i18n.getMessage("category_" + props.category)}
+ |
+
+
+
+ |
+
+ {props.category !== "chapter" &&
+
+ {
+ 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} />
+ |
}
- break;
+ {!["chapter", "exclusive_access"].includes(props.category) &&
+
+ {
+ 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} />
+ |
+ }
+
+
+
+
+ |
+ {chrome.i18n.getMessage("category_" + props.category + "_description")}
+ {' '}
+
+ {`${chrome.i18n.getMessage("LearnMore")}`}
+
+ |
+
+
+
+ >
+ );
+}
+
+function skipOptionSelected(event: React.ChangeEvent,
+ 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 (
- <>
-
- |
- {chrome.i18n.getMessage("category_" + this.props.category)}
- |
+ break;
+ }
-
-
- |
+ updateSelection(option);
+}
- {this.props.category !== "chapter" &&
-
- this.setColorState(event, false)}
- value={this.state.color} />
- |
- }
+function getCategorySkipOptions(category: Category, isDefaultConfig: boolean): JSX.Element[] {
+ const elements: JSX.Element[] = [];
- {!["chapter", "exclusive_access"].includes(this.props.category) &&
-
- this.setColorState(event, true)}
- value={this.state.previewColor} />
- |
- }
+ let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
+ if (category === "chapter") optionNames = ["disable", "showOverlay"]
+ else if (category === "exclusive_access") optionNames = ["disable", "showOverlay"];
-
+ if (!isDefaultConfig) {
+ optionNames = ["fallbackToDefault"].concat(optionNames);
+ }
-
- |
- {chrome.i18n.getMessage("category_" + this.props.category + "_description")}
- {' '}
-
- {`${chrome.i18n.getMessage("LearnMore")}`}
-
- |
-
-
- {this.getExtraOptionComponents(this.props.category)}
-
- >
+ for (const optionName of optionNames) {
+ elements.push(
+
);
}
- skipOptionSelected(event: React.ChangeEvent): 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(
-
- );
- }
-
- return elements;
- }
-
- setColorState(event: React.FormEvent, 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(
-
- |
-
- |
-
- )
- }
-
- 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;
\ No newline at end of file
+
+function ExtraOptionComponents(props: {category: string; selectedConfigurationID: ConfigurationID}): JSX.Element {
+ const result = [];
+ for (const option of getExtraOptions(props.category)) {
+ result.push(
+
+ )
+ }
+
+ 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 (
+
+ |
+ {
+ option.type === "toggle" ?
+ {
+ updateConfigurationValue(selectedConfigurationID, option.configKey, checked, setValue);
+ }}
+ onReset={() => {
+ updateConfigurationValue(selectedConfigurationID, option.configKey, null, setValue);
+ }}
+ label={option.label}
+ description={option.description}
+ style={{width: "inherit"}}
+ />
+ :
+ {
+ updateConfigurationValue(selectedConfigurationID, option.configKey, value, setValue);
+ }}
+ onReset={() => {
+ updateConfigurationValue(selectedConfigurationID, option.configKey, null, setValue);
+ }}
+ label={option.label}
+ description={option.description}
+ style={{width: "inherit"}}
+ />
+ }
+ |
+
+ );
+}
+
+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 [];
+ }
+}
\ No newline at end of file
diff --git a/src/components/options/NumberInputOptionComponent.tsx b/src/components/options/NumberInputOptionComponent.tsx
new file mode 100644
index 00000000..bf452c79
--- /dev/null
+++ b/src/components/options/NumberInputOptionComponent.tsx
@@ -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 (
+
+
+
+
+ {
+ props.showResetButton &&
+ {
+ props.onReset?.();
+ }}>
+
+
+ }
+
+
+ {
+ props.description &&
+
+ {props.description}
+
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/options/SelectOptionComponent.tsx b/src/components/options/SelectOptionComponent.tsx
new file mode 100644
index 00000000..08c07028
--- /dev/null
+++ b/src/components/options/SelectOptionComponent.tsx
@@ -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 (
+
+ {
+ props.label &&
+
+ }
+
+
+ {
+ props.showResetButton &&
+
{
+ props.onReset?.();
+ }}>
+
+
+ }
+
+ );
+};
+
+function getOptions(options: SelectOption[]): React.ReactNode[] {
+ return options.map((option) => {
+ return (
+
+ );
+ });
+}
\ No newline at end of file
diff --git a/src/components/options/ToggleOptionComponent.tsx b/src/components/options/ToggleOptionComponent.tsx
index fa7434d6..1f10a051 100644
--- a/src/components/options/ToggleOptionComponent.tsx
+++ b/src/components/options/ToggleOptionComponent.tsx
@@ -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 (
+
+
+
+
-class ToggleOptionComponent extends React.Component
{
-
- constructor(props: ToggleOptionProps) {
- super(props);
-
- // Setup state
- this.state = {
- enabled: Config.config[props.configKey]
- }
- }
-
- render(): React.ReactElement {
- return (
-
-
-
-
-
+ {
+ props.showResetButton &&
+
{
+ props.onReset?.();
+ }}>
+
+
+ }
- );
- }
- clicked(event: React.ChangeEvent): void {
- Config.config[this.props.configKey] = event.target.checked;
-
- this.setState({
- enabled: event.target.checked
- });
- }
-
-}
-
-export default ToggleOptionComponent;
\ No newline at end of file
+ {
+ props.description &&
+
+ {props.description}
+
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index 438e67e8..4091f6e1 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -11,7 +11,6 @@ interface SBConfig {
permissions: Record
;
defaultCategory: Category;
renderSegmentsAsChapters: boolean;
- whitelistedChannels: string[];
forceChannelCheck: boolean;
minutesSaved: number;
skipCount: number;
@@ -138,6 +137,20 @@ interface SBConfig {
export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[]; lastAccess: number };
+export type ConfigurationID = string & { __configurationID: never };
+
+export interface CustomConfiguration {
+ name: string;
+ categorySelections: CategorySelection[];
+ showAutogeneratedChapters: boolean | null;
+ autoSkipOnMusicVideos: boolean | null;
+ skipNonMusicOnlyOnYoutubeMusic: boolean | null;
+ muteSegments: boolean | null;
+ fullVideoSegments: boolean | null;
+ manualSkipOnFullVideo: boolean | null;
+ minDuration: number | null;
+}
+
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record;
@@ -149,6 +162,10 @@ interface SBStorage {
/* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record;
+ channelSkipProfileIDs: Record;
+ skipProfileTemp: { time: number; configID: ConfigurationID } | null;
+ skipProfiles: Record;
+
skipRules: AdvancedSkipRuleSet[];
}
@@ -168,7 +185,38 @@ class ConfigClass extends ProtoConfig {
}
}
-function migrateOldSyncFormats(config: SBConfig) {
+function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
+ if (config["whitelistedChannels"]) {
+ // convert to skipProfiles
+ const whitelistedChannels = config["whitelistedChannels"] as string[];
+ const skipProfileID: ConfigurationID = "default-whitelist" as ConfigurationID;
+
+ local.skipProfiles[skipProfileID] = {
+ name: chrome.i18n.getMessage("WhitelistedChannels"),
+ categorySelections: config.categorySelections
+ .filter((s) => !["exclusive_access", "chapter"].includes(s.name))
+ .map(s => ({
+ name: s.name,
+ option: CategorySkipOption.ShowOverlay
+ })),
+ showAutogeneratedChapters: null,
+ autoSkipOnMusicVideos: null,
+ skipNonMusicOnlyOnYoutubeMusic: null,
+ muteSegments: null,
+ fullVideoSegments: null,
+ manualSkipOnFullVideo: null,
+ minDuration: null
+ };
+ local.skipProfiles = local.skipProfiles;
+
+ for (const channelID of whitelistedChannels) {
+ local.channelSkipProfileIDs[channelID] = skipProfileID;
+ }
+ local.channelSkipProfileIDs = local.channelSkipProfileIDs;
+
+ chrome.storage.sync.remove("whitelistedChannels");
+ }
+
if (!config["changeChapterColor"]) {
config.barTypes["chapter"].color = "#ffd983";
config["changeChapterColor"] = true;
@@ -290,7 +338,6 @@ const syncDefaults = {
permissions: {},
defaultCategory: "chooseACategory" as Category,
renderSegmentsAsChapters: false,
- whitelistedChannels: [],
forceChannelCheck: false,
minutesSaved: 0,
skipCount: 0,
@@ -506,7 +553,11 @@ const localDefaults = {
alreadyInstalled: false,
unsubmittedSegments: {},
- skipRules: []
+ skipRules: [],
+
+ channelSkipProfileIDs: {},
+ skipProfiles: {},
+ skipProfileTemp: null
};
const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats);
@@ -529,7 +580,7 @@ export function generateDebugDetails(): string {
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
? "Default server address" : "Custom server address";
output.config.invidiousInstances = output.config.invidiousInstances.length;
- output.config.whitelistedChannels = output.config.whitelistedChannels.length;
+ output.config.skipRules = output.config.skipRules.length;
return JSON.stringify(output, null, 4);
}
\ No newline at end of file
diff --git a/src/content.ts b/src/content.ts
index 3fecfdae..71944097 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -3,7 +3,6 @@ import {
ActionType,
Category,
CategorySkipOption,
- ChannelIDInfo,
ChannelIDStatus,
ContentContainer,
ScheduledTime,
@@ -53,6 +52,7 @@ import { defaultPreviewTime } from "./utils/constants";
import { onVideoPage } from "../maze-utils/src/pageInfo";
import { getSegmentsForVideo } from "./utils/segmentData";
import { getCategoryDefaultSelection, getCategorySelection } from "./utils/skipRule";
+import { getSkipProfileBool, getSkipProfileIDForTab, hideTooShortSegments, setCurrentTabSkipProfile } from "./utils/skipProfiles";
cleanPage();
@@ -146,7 +146,6 @@ let lastCheckVideoTime = -1;
// To determine if a video resolution change is happening
let firstPlay = true;
-//is this channel whitelised from getting sponsors skipped
let channelWhitelisted = false;
let previewBar: PreviewBar = null;
@@ -227,7 +226,9 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
onMobileYouTube: isOnMobileYouTube(),
videoID: getVideoID(),
loopedChapter: loopedChapter?.UUID,
- channelWhitelisted
+ channelID: getChannelIDInfo().id,
+ channelAuthor: getChannelIDInfo().author,
+ currentTabSkipProfileID: getSkipProfileIDForTab()
});
if (!request.updating && popupInitialised && document.getElementById("sponsorBlockPopupContainer") != null) {
@@ -355,6 +356,10 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
warn: window["SBLogs"].warn
});
break;
+ case "setCurrentTabSkipProfile":
+ setCurrentTabSkipProfile(request.configID);
+ channelIDChange();
+ break;
}
sendResponse({});
@@ -372,7 +377,7 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
updateVisibilityOfPlayerControlsButton()
break;
case "categorySelections":
- sponsorsLookup(true, true);
+ channelIDChange();
break;
case "barTypes":
setCategoryColorCSSVariables();
@@ -384,11 +389,26 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
}
}
}
+function contentLocalConfigUpdateListener(changes: StorageChangesObject) {
+ for (const key in changes) {
+ switch(key) {
+ case "channelSkipProfileIDs":
+ case "skipProfiles":
+ case "skipProfileTemp":
+ channelIDChange();
+ break;
+ }
+ }
+}
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
Config.configSyncListeners.push(contentConfigUpdateListener);
}
+if (!Config.configLocalListeners.includes(contentLocalConfigUpdateListener)) {
+ Config.configLocalListeners.push(contentLocalConfigUpdateListener);
+}
+
function resetValues() {
lastCheckTime = 0;
lastCheckVideoTime = -1;
@@ -465,7 +485,8 @@ function videoIDChange(): void {
chrome.runtime.sendMessage({
message: "videoChanged",
videoID: getVideoID(),
- whitelisted: channelWhitelisted
+ channelID: getChannelIDInfo().id,
+ channelAuthor: getChannelIDInfo().author
});
sponsorsLookup();
@@ -1209,16 +1230,6 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
sponsorTimes = receivedSegments;
existingChaptersImported = false;
- // Hide all submissions smaller than the minimum duration
- if (Config.config.minDuration !== 0) {
- for (const segment of sponsorTimes) {
- const duration = segment.segment[1] - segment.segment[0];
- if (duration > 0 && duration < Config.config.minDuration) {
- segment.hidden = SponsorHideType.MinimumDuration;
- }
- }
- }
-
if (keepOldSubmissions) {
for (const segment of oldSegments) {
const otherSegment = sponsorTimes.find((other) => segment.UUID === other.UUID);
@@ -1243,6 +1254,8 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
}
}
+ hideTooShortSegments(sponsorTimes);
+
if (!getVideo()) {
//there is still no video here
await waitForVideo();
@@ -1275,7 +1288,9 @@ function notifyPopupOfSegments(): void {
onMobileYouTube: isOnMobileYouTube(),
videoID: getVideoID(),
loopedChapter: loopedChapter?.UUID,
- channelWhitelisted
+ channelID: getChannelIDInfo().id,
+ channelAuthor: getChannelIDInfo().author,
+ currentTabSkipProfileID: getSkipProfileIDForTab()
});
}
@@ -1303,7 +1318,7 @@ function importExistingChapters(wait: boolean) {
}
}).catch(() => { importingChaptersWaiting = false; });
- if (!Config.config.showAutogeneratedChapters) {
+ if (!getSkipProfileBool("showAutogeneratedChapters")) {
waitFor(() => hasAutogeneratedChapters(), wait ? 15000 : 0, 400).then(() => {
updateActiveSegment(getCurrentTime());
}).catch(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function
@@ -1312,6 +1327,12 @@ function importExistingChapters(wait: boolean) {
}
}
+function handleExistingChaptersChannelChange() {
+ if (existingChaptersImported && hasAutogeneratedChapters() && !getSkipProfileBool("showAutogeneratedChapters")) {
+ sponsorTimes = sponsorTimes.filter((segment) => segment.source !== SponsorSourceType.Autogenerated);
+ }
+}
+
async function lockedCategoriesLookup(): Promise {
const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4);
const response = await asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix);
@@ -1377,13 +1398,6 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
- const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
- if (fullVideoSegment) {
- waitFor(() => categoryPill).then(() => {
- categoryPill?.setSegment(fullVideoSegment);
- });
- }
-
if (startingSegmentTime !== -1) {
startSponsorSchedule(undefined, startingSegmentTime);
} else {
@@ -1455,20 +1469,24 @@ function updatePreviewBar(): void {
}
}
-//checks if this channel is whitelisted, should be done only after the channelID has been loaded
-async function channelIDChange(channelIDInfo: ChannelIDInfo) {
- const whitelistedChannels = Config.config.whitelistedChannels;
-
- //see if this is a whitelisted channel
- if (whitelistedChannels != undefined &&
- channelIDInfo.status === ChannelIDStatus.Found && whitelistedChannels.includes(channelIDInfo.id)) {
- channelWhitelisted = true;
+function updateCategoryPill() {
+ const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
+ if (fullVideoSegment && getSkipProfileBool("fullVideoSegments")) {
+ categoryPill?.setSegment(fullVideoSegment);
+ } else {
+ categoryPill?.setVisibility(false);
}
+}
+//checks if this channel is whitelisted, should be done only after the channelID has been loaded
+async function channelIDChange() {
// check if the start of segments were missed
if (Config.config.forceChannelCheck && sponsorTimes?.length > 0) startSkipScheduleCheckingForStartSponsors();
+ hideTooShortSegments(sponsorTimes);
+ handleExistingChaptersChannelChange();
updatePreviewBar();
+ updateCategoryPill();
notifyPopupOfSegments();
}
@@ -1934,9 +1952,9 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
return false;
}
- return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
+ return (!getSkipProfileBool("manualSkipOnFullVideo") || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
&& (getCategorySelection(segment)?.option === CategorySkipOption.AutoSkip ||
- (Config.config.autoSkipOnMusicVideos && canSkipNonMusic && sponsorTimes?.some((s) => s.category === "music_offtopic")
+ (getSkipProfileBool("autoSkipOnMusicVideos") && canSkipNonMusic && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType === ActionType.Skip)
|| sponsorTimesSubmitting.some((s) => s.segment === segment.segment))
|| isLoopedChapter(segment);
@@ -1945,7 +1963,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
function shouldSkip(segment: SponsorTime): boolean {
return segment.hidden === SponsorHideType.Visible && (segment.actionType !== ActionType.Full
&& getCategorySelection(segment)?.option > CategorySkipOption.ShowOverlay)
- || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
+ || (getSkipProfileBool("autoSkipOnMusicVideos") && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType === ActionType.Skip)
|| isLoopedChapter(segment);
}
@@ -2508,11 +2526,7 @@ async function sendSubmitMessage(): Promise {
sponsorTimesSubmitting = [];
updatePreviewBar();
-
- const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
- if (fullVideoSegment) {
- categoryPill?.setSegment(fullVideoSegment);
- }
+ updateCategoryPill();
return true;
} else {
diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts
index 4acf4964..793bdba2 100644
--- a/src/js-components/previewBar.ts
+++ b/src/js-components/previewBar.ts
@@ -15,6 +15,7 @@ import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils";
import { isVorapisInstalled } from "../utils/compatibility";
import { isOnYTTV } from "../../maze-utils/src/video";
import { getCategorySelection } from "../utils/skipRule";
+import { getSkipProfileBool } from "../utils/skipProfiles";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003;
@@ -185,7 +186,7 @@ class PreviewBar {
}
const hasAYouTubeChapterRemoved = this.hasYouTubeChapters
- || (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters());
+ || (!getSkipProfileBool("showAutogeneratedChapters") && hasAutogeneratedChapters());
if (hasAYouTubeChapterRemoved) {
// Hide original tooltip if some chapter has been filtered out
originalTooltip.style.display = "none";
@@ -461,7 +462,7 @@ class PreviewBar {
|| (!Config.config.renderSegmentsAsChapters
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|| [SponsorSourceType.YouTube, SponsorSourceType.Autogenerated].includes(segment.source))))
- && !(hasAutogeneratedChapters() && !Config.config.showAutogeneratedChapters)) {
+ && !(hasAutogeneratedChapters() && !getSkipProfileBool("showAutogeneratedChapters"))) {
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
this.originalChapterBar.style.removeProperty("display");
@@ -488,7 +489,7 @@ class PreviewBar {
this.chapterGroups = this.unfilteredChapterGroups;
}
- if (this.chapterGroups.length === 0 && !Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) {
+ if (this.chapterGroups.length === 0 && !getSkipProfileBool("showAutogeneratedChapters") && hasAutogeneratedChapters()) {
// Add placeholder chapter group for whole video
this.chapterGroups = [{
segment: [0, this.videoDuration],
@@ -890,7 +891,7 @@ class PreviewBar {
if (!Config.config.showSegmentNameInChapterBar
|| Config.config.disableSkipping
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0
- && (Config.config.showAutogeneratedChapters || !hasAutogeneratedChapters()))) {
+ && (getSkipProfileBool("showAutogeneratedChapters") || !hasAutogeneratedChapters()))) {
const chaptersContainer = this.getChaptersContainer();
if (chaptersContainer) {
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
@@ -992,7 +993,7 @@ class PreviewBar {
} else {
this.chapterVote.setVisibility(false);
}
- } else if (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) {
+ } else if (!getSkipProfileBool("showAutogeneratedChapters") && hasAutogeneratedChapters()) {
// Keep original hidden
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
diff --git a/src/messageTypes.ts b/src/messageTypes.ts
index f4ed8c39..ea18a464 100644
--- a/src/messageTypes.ts
+++ b/src/messageTypes.ts
@@ -2,6 +2,7 @@
// Message and Response Types
//
+import { ConfigurationID } from "./config";
import { SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "./types";
interface BaseMessage {
@@ -73,7 +74,12 @@ interface KeyDownMessage {
metaKey: boolean;
}
-export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage | LoopChapterMessage);
+interface SetCurrentTabSkipProfileResponse {
+ message: "setCurrentTabSkipProfile";
+ configID: ConfigurationID | null;
+}
+
+export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage | LoopChapterMessage | SetCurrentTabSkipProfileResponse);
export interface IsInfoFoundMessageResponse {
found: boolean;
@@ -83,7 +89,9 @@ export interface IsInfoFoundMessageResponse {
onMobileYouTube: boolean;
videoID: VideoID;
loopedChapter: SegmentUUID | null;
- channelWhitelisted: boolean;
+ channelID: string;
+ channelAuthor: string;
+ currentTabSkipProfileID: ConfigurationID | null;
}
interface GetVideoIdResponse {
@@ -151,7 +159,8 @@ export type InfoUpdatedMessage = IsInfoFoundMessageResponse & {
export interface VideoChangedPopupMessage {
message: "videoChanged";
videoID: string;
- whitelisted: boolean;
+ channelID: string;
+ channelAuthor: string;
}
export type PopupMessage = TimeUpdateMessage | InfoUpdatedMessage | VideoChangedPopupMessage;
diff --git a/src/options.ts b/src/options.ts
index 75cbf86e..b5c7d75b 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -414,7 +414,7 @@ async function shouldHideOption(element: Element): Promise {
/**
* Called when the config is updated
*/
-function optionsConfigUpdateListener(changes: StorageChangesObject) {
+function optionsConfigUpdateListener() {
const optionsContainer = document.getElementById("options");
const optionsElements = optionsContainer.querySelectorAll("*");
@@ -425,12 +425,6 @@ function optionsConfigUpdateListener(changes: StorageChangesObject) {
break;
}
}
-
- if (changes.categorySelections || changes.payments) {
- for (const chooser of categoryChoosers) {
- chooser.update();
- }
- }
}
function optionsLocalConfigUpdateListener(changes: StorageChangesObject) {
diff --git a/src/popup/PopupComponent.tsx b/src/popup/PopupComponent.tsx
index 0d367d52..67ac932f 100644
--- a/src/popup/PopupComponent.tsx
+++ b/src/popup/PopupComponent.tsx
@@ -1,16 +1,17 @@
import * as React from "react";
import { YourWorkComponent } from "./YourWorkComponent";
-// import { ToggleOptionComponent } from "./ToggleOptionComponent";
-// import { FormattingOptionsComponent } from "./FormattingOptionsComponent";
import { isSafari } from "../../maze-utils/src/config";
import { showDonationLink } from "../utils/configUtils";
-import Config, { generateDebugDetails } from "../config";
-import { GetChannelIDResponse, IsInfoFoundMessageResponse, LogResponse, Message, MessageResponse, PopupMessage } from "../messageTypes";
+import Config, { ConfigurationID, generateDebugDetails } from "../config";
+import { IsInfoFoundMessageResponse, LogResponse, Message, MessageResponse, PopupMessage } from "../messageTypes";
import { AnimationUtils } from "../../maze-utils/src/animationUtils";
import { SegmentListComponent } from "./SegmentListComponent";
import { ActionType, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
import { SegmentSubmissionComponent } from "./SegmentSubmissionComponent";
import { copyToClipboardPopup } from "./popupUtils";
+import { getSkipProfileID, getSkipProfileIDForChannel, getSkipProfileIDForTab, getSkipProfileIDForTime, getSkipProfileIDForVideo, setCurrentTabSkipProfile } from "../utils/skipProfiles";
+import { SelectOptionComponent } from "../components/options/SelectOptionComponent";
+import * as Video from "../../maze-utils/src/video";
export enum LoadingStatus {
Loading,
@@ -26,6 +27,40 @@ export interface LoadingData {
code?: number;
}
+type SkipProfileAction = "forJustThisVideo" | "forThisChannel" | "forThisTab" | "forAnHour" | null;
+interface SkipProfileOption {
+ name: SkipProfileAction;
+ active: () => boolean;
+}
+
+interface SegmentsLoadedProps {
+ setStatus: (status: LoadingData) => void;
+ setVideoID: (videoID: string | null) => void;
+ setCurrentTime: (time: number) => void;
+ setSegments: (segments: SponsorTime[]) => void;
+ setLoopedChapter: (loopedChapter: SegmentUUID | null) => void;
+}
+
+interface LoadSegmentsProps extends SegmentsLoadedProps {
+ updating: boolean;
+}
+
+interface SkipProfileRadioButtonsProps {
+ selected: SkipProfileAction;
+ setSelected: (s: SkipProfileAction, updateConfig: boolean) => void;
+
+ disabled: boolean;
+}
+
+interface SkipOptionActionComponentProps {
+ selected: boolean;
+ setSelected: (s: boolean) => void;
+ highlighted: boolean;
+ disabled: boolean;
+ overridden: boolean;
+ label: string;
+}
+
let loadRetryCount = 0;
export const PopupComponent = () => {
@@ -33,7 +68,6 @@ export const PopupComponent = () => {
status: LoadingStatus.Loading
});
const [extensionEnabled, setExtensionEnabled] = React.useState(!Config.config!.disableSkipping);
- const [channelWhitelisted, setChannelWhitelisted] = React.useState(null);
const [showForceChannelCheckWarning, setShowForceChannelCheckWarning] = React.useState(false);
const [showNoticeButton, setShowNoticeButton] = React.useState(Config.config!.dontShowNotice);
@@ -47,7 +81,6 @@ export const PopupComponent = () => {
loadSegments({
updating: false,
setStatus,
- setChannelWhitelisted,
setVideoID,
setCurrentTime,
setSegments,
@@ -56,7 +89,6 @@ export const PopupComponent = () => {
setupComPort({
setStatus,
- setChannelWhitelisted,
setVideoID,
setCurrentTime,
setSegments,
@@ -107,7 +139,6 @@ export const PopupComponent = () => {
loadSegments({
updating: true,
setStatus,
- setChannelWhitelisted,
setVideoID,
setCurrentTime,
setSegments,
@@ -129,54 +160,10 @@ export const PopupComponent = () => {
{/* Toggle Box */}
- {/* github: mbledkowski/toggle-switch */}
- {channelWhitelisted !== null && (
-
- )}
+