diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index 5438bbab..fe9b9a58 100644
--- a/public/_locales/en/messages.json
+++ b/public/_locales/en/messages.json
@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
- "message": "Set key for skipping a segment"
+ "message": "Skip segment",
+ "description": "Keybind label"
},
"setStartSponsorShortcut": {
- "message": "Set key for start/stop segment keybind"
+ "message": "Start/stop segment",
+ "description": "Keybind label"
},
"setSubmitKeybind": {
- "message": "Set key for submission keybind"
+ "message": "Submit segments",
+ "description": "Keybind label"
},
"keybindDescription": {
- "message": "Select a key by typing it"
- },
- "keybindDescriptionComplete": {
- "message": "The keybind has been set to: "
+ "message": "Select a key by typing it and choose any modifier keys you wish to use."
},
"0": {
"message": "Connection Timeout. Check your internet connection. If your internet is working, the server is probably overloaded or down."
@@ -384,9 +384,6 @@
"createdBy": {
"message": "Created By"
},
- "keybindCurrentlySet": {
- "message": ". It is currently set to:"
- },
"supportOtherSites": {
"message": "Support 3rd Party YouTube-Sites"
},
@@ -511,11 +508,8 @@
"copyDebugInformationComplete": {
"message": "The debug information has been copied to the clip board. Feel free to remove any information you would rather not share. Save this in a text file or paste into the bug report."
},
- "theKey": {
- "message": "The key"
- },
"keyAlreadyUsed": {
- "message": "is bound to another action. Please select another key."
+ "message": "This shortcut is bound to another action. Please select a different one."
},
"to": {
"message": "to",
@@ -859,7 +853,7 @@
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
- "message": "Key Bindings",
+ "message": "Keyboard shortcuts",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabPrivacy": {
@@ -877,5 +871,18 @@
"noticeVisibilityLabel": {
"message": "Skip notice appearance",
"description": "Option label"
+ },
+ "unbind": {
+ "message": "Unbind",
+ "description": "Unbind keyboard shortcut"
+ },
+ "notSet": {
+ "message": "Not set"
+ },
+ "change": {
+ "message": "Change"
+ },
+ "youtubeKeybindWarning": {
+ "message": "This is a built-in YouTube shortcut. Are you sure you want to use it?"
}
}
diff --git a/public/options/options.css b/public/options/options.css
index 1742e0d5..fabd9c4e 100644
--- a/public/options/options.css
+++ b/public/options/options.css
@@ -7,7 +7,7 @@ html {
body {
background-color: #333333;
}
-#navigation {
+#navigation, #keybind-dialog .dialog {
background-color: #181818;
color: white;
}
@@ -41,6 +41,9 @@ h1,h2,h3,h4,h5,h6 {
.categoryTableElement td {
border-top: 1px solid #484848;
}
+.key, #keybind-dialog .dialog {
+ border-color: white;
+}
/* Light mode, if requested */
@media only screen and (prefers-color-scheme: light) {
@@ -50,7 +53,7 @@ h1,h2,h3,h4,h5,h6 {
body {
background-color: #f9f9f9;
}
- #navigation {
+ #navigation, #keybind-dialog .dialog {
background-color: #dbdbdb;
color: #212121;
}
@@ -84,6 +87,9 @@ h1,h2,h3,h4,h5,h6 {
.categoryTableElement td {
border-top: 1px solid #d9d9d9;
}
+ .key, #keybind-dialog .dialog {
+ border-color: black;
+ }
}
html, body {
@@ -140,7 +146,7 @@ html, body {
padding: 20px 0;
}
-.option-group > div:last-child {
+.option-group > div:last-child, .option-group > #keybind-dialog {
border-bottom: inherit;
}
@@ -148,10 +154,94 @@ html, body {
font-size: 14px;
}
+div[data-type="keybind-change"] .optionLabel {
+ display: inline-block;
+ min-width: 150px;
+ margin-right: 20px;
+}
+
input[type='number'] {
width: 50px;
}
+.key {
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 5px;
+ display: inline-block;
+ min-width: 33px;
+ text-align: center;
+ font-weight: bold;
+}
+
+.unbound, .key {
+ padding: 8px;
+}
+
+.keybind-buttons {
+ border-radius: 5px;
+ padding: 5px 3px;
+ cursor: pointer;
+ margin-right: 10px;
+}
+
+.keybind-buttons:hover {
+ background-color: #00000080;
+}
+
+.keybind-buttons > div, .keybind-buttons > span {
+ margin: 0 2px;
+}
+
+#keybind-dialog .dialog {
+ position: fixed;
+ border-width: 3px;
+ border-style: solid;
+ border-radius: 15px;
+ max-height: 100vh;
+ width: 400px;
+ overflow-x: auto;
+ z-index: 100;
+ padding: 15px;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+}
+
+#change-keybind-buttons {
+ float: right;
+}
+
+#change-keybind-buttons > .option-button {
+ margin: 0 2px;
+}
+
+#change-keybind-settings {
+ margin: 15px 15px 30px;
+}
+
+#change-keybind-settings .key {
+ vertical-align: top;
+ margin: 15px 0 0 40px;
+ height: 34px;
+}
+
+#change-keybind-error {
+ margin-bottom: 15px;
+ color: red;
+ font-weight: bold;
+}
+
+.blocker {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 90;
+ background-color: #00000080;
+}
+
.low-profile {
height: 23px;
line-height: 5px;
diff --git a/public/options/options.html b/public/options/options.html
index b843c108..c907889e 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -289,54 +289,19 @@
-
-
- __MSG_setSkipShortcut__
-
-
-
-
- __MSG_keybindDescription__
-
-
-
-
-
-
+
+
+
-
-
-
- __MSG_setStartSponsorShortcut__
-
-
-
-
-
-
- __MSG_keybindDescription__
-
-
-
-
-
-
+
+
+
+
-
-
-
- __MSG_setSubmitKeybind__
-
-
-
-
- __MSG_keybindDescription__
-
-
-
-
-
-
+
+
+
+
diff --git a/src/components/KeybindComponent.tsx b/src/components/KeybindComponent.tsx
new file mode 100644
index 00000000..2c3146ea
--- /dev/null
+++ b/src/components/KeybindComponent.tsx
@@ -0,0 +1,75 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import Config from "../config";
+import { Keybind } from "../types";
+import KeybindDialogComponent from "./KeybindDialogComponent";
+import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
+
+export interface KeybindProps {
+ option: string;
+}
+
+export interface KeybindState {
+ keybind: Keybind;
+}
+
+let dialog;
+
+class KeybindComponent extends React.Component
{
+ constructor(props: KeybindProps) {
+ super(props);
+ this.state = {keybind: Config.config[this.props.option]};
+ }
+
+ render(): React.ReactElement {
+ return(
+ <>
+ this.openEditDialog()}>
+ {this.state.keybind?.ctrl &&
Ctrl
}
+ {this.state.keybind?.ctrl &&
+}
+ {this.state.keybind?.alt &&
Alt
}
+ {this.state.keybind?.alt &&
+}
+ {this.state.keybind?.shift &&
Shift
}
+ {this.state.keybind?.shift &&
+}
+ {this.state.keybind?.key != null &&
{formatKey(this.state.keybind.key)}
}
+ {this.state.keybind == null &&
{chrome.i18n.getMessage("notSet")}}
+
+
+ {this.state.keybind != null &&
+ this.unbind()}>
+ {chrome.i18n.getMessage("unbind")}
+
+ }
+ >
+ );
+ }
+
+ equals(other: Keybind): boolean {
+ return keybindEquals(this.state.keybind, other);
+ }
+
+ toString(): string {
+ return keybindToString(this.state.keybind);
+ }
+
+ openEditDialog(): void {
+ dialog = document.createElement("div");
+ dialog.id = "keybind-dialog";
+ document.body.prepend(dialog);
+ ReactDOM.render( this.closeEditDialog(updateWith)} />, dialog);
+ }
+
+ closeEditDialog(updateWith: Keybind): void {
+ ReactDOM.unmountComponentAtNode(dialog);
+ dialog.remove();
+ if (updateWith != null)
+ this.setState({keybind: updateWith});
+ }
+
+ unbind(): void {
+ this.setState({keybind: null});
+ Config.config[this.props.option] = null;
+ }
+}
+
+export default KeybindComponent;
\ No newline at end of file
diff --git a/src/components/KeybindDialogComponent.tsx b/src/components/KeybindDialogComponent.tsx
new file mode 100644
index 00000000..662256ca
--- /dev/null
+++ b/src/components/KeybindDialogComponent.tsx
@@ -0,0 +1,163 @@
+import * as React from "react";
+import { ChangeEvent } from "react";
+import Config from "../config";
+import { Keybind } from "../types";
+import { keybindEquals, formatKey } from "../utils/configUtils";
+
+export interface KeybindDialogProps {
+ option: string;
+ closeListener: (updateWith) => void;
+}
+
+export interface KeybindDialogState {
+ key: Keybind;
+ error: ErrorMessage;
+}
+
+interface ErrorMessage {
+ message: string;
+ blocking: boolean;
+}
+
+class KeybindDialogComponent extends React.Component {
+
+ constructor(props: KeybindDialogProps) {
+ super(props);
+ this.state = {
+ key: {
+ key: null,
+ code: null,
+ ctrl: false,
+ alt: false,
+ shift: false
+ },
+ error: {
+ message: null,
+ blocking: false
+ }
+ };
+ }
+
+ render(): React.ReactElement {
+ return(
+ <>
+
+
+
{chrome.i18n.getMessage("keybindDescription")}
+
+
+
{formatKey(this.state.key.key)}
+
+
{this.state.error?.message}
+
+
+ >
+ );
+ }
+
+ componentDidMount(): void {
+ document.addEventListener("keydown", this.keybindKeyPressed);
+ }
+
+ componentWillUnmount(): void {
+ document.removeEventListener("keydown", this.keybindKeyPressed);
+ }
+
+ keybindKeyPressed = (e: KeyboardEvent): void => {
+ if (!e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.getModifierState("AltGraph")) {
+ if (e.code == "Escape") {
+ this.props.closeListener(null);
+ return;
+ }
+
+ this.setState({
+ key: {
+ key: e.key,
+ code: e.code,
+ ctrl: this.state.key.ctrl,
+ alt: this.state.key.alt,
+ shift: this.state.key.shift}
+ }, () => this.setState({ error: this.isKeybindAvailable() }));
+ }
+ }
+
+ keybindModifierChecked = (e: ChangeEvent): void => {
+ const id = e.target.id;
+ const val = e.target.checked;
+
+ this.setState({
+ key: {
+ key: this.state.key.key,
+ code: this.state.key.code,
+ ctrl: id == "change-keybind-ctrl" ? val: this.state.key.ctrl,
+ alt: id == "change-keybind-alt" ? val: this.state.key.alt,
+ shift: id == "change-keybind-shift" ? val: this.state.key.shift}
+ }, () => this.setState({ error: this.isKeybindAvailable() }));
+ }
+
+ isKeybindAvailable(): ErrorMessage {
+ if (this.state.key.key == null)
+ return null;
+
+ let youtubeShortcuts: Keybind[];
+ if (/[a-zA-Z0-9,.+\-\][:]/.test(this.state.key.key)) {
+ youtubeShortcuts = [{key: "k"}, {key: "j"}, {key: "l"}, {key: "p", shift: true}, {key: "n", shift: true}, {key: ","}, {key: "."}, {key: ",", shift: true}, {key: ".", shift: true},
+ {key: "ArrowRight"}, {key: "ArrowLeft"}, {key: "ArrowUp"}, {key: "ArrowDown"}, {key: "ArrowRight", ctrl: true}, {key: "ArrowLeft", ctrl: true}, {key: "c"}, {key: "o"},
+ {key: "w"}, {key: "+"}, {key: "-"}, {key: "f"}, {key: "t"}, {key: "i"}, {key: "m"}, {key: "a"}, {key: "s"}, {key: "d"}, {key: "Home"}, {key: "End"},
+ {key: "0"}, {key: "1"}, {key: "2"}, {key: "3"}, {key: "4"}, {key: "5"}, {key: "6"}, {key: "7"}, {key: "8"}, {key: "9"}, {key: "]"}, {key: "["}];
+ } else {
+ youtubeShortcuts = [{key: null, code: "KeyK"}, {key: null, code: "KeyJ"}, {key: null, code: "KeyL"}, {key: null, code: "KeyP", shift: true}, {key: null, code: "KeyN", shift: true},
+ {key: null, code: "Comma"}, {key: null, code: "Period"}, {key: null, code: "Comma", shift: true}, {key: null, code: "Period", shift: true}, {key: null, code: "Space"},
+ {key: null, code: "KeyC"}, {key: null, code: "KeyO"}, {key: null, code: "KeyW"}, {key: null, code: "Equal"}, {key: null, code: "Minus"}, {key: null, code: "KeyF"}, {key: null, code: "KeyT"},
+ {key: null, code: "KeyI"}, {key: null, code: "KeyM"}, {key: null, code: "KeyA"}, {key: null, code: "KeyS"}, {key: null, code: "KeyD"}, {key: null, code: "BracketLeft"}, {key: null, code: "BracketRight"}];
+ }
+
+ for (const shortcut of youtubeShortcuts) {
+ const withShift = Object.assign({}, shortcut);
+ if (!/[0-9]/.test(this.state.key.key)) //shift+numbers don't seem to do anything on youtube, all other keys do
+ withShift.shift = true;
+ if (this.equals(shortcut) || this.equals(withShift))
+ return {message: chrome.i18n.getMessage("youtubeKeybindWarning"), blocking: false};
+ }
+
+ if (this.props.option != "skipKeybind" && this.equals(Config.config['skipKeybind']) ||
+ this.props.option != "submitKeybind" && this.equals(Config.config['submitKeybind']) ||
+ this.props.option != "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind']))
+ return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true};
+
+ return null;
+ }
+
+ equals(other: Keybind): boolean {
+ return keybindEquals(this.state.key, other);
+ }
+
+ save(): void {
+ if (this.state.key.key != null && !this.state.error?.blocking) {
+ Config.config[this.props.option] = this.state.key;
+ this.props.closeListener(this.state.key);
+ }
+ }
+}
+
+export default KeybindDialogComponent;
\ No newline at end of file
diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx
index 6e8876ff..3ad609e5 100644
--- a/src/components/SkipNoticeComponent.tsx
+++ b/src/components/SkipNoticeComponent.tsx
@@ -4,11 +4,11 @@ import Config from "../config"
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
-import SubmissionNotice from "../render/SubmissionNotice";
import Utils from "../utils";
const utils = new Utils();
import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils";
+import { keybindToString } from "../utils/configUtils";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
@@ -367,7 +367,7 @@ class SkipNoticeComponent extends React.Component this.prepAction(SkipNoticeAction.Unskip)}>
- {this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")}
+ {this.state.skipButtonText + (this.state.showKeybindHint ? " (" + keybindToString(Config.config.skipKeybind) + ")" : "")}
);
diff --git a/src/config.ts b/src/config.ts
index 344a7b62..d097133a 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,6 @@
import * as CompileConfig from "../config.json";
-import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types";
+import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes, Keybind } from "./types";
+import { keybindEquals } from "./utils/configUtils";
interface SBConfig {
userID: string,
@@ -10,9 +11,6 @@ interface SBConfig {
defaultCategory: Category,
whitelistedChannels: string[],
forceChannelCheck: boolean,
- skipKeybind: string,
- startSponsorKeybind: string,
- submitKeybind: string,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
@@ -53,6 +51,10 @@ interface SBConfig {
scrollToEditTimeUpdate: boolean,
fillerUpdate: boolean,
+ skipKeybind: Keybind,
+ startSponsorKeybind: Keybind,
+ submitKeybind: Keybind,
+
// What categories should be skipped
categorySelections: CategorySelection[],
@@ -167,9 +169,6 @@ const Config: SBObject = {
defaultCategory: "chooseACategory" as Category,
whitelistedChannels: [],
forceChannelCheck: false,
- skipKeybind: "Enter",
- startSponsorKeybind: ";",
- submitKeybind: "'",
minutesSaved: 0,
skipCount: 0,
sponsorTimesContributed: 0,
@@ -205,6 +204,17 @@ const Config: SBObject = {
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
fillerUpdate: false,
+ /**
+ * Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none).
+ * Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales.
+ * The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't.
+ * Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"!
+ * TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map?
+ */
+ skipKeybind: {key: "Enter"},
+ startSponsorKeybind: {key: ";"},
+ submitKeybind: {key: "'"},
+
categorySelections: [{
name: "sponsor" as Category,
option: CategorySkipOption.AutoSkip
@@ -425,6 +435,29 @@ function migrateOldFormats(config: SBConfig) {
}
}
+ if (typeof config["skipKeybind"] == "string") {
+ config["skipKeybind"] = {key: config["skipKeybind"]};
+ }
+
+ if (typeof config["startSponsorKeybind"] == "string") {
+ config["startSponsorKeybind"] = {key: config["startSponsorKeybind"]};
+ }
+
+ if (typeof config["submitKeybind"] == "string") {
+ config["submitKeybind"] = {key: config["submitKeybind"]};
+ }
+
+ // Unbind key if it matches a previous one set by the user (should be ordered oldest to newest)
+ const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"];
+ for (let i = keybinds.length-1; i >= 0; i--) {
+ for (let j = 0; j < keybinds.length; j++) {
+ if (i == j)
+ continue;
+ if (keybindEquals(config[keybinds[i]], config[keybinds[j]]))
+ config[keybinds[i]] = null;
+ }
+ }
+
// Remove some old unused options
if (config["sponsorVideoID"] !== undefined) {
chrome.storage.sync.remove("sponsorVideoID");
diff --git a/src/content.ts b/src/content.ts
index c4bdafef..acf34ce2 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -1,7 +1,7 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types";
-import { ContentContainer } from "./types";
+import { ContentContainer, Keybind } from "./types";
import Utils from "./utils";
const utils = new Utils();
@@ -18,6 +18,7 @@ import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { Tooltip } from "./render/Tooltip";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { getControls } from "./utils/pageUtils";
+import { keybindEquals } from "./utils/configUtils";
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -132,6 +133,9 @@ const manualSkipPercentCount = 0.5;
//get messages from the background script and the popup
chrome.runtime.onMessage.addListener(messageListener);
+
+//store pressed modifier keys
+const pressedKeys = new Set();
function messageListener(request: Message, sender: unknown, sendResponse: (response: MessageResponse) => void): void | boolean {
//messages from popup script
@@ -1249,7 +1253,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
&& skippingSegments.length === 1
&& getCategoryActionType(skippingSegments[0].category) === CategoryActionType.POI) {
skipButtonControlBar.enable(skippingSegments[0]);
- if (onMobileYouTube) skipButtonControlBar.setShowKeybindHint(false);
+ if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar;
@@ -1258,7 +1262,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
//send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) {
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime);
- if (onMobileYouTube) newSkipNotice.setShowKeybindHint(false);
+ if (onMobileYouTube || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
skipNotices.push(newSkipNotice);
activeSkipKeybindElement?.setShowKeybindHint(false);
@@ -1839,30 +1843,46 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener);
+ document.addEventListener("keyup", (e) => pressedKeys.delete(e.key));
}
function hotkeyListener(e: KeyboardEvent): void {
if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|| document.activeElement?.id?.toLowerCase()?.includes("editable")) return;
- const key = e.key;
+ if (["Alt", "Control", "Shift", "AltGraph"].includes(e.key)) {
+ pressedKeys.add(e.key);
+ return;
+ }
+
+ const key:Keybind = {key: e.key, code: e.code, alt: pressedKeys.has("Alt"), ctrl: pressedKeys.has("Control"), shift: pressedKeys.has("Shift")};
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
- switch (key) {
- case skipKey:
- if (activeSkipKeybindElement) {
+ if (!pressedKeys.has("AltGraph")) {
+ if (keybindEquals(key, skipKey)) {
+ if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
- }
- break;
- case startSponsorKey:
+ return;
+ } else if (keybindEquals(key, startSponsorKey)) {
startOrEndTimingNewSegment();
- break;
- case submitKey:
+ return;
+ } else if (keybindEquals(key, submitKey)) {
submitSponsorTimes();
- break;
+ return;
+ }
+ }
+
+ //legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
+ if (key.key == skipKey?.key && skipKey.code == null && !keybindEquals(Config.defaults.skipKeybind, skipKey)) {
+ if (activeSkipKeybindElement)
+ activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
+ } else if (key.key == startSponsorKey?.key && startSponsorKey.code == null && !keybindEquals(Config.defaults.startSponsorKeybind, startSponsorKey)) {
+ startOrEndTimingNewSegment();
+ } else if (key.key == submitKey?.key && submitKey.code == null && !keybindEquals(Config.defaults.submitKeybind, submitKey)) {
+ submitSponsorTimes();
}
}
diff --git a/src/js-components/skipButtonControlBar.ts b/src/js-components/skipButtonControlBar.ts
index 32018307..48048632 100644
--- a/src/js-components/skipButtonControlBar.ts
+++ b/src/js-components/skipButtonControlBar.ts
@@ -1,6 +1,7 @@
import Config from "../config";
import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
+import { keybindToString } from "../utils/configUtils";
import Utils from "../utils";
const utils = new Utils();
@@ -179,7 +180,7 @@ export class SkipButtonControlBar {
}
private getTitle(): string {
- return getSkippingText([this.segment], false) + (this.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "");
+ return getSkippingText([this.segment], false) + (this.showKeybindHint ? " (" + keybindToString(Config.config.skipKeybind) + ")" : "");
}
private getChapterPrefix(): HTMLElement {
diff --git a/src/options.ts b/src/options.ts
index 2e714ce4..6f82cc9f 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -1,3 +1,6 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+
import Config from "./config";
import * as CompileConfig from "../config.json";
@@ -6,6 +9,7 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
+import KeybindComponent from "./components/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
const utils = new Utils();
@@ -193,9 +197,7 @@ async function init() {
break;
}
case "keybind-change": {
- const keybindButton = optionsElements[i].querySelector(".trigger-button");
- keybindButton.addEventListener("click", () => activateKeybindChange( optionsElements[i]));
-
+ ReactDOM.render(React.createElement(KeybindComponent, {option: option}), optionsElements[i].querySelector("div"));
break;
}
case "display": {
@@ -448,91 +450,6 @@ async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Pro
});
}
-/**
- * Will trigger the container to ask the user for a keybind.
- *
- * @param element
- */
-function activateKeybindChange(element: HTMLElement) {
- const button = element.querySelector(".trigger-button");
- if (button.classList.contains("disabled")) return;
-
- button.classList.add("disabled");
-
- const option = element.getAttribute("data-sync");
-
- const currentlySet = Config.config[option] !== null ? chrome.i18n.getMessage("keybindCurrentlySet") : "";
-
- const status = element.querySelector(".option-hidden-section > .keybind-status");
- status.innerText = chrome.i18n.getMessage("keybindDescription") + currentlySet;
-
- if (Config.config[option] !== null) {
- const statusKey = element.querySelector(".option-hidden-section > .keybind-status-key");
- statusKey.innerText = Config.config[option];
- }
-
- element.querySelector(".option-hidden-section").classList.remove("hidden");
-
- document.addEventListener("keydown", (e) => keybindKeyPressed(element, e), {once: true});
-}
-
-/**
- * Called when a key is pressed in an activiated keybind change option.
- *
- * @param element
- * @param e
- */
-function keybindKeyPressed(element: HTMLElement, e: KeyboardEvent) {
- const key = e.key;
-
- if (["Shift", "Control", "Meta", "Alt", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"].indexOf(key) !== -1) {
-
- // Wait for more
- document.addEventListener("keydown", (e) => keybindKeyPressed(element, e), {once: true});
- } else {
- const button: HTMLElement = element.querySelector(".trigger-button");
- const option = element.getAttribute("data-sync");
-
- // Make sure keybind isn't used by the other listener
- // TODO: If other keybindings are going to be added, we need a better way to find the other keys used.
- const otherKeybind = (option === "startSponsorKeybind") ? Config.config['submitKeybind'] : Config.config['startSponsorKeybind'];
- if (key === otherKeybind) {
- closeKeybindOption(element, button);
-
- alert(chrome.i18n.getMessage("theKey") + " " + key + " " + chrome.i18n.getMessage("keyAlreadyUsed"));
- return;
- }
-
- // cancel setting a keybind
- if (key === "Escape") {
- closeKeybindOption(element, button);
-
- return;
- }
-
- Config.config[option] = key;
-
- const status = element.querySelector(".option-hidden-section > .keybind-status");
- status.innerText = chrome.i18n.getMessage("keybindDescriptionComplete");
-
- const statusKey = element.querySelector(".option-hidden-section > .keybind-status-key");
- statusKey.innerText = key;
-
- button.classList.remove("disabled");
- }
-}
-
-/**
- * Closes the menu for editing the keybind
- *
- * @param element
- * @param button
- */
-function closeKeybindOption(element: HTMLElement, button: HTMLElement) {
- element.querySelector(".option-hidden-section").classList.add("hidden");
- button.classList.remove("disabled");
-}
-
/**
* Will trigger the textbox to appear to be able to change an option's text.
*
diff --git a/src/types.ts b/src/types.ts
index 1caf257c..30e8f38a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -222,4 +222,12 @@ export enum NoticeVisbilityMode {
MiniForAll = 2,
FadedForAutoSkip = 3,
FadedForAll = 4
+}
+
+export type Keybind = {
+ key: string,
+ code?: string,
+ ctrl?: boolean,
+ alt?: boolean,
+ shift?: boolean
}
\ No newline at end of file
diff --git a/src/utils/configUtils.ts b/src/utils/configUtils.ts
index 8aec5208..e0939858 100644
--- a/src/utils/configUtils.ts
+++ b/src/utils/configUtils.ts
@@ -1,5 +1,44 @@
import Config from "../config";
+import { Keybind } from "../types";
export function showDonationLink(): boolean {
return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink;
+}
+
+export function keybindEquals(first: Keybind, second: Keybind): boolean {
+ if (first == null || second == null ||
+ Boolean(first.alt) != Boolean(second.alt) || Boolean(first.ctrl) != Boolean(second.ctrl) || Boolean(first.shift) != Boolean(second.shift) ||
+ first.key == null && first.code == null || second.key == null && second.code == null)
+ return false;
+ if (first.code != null && second.code != null)
+ return first.code === second.code;
+ if (first.key != null && second.key != null)
+ return first.key.toUpperCase() === second.key.toUpperCase();
+ return false;
+}
+
+export function formatKey(key: string): string {
+ if (key == null)
+ return "";
+ else if (key == " ")
+ return "Space";
+ else if (key.length == 1)
+ return key.toUpperCase();
+ else
+ return key;
+}
+
+export function keybindToString(keybind: Keybind): string {
+ if (keybind == null || keybind.key == null)
+ return "";
+
+ let ret = "";
+ if (keybind.ctrl)
+ ret += "Ctrl+";
+ if (keybind.alt)
+ ret += "Alt+";
+ if (keybind.shift)
+ ret += "Shift+";
+
+ return ret += formatKey(keybind.key);
}
\ No newline at end of file