aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAjay Ramachandran <[email protected]>2022-01-24 23:41:21 -0500
committerGitHub <[email protected]>2022-01-24 23:41:21 -0500
commitf555a8e7bbb84e34f7547be47dbc00ba51df63f4 (patch)
tree74fc35ede4281b0fed0b7916b2157d0ad5a8d29f
parent23a6940894b63d314a1de211e0505aceda7e2553 (diff)
parentaac2572b4e6c61226d3b5f8efa30ea95a13571c5 (diff)
downloadSponsorBlock-f555a8e7bbb84e34f7547be47dbc00ba51df63f4.tar.gz
SponsorBlock-f555a8e7bbb84e34f7547be47dbc00ba51df63f4.zip
Merge pull request #1093 from AronHK/settings
Settings rework
-rw-r--r--public/_locales/en/messages.json71
-rw-r--r--public/help/index.html1
-rw-r--r--public/help/styles.css135
-rw-r--r--public/options/options.css423
-rw-r--r--public/options/options.html878
-rw-r--r--src/components/CategoryChooserComponent.tsx16
-rw-r--r--src/components/KeybindComponent.tsx75
-rw-r--r--src/components/KeybindDialogComponent.tsx165
-rw-r--r--src/components/SkipNoticeComponent.tsx4
-rw-r--r--src/components/SponsorTimeEditComponent.tsx2
-rw-r--r--src/config.ts49
-rw-r--r--src/content.ts46
-rw-r--r--src/help.ts4
-rw-r--r--src/js-components/skipButtonControlBar.ts3
-rw-r--r--src/options.ts374
-rw-r--r--src/popup.ts6
-rw-r--r--src/types.ts8
-rw-r--r--src/utils/configUtils.ts39
18 files changed, 1537 insertions, 762 deletions
diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index 90f5df92..b1be0523 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."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Created By"
},
- "keybindCurrentlySet": {
- "message": ". It is currently set to:"
- },
"supportOtherSites": {
"message": "Support 3rd Party YouTube-Sites"
},
@@ -470,6 +467,15 @@
"exportOptions": {
"message": "Import/Export All Options"
},
+ "exportOptionsCopy": {
+ "message": "Edit/copy"
+ },
+ "exportOptionsDownload": {
+ "message": "Save to file"
+ },
+ "exportOptionsUpload": {
+ "message": "Load from file"
+ },
"whatExportOptions": {
"message": "This is your entire configuration in JSON. This includes your userID, so be sure to share this wisely."
},
@@ -518,11 +524,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",
@@ -787,6 +790,9 @@
"hideDonationLink": {
"message": "Hide Donation Link"
},
+ "darkModeOptionsPage": {
+ "message": "Dark Mode On Options Page"
+ },
"helpPageThanksForInstalling": {
"message": "Thanks for installing SponsorBlock."
},
@@ -875,5 +881,42 @@
"hourAbbreviation": {
"message": "h",
"description": "100h"
+ },
+ "optionsTabBehavior": {
+ "message": "Behavior",
+ "description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
+ },
+ "optionsTabInterface": {
+ "message": "Interface",
+ "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": "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)."
+ },
+ "optionsTabBackup": {
+ "message": "Backup/Restore",
+ "description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
+ },
+ "optionsTabAdvanced": {
+ "message": "Miscellaneous",
+ "description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
+ },
+ "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/help/index.html b/public/help/index.html
index 88742e0d..ee886089 100644
--- a/public/help/index.html
+++ b/public/help/index.html
@@ -4,6 +4,7 @@
<title> SponsorBlock </title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="../icons/IconSponsorBlocker32px.png" type="image/png">
<link href="styles.css" rel="stylesheet"/>
diff --git a/public/help/styles.css b/public/help/styles.css
index fea609a2..99ff809d 100644
--- a/public/help/styles.css
+++ b/public/help/styles.css
@@ -1,5 +1,31 @@
+:root {
+ --color-scheme: dark;
+ --background: #333333;
+ --header-color: #212121;
+ --dialog-background: #181818;
+ --dialog-border: white;
+ --text: #c4c4c4;
+ --title: #dad8d8;
+ --disabled: #520000;
+ --black: black;
+ --white: white;
+}
+
+[data-theme="light"] {
+ --color-scheme: light;
+ --background: #f9f9f9;
+ --header-color: white;
+ --dialog-background: #f9f9f9;
+ --dialog-border: #282828;
+ --text: #262626;
+ --title: #707070;
+ --disabled: #ffcaca;
+ --black: white;
+ --white: black;
+}
+
html {
- color-scheme: dark;
+ color-scheme: var(--color-scheme);
}
.bigText {
@@ -7,7 +33,7 @@ html {
}
body {
- background-color: #333333;
+ background-color: var(--background);
font-family: sans-serif;
}
@@ -15,6 +41,10 @@ body {
text-align: center;
}
+.inline {
+ display: inline-block;
+}
+
.container {
max-width: 60%;
margin: auto;
@@ -54,12 +84,14 @@ body {
vertical-align: middle;
font-size: 50px;
- color: #212121;
+ color: var(--header-color);
padding: 20px;
text-decoration: none;
+ border-radius: 15px;
+
transition: font-size 1s;
}
@@ -125,8 +157,8 @@ p,li {
font-size: 16px;
}
-p,li,a {
- color: #c4c4c4;
+p,li,a,span,div {
+ color: var(--text);
}
p,li,code,a {
@@ -160,7 +192,7 @@ img {
}
h1,h2,h3,h4,h5,h6 {
- color: #dad8d8;
+ color: var(--title);
text-align: center;
}
@@ -199,4 +231,95 @@ svg {
p,li,code,a {
text-align: center;
}
+}
+
+/* keybind dialog */
+.key {
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 5px;
+ display: inline-block;
+ min-width: 33px;
+ text-align: center;
+ font-weight: bold;
+ border-color: var(--white);
+ box-sizing: border-box;
+}
+
+.unbound, .key {
+ padding: 8px;
+}
+
+#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%);
+ font-size: 14px;
+ background-color: var(--dialog-background);
+ border-color: var(--dialog-border);
+}
+
+#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;
+}
+
+.option-button {
+ cursor: pointer;
+
+ background-color: #c00000;
+ padding: 10px;
+ color: white;
+ border-radius: 5px;
+ font-size: 14px;
+
+ width: max-content;
+}
+
+.option-button:hover:not(.disabled) {
+ background-color: #fc0303;
+}
+
+.option-button.disabled {
+ cursor: default;
+ background-color: var(--disabled);
+ color: grey;
} \ No newline at end of file
diff --git a/public/options/options.css b/public/options/options.css
index 708139ab..c43f8c67 100644
--- a/public/options/options.css
+++ b/public/options/options.css
@@ -1,10 +1,232 @@
/* Options page CSS */
-html {
- color-scheme: dark;
+
+:root {
+ --color-scheme: dark;
+ --background: #333333;
+ --menu-background: #181818;
+ --menu-foreground: white;
+ --dialog-background: #181818;
+ --dialog-border: white;
+ --tab-color: #242424;
+ --tab-button-hover: #4d0000;
+ --tab-hover: white;
+ --description: #dfdfdf;
+ --disabled: #520000;
+ --slider: #707070;
+ --title: #dad8d8;
+ --border-color: #484848;
+ --black: black;
+ --white: white;
+}
+
+[data-theme="light"] {
+ --color-scheme: light;
+ --background: #f9f9f9;
+ --menu-background: #dbdbdb;
+ --menu-foreground: #212121;
+ --dialog-background: #f9f9f9;
+ --dialog-border: #282828;
+ --tab-color: #ababab;
+ --tab-button-hover: #750000;
+ --tab-hover: #2e2e2e;
+ --description: #262626;
+ --disabled: #ffcaca;
+ --slider: #bfbebe;
+ --title: #707070;
+ --border-color: #d9d9d9;
+ --black: white;
+ --white: black;
+}
+
+.medium-description, .switch-container, .optionLabel, .categoryTableElement {
+ color: var(--white);
+}
+
+.small-description, p, li, span, div {
+ color: var(--description);
+}
+
+h1,h2,h3,h4,h5,h6 {
+ color: var(--title);
}
-body {
+html, body {
+ color-scheme: var(--color-scheme);
font-family: sans-serif;
+ margin: 0;
+ font-size: 14px;
+ background-color: var(--background);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+#options-container {
+ display: flex;
+}
+
+#menubar {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ flex-basis: 20%;
+ min-width: 300px;
+ max-width: 600px;
+ border-radius: 15px;
+ margin: 15px;
+ z-index: 10;
+ background-color: var(--menu-background);
+ color: var(--menu-foreground);
+}
+
+#navigation {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
+
+.tab-heading {
+ font-size: 18px;
+ height: 55px;
+ line-height: 55px;
+ width: 80%;
+ margin: 0 auto;
+ border-radius: 15px;
+ cursor: pointer;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ background-color: var(--tab-color);
+ color: var(--white);
+}
+
+.tab-heading:hover {
+ background-color: var(--tab-button-hover);
+ color: white;
+}
+
+.tab-heading.selected {
+ background-color: #c00000;
+ color: white;
+}
+
+.tab-heading:active {
+ background-color: #950000;
+ color: white;
+}
+
+.option-group > div {
+ min-height: 50px;
+ padding: 20px 0;
+ border-bottom: 1px solid var(--border-color);
+ border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
+}
+
+.option-group > div:last-child, .option-group > #keybind-dialog {
+ border-bottom: inherit;
+}
+
+.optionLabel, #version {
+ font-size: 14px;
+ height: 15px;
+}
+
+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;
+ border-color: var(--white);
+}
+
+.unbound, .key {
+ padding: 8px;
+}
+
+.keybind-buttons {
+ border-radius: 5px;
+ padding: 5px 3px;
+ cursor: pointer;
+ margin-right: 10px;
+}
+
+.keybind-buttons:hover {
+ background-color: #00000030;
+}
+
+.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%);
+ background-color: var(--dialog-background);
+ border-color: var(--dialog-border);
+}
+
+#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;
+ vertical-align: middle;
}
.center {
@@ -15,25 +237,49 @@ body {
display: inline-block;
}
+.next-line {
+ padding: 15px 0 0 0;
+}
+
.bold {
font-weight: bold;
}
+.hiding {
+ opacity: 0;
+}
+
.hidden {
display: none !important;
}
+.spacing {
+ margin-top: 15px;
+}
+
.keybind-status {
display: inline;
}
.small-description {
- color: white;
font-size: 13px;
+ padding: 15px 0 0 20px;
+}
+
+.small-description td {
+ padding: 10px 0 20px 20px;
+}
+
+.indent {
+ padding-left: 20px;
+}
+
+.categoryTableElement td {
+ padding-top: 10px;
+ border-top: 1px solid var(--border-color);
}
.medium-description {
- color: white;
font-size: 15px;
}
@@ -53,36 +299,46 @@ body {
width: max-content;
}
-.option-button:hover {
+.option-button:hover:not(.disabled) {
background-color: #fc0303;
}
.option-button.disabled {
cursor: default;
-
- background-color: #520000;
+ background-color: var(--disabled);
color: grey;
}
#options {
- max-width: 60%;
+ height: 100vh;
+ flex-basis: 80%;
+ overflow: auto;
text-align: left;
- display: inline-block;
+ padding: 80px 15% 0 3%;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+
+ transition: padding 0.3s;
}
-#options.embed {
+#options > div {
+ max-width: 60%;
+}
+
+#options.embed > div {
max-width: 100%;
- text-align: left;
- display: inline-block;
+}
+
+#title .profilepic {
+ height: 60px;
}
.switch-container {
content: attr(label-name);
- position: absolute;
width: max-content;
font-size: 14px;
- color: white;
display: table;
}
@@ -94,11 +350,6 @@ body {
padding: 4px;
}
-.text-label-container {
- font-size: 14px;
- color: white;
-}
-
.switch {
position: relative;
display: inline-block;
@@ -119,7 +370,7 @@ body {
left: 0;
right: 0;
bottom: 0;
- background-color: #707070;
+ background-color: var(--slider);
}
.animated * {
@@ -162,11 +413,8 @@ input:checked + .slider:before {
}
-/* Boilerplate CSS from https://ajay.app */
-body {
- background-color: #333333;
-}
+/* Boilerplate CSS from https://ajay.app (edited) */
.projectPreview {
position: relative;
@@ -196,29 +444,25 @@ body {
transform: translateY(-50%);
}
-.createdBy {
- font-size: 14px;
+#createdBy {
text-align: center;
- padding-top: 0px;
- padding-bottom: 0px;
+ margin: auto 0 10px 0;
+ height: 50px;
+}
- display: inline-block;
+#createdBy > * {
+ font-size: 14px;
}
#title {
- background-color: #636363;
-
text-align: center;
vertical-align: middle;
- font-size: 50px;
- color: #212121;
+ font-size: 40px;
- padding: 20px;
+ padding: 40px 20px;
text-decoration: none;
-
- transition: font-size 1s;
}
.subtitle {
@@ -237,7 +481,6 @@ body {
}
.profilepic {
- background-color: #636363 !important;
vertical-align: middle;
}
@@ -281,21 +524,9 @@ a {
p,li {
font-size: 20px;
- color: #c4c4c4;
-
padding: 10px;
}
-@media screen and (orientation:portrait) {
- #options {
- max-width: 100%;
- }
-
- .previewColorOption {
- display: none;
- }
-}
-
.previewImage {
max-height: 200px;
}
@@ -316,10 +547,6 @@ img {
color: #dad8d8;
}
-h1,h2,h3,h4,h5,h6 {
- color: #dad8d8;
-}
-
svg {
text-decoration: none;
}
@@ -337,8 +564,6 @@ svg {
.categoryTableElement {
font-size: 16px;
-
- color: white;
}
.categoryTableElement > * {
@@ -368,4 +593,88 @@ svg {
#sbDonate {
font-size: 10px;
-} \ No newline at end of file
+}
+
+
+/* Handle smaller screensizes */
+@media only screen and (max-width: 1600px){
+ #options {
+ padding-right: 8%;
+ }
+}
+
+@media only screen and (max-height: 770px), only screen and (max-width: 1200px) {
+ #options-container {
+ flex-direction: column;
+ }
+ #menubar {
+ gap: 8px;
+ min-width: unset;
+ max-width: unset;
+ padding: 8px;
+ }
+ #navigation {
+ gap: 8px;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ #options {
+ padding: 0 50px;
+ }
+
+ .tab-heading {
+ width: unset;
+ min-width: unset;
+ height: 35px;
+ line-height: 35px;
+ font-size: 16px;
+ padding: 0 10px;
+ margin: 0;
+ }
+ #title {
+ width: 100%;
+ font-size: 30px;
+ padding: 10px;
+ }
+ #title .profilepic {
+ height: 40px;
+ }
+ #createdBy {
+ margin: 10px 0 0 0;
+ height: unset;
+ width: 100%;
+ }
+ #createdBy > div {
+ display: inline-block;
+ }
+ #sbDonate {
+ position: absolute;
+ right: 30px;
+ margin-top: 10px;
+ }
+ #version {
+ font-size: 10px;
+ height: 10px;
+ transform: translate(-50px, -5px);
+ }
+ .sticky #menubar {
+ position: fixed;
+ left: 0;
+ right: 0;
+ margin: 0 15px;
+ }
+ .sticky #title, .sticky #createdBy {
+ display: none;
+ }
+ }
+
+ @media only screen and (max-width: 800px) {
+ #options {
+ padding: 0 15px;
+ }
+
+ #options > div {
+ max-width: 100%;
+ }
+ } \ No newline at end of file
diff --git a/public/options/options.html b/public/options/options.html
index 657b2d4d..189f23ad 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -1,9 +1,11 @@
<!DOCTYPE html>
+<!-- Link to specific tabs by using their ID in the URL like: options.html#keybinds -->
<head>
<title>Options - SponsorBlock</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="../icons/IconSponsorBlocker32px.png" type="image/png">
<link href="options.css" rel="stylesheet"/>
@@ -13,608 +15,478 @@
<body class="sponsorBlockPageBody">
- <div id="title" class="titleBar">
- <img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic"/>
- SponsorBlock
- </div>
-
- <div class="center">
- <p class="createdBy titleBar">
- <img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle"/>
- __MSG_createdBy__
- <a href="https://ajay.app">Ajay Ramachandran</a>
- <a href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener" id="sbDonate">(__MSG_Donate__)</a>
- </p>
-
- <h1>__MSG_Options__</h1>
-
- <div id="options" class="hidden">
+ <div id="options-container">
- <div id="category-type" option-type="react-CategoryChooserComponent">
+ <div id="menubar" class="center">
+ <div id="title" class="titleBar">
+ <img src="../icons/LogoSponsorBlocker256px.png" class="profilepic" alt="SponsorBlock logo"/>
+ SponsorBlock
+ <div id="version"></div>
</div>
- <div option-type="toggle" sync-option="autoSkipOnMusicVideos">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_autoSkipOnMusicVideos__
- </div>
- </label>
+ <div id="navigation">
+ <div class="tab-heading" data-for="behavior">
+ __MSG_optionsTabBehavior__
+ </div>
- <br/>
- <br/>
- <br/>
- </div>
+ <div class="tab-heading" data-for="interface">
+ __MSG_optionsTabInterface__
+ </div>
- <div option-type="toggle" sync-option="muteSegments">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_muteSegments__
- </div>
- </label>
+ <div class="tab-heading" data-for="keybinds">
+ __MSG_optionsTabKeyBinds__
+ </div>
- <br/>
- <br/>
- <br/>
- </div>
+ <div class="tab-heading" data-for="import">
+ __MSG_optionsTabBackup__
+ </div>
- <div option-type="toggle" sync-option="fullVideoSegments">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_fullVideoSegments__
- </div>
- </label>
+ <div class="tab-heading" data-for="advanced">
+ __MSG_optionsTabAdvanced__
+ </div>
+ </div>
- <br/>
- <br/>
- <br/>
+ <div id="createdBy" class="titleBar">
+ <div>
+ <img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle" alt="profile picture of creator"/>
+ __MSG_createdBy__
+ <a href="https://ajay.app">Ajay Ramachandran</a>
+ </div>
+ <a href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener" id="sbDonate">(__MSG_Donate__)</a>
</div>
- <br/>
- <br/>
+ </div>
- <div id="support-invidious" option-type="toggle" sync-option="supportInvidious" no-safari="true">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox">
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_supportOtherSites__
- </div>
- </label>
-
- <br/>
- <br/>
- <br/>
-
- <div class="small-description">(__MSG_supportedSites__ Invidious, CloudTube)</div>
- <br/>
- <span class="small-description">__MSG_supportOtherSitesDescription__ </span>
+ <div id="options">
- <br/>
- <br/>
- <br/>
- </div>
-
- <div option-type="private-text-change" sync-option="invidiousInstances" no-safari="true">
- <div class="option-button trigger-button">
- __MSG_addInvidiousInstance__
- </div>
-
- <br/>
-
- <div class="small-description">__MSG_addInvidiousInstanceDescription__</div>
-
- <div class="option-hidden-section hidden">
- <br/>
-
- <input class="option-text-box" type="text">
-
- <br/>
- <br/>
-
- <div class="option-button text-change-set inline">
- __MSG_add__
- </div>
+ <div id="behavior" class="option-group hidden">
- <div class="option-button invidious-instance-reset inline">
- __MSG_resetInvidiousInstance__
- </div>
-
- <br/>
- <br/>
+ <div id="category-type" data-type="react-CategoryChooserComponent">
- <span class="small-description">__MSG_currentInstances__</span>
- <span class="small-description" option-type="display" sync-option="invidiousInstances"></span>
</div>
- <br/>
- <br/>
- </div>
-
- <div option-type="keybind-change" sync-option="skipKeybind">
- <div class="option-button trigger-button">
- __MSG_setSkipShortcut__
+ <div data-type="toggle" data-sync="autoSkipOnMusicVideos">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="autoSkipOnMusicVideos" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="autoSkipOnMusicVideos">
+ __MSG_autoSkipOnMusicVideos__
+ </label>
+ </div>
</div>
- <div class="option-hidden-section hidden">
- <br/>
-
- <div class="medium-description keybind-status">
- __MSG_keybindDescription__
+ <div data-type="toggle" data-sync="muteSegments">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="muteSegments" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="muteSegments">
+ __MSG_muteSegments__
+ </label>
+ </div>
+ </div>
+
+ <div option-type="toggle" data-sync="fullVideoSegments">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="fullVideoSegments" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="fullVideoSegments">
+ __MSG_fullVideoSegments__
+ </label>
</div>
-
- <span class="medium-description bold keybind-status-key">
-
- </span>
</div>
- </div>
- <br/>
- <br/>
+ <div data-type="number-change" data-sync="minDuration">
+ <label class="number-container">
+ <span class="optionLabel">__MSG_minDuration__</span>
+ <input type="number" step="0.1" min="0">
+ </label>
- <div option-type="keybind-change" sync-option="startSponsorKeybind">
- <div class="option-button trigger-button">
- __MSG_setStartSponsorShortcut__
+ <div class="small-description">__MSG_minDurationDescription__</div>
</div>
-
- <div class="option-hidden-section hidden">
- <br/>
-
- <div class="medium-description keybind-status">
- __MSG_keybindDescription__
+
+ <div data-type="toggle" data-sync="forceChannelCheck">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="forceChannelCheck" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="forceChannelCheck">
+ __MSG_forceChannelCheck__
+ </label>
</div>
+
+ <div class="small-description">__MSG_whatForceChannelCheck__</div>
+ </div>
- <span class="medium-description bold keybind-status-key">
-
- </span>
+ <div data-type="toggle" data-sync="refetchWhenNotFound">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="refetchWhenNotFound" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="refetchWhenNotFound">
+ __MSG_enableRefetchWhenNotFound__
+ </label>
+ </div>
+
+ <div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
</div>
+
</div>
- <br/>
- <br/>
+ <div id="interface" class="option-group hidden">
+
+ <div data-type="number-change" data-sync="skipNoticeDuration">
+ <label class="number-container">
+ <span class="optionLabel">__MSG_skipNoticeDuration__</span>
+ <input type="number" step="1" min="1">
+ </label>
- <div option-type="keybind-change" sync-option="submitKeybind">
- <div class="option-button trigger-button">
- __MSG_setSubmitKeybind__
+ <div class="small-description">__MSG_skipNoticeDurationDescription__</div>
</div>
- <div class="option-hidden-section hidden">
- <br/>
-
- <div class="medium-description keybind-status">
- __MSG_keybindDescription__
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowNotice">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="dontShowNotice" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="dontShowNotice">
+ __MSG_showSkipNotice__
+ </label>
</div>
-
- <span class="medium-description bold keybind-status-key">
-
- </span>
</div>
- </div>
-
- <br/>
- <br/>
-
- <div option-type="number-change" sync-option="skipNoticeDuration">
- <label class="number-container">
- <input type="number" step="1" min="1">
- </label>
-
- <br/>
- <br/>
-
- <div class="small-description">__MSG_skipNoticeDurationDescription__</div>
- </div>
-
- <br/>
- <br/>
- <div option-type="number-change" sync-option="minDuration">
- <label class="number-container">
- <input type="number" step="0.1" min="0">
- </label>
-
- <br/>
- <br/>
-
- <div class="small-description">__MSG_minDurationDescription__</div>
- </div>
+ <div data-type="selector" data-sync="noticeVisibilityMode">
+ <label class="optionLabel" for="noticeVisibilityMode">__MSG_noticeVisibilityLabel__:</label>
- <br/>
- <br/>
+ <select id="noticeVisibilityMode" class="selector-element optionsSelector" >
+ <option value="0">__MSG_noticeVisibilityMode0__</option>
+ <option value="1">__MSG_noticeVisibilityMode1__</option>
+ <option value="2">__MSG_noticeVisibilityMode2__</option>
+ <option value="3">__MSG_noticeVisibilityMode3__</option>
+ <option value="4">__MSG_noticeVisibilityMode4__</option>
+ </select>
+ </div>
- <div option-type="toggle" toggle-type="reverse" sync-option="dontShowNotice">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showSkipNotice__
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="hideVideoPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="hideVideoPlayerControls" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="hideVideoPlayerControls">
+ __MSG_showButtons__
+ </label>
</div>
- </label>
- </div>
-
- <br/>
- <br/>
- <br/>
-
- <div option-type="selector" sync-option="noticeVisibilityMode">
- <select class="selector-element optionsSelector" >
- <option value="0">__MSG_noticeVisibilityMode0__</option>
- <option value="1">__MSG_noticeVisibilityMode1__</option>
- <option value="2">__MSG_noticeVisibilityMode2__</option>
- <option value="3">__MSG_noticeVisibilityMode3__</option>
- <option value="4">__MSG_noticeVisibilityMode4__</option>
- </select>
- </div>
- <br/>
- <br/>
+ <div class="small-description">__MSG_hideButtonsDescription__</div>
+ </div>
- <div option-type="toggle" sync-option="forceChannelCheck">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_forceChannelCheck__
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="hideDeleteButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="hideDeleteButtonPlayerControls" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="hideDeleteButtonPlayerControls">
+ __MSG_showDeleteButton__
+ </label>
</div>
- </label>
-
- <br/>
- <br/>
- <br/>
+ </div>
- <div class="small-description">__MSG_whatForceChannelCheck__</div>
- </div>
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="hideUploadButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="hideUploadButtonPlayerControls" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="hideUploadButtonPlayerControls">
+ __MSG_showUploadButton__
+ </label>
+ </div>
+ </div>
- <br/>
- <br/>
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="hideSkipButtonPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="hideSkipButtonPlayerControls" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="hideSkipButtonPlayerControls">
+ __MSG_showSkipButton__
+ </label>
+ </div>
+ </div>
- <div option-type="toggle" toggle-type="reverse" sync-option="hideVideoPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showButtons__
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="hideInfoButtonPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="hideInfoButtonPlayerControls" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="hideInfoButtonPlayerControls">
+ __MSG_showInfoButton__
+ </label>
</div>
- </label>
+ </div>
- <br/>
- <br/>
- <br/>
+ <div data-type="toggle" data-sync="autoHideInfoButton" data-dependent-on="hideInfoButtonPlayerControls">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="autoHideInfoButton" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="autoHideInfoButton">
+ __MSG_autoHideInfoButton__
+ </label>
+ </div>
+ </div>
- <div class="small-description">__MSG_hideButtonsDescription__</div>
- </div>
-
- <br/>
-
- <div option-type="toggle" toggle-type="reverse" sync-option="hideSkipButtonPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showSkipButton__
+ <div data-type="toggle" data-sync="audioNotificationOnSkip">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="audioNotificationOnSkip" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="audioNotificationOnSkip">
+ __MSG_audioNotification__
+ </label>
</div>
- </label>
- </div>
- <br/>
- <br/>
- <br/>
+ <div class="small-description">__MSG_audioNotificationDescription__</div>
+ </div>
- <div option-type="toggle" toggle-type="reverse" sync-option="hideInfoButtonPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showInfoButton__
+ <div data-type="toggle" data-sync="showTimeWithSkips">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="showTimeWithSkips" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="showTimeWithSkips">
+ __MSG_showTimeWithSkips__
+ </label>
</div>
- </label>
- </div>
-
- <br/>
- <br/>
- <br/>
+
+ <div class="small-description">__MSG_showTimeWithSkipsDescription__</div>
+ </div>
- <div option-type="toggle" sync-option="autoHideInfoButton" if-false="hideInfoButtonPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_autoHideInfoButton__
+ <div data-type="toggle" data-sync="darkMode">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="darkMode" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="darkMode">
+ __MSG_darkModeOptionsPage__
+ </label>
</div>
- </label>
- </div>
+ </div>
- <br/>
- <br/>
- <br/>
-
- <div option-type="toggle" toggle-type="reverse" sync-option="hideDeleteButtonPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showDeleteButton__
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="showDonationLink" data-no-safari="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="showDonationLink" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="showDonationLink">
+ __MSG_hideDonationLink__
+ </label>
</div>
- </label>
- </div>
+ </div>
- <br/>
- <br/>
- <br/>
-
- <div option-type="toggle" toggle-type="reverse" sync-option="hideUploadButtonPlayerControls">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showUploadButton__
- </div>
- </label>
</div>
- <br/>
- <br/>
- <br/>
- <br/>
+ <div id="keybinds" class="option-group hidden">
+
+ <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">
+ <label class="optionLabel">__MSG_setStartSponsorShortcut__:</label>
+ <div class="inline"></div>
+ </div>
+
+ <div data-type="keybind-change" data-sync="submitKeybind">
+ <label class="optionLabel">__MSG_setSubmitKeybind__:</label>
+ <div class="inline"></div>
+ </div>
- <div option-type="toggle" sync-option="audioNotificationOnSkip">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_audioNotification__
- </div>
- </label>
-
- <br/>
- <br/>
- <br/>
-
- <div class="small-description">__MSG_audioNotificationDescription__</div>
</div>
- <br/>
- <br/>
+ <div id="import" class="option-group hidden">
- <div option-type="toggle" sync-option="showTimeWithSkips">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_showTimeWithSkips__
+ <div data-type="private-text-change" data-sync="userID" data-confirm-message="userIDChangeWarning">
+ <div class="option-button trigger-button">
+ __MSG_changeUserID__
</div>
- </label>
- <br/>
- <br/>
- <br/>
+ <div class="small-description">__MSG_whatChangeUserID__</div>
- <div class="small-description">__MSG_showTimeWithSkipsDescription__</div>
- </div>
-
- <br/>
- <br/>
+ <div class="option-hidden-section hidden spacing indent">
+ <input class="option-text-box" type="text">
- <div option-type="toggle" sync-option="trackViewCount">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_enableViewTracking__
+ <div class="option-button text-change-set inline low-profile">
+ __MSG_setUserID__
+ </div>
+ </div>
+ </div>
+
+ <div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
+ <h2>__MSG_exportOptions__</h2>
+
+ <div>
+ <div class="option-button trigger-button inline">
+ __MSG_exportOptionsCopy__
+ </div>
+ <div class="option-button download-button inline">
+ __MSG_exportOptionsDownload__
+ </div>
+ <label for="importOptions" class="option-button inline">
+ __MSG_exportOptionsUpload__
+ </label>
+ <input id="importOptions" type="file" class="upload-button hidden" />
</div>
- </label>
- <br/>
- <br/>
- <br/>
+ <div class="small-description">__MSG_whatExportOptions__</div>
- <div class="small-description">__MSG_whatViewTracking__</div>
- </div>
-
- <br/>
- <br/>
-
- <div option-type="toggle" sync-option="trackViewCountInPrivate" private-mode-only="true">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_enableViewTrackingInPrivate__
+ <div class="option-hidden-section hidden spacing indent">
+ <textarea class="option-text-box" rows="10" style="width:80%"></textarea>
+
+ <div class="option-button text-change-set">
+ __MSG_setOptions__
+ </div>
</div>
- </label>
+ </div>
- <br/>
- <br/>
- <br/>
- <br/>
</div>
- <div option-type="toggle" sync-option="refetchWhenNotFound">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_enableRefetchWhenNotFound__
+ <div id="advanced" class="option-group hidden">
+
+ <div id="support-invidious" data-type="toggle" data-sync="supportInvidious" data-no-safari="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="supportInvidious" type="checkbox">
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="supportInvidious">
+ __MSG_supportOtherSites__
+ </label>
</div>
- </label>
- <br/>
- <br/>
- <br/>
+ <div class="small-description">(__MSG_supportedSites__ Invidious, CloudTube)</div>
+ <div class="small-description">__MSG_supportOtherSitesDescription__ </div>
+ </div>
- <div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
- </div>
-
- <br/>
- <br/>
-
- <div option-type="toggle" toggle-type="reverse" sync-option="showDonationLink" no-safari="true">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_hideDonationLink__
+ <div data-type="private-text-change" data-sync="invidiousInstances" data-no-safari="true" data-dependent-on="supportInvidious">
+ <div class="option-button trigger-button">
+ __MSG_addInvidiousInstance__
</div>
- </label>
- </div>
+ <div class="small-description">__MSG_addInvidiousInstanceDescription__</div>
+
+ <div class="indent option-hidden-section hidden spacing">
+ <input class="option-text-box" type="text">
+ <div class="inline">
+ <div class="option-button text-change-set inline low-profile">
+ __MSG_add__
+ </div>
+ <div class="option-button text-change-reset inline low-profile">
+ __MSG_cancel__
+ </div>
+ </div>
+ </div>
- <br/>
- <br/>
- <br/>
- <br/>
-
- <div option-type="private-text-change" sync-option="userID" confirm-message="userIDChangeWarning">
- <div class="option-button trigger-button">
- __MSG_changeUserID__
- </div>
-
- <br/>
-
- <div class="small-description">__MSG_whatChangeUserID__</div>
-
- <div class="option-hidden-section hidden">
- <br/>
-
- <input class="option-text-box" type="text">
-
- <br/>
- <br/>
-
- <div class="option-button text-change-set">
- __MSG_setUserID__
+ <div style="margin-top:15px">
+ <span>__MSG_currentInstances__</span>
+ <span data-type="display" data-sync="invidiousInstances"></span>
+ <div class="option-button invidious-instance-reset spacing hidden">
+ __MSG_resetInvidiousInstance__
+ </div>
</div>
</div>
- </div>
- <br/>
- <br/>
+ <div data-type="toggle" data-sync="trackViewCount">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="trackViewCount" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="trackViewCount">
+ __MSG_enableViewTracking__
+ </label>
+ </div>
- <div option-type="private-text-change" sync-option="*" confirm-message="exportOptionsWarning">
- <div class="option-button trigger-button">
- __MSG_exportOptions__
+ <div class="small-description">__MSG_whatViewTracking__</div>
</div>
- <br/>
-
- <div class="small-description">__MSG_whatExportOptions__</div>
-
- <div class="option-hidden-section hidden">
- <br/>
+ <div data-type="toggle" data-sync="trackViewCountInPrivate" data-dependent-on="trackViewCount" data-private-only="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="trackViewCountInPrivate" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="trackViewCountInPrivate">
+ __MSG_enableViewTrackingInPrivate__
+ </label>
+ </div>
+ </div>
- <input class="option-text-box" type="text">
+ <div data-type="button-press" data-sync="copyDebugInformation" data-confirm-message="copyDebugInformation">
+ <div class="option-button trigger-button">
+ __MSG_copyDebugInformation__
+ </div>
- <br/>
- <br/>
+ <div class="small-description">__MSG_copyDebugInformationOptions__</div>
+ </div>
- <div class="option-button text-change-set">
- __MSG_setOptions__
+ <div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="testingServer" type="checkbox">
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="testingServer">
+ __MSG_enableTestingServer__
+ </label>
</div>
- </div>
- </div>
-
- <br/>
- <br/>
-
- <div option-type="button-press" sync-option="copyDebugInformation" confirm-message="copyDebugInformation">
- <div class="option-button trigger-button">
- __MSG_copyDebugInformation__
+
+ <div class="small-description">__MSG_whatEnableTestingServer__</div>
</div>
- <br/>
+ <div data-type="text-change" data-sync="serverAddress" data-dependent-on="testingServer" data-dependent-on-inverted="true">
+ <label class="optionLabel inline">
+ <span class="optionLabel">__MSG_customServerAddress__:</span>
- <div class="small-description">__MSG_copyDebugInformationOptions__</div>
- </div>
+ <input class="option-text-box" type="text" style="margin-right:10px">
+ </label>
- <br/>
- <br/>
+ <div class="small-description">__MSG_customServerAddressDescription__</div>
- <div option-type="toggle" sync-option="testingServer" confirm-message="testingServerWarning" no-safari="true">
- <label class="switch-container">
- <label class="switch">
- <input type="checkbox">
- <span class="slider round"></span>
- </label>
- <div class="switch-label">
- __MSG_enableTestingServer__
+ <div class="next-line">
+ <div class="option-button text-change-set inline">
+ __MSG_save__
+ </div>
+
+ <div class="option-button text-change-reset inline">
+ __MSG_reset__
+ </div>
</div>
- </label>
-
- <br/>
- <br/>
- <br/>
-
- <div class="small-description">__MSG_whatEnableTestingServer__</div>
-
- <br/>
- <br/>
- <br/>
- </div>
-
- <div option-type="text-change" sync-option="serverAddress">
- <label class="text-label-container">
- <div>__MSG_customServerAddress__</div>
-
- <input class="option-text-box" type="text">
- </label>
-
- <div class="option-button text-change-set inline">
- __MSG_save__
</div>
- <div class="option-button text-change-reset inline">
- __MSG_reset__
- </div>
-
- <br/>
- <br/>
-
- <div class="small-description">__MSG_customServerAddressDescription__</div>
</div>
-
+
</div>
+
</div>
</body>
diff --git a/src/components/CategoryChooserComponent.tsx b/src/components/CategoryChooserComponent.tsx
index b28bd62a..763254b9 100644
--- a/src/components/CategoryChooserComponent.tsx
+++ b/src/components/CategoryChooserComponent.tsx
@@ -31,24 +31,24 @@ class CategoryChooserComponent extends React.Component<CategoryChooserProps, Cat
{/* Headers */}
<tr id={"CategoryOptionsRow"}
className="categoryTableElement categoryTableHeader">
- <td id={"CategoryOptionName"}>
+ <th id={"CategoryOptionName"}>
{chrome.i18n.getMessage("category")}
- </td>
+ </th>
- <td id={"CategorySkipOption"}
+ <th id={"CategorySkipOption"}
className="skipOption">
{chrome.i18n.getMessage("skipOption")}
- </td>
+ </th>
- <td id={"CategoryColorOption"}
+ <th id={"CategoryColorOption"}
className="colorOption">
{chrome.i18n.getMessage("seekBarColor")}
- </td>
+ </th>
- <td id={"CategoryPreviewColorOption"}
+ <th id={"CategoryPreviewColorOption"}
className="previewColorOption">
{chrome.i18n.getMessage("previewColor")}
- </td>
+ </th>
</tr>
{this.getCategorySkipOptions()}
diff --git a/src/components/KeybindComponent.tsx b/src/components/KeybindComponent.tsx
new file mode 100644
index 00000000..17aefcbb
--- /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<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 = parent.document.createElement("div");
+ dialog.id = "keybind-dialog";
+ parent.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; \ No newline at end of file
diff --git a/src/components/KeybindDialogComponent.tsx b/src/components/KeybindDialogComponent.tsx
new file mode 100644
index 00000000..c4e7cf48
--- /dev/null
+++ b/src/components/KeybindDialogComponent.tsx
@@ -0,0 +1,165 @@
+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 {
+ parent.document.addEventListener("keydown", this.keybindKeyPressed);
+ document.addEventListener("keydown", this.keybindKeyPressed);
+ }
+
+ componentWillUnmount(): void {
+ parent.document.removeEventListener("keydown", this.keybindKeyPressed);
+ 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; \ No newline at end of file
diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx
index 588405fe..41daac44 100644
--- a/src/components/SkipNoticeComponent.tsx
+++ b/src/components/SkipNoticeComponent.tsx
@@ -6,8 +6,8 @@ import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils";
const utils = new Utils();
-
import { 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";
@@ -344,7 +344,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>
);
diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx
index 9b91e5c2..cdc21617 100644
--- a/src/components/SponsorTimeEditComponent.tsx
+++ b/src/components/SponsorTimeEditComponent.tsx
@@ -374,7 +374,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst")
.replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) {
// Open options page
- chrome.runtime.sendMessage({message: "openConfig", hash: chosenCategory + "OptionsName"});
+ chrome.runtime.sendMessage({message: "openConfig", hash: "behavior"});
}
return;
diff --git a/src/config.ts b/src/config.ts
index 14f029bc..a890f94a 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,6 +1,7 @@
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.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,
@@ -11,9 +12,6 @@ interface SBConfig {
defaultCategory: Category,
whitelistedChannels: string[],
forceChannelCheck: boolean,
- skipKeybind: string,
- startSponsorKeybind: string,
- submitKeybind: string,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
@@ -54,6 +52,11 @@ interface SBConfig {
},
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
+ darkMode: boolean,
+
+ skipKeybind: Keybind,
+ startSponsorKeybind: Keybind,
+ submitKeybind: Keybind,
// What categories should be skipped
categorySelections: CategorySelection[],
@@ -170,9 +173,6 @@ const Config: SBObject = {
defaultCategory: "chooseACategory" as Category,
whitelistedChannels: [],
forceChannelCheck: false,
- skipKeybind: "Enter",
- startSponsorKeybind: ";",
- submitKeybind: "'",
minutesSaved: 0,
skipCount: 0,
sponsorTimesContributed: 0,
@@ -208,6 +208,18 @@ const Config: SBObject = {
autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
+ darkMode: true,
+
+ /**
+ * 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,
@@ -450,6 +462,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 f81200b2..b321df38 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -1,7 +1,7 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, 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();
@@ -16,6 +16,7 @@ import * as Chat from "./js-components/chat";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
+import { keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
@@ -134,6 +135,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
@@ -1279,7 +1283,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
&& skippingSegments.length === 1
&& skippingSegments[0].actionType === ActionType.Poi) {
skipButtonControlBar.enable(skippingSegments[0]);
- if (onMobileYouTube) skipButtonControlBar.setShowKeybindHint(false);
+ if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar;
@@ -1288,7 +1292,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);
@@ -1894,30 +1898,46 @@ function addPageListeners(): void {
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/help.ts b/src/help.ts
index 80ac974a..6f3945df 100644
--- a/src/help.ts
+++ b/src/help.ts
@@ -11,6 +11,10 @@ async function init() {
await utils.wait(() => Config.config !== null);
+ if (!Config.config.darkMode) {
+ document.documentElement.setAttribute("data-theme", "light");
+ }
+
if (!showDonationLink()) {
document.getElementById("sbDonate").style.display = "none";
}
diff --git a/src/js-components/skipButtonControlBar.ts b/src/js-components/skipButtonControlBar.ts
index a27eefd0..62f572f0 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";
import { AnimationUtils } from "../utils/animationUtils";
@@ -180,7 +181,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 49eed8a1..9f1f0cb2 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";
import * as invidiousList from "../ci/invidiouslist.json";
@@ -7,21 +10,38 @@ 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();
+let embed = false;
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
+ // selected tab
+ if (location.hash != "") {
+ const substr = location.hash.substring(1);
+ let menuItem = document.querySelector(`[data-for='${substr}']`);
+ if (menuItem == null)
+ menuItem = document.querySelector(`[data-for='behavior']`);
+ menuItem.classList.add("selected");
+ } else {
+ document.querySelector(`[data-for='behavior']`).classList.add("selected");
+ }
+
+ document.getElementById("version").innerText = "v. " + chrome.runtime.getManifest().version;
+
// Remove header if needed
if (window.location.hash === "#embed") {
+ embed = true;
for (const element of document.getElementsByClassName("titleBar")) {
element.classList.add("hidden");
}
document.getElementById("options").classList.add("embed");
+ createStickyHeader();
}
if (!Config.configListeners.includes(optionsConfigUpdateListener)) {
@@ -30,8 +50,12 @@ async function init() {
await utils.wait(() => Config.config !== null);
+ if (!Config.config.darkMode) {
+ document.documentElement.setAttribute("data-theme", "light");
+ }
+
if (!showDonationLink()) {
- document.getElementById("sbDonate").style.visibility = "hidden";
+ document.getElementById("sbDonate").classList.add("hidden");
}
// Set all of the toggle options to the correct option
@@ -39,31 +63,31 @@ async function init() {
const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
- if ((optionsElements[i].getAttribute("private-mode-only") === "true" && !(await isIncognitoAllowed()))
- || (optionsElements[i].getAttribute("no-safari") === "true" && navigator.vendor === "Apple Computer, Inc.")
- || (optionsElements[i].getAttribute("if-false") && Config.config[optionsElements[i].getAttribute("if-false")])) {
- optionsElements[i].classList.add("hidden");
- continue;
+ const dependentOnName = optionsElements[i].getAttribute("data-dependent-on");
+ const dependentOn = optionsContainer.querySelector(`[data-sync='${dependentOnName}']`);
+ let isDependentOnReversed = false;
+ if (dependentOn)
+ isDependentOnReversed = dependentOn.getAttribute("data-toggle-type") === "reverse" || optionsElements[i].getAttribute("data-dependent-on-inverted") === "true";
+
+ if (await shouldHideOption(optionsElements[i]) || (dependentOn && (isDependentOnReversed ? Config.config[dependentOnName] : !Config.config[dependentOnName]))) {
+ optionsElements[i].classList.add("hidden", "hiding");
+ if (!dependentOn)
+ continue;
}
- const option = optionsElements[i].getAttribute("sync-option");
+ const option = optionsElements[i].getAttribute("data-sync");
- switch (optionsElements[i].getAttribute("option-type")) {
+ switch (optionsElements[i].getAttribute("data-type")) {
case "toggle": {
const optionResult = Config.config[option];
const checkbox = optionsElements[i].querySelector("input");
- const reverse = optionsElements[i].getAttribute("toggle-type") === "reverse";
+ const reverse = optionsElements[i].getAttribute("data-toggle-type") === "reverse";
- const confirmMessage = optionsElements[i].getAttribute("confirm-message");
+ const confirmMessage = optionsElements[i].getAttribute("data-confirm-message");
- if (optionResult != undefined) {
- checkbox.checked = optionResult;
-
- if (reverse) {
- optionsElements[i].querySelector("input").checked = !optionResult;
- }
- }
+ if (optionResult != undefined)
+ checkbox.checked = reverse ? !optionResult : optionResult;
// See if anything extra should be run first time
switch (option) {
@@ -73,7 +97,7 @@ async function init() {
}
// Add click listener
- checkbox.addEventListener("click", () => {
+ checkbox.addEventListener("click", async () => {
// Confirm if required
if (checkbox.checked && confirmMessage && !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = false;
@@ -92,11 +116,36 @@ async function init() {
// Enable the notice
Config.config["dontShowNotice"] = false;
- const showNoticeSwitch = <HTMLInputElement> document.querySelector("[sync-option='dontShowNotice'] > label > label > input");
+ const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
showNoticeSwitch.checked = true;
}
-
break;
+ case "showDonationLink":
+ if (checkbox.checked)
+ document.getElementById("sbDonate").classList.add("hidden");
+ else
+ document.getElementById("sbDonate").classList.remove("hidden");
+ break;
+ case "darkMode":
+ if (checkbox.checked) {
+ document.documentElement.setAttribute("data-theme", "dark");
+ } else {
+ document.documentElement.setAttribute("data-theme", "light");
+ }
+ break;
+ }
+
+ // If other options depend on this, hide/show them
+ const dependents = optionsContainer.querySelectorAll(`[data-dependent-on='${option}']`);
+ for (let j = 0; j < dependents.length; j++) {
+ const disableWhenChecked = dependents[j].getAttribute("data-dependent-on-inverted") === "true";
+ if (!await shouldHideOption(dependents[j]) && (!disableWhenChecked && checkbox.checked || disableWhenChecked && !checkbox.checked)) {
+ dependents[j].classList.remove("hidden");
+ setTimeout(() => dependents[j].classList.remove("hiding"), 1);
+ } else {
+ dependents[j].classList.add("hiding");
+ setTimeout(() => dependents[j].classList.add("hidden"), 400);
+ }
}
});
break;
@@ -155,7 +204,15 @@ async function init() {
const button = optionsElements[i].querySelector(".trigger-button");
button.addEventListener("click", () => activatePrivateTextChange(<HTMLElement> optionsElements[i]));
- const privateTextChangeOption = optionsElements[i].getAttribute("sync-option");
+ if (option == "*") {
+ const downloadButton = optionsElements[i].querySelector(".download-button");
+ downloadButton.addEventListener("click", downloadConfig);
+
+ const uploadButton = optionsElements[i].querySelector(".upload-button");
+ uploadButton.addEventListener("change", (e) => uploadConfig(e));
+ }
+
+ const privateTextChangeOption = optionsElements[i].getAttribute("data-sync");
// See if anything extra must be done
switch (privateTextChangeOption) {
case "invidiousInstances":
@@ -167,7 +224,7 @@ async function init() {
case "button-press": {
const actionButton = optionsElements[i].querySelector(".trigger-button");
- switch(optionsElements[i].getAttribute("sync-option")) {
+ switch(optionsElements[i].getAttribute("data-sync")) {
case "copyDebugInformation":
actionButton.addEventListener("click", copyDebugOutputToClipboard);
break;
@@ -176,9 +233,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": {
@@ -220,10 +275,57 @@ async function init() {
}
}
- optionsContainer.classList.remove("hidden");
+ // Tab interaction
+ const tabElements = document.getElementsByClassName("tab-heading");
+ for (let i = 0; i < tabElements.length; i++) {
+ const tabFor = tabElements[i].getAttribute("data-for");
+
+ if (tabElements[i].classList.contains("selected"))
+ document.getElementById(tabFor).classList.remove("hidden");
+
+ tabElements[i].addEventListener("click", () => {
+ if (!embed) location.hash = tabFor;
+
+ createStickyHeader();
+
+ document.querySelectorAll(".tab-heading").forEach(element => { element.classList.remove("selected"); });
+ optionsContainer.querySelectorAll(".option-group").forEach(element => { element.classList.add("hidden"); });
+
+ tabElements[i].classList.add("selected");
+ document.getElementById(tabFor).classList.remove("hidden");
+ });
+ }
+
+ window.addEventListener("scroll", () => createStickyHeader());
+
optionsContainer.classList.add("animated");
}
+function createStickyHeader() {
+ const container = document.getElementById("options-container");
+ const options = document.getElementById("options");
+
+ if (!embed && window.pageYOffset > 90 && (window.innerHeight <= 770 || window.innerWidth <= 1200)) {
+ if (!container.classList.contains("sticky")) {
+ options.style.marginTop = options.offsetTop.toString()+"px";
+ container.classList.add("sticky");
+ }
+ } else {
+ options.style.marginTop = "unset";
+ container.classList.remove("sticky");
+ }
+}
+
+/**
+ * Handle special cases where an option shouldn't show
+ *
+ * @param {String} element
+ */
+async function shouldHideOption(element: Element): Promise<boolean> {
+ return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
+ || (element.getAttribute("data-no-safari") === "true" && navigator.vendor === "Apple Computer, Inc.");
+}
+
/**
* Called when the config is updated
*
@@ -234,7 +336,7 @@ function optionsConfigUpdateListener() {
const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
- switch (optionsElements[i].getAttribute("option-type")) {
+ switch (optionsElements[i].getAttribute("data-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
}
@@ -247,15 +349,25 @@ function optionsConfigUpdateListener() {
* @param element
*/
function updateDisplayElement(element: HTMLElement) {
- const displayOption = element.getAttribute("sync-option")
+ const displayOption = element.getAttribute("data-sync")
const displayText = Config.config[displayOption];
element.innerText = displayText;
// See if anything extra must be run
switch (displayOption) {
- case "invidiousInstances":
+ case "invidiousInstances": {
element.innerText = displayText.join(', ');
+ let allEquals = displayText.length == invidiousList.length;
+ for (let i = 0; i < invidiousList.length && allEquals; i++) {
+ if (displayText[i] != invidiousList[i])
+ allEquals = false;
+ }
+ if (!allEquals) {
+ const resetButton = element.parentElement.querySelector(".invidious-instance-reset");
+ resetButton.classList.remove("hidden");
+ }
break;
+ }
}
}
@@ -270,6 +382,8 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const button = element.querySelector(".trigger-button");
const setButton = element.querySelector(".text-change-set");
+ const cancelButton = element.querySelector(".text-change-reset");
+ const resetButton = element.querySelector(".invidious-instance-reset");
setButton.addEventListener("click", async function() {
if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http")) {
alert(chrome.i18n.getMessage("addInvidiousInstanceError"));
@@ -287,19 +401,26 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
invidiousOnClick(checkbox, "supportInvidious");
- textBox.value = "";
+ resetButton.classList.remove("hidden");
// Hide this section again
+ textBox.value = "";
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
}
});
- const resetButton = element.querySelector(".invidious-instance-reset");
+ cancelButton.addEventListener("click", async function() {
+ textBox.value = "";
+ element.querySelector(".option-hidden-section").classList.add("hidden");
+ button.classList.remove("disabled");
+ });
+
resetButton.addEventListener("click", function() {
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
// Set to CI populated list
Config.config[option] = invidiousList;
+ resetButton.classList.add("hidden");
}
});
}
@@ -352,91 +473,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("sync-option");
-
- 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("sync-option");
-
- // 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.
*
* @param element
@@ -448,7 +484,7 @@ function activatePrivateTextChange(element: HTMLElement) {
button.classList.add("disabled");
const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
- const option = element.getAttribute("sync-option");
+ const option = element.getAttribute("data-sync");
// See if anything extra must be done
switch (option) {
@@ -476,38 +512,7 @@ function activatePrivateTextChange(element: HTMLElement) {
const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => {
- const confirmMessage = element.getAttribute("confirm-message");
-
- if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
-
- // See if anything extra must be done
- switch (option) {
- case "*":
- try {
- const newConfig = JSON.parse(textBox.value);
- for (const key in newConfig) {
- Config.config[key] = newConfig[key];
- }
- Config.convertJSON();
-
- if (newConfig.supportInvidious) {
- const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > label > label > input");
-
- checkbox.checked = true;
- await invidiousOnClick(checkbox, "supportInvidious");
- }
-
- window.location.reload();
-
- } catch (e) {
- alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
- }
-
- break;
- default:
- Config.config[option] = textBox.value;
- }
- }
+ setTextOption(option, element, textBox.value);
});
// See if anything extra must be done
@@ -532,6 +537,77 @@ function activatePrivateTextChange(element: HTMLElement) {
}
/**
+ * Function to run when a textbox change is submitted
+ *
+ * @param option data-sync value
+ * @param element main container div
+ * @param value new text
+ * @param callbackOnError function to run if confirmMessage was denied
+ */
+async function setTextOption(option: string, element: HTMLElement, value: string, callbackOnError?: () => void) {
+ const confirmMessage = element.getAttribute("data-confirm-message");
+
+ if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
+
+ // See if anything extra must be done
+ switch (option) {
+ case "*":
+ try {
+ const newConfig = JSON.parse(value);
+ for (const key in newConfig) {
+ Config.config[key] = newConfig[key];
+ }
+ Config.convertJSON();
+
+ if (newConfig.supportInvidious) {
+ const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
+
+ checkbox.checked = true;
+ await invidiousOnClick(checkbox, "supportInvidious");
+ }
+
+ window.location.reload();
+
+ } catch (e) {
+ alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
+ }
+
+ break;
+ default:
+ Config.config[option] = value;
+ }
+ } else {
+ if (typeof callbackOnError == "function")
+ callbackOnError();
+ }
+}
+
+function downloadConfig() {
+ const file = document.createElement("a");
+ const jsonData = JSON.parse(JSON.stringify(Config.localConfig));
+ jsonData.segmentTimes = Config.encodeStoredItem(Config.localConfig.segmentTimes);
+ file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData)));
+ file.setAttribute("download", "SponsorBlockConfig.json");
+ document.body.append(file);
+ file.click();
+ file.remove();
+}
+
+function uploadConfig(e) {
+ if (e.target.files.length == 1) {
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ const element = document.querySelector("[data-sync='*']") as HTMLElement;
+ reader.onload = function(ev) {
+ setTextOption("*", element, ev.target.result as string, () => {
+ e.target.value = null;
+ });
+ };
+ reader.readAsText(file);
+ }
+}
+
+/**
* Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue.
*
diff --git a/src/popup.ts b/src/popup.ts
index 5a16a508..d84beda1 100644
--- a/src/popup.ts
+++ b/src/popup.ts
@@ -125,7 +125,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
unwhitelistChannel();
}
});
- PageElements.whitelistForceCheck.addEventListener("click", openOptions);
+ PageElements.whitelistForceCheck.addEventListener("click", () => {openOptionsAt("behavior")});
PageElements.toggleSwitch.addEventListener("change", function () {
toggleSkipping(!this.checked);
});
@@ -516,6 +516,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
chrome.runtime.sendMessage({ "message": "openConfig" });
}
+ function openOptionsAt(location) {
+ chrome.runtime.sendMessage({ "message": "openConfig", "hash": location });
+ }
+
function openHelp() {
chrome.runtime.sendMessage({ "message": "openHelp" });
}
diff --git a/src/types.ts b/src/types.ts
index 94d85784..297c3ae5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -219,4 +219,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