multikey shortcuts

This commit is contained in:
Áron Hegymegi-Kiss
2022-01-08 17:07:37 +01:00
parent 3b54fde3f9
commit 90eaab4e50
12 changed files with 493 additions and 175 deletions

View File

@@ -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?"
}
}

View File

@@ -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;

View File

@@ -290,53 +290,18 @@
<div id="keybinds" class="option-group hidden">
<div data-type="keybind-change" data-sync="skipKeybind">
<div class="option-button trigger-button">
__MSG_setSkipShortcut__
</div>
<div class="option-hidden-section hidden">
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
<label class="optionLabel">__MSG_setSkipShortcut__:</label>
<div class="inline"></div>
</div>
<div data-type="keybind-change" data-sync="startSponsorKeybind">
<div class="option-button trigger-button">
__MSG_setStartSponsorShortcut__
</div>
<div class="option-hidden-section hidden">
<br/>
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
<label class="optionLabel">__MSG_setStartSponsorShortcut__:</label>
<div class="inline"></div>
</div>
<div data-type="keybind-change" data-sync="submitKeybind">
<div class="option-button trigger-button">
__MSG_setSubmitKeybind__
</div>
<div class="option-hidden-section hidden">
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
<label class="optionLabel">__MSG_setSubmitKeybind__:</label>
<div class="inline"></div>
</div>
</div>

View File

@@ -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<KeybindProps, KeybindState> {
constructor(props: KeybindProps) {
super(props);
this.state = {keybind: Config.config[this.props.option]};
}
render(): React.ReactElement {
return(
<>
<div className="keybind-buttons inline" title={chrome.i18n.getMessage("change")} onClick={() => this.openEditDialog()}>
{this.state.keybind?.ctrl && <div className="key keyControl">Ctrl</div>}
{this.state.keybind?.ctrl && <span className="keyControl">+</span>}
{this.state.keybind?.alt && <div className="key keyAlt">Alt</div>}
{this.state.keybind?.alt && <span className="keyAlt">+</span>}
{this.state.keybind?.shift && <div className="key keyShift">Shift</div>}
{this.state.keybind?.shift && <span className="keyShift">+</span>}
{this.state.keybind?.key != null && <div className="key keyBase">{formatKey(this.state.keybind.key)}</div>}
{this.state.keybind == null && <span className="unbound">{chrome.i18n.getMessage("notSet")}</span>}
</div>
{this.state.keybind != null &&
<div className="option-button trigger-button inline" onClick={() => this.unbind()}>
{chrome.i18n.getMessage("unbind")}
</div>
}
</>
);
}
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(<KeybindDialogComponent option={this.props.option} closeListener={(updateWith) => 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;

View File

@@ -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<KeybindDialogProps, KeybindDialogState> {
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(
<>
<div className="blocker"></div>
<div className="dialog">
<div id="change-keybind-description">{chrome.i18n.getMessage("keybindDescription")}</div>
<div id="change-keybind-settings">
<div id="change-keybind-modifiers" className="inline">
<div>
<input id="change-keybind-ctrl" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-ctrl">Ctrl</label>
</div>
<div>
<input id="change-keybind-alt" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-alt">Alt</label>
</div>
<div>
<input id="change-keybind-shift" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-shift">Shift</label>
</div>
</div>
<div className="key inline">{formatKey(this.state.key.key)}</div>
</div>
<div id="change-keybind-error">{this.state.error?.message}</div>
<div id="change-keybind-buttons">
<div className={"option-button save-button inline" + ((this.state.error?.blocking || this.state.key.key == null) ? " disabled" : "")} onClick={() => this.save()}>
{chrome.i18n.getMessage("save")}
</div>
<div className="option-button cancel-button inline" onClick={() => this.props.closeListener(null)}>
{chrome.i18n.getMessage("cancel")}
</div>
</div>
</div>
</>
);
}
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<HTMLInputElement>): 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;

View File

@@ -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<SkipNoticeProps, SkipNoticeSta
className="sponsorSkipObject sponsorSkipNoticeButton"
style={style}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")}
{this.state.skipButtonText + (this.state.showKeybindHint ? " (" + keybindToString(Config.config.skipKeybind) + ")" : "")}
</button>
</span>
);

View File

@@ -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");

View File

@@ -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);
@@ -133,6 +134,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
switch(request.message){
@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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(<HTMLElement> 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 = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescription") + currentlySet;
if (Config.config[option] !== null) {
const statusKey = <HTMLElement> 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 = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescriptionComplete");
const statusKey = <HTMLElement> 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.
*

View File

@@ -223,3 +223,11 @@ export enum NoticeVisbilityMode {
FadedForAutoSkip = 3,
FadedForAll = 4
}
export type Keybind = {
key: string,
code?: string,
ctrl?: boolean,
alt?: boolean,
shift?: boolean
}

View File

@@ -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);
}