mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-08 12:37:05 +03:00
multikey shortcuts
This commit is contained in:
@@ -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?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -289,54 +289,19 @@
|
||||
|
||||
<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>
|
||||
<div data-type="keybind-change" data-sync="skipKeybind">
|
||||
<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>
|
||||
|
||||
<div data-type="keybind-change" data-sync="startSponsorKeybind">
|
||||
<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>
|
||||
|
||||
<div data-type="keybind-change" data-sync="submitKeybind">
|
||||
<label class="optionLabel">__MSG_setSubmitKeybind__:</label>
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
75
src/components/KeybindComponent.tsx
Normal file
75
src/components/KeybindComponent.tsx
Normal 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;
|
||||
163
src/components/KeybindDialogComponent.tsx
Normal file
163
src/components/KeybindDialogComponent.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user