diff options
author | Ajay Ramachandran <[email protected]> | 2022-01-24 23:41:21 -0500 |
---|---|---|
committer | GitHub <[email protected]> | 2022-01-24 23:41:21 -0500 |
commit | f555a8e7bbb84e34f7547be47dbc00ba51df63f4 (patch) | |
tree | 74fc35ede4281b0fed0b7916b2157d0ad5a8d29f | |
parent | 23a6940894b63d314a1de211e0505aceda7e2553 (diff) | |
parent | aac2572b4e6c61226d3b5f8efa30ea95a13571c5 (diff) | |
download | SponsorBlock-f555a8e7bbb84e34f7547be47dbc00ba51df63f4.tar.gz SponsorBlock-f555a8e7bbb84e34f7547be47dbc00ba51df63f4.zip |
Merge pull request #1093 from AronHK/settings
Settings rework
-rw-r--r-- | public/_locales/en/messages.json | 71 | ||||
-rw-r--r-- | public/help/index.html | 1 | ||||
-rw-r--r-- | public/help/styles.css | 135 | ||||
-rw-r--r-- | public/options/options.css | 423 | ||||
-rw-r--r-- | public/options/options.html | 878 | ||||
-rw-r--r-- | src/components/CategoryChooserComponent.tsx | 16 | ||||
-rw-r--r-- | src/components/KeybindComponent.tsx | 75 | ||||
-rw-r--r-- | src/components/KeybindDialogComponent.tsx | 165 | ||||
-rw-r--r-- | src/components/SkipNoticeComponent.tsx | 4 | ||||
-rw-r--r-- | src/components/SponsorTimeEditComponent.tsx | 2 | ||||
-rw-r--r-- | src/config.ts | 49 | ||||
-rw-r--r-- | src/content.ts | 46 | ||||
-rw-r--r-- | src/help.ts | 4 | ||||
-rw-r--r-- | src/js-components/skipButtonControlBar.ts | 3 | ||||
-rw-r--r-- | src/options.ts | 374 | ||||
-rw-r--r-- | src/popup.ts | 6 | ||||
-rw-r--r-- | src/types.ts | 8 | ||||
-rw-r--r-- | src/utils/configUtils.ts | 39 |
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 |