Merge pull request #1093 from AronHK/settings

Settings rework
This commit is contained in:
Ajay Ramachandran 2022-01-24 23:41:21 -05:00 committed by GitHub
commit f555a8e7bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1626 additions and 851 deletions

View file

@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded." "description": "The second line of the message displayed after the notice was upgraded."
}, },
"setSkipShortcut": { "setSkipShortcut": {
"message": "Set key for skipping a segment" "message": "Skip segment",
"description": "Keybind label"
}, },
"setStartSponsorShortcut": { "setStartSponsorShortcut": {
"message": "Set key for start/stop segment keybind" "message": "Start/stop segment",
"description": "Keybind label"
}, },
"setSubmitKeybind": { "setSubmitKeybind": {
"message": "Set key for submission keybind" "message": "Submit segments",
"description": "Keybind label"
}, },
"keybindDescription": { "keybindDescription": {
"message": "Select a key by typing it" "message": "Select a key by typing it and choose any modifier keys you wish to use."
},
"keybindDescriptionComplete": {
"message": "The keybind has been set to: "
}, },
"0": { "0": {
"message": "Connection Timeout. Check your internet connection. If your internet is working, the server is probably overloaded or down." "message": "Connection Timeout. Check your internet connection. If your internet is working, the server is probably overloaded or down."
@ -388,9 +388,6 @@
"createdBy": { "createdBy": {
"message": "Created By" "message": "Created By"
}, },
"keybindCurrentlySet": {
"message": ". It is currently set to:"
},
"supportOtherSites": { "supportOtherSites": {
"message": "Support 3rd Party YouTube-Sites" "message": "Support 3rd Party YouTube-Sites"
}, },
@ -470,6 +467,15 @@
"exportOptions": { "exportOptions": {
"message": "Import/Export All Options" "message": "Import/Export All Options"
}, },
"exportOptionsCopy": {
"message": "Edit/copy"
},
"exportOptionsDownload": {
"message": "Save to file"
},
"exportOptionsUpload": {
"message": "Load from file"
},
"whatExportOptions": { "whatExportOptions": {
"message": "This is your entire configuration in JSON. This includes your userID, so be sure to share this wisely." "message": "This is your entire configuration in JSON. This includes your userID, so be sure to share this wisely."
}, },
@ -518,11 +524,8 @@
"copyDebugInformationComplete": { "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." "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": { "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": { "to": {
"message": "to", "message": "to",
@ -787,6 +790,9 @@
"hideDonationLink": { "hideDonationLink": {
"message": "Hide Donation Link" "message": "Hide Donation Link"
}, },
"darkModeOptionsPage": {
"message": "Dark Mode On Options Page"
},
"helpPageThanksForInstalling": { "helpPageThanksForInstalling": {
"message": "Thanks for installing SponsorBlock." "message": "Thanks for installing SponsorBlock."
}, },
@ -875,5 +881,42 @@
"hourAbbreviation": { "hourAbbreviation": {
"message": "h", "message": "h",
"description": "100h" "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?"
} }
} }

View file

@ -4,6 +4,7 @@
<title> SponsorBlock </title> <title> SponsorBlock </title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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"/> <link href="styles.css" rel="stylesheet"/>

View file

@ -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 { html {
color-scheme: dark; color-scheme: var(--color-scheme);
} }
.bigText { .bigText {
@ -7,7 +33,7 @@ html {
} }
body { body {
background-color: #333333; background-color: var(--background);
font-family: sans-serif; font-family: sans-serif;
} }
@ -15,6 +41,10 @@ body {
text-align: center; text-align: center;
} }
.inline {
display: inline-block;
}
.container { .container {
max-width: 60%; max-width: 60%;
margin: auto; margin: auto;
@ -54,12 +84,14 @@ body {
vertical-align: middle; vertical-align: middle;
font-size: 50px; font-size: 50px;
color: #212121; color: var(--header-color);
padding: 20px; padding: 20px;
text-decoration: none; text-decoration: none;
border-radius: 15px;
transition: font-size 1s; transition: font-size 1s;
} }
@ -125,8 +157,8 @@ p,li {
font-size: 16px; font-size: 16px;
} }
p,li,a { p,li,a,span,div {
color: #c4c4c4; color: var(--text);
} }
p,li,code,a { p,li,code,a {
@ -160,7 +192,7 @@ img {
} }
h1,h2,h3,h4,h5,h6 { h1,h2,h3,h4,h5,h6 {
color: #dad8d8; color: var(--title);
text-align: center; text-align: center;
} }
@ -200,3 +232,94 @@ svg {
text-align: center; 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;
}

View file

@ -1,10 +1,232 @@
/* Options page CSS */ /* 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;
} }
body { [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);
}
html, body {
color-scheme: var(--color-scheme);
font-family: sans-serif; 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 { .center {
@ -15,25 +237,49 @@ body {
display: inline-block; display: inline-block;
} }
.next-line {
padding: 15px 0 0 0;
}
.bold { .bold {
font-weight: bold; font-weight: bold;
} }
.hiding {
opacity: 0;
}
.hidden { .hidden {
display: none !important; display: none !important;
} }
.spacing {
margin-top: 15px;
}
.keybind-status { .keybind-status {
display: inline; display: inline;
} }
.small-description { .small-description {
color: white;
font-size: 13px; 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 { .medium-description {
color: white;
font-size: 15px; font-size: 15px;
} }
@ -53,36 +299,46 @@ body {
width: max-content; width: max-content;
} }
.option-button:hover { .option-button:hover:not(.disabled) {
background-color: #fc0303; background-color: #fc0303;
} }
.option-button.disabled { .option-button.disabled {
cursor: default; cursor: default;
background-color: var(--disabled);
background-color: #520000;
color: grey; color: grey;
} }
#options { #options {
max-width: 60%; height: 100vh;
flex-basis: 80%;
overflow: auto;
text-align: left; 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%; max-width: 100%;
text-align: left; }
display: inline-block;
#title .profilepic {
height: 60px;
} }
.switch-container { .switch-container {
content: attr(label-name); content: attr(label-name);
position: absolute;
width: max-content; width: max-content;
font-size: 14px; font-size: 14px;
color: white;
display: table; display: table;
} }
@ -94,11 +350,6 @@ body {
padding: 4px; padding: 4px;
} }
.text-label-container {
font-size: 14px;
color: white;
}
.switch { .switch {
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -119,7 +370,7 @@ body {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: #707070; background-color: var(--slider);
} }
.animated * { .animated * {
@ -162,11 +413,8 @@ input:checked + .slider:before {
} }
/* Boilerplate CSS from https://ajay.app */
body { /* Boilerplate CSS from https://ajay.app (edited) */
background-color: #333333;
}
.projectPreview { .projectPreview {
position: relative; position: relative;
@ -196,29 +444,25 @@ body {
transform: translateY(-50%); transform: translateY(-50%);
} }
.createdBy { #createdBy {
font-size: 14px;
text-align: center; text-align: center;
padding-top: 0px; margin: auto 0 10px 0;
padding-bottom: 0px; height: 50px;
}
display: inline-block; #createdBy > * {
font-size: 14px;
} }
#title { #title {
background-color: #636363;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
font-size: 50px; font-size: 40px;
color: #212121;
padding: 20px; padding: 40px 20px;
text-decoration: none; text-decoration: none;
transition: font-size 1s;
} }
.subtitle { .subtitle {
@ -237,7 +481,6 @@ body {
} }
.profilepic { .profilepic {
background-color: #636363 !important;
vertical-align: middle; vertical-align: middle;
} }
@ -281,21 +524,9 @@ a {
p,li { p,li {
font-size: 20px; font-size: 20px;
color: #c4c4c4;
padding: 10px; padding: 10px;
} }
@media screen and (orientation:portrait) {
#options {
max-width: 100%;
}
.previewColorOption {
display: none;
}
}
.previewImage { .previewImage {
max-height: 200px; max-height: 200px;
} }
@ -316,10 +547,6 @@ img {
color: #dad8d8; color: #dad8d8;
} }
h1,h2,h3,h4,h5,h6 {
color: #dad8d8;
}
svg { svg {
text-decoration: none; text-decoration: none;
} }
@ -337,8 +564,6 @@ svg {
.categoryTableElement { .categoryTableElement {
font-size: 16px; font-size: 16px;
color: white;
} }
.categoryTableElement > * { .categoryTableElement > * {
@ -369,3 +594,87 @@ svg {
#sbDonate { #sbDonate {
font-size: 10px; font-size: 10px;
} }
/* 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%;
}
}

View file

@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- Link to specific tabs by using their ID in the URL like: options.html#keybinds -->
<head> <head>
<title>Options - SponsorBlock</title> <title>Options - SponsorBlock</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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"/> <link href="options.css" rel="stylesheet"/>
@ -13,247 +15,159 @@
<body class="sponsorBlockPageBody"> <body class="sponsorBlockPageBody">
<div id="options-container">
<div id="menubar" class="center">
<div id="title" class="titleBar"> <div id="title" class="titleBar">
<img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic"/> <img src="../icons/LogoSponsorBlocker256px.png" class="profilepic" alt="SponsorBlock logo"/>
SponsorBlock SponsorBlock
<div id="version"></div>
</div> </div>
<div class="center"> <div id="navigation">
<p class="createdBy titleBar"> <div class="tab-heading" data-for="behavior">
<img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle"/> __MSG_optionsTabBehavior__
</div>
<div class="tab-heading" data-for="interface">
__MSG_optionsTabInterface__
</div>
<div class="tab-heading" data-for="keybinds">
__MSG_optionsTabKeyBinds__
</div>
<div class="tab-heading" data-for="import">
__MSG_optionsTabBackup__
</div>
<div class="tab-heading" data-for="advanced">
__MSG_optionsTabAdvanced__
</div>
</div>
<div id="createdBy" class="titleBar">
<div>
<img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle" alt="profile picture of creator"/>
__MSG_createdBy__ __MSG_createdBy__
<a href="https://ajay.app">Ajay Ramachandran</a> <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> <a href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener" id="sbDonate">(__MSG_Donate__)</a>
</p> </div>
<h1>__MSG_Options__</h1>
<div id="options" class="hidden">
<div id="category-type" option-type="react-CategoryChooserComponent">
</div> </div>
<div option-type="toggle" sync-option="autoSkipOnMusicVideos"> <div id="options">
<label class="switch-container">
<div id="behavior" class="option-group hidden">
<div id="category-type" data-type="react-CategoryChooserComponent">
</div>
<div data-type="toggle" data-sync="autoSkipOnMusicVideos">
<div class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="autoSkipOnMusicVideos" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="autoSkipOnMusicVideos">
__MSG_autoSkipOnMusicVideos__ __MSG_autoSkipOnMusicVideos__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
</div> </div>
<div option-type="toggle" sync-option="muteSegments"> <div data-type="toggle" data-sync="muteSegments">
<label class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="muteSegments" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="muteSegments">
__MSG_muteSegments__ __MSG_muteSegments__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
</div> </div>
<div option-type="toggle" sync-option="fullVideoSegments"> <div option-type="toggle" data-sync="fullVideoSegments">
<label class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="fullVideoSegments" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="fullVideoSegments">
__MSG_fullVideoSegments__ __MSG_fullVideoSegments__
</div>
</label> </label>
<br/>
<br/>
<br/>
</div>
<br/>
<br/>
<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>
<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 class="option-button invidious-instance-reset inline">
__MSG_resetInvidiousInstance__
</div>
<br/>
<br/>
<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>
<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>
</div> </div>
<br/> <div data-type="number-change" data-sync="minDuration">
<br/>
<div option-type="keybind-change" sync-option="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>
<br/>
<br/>
<div option-type="keybind-change" sync-option="submitKeybind">
<div class="option-button trigger-button">
__MSG_setSubmitKeybind__
</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>
<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"> <label class="number-container">
<span class="optionLabel">__MSG_minDuration__</span>
<input type="number" step="0.1" min="0"> <input type="number" step="0.1" min="0">
</label> </label>
<br/>
<br/>
<div class="small-description">__MSG_minDurationDescription__</div> <div class="small-description">__MSG_minDurationDescription__</div>
</div> </div>
<br/> <div data-type="toggle" data-sync="forceChannelCheck">
<br/> <div class="switch-container">
<div option-type="toggle" toggle-type="reverse" sync-option="dontShowNotice">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="forceChannelCheck" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="forceChannelCheck">
__MSG_showSkipNotice__ __MSG_forceChannelCheck__
</div>
</label> </label>
</div> </div>
<br/> <div class="small-description">__MSG_whatForceChannelCheck__</div>
<br/> </div>
<br/>
<div option-type="selector" sync-option="noticeVisibilityMode"> <div data-type="toggle" data-sync="refetchWhenNotFound">
<select class="selector-element optionsSelector" > <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>
<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 class="small-description">__MSG_skipNoticeDurationDescription__</div>
</div>
<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>
</div>
<div data-type="selector" data-sync="noticeVisibilityMode">
<label class="optionLabel" for="noticeVisibilityMode">__MSG_noticeVisibilityLabel__:</label>
<select id="noticeVisibilityMode" class="selector-element optionsSelector" >
<option value="0">__MSG_noticeVisibilityMode0__</option> <option value="0">__MSG_noticeVisibilityMode0__</option>
<option value="1">__MSG_noticeVisibilityMode1__</option> <option value="1">__MSG_noticeVisibilityMode1__</option>
<option value="2">__MSG_noticeVisibilityMode2__</option> <option value="2">__MSG_noticeVisibilityMode2__</option>
@ -262,292 +176,191 @@
</select> </select>
</div> </div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="hideVideoPlayerControls">
<br/> <div class="switch-container">
<div option-type="toggle" sync-option="forceChannelCheck">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="hideVideoPlayerControls" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="hideVideoPlayerControls">
__MSG_forceChannelCheck__
</div>
</label>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatForceChannelCheck__</div>
</div>
<br/>
<br/>
<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__ __MSG_showButtons__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_hideButtonsDescription__</div> <div class="small-description">__MSG_hideButtonsDescription__</div>
</div> </div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="hideDeleteButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
<div class="switch-container">
<div option-type="toggle" toggle-type="reverse" sync-option="hideSkipButtonPlayerControls">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="hideDeleteButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="hideDeleteButtonPlayerControls">
__MSG_showSkipButton__
</div>
</label>
</div>
<br/>
<br/>
<br/>
<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>
</label>
</div>
<br/>
<br/>
<br/>
<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>
</label>
</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__ __MSG_showDeleteButton__
</div>
</label> </label>
</div> </div>
</div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="hideUploadButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
<br/> <div class="switch-container">
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideUploadButtonPlayerControls">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="hideUploadButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="hideUploadButtonPlayerControls">
__MSG_showUploadButton__ __MSG_showUploadButton__
</div>
</label> </label>
</div> </div>
</div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="hideSkipButtonPlayerControls">
<br/> <div class="switch-container">
<br/>
<br/>
<div option-type="toggle" sync-option="audioNotificationOnSkip">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="hideSkipButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="hideSkipButtonPlayerControls">
__MSG_audioNotification__ __MSG_showSkipButton__
</div>
</label> </label>
</div>
</div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="hideInfoButtonPlayerControls">
<br/> <div class="switch-container">
<br/> <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>
</div>
<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 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>
<div class="small-description">__MSG_audioNotificationDescription__</div> <div class="small-description">__MSG_audioNotificationDescription__</div>
</div> </div>
<br/> <div data-type="toggle" data-sync="showTimeWithSkips">
<br/> <div class="switch-container">
<div option-type="toggle" sync-option="showTimeWithSkips">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="showTimeWithSkips" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="showTimeWithSkips">
__MSG_showTimeWithSkips__ __MSG_showTimeWithSkips__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_showTimeWithSkipsDescription__</div> <div class="small-description">__MSG_showTimeWithSkipsDescription__</div>
</div> </div>
<br/> <div data-type="toggle" data-sync="darkMode">
<br/> <div class="switch-container">
<div option-type="toggle" sync-option="trackViewCount">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="darkMode" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="darkMode">
__MSG_enableViewTracking__ __MSG_darkModeOptionsPage__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatViewTracking__</div>
</div> </div>
<br/> <div data-type="toggle" data-toggle-type="reverse" data-sync="showDonationLink" data-no-safari="true">
<br/> <div class="switch-container">
<div option-type="toggle" sync-option="trackViewCountInPrivate" private-mode-only="true">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox" checked> <input id="showDonationLink" type="checkbox" checked>
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="showDonationLink">
__MSG_enableViewTrackingInPrivate__
</div>
</label>
<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>
</label>
<br/>
<br/>
<br/>
<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__ __MSG_hideDonationLink__
</div>
</label> </label>
</div>
</div>
</div> </div>
<br/> <div id="keybinds" class="option-group hidden">
<br/>
<br/>
<br/>
<div option-type="private-text-change" sync-option="userID" confirm-message="userIDChangeWarning"> <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>
<div id="import" class="option-group hidden">
<div data-type="private-text-change" data-sync="userID" data-confirm-message="userIDChangeWarning">
<div class="option-button trigger-button"> <div class="option-button trigger-button">
__MSG_changeUserID__ __MSG_changeUserID__
</div> </div>
<br/>
<div class="small-description">__MSG_whatChangeUserID__</div> <div class="small-description">__MSG_whatChangeUserID__</div>
<div class="option-hidden-section hidden"> <div class="option-hidden-section hidden spacing indent">
<br/>
<input class="option-text-box" type="text"> <input class="option-text-box" type="text">
<br/> <div class="option-button text-change-set inline low-profile">
<br/>
<div class="option-button text-change-set">
__MSG_setUserID__ __MSG_setUserID__
</div> </div>
</div> </div>
</div> </div>
<br/> <div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
<br/> <h2>__MSG_exportOptions__</h2>
<div option-type="private-text-change" sync-option="*" confirm-message="exportOptionsWarning"> <div>
<div class="option-button trigger-button"> <div class="option-button trigger-button inline">
__MSG_exportOptions__ __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> </div>
<br/>
<div class="small-description">__MSG_whatExportOptions__</div> <div class="small-description">__MSG_whatExportOptions__</div>
<div class="option-hidden-section hidden"> <div class="option-hidden-section hidden spacing indent">
<br/> <textarea class="option-text-box" rows="10" style="width:80%"></textarea>
<input class="option-text-box" type="text">
<br/>
<br/>
<div class="option-button text-change-set"> <div class="option-button text-change-set">
__MSG_setOptions__ __MSG_setOptions__
@ -555,51 +368,111 @@
</div> </div>
</div> </div>
<br/> </div>
<br/>
<div option-type="button-press" sync-option="copyDebugInformation" confirm-message="copyDebugInformation"> <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>
<div class="small-description">(__MSG_supportedSites__ Invidious, CloudTube)</div>
<div class="small-description">__MSG_supportOtherSitesDescription__ </div>
</div>
<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>
<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>
<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 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 class="small-description">__MSG_whatViewTracking__</div>
</div>
<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>
<div data-type="button-press" data-sync="copyDebugInformation" data-confirm-message="copyDebugInformation">
<div class="option-button trigger-button"> <div class="option-button trigger-button">
__MSG_copyDebugInformation__ __MSG_copyDebugInformation__
</div> </div>
<br/>
<div class="small-description">__MSG_copyDebugInformationOptions__</div> <div class="small-description">__MSG_copyDebugInformationOptions__</div>
</div> </div>
<br/> <div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
<br/> <div class="switch-container">
<div option-type="toggle" sync-option="testingServer" confirm-message="testingServerWarning" no-safari="true">
<label class="switch-container">
<label class="switch"> <label class="switch">
<input type="checkbox"> <input id="testingServer" type="checkbox">
<span class="slider round"></span> <span class="slider round"></span>
</label> </label>
<div class="switch-label"> <label class="switch-label" for="testingServer">
__MSG_enableTestingServer__ __MSG_enableTestingServer__
</div>
</label> </label>
</div>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatEnableTestingServer__</div> <div class="small-description">__MSG_whatEnableTestingServer__</div>
<br/>
<br/>
<br/>
</div> </div>
<div option-type="text-change" sync-option="serverAddress"> <div data-type="text-change" data-sync="serverAddress" data-dependent-on="testingServer" data-dependent-on-inverted="true">
<label class="text-label-container"> <label class="optionLabel inline">
<div>__MSG_customServerAddress__</div> <span class="optionLabel">__MSG_customServerAddress__:</span>
<input class="option-text-box" type="text"> <input class="option-text-box" type="text" style="margin-right:10px">
</label> </label>
<div class="small-description">__MSG_customServerAddressDescription__</div>
<div class="next-line">
<div class="option-button text-change-set inline"> <div class="option-button text-change-set inline">
__MSG_save__ __MSG_save__
</div> </div>
@ -607,14 +480,13 @@
<div class="option-button text-change-reset inline"> <div class="option-button text-change-reset inline">
__MSG_reset__ __MSG_reset__
</div> </div>
</div>
<br/>
<br/>
<div class="small-description">__MSG_customServerAddressDescription__</div>
</div> </div>
</div> </div>
</div>
</div> </div>
</body> </body>

View file

@ -31,24 +31,24 @@ class CategoryChooserComponent extends React.Component<CategoryChooserProps, Cat
{/* Headers */} {/* Headers */}
<tr id={"CategoryOptionsRow"} <tr id={"CategoryOptionsRow"}
className="categoryTableElement categoryTableHeader"> className="categoryTableElement categoryTableHeader">
<td id={"CategoryOptionName"}> <th id={"CategoryOptionName"}>
{chrome.i18n.getMessage("category")} {chrome.i18n.getMessage("category")}
</td> </th>
<td id={"CategorySkipOption"} <th id={"CategorySkipOption"}
className="skipOption"> className="skipOption">
{chrome.i18n.getMessage("skipOption")} {chrome.i18n.getMessage("skipOption")}
</td> </th>
<td id={"CategoryColorOption"} <th id={"CategoryColorOption"}
className="colorOption"> className="colorOption">
{chrome.i18n.getMessage("seekBarColor")} {chrome.i18n.getMessage("seekBarColor")}
</td> </th>
<td id={"CategoryPreviewColorOption"} <th id={"CategoryPreviewColorOption"}
className="previewColorOption"> className="previewColorOption">
{chrome.i18n.getMessage("previewColor")} {chrome.i18n.getMessage("previewColor")}
</td> </th>
</tr> </tr>
{this.getCategorySkipOptions()} {this.getCategorySkipOptions()}

View file

@ -0,0 +1,75 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import Config from "../config";
import { Keybind } from "../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
export interface KeybindProps {
option: string;
}
export interface KeybindState {
keybind: Keybind;
}
let dialog;
class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
constructor(props: KeybindProps) {
super(props);
this.state = {keybind: Config.config[this.props.option]};
}
render(): React.ReactElement {
return(
<>
<div className="keybind-buttons inline" title={chrome.i18n.getMessage("change")} onClick={() => this.openEditDialog()}>
{this.state.keybind?.ctrl && <div className="key keyControl">Ctrl</div>}
{this.state.keybind?.ctrl && <span className="keyControl">+</span>}
{this.state.keybind?.alt && <div className="key keyAlt">Alt</div>}
{this.state.keybind?.alt && <span className="keyAlt">+</span>}
{this.state.keybind?.shift && <div className="key keyShift">Shift</div>}
{this.state.keybind?.shift && <span className="keyShift">+</span>}
{this.state.keybind?.key != null && <div className="key keyBase">{formatKey(this.state.keybind.key)}</div>}
{this.state.keybind == null && <span className="unbound">{chrome.i18n.getMessage("notSet")}</span>}
</div>
{this.state.keybind != null &&
<div className="option-button trigger-button inline" onClick={() => this.unbind()}>
{chrome.i18n.getMessage("unbind")}
</div>
}
</>
);
}
equals(other: Keybind): boolean {
return keybindEquals(this.state.keybind, other);
}
toString(): string {
return keybindToString(this.state.keybind);
}
openEditDialog(): void {
dialog = 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;

View file

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

View file

@ -6,8 +6,8 @@ import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils"; import Utils from "../utils";
const utils = new Utils(); const utils = new Utils();
import { getSkippingText } from "../utils/categoryUtils"; import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg"; import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
@ -344,7 +344,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
className="sponsorSkipObject sponsorSkipNoticeButton" className="sponsorSkipObject sponsorSkipNoticeButton"
style={style} style={style}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}> 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> </button>
</span> </span>
); );

View file

@ -374,7 +374,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst") if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst")
.replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) { .replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) {
// Open options page // Open options page
chrome.runtime.sendMessage({message: "openConfig", hash: chosenCategory + "OptionsName"}); chrome.runtime.sendMessage({message: "openConfig", hash: "behavior"});
} }
return; return;

View file

@ -1,6 +1,7 @@
import * as CompileConfig from "../config.json"; import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.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 { interface SBConfig {
userID: string, userID: string,
@ -11,9 +12,6 @@ interface SBConfig {
defaultCategory: Category, defaultCategory: Category,
whitelistedChannels: string[], whitelistedChannels: string[],
forceChannelCheck: boolean, forceChannelCheck: boolean,
skipKeybind: string,
startSponsorKeybind: string,
submitKeybind: string,
minutesSaved: number, minutesSaved: number,
skipCount: number, skipCount: number,
sponsorTimesContributed: number, sponsorTimesContributed: number,
@ -54,6 +52,11 @@ interface SBConfig {
}, },
scrollToEditTimeUpdate: boolean, scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean, categoryPillUpdate: boolean,
darkMode: boolean,
skipKeybind: Keybind,
startSponsorKeybind: Keybind,
submitKeybind: Keybind,
// What categories should be skipped // What categories should be skipped
categorySelections: CategorySelection[], categorySelections: CategorySelection[],
@ -170,9 +173,6 @@ const Config: SBObject = {
defaultCategory: "chooseACategory" as Category, defaultCategory: "chooseACategory" as Category,
whitelistedChannels: [], whitelistedChannels: [],
forceChannelCheck: false, forceChannelCheck: false,
skipKeybind: "Enter",
startSponsorKeybind: ";",
submitKeybind: "'",
minutesSaved: 0, minutesSaved: 0,
skipCount: 0, skipCount: 0,
sponsorTimesContributed: 0, sponsorTimesContributed: 0,
@ -208,6 +208,18 @@ const Config: SBObject = {
autoSkipOnMusicVideos: false, autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false, 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: [{ categorySelections: [{
name: "sponsor" as Category, 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 // Remove some old unused options
if (config["sponsorVideoID"] !== undefined) { if (config["sponsorVideoID"] !== undefined) {
chrome.storage.sync.remove("sponsorVideoID"); chrome.storage.sync.remove("sponsorVideoID");

View file

@ -1,7 +1,7 @@
import Config from "./config"; import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types"; 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"; import Utils from "./utils";
const utils = new Utils(); const utils = new Utils();
@ -16,6 +16,7 @@ import * as Chat from "./js-components/chat";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser"; import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils"; import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
import { keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill"; import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils"; import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils"; import { GenericUtils } from "./utils/genericUtils";
@ -135,6 +136,9 @@ const manualSkipPercentCount = 0.5;
//get messages from the background script and the popup //get messages from the background script and the popup
chrome.runtime.onMessage.addListener(messageListener); 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 { function messageListener(request: Message, sender: unknown, sendResponse: (response: MessageResponse) => void): void | boolean {
//messages from popup script //messages from popup script
switch(request.message){ switch(request.message){
@ -1279,7 +1283,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
&& skippingSegments.length === 1 && skippingSegments.length === 1
&& skippingSegments[0].actionType === ActionType.Poi) { && skippingSegments[0].actionType === ActionType.Poi) {
skipButtonControlBar.enable(skippingSegments[0]); skipButtonControlBar.enable(skippingSegments[0]);
if (onMobileYouTube) skipButtonControlBar.setShowKeybindHint(false); if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
activeSkipKeybindElement?.setShowKeybindHint(false); activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar; 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 //send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) { if (!Config.config.dontShowNotice || !autoSkip) {
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime); 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); skipNotices.push(newSkipNotice);
activeSkipKeybindElement?.setShowKeybindHint(false); activeSkipKeybindElement?.setShowKeybindHint(false);
@ -1894,30 +1898,46 @@ function addPageListeners(): void {
function addHotkeyListener(): void { function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener); document.addEventListener("keydown", hotkeyListener);
document.addEventListener("keyup", (e) => pressedKeys.delete(e.key));
} }
function hotkeyListener(e: KeyboardEvent): void { function hotkeyListener(e: KeyboardEvent): void {
if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase()) if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|| document.activeElement?.id?.toLowerCase()?.includes("editable")) return; || 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 skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind; const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind; const submitKey = Config.config.submitKeybind;
switch (key) { if (!pressedKeys.has("AltGraph")) {
case skipKey: if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement) { if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement); activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
} return;
break; } else if (keybindEquals(key, startSponsorKey)) {
case startSponsorKey:
startOrEndTimingNewSegment(); startOrEndTimingNewSegment();
break; return;
case submitKey: } else if (keybindEquals(key, submitKey)) {
submitSponsorTimes();
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(); submitSponsorTimes();
break;
} }
} }

View file

@ -11,6 +11,10 @@ async function init() {
await utils.wait(() => Config.config !== null); await utils.wait(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");
}
if (!showDonationLink()) { if (!showDonationLink()) {
document.getElementById("sbDonate").style.display = "none"; document.getElementById("sbDonate").style.display = "none";
} }

View file

@ -1,6 +1,7 @@
import Config from "../config"; import Config from "../config";
import { SponsorTime } from "../types"; import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils"; import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import Utils from "../utils"; import Utils from "../utils";
import { AnimationUtils } from "../utils/animationUtils"; import { AnimationUtils } from "../utils/animationUtils";
@ -180,7 +181,7 @@ export class SkipButtonControlBar {
} }
private getTitle(): string { 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 { private getChapterPrefix(): HTMLElement {

View file

@ -1,3 +1,6 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import Config from "./config"; import Config from "./config";
import * as CompileConfig from "../config.json"; import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json"; import * as invidiousList from "../ci/invidiouslist.json";
@ -7,21 +10,38 @@ window.SB = Config;
import Utils from "./utils"; import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser"; import CategoryChooser from "./render/CategoryChooser";
import KeybindComponent from "./components/KeybindComponent";
import { showDonationLink } from "./utils/configUtils"; import { showDonationLink } from "./utils/configUtils";
const utils = new Utils(); const utils = new Utils();
let embed = false;
window.addEventListener('DOMContentLoaded', init); window.addEventListener('DOMContentLoaded', init);
async function init() { async function init() {
utils.localizeHtmlPage(); 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 // Remove header if needed
if (window.location.hash === "#embed") { if (window.location.hash === "#embed") {
embed = true;
for (const element of document.getElementsByClassName("titleBar")) { for (const element of document.getElementsByClassName("titleBar")) {
element.classList.add("hidden"); element.classList.add("hidden");
} }
document.getElementById("options").classList.add("embed"); document.getElementById("options").classList.add("embed");
createStickyHeader();
} }
if (!Config.configListeners.includes(optionsConfigUpdateListener)) { if (!Config.configListeners.includes(optionsConfigUpdateListener)) {
@ -30,8 +50,12 @@ async function init() {
await utils.wait(() => Config.config !== null); await utils.wait(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");
}
if (!showDonationLink()) { if (!showDonationLink()) {
document.getElementById("sbDonate").style.visibility = "hidden"; document.getElementById("sbDonate").classList.add("hidden");
} }
// Set all of the toggle options to the correct option // Set all of the toggle options to the correct option
@ -39,31 +63,31 @@ async function init() {
const optionsElements = optionsContainer.querySelectorAll("*"); const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) { for (let i = 0; i < optionsElements.length; i++) {
if ((optionsElements[i].getAttribute("private-mode-only") === "true" && !(await isIncognitoAllowed())) const dependentOnName = optionsElements[i].getAttribute("data-dependent-on");
|| (optionsElements[i].getAttribute("no-safari") === "true" && navigator.vendor === "Apple Computer, Inc.") const dependentOn = optionsContainer.querySelector(`[data-sync='${dependentOnName}']`);
|| (optionsElements[i].getAttribute("if-false") && Config.config[optionsElements[i].getAttribute("if-false")])) { let isDependentOnReversed = false;
optionsElements[i].classList.add("hidden"); 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; 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": { case "toggle": {
const optionResult = Config.config[option]; const optionResult = Config.config[option];
const checkbox = optionsElements[i].querySelector("input"); 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) { if (optionResult != undefined)
checkbox.checked = optionResult; checkbox.checked = reverse ? !optionResult : optionResult;
if (reverse) {
optionsElements[i].querySelector("input").checked = !optionResult;
}
}
// See if anything extra should be run first time // See if anything extra should be run first time
switch (option) { switch (option) {
@ -73,7 +97,7 @@ async function init() {
} }
// Add click listener // Add click listener
checkbox.addEventListener("click", () => { checkbox.addEventListener("click", async () => {
// Confirm if required // Confirm if required
if (checkbox.checked && confirmMessage && !confirm(chrome.i18n.getMessage(confirmMessage))){ if (checkbox.checked && confirmMessage && !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = false; checkbox.checked = false;
@ -92,11 +116,36 @@ async function init() {
// Enable the notice // Enable the notice
Config.config["dontShowNotice"] = false; 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; showNoticeSwitch.checked = true;
} }
break; 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; break;
@ -155,7 +204,15 @@ async function init() {
const button = optionsElements[i].querySelector(".trigger-button"); const button = optionsElements[i].querySelector(".trigger-button");
button.addEventListener("click", () => activatePrivateTextChange(<HTMLElement> optionsElements[i])); 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 // See if anything extra must be done
switch (privateTextChangeOption) { switch (privateTextChangeOption) {
case "invidiousInstances": case "invidiousInstances":
@ -167,7 +224,7 @@ async function init() {
case "button-press": { case "button-press": {
const actionButton = optionsElements[i].querySelector(".trigger-button"); const actionButton = optionsElements[i].querySelector(".trigger-button");
switch(optionsElements[i].getAttribute("sync-option")) { switch(optionsElements[i].getAttribute("data-sync")) {
case "copyDebugInformation": case "copyDebugInformation":
actionButton.addEventListener("click", copyDebugOutputToClipboard); actionButton.addEventListener("click", copyDebugOutputToClipboard);
break; break;
@ -176,9 +233,7 @@ async function init() {
break; break;
} }
case "keybind-change": { case "keybind-change": {
const keybindButton = optionsElements[i].querySelector(".trigger-button"); ReactDOM.render(React.createElement(KeybindComponent, {option: option}), optionsElements[i].querySelector("div"));
keybindButton.addEventListener("click", () => activateKeybindChange(<HTMLElement> optionsElements[i]));
break; break;
} }
case "display": { 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"); 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 * Called when the config is updated
* *
@ -234,7 +336,7 @@ function optionsConfigUpdateListener() {
const optionsElements = optionsContainer.querySelectorAll("*"); const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) { for (let i = 0; i < optionsElements.length; i++) {
switch (optionsElements[i].getAttribute("option-type")) { switch (optionsElements[i].getAttribute("data-type")) {
case "display": case "display":
updateDisplayElement(<HTMLElement> optionsElements[i]) updateDisplayElement(<HTMLElement> optionsElements[i])
} }
@ -247,16 +349,26 @@ function optionsConfigUpdateListener() {
* @param element * @param element
*/ */
function updateDisplayElement(element: HTMLElement) { function updateDisplayElement(element: HTMLElement) {
const displayOption = element.getAttribute("sync-option") const displayOption = element.getAttribute("data-sync")
const displayText = Config.config[displayOption]; const displayText = Config.config[displayOption];
element.innerText = displayText; element.innerText = displayText;
// See if anything extra must be run // See if anything extra must be run
switch (displayOption) { switch (displayOption) {
case "invidiousInstances": case "invidiousInstances": {
element.innerText = displayText.join(', '); 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; break;
} }
}
} }
/** /**
@ -270,6 +382,8 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const button = element.querySelector(".trigger-button"); const button = element.querySelector(".trigger-button");
const setButton = element.querySelector(".text-change-set"); 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() { setButton.addEventListener("click", async function() {
if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http")) { if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http")) {
alert(chrome.i18n.getMessage("addInvidiousInstanceError")); alert(chrome.i18n.getMessage("addInvidiousInstanceError"));
@ -287,19 +401,26 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
invidiousOnClick(checkbox, "supportInvidious"); invidiousOnClick(checkbox, "supportInvidious");
textBox.value = ""; resetButton.classList.remove("hidden");
// Hide this section again // Hide this section again
textBox.value = "";
element.querySelector(".option-hidden-section").classList.add("hidden"); element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled"); 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() { resetButton.addEventListener("click", function() {
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) { if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
// Set to CI populated list // Set to CI populated list
Config.config[option] = invidiousList; Config.config[option] = invidiousList;
resetButton.classList.add("hidden");
} }
}); });
} }
@ -351,91 +472,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. * Will trigger the textbox to appear to be able to change an option's text.
* *
@ -448,7 +484,7 @@ function activatePrivateTextChange(element: HTMLElement) {
button.classList.add("disabled"); button.classList.add("disabled");
const textBox = <HTMLInputElement> element.querySelector(".option-text-box"); 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 // See if anything extra must be done
switch (option) { switch (option) {
@ -476,38 +512,7 @@ function activatePrivateTextChange(element: HTMLElement) {
const setButton = element.querySelector(".text-change-set"); const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => { setButton.addEventListener("click", async () => {
const confirmMessage = element.getAttribute("confirm-message"); setTextOption(option, element, textBox.value);
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;
}
}
}); });
// See if anything extra must be done // See if anything extra must be done
@ -531,6 +536,77 @@ function activatePrivateTextChange(element: HTMLElement) {
element.querySelector(".option-hidden-section").classList.remove("hidden"); element.querySelector(".option-hidden-section").classList.remove("hidden");
} }
/**
* 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. * Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue. * Returns null and alerts the user if there is an issue.

View file

@ -125,7 +125,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
unwhitelistChannel(); unwhitelistChannel();
} }
}); });
PageElements.whitelistForceCheck.addEventListener("click", openOptions); PageElements.whitelistForceCheck.addEventListener("click", () => {openOptionsAt("behavior")});
PageElements.toggleSwitch.addEventListener("change", function () { PageElements.toggleSwitch.addEventListener("change", function () {
toggleSkipping(!this.checked); toggleSkipping(!this.checked);
}); });
@ -516,6 +516,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
chrome.runtime.sendMessage({ "message": "openConfig" }); chrome.runtime.sendMessage({ "message": "openConfig" });
} }
function openOptionsAt(location) {
chrome.runtime.sendMessage({ "message": "openConfig", "hash": location });
}
function openHelp() { function openHelp() {
chrome.runtime.sendMessage({ "message": "openHelp" }); chrome.runtime.sendMessage({ "message": "openHelp" });
} }

View file

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

View file

@ -1,5 +1,44 @@
import Config from "../config"; import Config from "../config";
import { Keybind } from "../types";
export function showDonationLink(): boolean { export function showDonationLink(): boolean {
return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink; 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);
}