mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-06 11:37:02 +03:00
@@ -2,7 +2,7 @@
|
||||
"serverAddress": "https://sponsor.ajay.app",
|
||||
"testingServerAddress": "https://sponsor.ajay.app/test",
|
||||
"serverAddressComment": "This specifies the default SponsorBlock server to connect to",
|
||||
"categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "music_offtopic"],
|
||||
"categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "chapter", "music_offtopic"],
|
||||
"categorySupport": {
|
||||
"sponsor": ["skip", "mute", "full"],
|
||||
"selfpromo": ["skip", "mute", "full"],
|
||||
@@ -13,7 +13,8 @@
|
||||
"preview": ["skip", "mute"],
|
||||
"filler": ["skip", "mute"],
|
||||
"music_offtopic": ["skip"],
|
||||
"poi_highlight": ["poi"]
|
||||
"poi_highlight": ["poi"],
|
||||
"chapter": ["chapter"]
|
||||
},
|
||||
"wikiLinks": {
|
||||
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
|
||||
@@ -27,6 +28,7 @@
|
||||
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
|
||||
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
|
||||
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines",
|
||||
"mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment"
|
||||
"mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment",
|
||||
"chapter": "https://wiki.sponsor.ajay.app/w/Chapter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
],
|
||||
"css": [
|
||||
"content.css",
|
||||
"shared.css",
|
||||
"./libs/Source+Sans+Pro.css",
|
||||
"popup.css"
|
||||
]
|
||||
@@ -48,9 +49,11 @@
|
||||
"icons/beep.ogg",
|
||||
"icons/pause.svg",
|
||||
"icons/stop.svg",
|
||||
"icons/skip.svg",
|
||||
"icons/heart.svg",
|
||||
"icons/visible.svg",
|
||||
"icons/not_visible.svg",
|
||||
"icons/sort.svg",
|
||||
"icons/money.svg",
|
||||
"icons/segway.png",
|
||||
"icons/close-smaller.svg",
|
||||
@@ -61,6 +64,8 @@
|
||||
"icons/bolt.svg",
|
||||
"icons/stopwatch.svg",
|
||||
"icons/music-note.svg",
|
||||
"icons/import.svg",
|
||||
"icons/export.svg",
|
||||
"icons/PlayerInfoIconSponsorBlocker.svg",
|
||||
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
||||
"popup.html",
|
||||
|
||||
1089
package-lock.json
generated
1089
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.13",
|
||||
"jest": "^28.1.3",
|
||||
"jest-environment-jsdom": "^28.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"schema-utils": "^4.0.0",
|
||||
"selenium-webdriver": "^4.3.1",
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
"Segments": {
|
||||
"message": "segments"
|
||||
},
|
||||
"SegmentsCap": {
|
||||
"message": "Segments"
|
||||
},
|
||||
"Chapters": {
|
||||
"message": "Chapters"
|
||||
},
|
||||
"renderAsChapters": {
|
||||
"message": "Render segments as chapters",
|
||||
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
|
||||
},
|
||||
"upvoteButtonInfo": {
|
||||
"message": "Upvote this submission"
|
||||
},
|
||||
@@ -289,6 +299,14 @@
|
||||
"message": "Submit segments",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"nextChapterKeybind": {
|
||||
"message": "Next chapter",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"previousChapterKeybind": {
|
||||
"message": "Previous chapter",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"keybindDescription": {
|
||||
"message": "Select a key by typing it and choose any modifier keys you wish to use."
|
||||
},
|
||||
@@ -545,6 +563,10 @@
|
||||
"message": "to",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"CopiedExclamation": {
|
||||
"message": "Copied!",
|
||||
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Include segue transitions"
|
||||
},
|
||||
@@ -696,6 +718,21 @@
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Can skip to the title or thumbnail"
|
||||
},
|
||||
"category_chapter": {
|
||||
"message": "Chapter"
|
||||
},
|
||||
"category_chapter_description": {
|
||||
"message": "Custom named chapters describing major sections of a video."
|
||||
},
|
||||
"category_chapter_guideline1": {
|
||||
"message": "Don't mention sponsor brand names"
|
||||
},
|
||||
"category_chapter_guideline2": {
|
||||
"message": "Use larger chapters for general sections"
|
||||
},
|
||||
"category_chapter_guideline3": {
|
||||
"message": "Smaller chapters can be placed inside larger ones"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Livestream: Donation/Message Readings"
|
||||
},
|
||||
@@ -726,6 +763,9 @@
|
||||
"showOverlay_full": {
|
||||
"message": "Show Label"
|
||||
},
|
||||
"showOverlay_chapter": {
|
||||
"message": "Show Chapters"
|
||||
},
|
||||
"autoSkipOnMusicVideos": {
|
||||
"message": "Auto skip all segments when there is a non-music segment"
|
||||
},
|
||||
@@ -781,6 +821,10 @@
|
||||
"bracketEnd": {
|
||||
"message": "(End)"
|
||||
},
|
||||
"End": {
|
||||
"message": "End",
|
||||
"description": "Button that skips to the end of a segment"
|
||||
},
|
||||
"hiddenDueToDownvote": {
|
||||
"message": "hidden: downvote"
|
||||
},
|
||||
@@ -821,6 +865,13 @@
|
||||
"downvoteDescription": {
|
||||
"message": "Incorrect/Wrong Timing"
|
||||
},
|
||||
"incorrectVote": {
|
||||
"message": "Incorrect"
|
||||
},
|
||||
"harmfulVote": {
|
||||
"message": "Harmful",
|
||||
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
|
||||
},
|
||||
"incorrectCategory": {
|
||||
"message": "Change Category"
|
||||
},
|
||||
@@ -856,6 +907,9 @@
|
||||
"categoryPillTitleText": {
|
||||
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate"
|
||||
},
|
||||
"chapterNameTooltipWarning": {
|
||||
"message": "One of your chapter names is similar to a category. You should use categories when possible instead."
|
||||
},
|
||||
"experiementOptOut": {
|
||||
"message": "Opt-out of all future experiments",
|
||||
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."
|
||||
@@ -1039,5 +1093,62 @@
|
||||
},
|
||||
"confirmResetToDefault": {
|
||||
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone."
|
||||
},
|
||||
"exportSegments": {
|
||||
"message": "Export segments"
|
||||
},
|
||||
"importSegments": {
|
||||
"message": "Import chapters"
|
||||
},
|
||||
"Import": {
|
||||
"message": "Import",
|
||||
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
|
||||
},
|
||||
"redeemSuccess": {
|
||||
"message": "Reedem Successful!"
|
||||
},
|
||||
"redeemFailed": {
|
||||
"message": "License key is invalid"
|
||||
},
|
||||
"hideUpsells": {
|
||||
"message": "Hide options not available without extra payment"
|
||||
},
|
||||
"chooseACountry": {
|
||||
"message": "Choose a country"
|
||||
},
|
||||
"noDiscount": {
|
||||
"message": "You do not qualify for a discount"
|
||||
},
|
||||
"discountLink": {
|
||||
"message": "Discount Link (See the pink price)"
|
||||
},
|
||||
"selectYourCountry": {
|
||||
"message": "Select your country"
|
||||
},
|
||||
"alreadyDonated": {
|
||||
"message": "If you've donated any amount before now, you may redeem free access by emailing:",
|
||||
"description": "After the colon is an email address"
|
||||
},
|
||||
"cantAfford": {
|
||||
"message": "If you can't afford to purchase a license, click {here} to see if you are eligible for a discount",
|
||||
"description": "Keep the curly braces"
|
||||
},
|
||||
"patreonSignIn": {
|
||||
"message": "Sign in with Patreon"
|
||||
},
|
||||
"redeem": {
|
||||
"message": "Redeem"
|
||||
},
|
||||
"joinOnPatreon": {
|
||||
"message": "Subscribe on Patreon"
|
||||
},
|
||||
"oneTimePurchase": {
|
||||
"message": "One Time Purchase"
|
||||
},
|
||||
"enterLicenseKey": {
|
||||
"message": "Enter License Key"
|
||||
},
|
||||
"chaptersPage1": {
|
||||
"message": "SponsorBlock crowd-sourced chapters feature is only available to people who purchase a license, or for people who are granted access for free due their past contributions"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
:root {
|
||||
--skip-notice-right: 10px;
|
||||
--skip-notice-padding: 5px;
|
||||
--skip-notice-margin: 5px;
|
||||
--skip-notice-border-horizontal: 5px;
|
||||
--skip-notice-border-vertical: 10px;
|
||||
--sb-dark-red-outline: rgb(130,0,0,0.9);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -12,7 +21,7 @@
|
||||
|
||||
height: 100%;
|
||||
transform: scaleY(0.6) translateY(-30%) translateY(1.5px);
|
||||
z-index: 40;
|
||||
z-index: 42;
|
||||
|
||||
transition: transform .1s cubic-bezier(0,0,0.2,1);
|
||||
}
|
||||
@@ -45,23 +54,48 @@
|
||||
transform: translateY(-1em) !important;
|
||||
}
|
||||
|
||||
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
|
||||
transform: translateY(-2em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||
transform: translateY(-2em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
|
||||
transform: translateY(-4em) !important;
|
||||
}
|
||||
|
||||
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(1em) !important;
|
||||
}
|
||||
|
||||
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(2em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(0.5em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
|
||||
transform: translateY(1em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||
display: block !important;
|
||||
transform: translateY(1em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||
display: block !important;
|
||||
transform: translateY(2em) !important;
|
||||
}
|
||||
|
||||
div:hover > .sponsorBlockChapterBar {
|
||||
z-index: 41 !important;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
.popup {
|
||||
@@ -88,6 +122,16 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Removes auto width from being a ytp-player-button */
|
||||
.sbPlayerDownvote {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* Adds back the padding */
|
||||
.sbPlayerDownvote svg {
|
||||
padding-right: 3.6px;
|
||||
}
|
||||
|
||||
.autoHiding {
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -113,8 +157,8 @@
|
||||
.sponsorSkipObject {
|
||||
font-family: Roboto, Arial, Helvetica, sans-serif;
|
||||
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
margin-left: var(--skip-notice-margin);
|
||||
margin-right: var(--skip-notice-margin);
|
||||
}
|
||||
|
||||
.sponsorSkipLogo {
|
||||
@@ -145,7 +189,7 @@
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 100px;
|
||||
right: 10px;
|
||||
right: var(--skip-notice-right);
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeParent {
|
||||
@@ -525,7 +569,7 @@ input::-webkit-inner-spin-button {
|
||||
margin-bottom: 5px;
|
||||
|
||||
background-color: rgba(28, 28, 28, 0.9);
|
||||
border-color: rgb(130,0,0,0.9);
|
||||
border-color: var(--sb-dark-red-outline);
|
||||
color: white;
|
||||
border-width: 3px;
|
||||
padding: 3px;
|
||||
@@ -536,6 +580,45 @@ input::-webkit-inner-spin-button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Start SelectorComponent */
|
||||
|
||||
.sbSelector {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: calc(100% - var(--skip-notice-right) - var(--skip-notice-padding) * 2 - var(--skip-notice-margin) * 2 - var(--skip-notice-border-horizontal) * 2);
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.sbSelectorBackground {
|
||||
text-align: center;
|
||||
|
||||
background-color: rgba(28, 28, 28, 0.9);
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
margin: auto;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.sbSelectorOption {
|
||||
cursor: pointer;
|
||||
background-color: rgb(43, 43, 43);
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.sbSelectorOption:hover {
|
||||
background-color: #3a0000;
|
||||
}
|
||||
|
||||
/* End SelectorComponent */
|
||||
|
||||
.helpButton {
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
@@ -623,6 +706,11 @@ input::-webkit-inner-spin-button {
|
||||
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.sponsorBlockLockedColor {
|
||||
color: #ffc83d;
|
||||
}
|
||||
|
||||
106
public/icons/export.svg
Normal file
106
public/icons/export.svg
Normal file
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 67.671 67.671"
|
||||
style="enable-background:new 0 0 67.671 67.671;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="export.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs41" /><sodipodi:namedview
|
||||
id="namedview39"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.309749"
|
||||
inkscape:cx="33.835499"
|
||||
inkscape:cy="16.649214"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="731"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1" />
|
||||
<g
|
||||
id="g6"
|
||||
style="fill:#ffffff">
|
||||
<path
|
||||
d="M 52.946,23.348 H 42.834 v 6 h 10.112 c 3.007,0 5.34,1.536 5.34,2.858 v 26.606 c 0,1.322 -2.333,2.858 -5.34,2.858 H 14.724 c -3.007,0 -5.34,-1.536 -5.34,-2.858 V 32.207 c 0,-1.322 2.333,-2.858 5.34,-2.858 h 10.11 v -6 h -10.11 c -6.359,0 -11.34,3.891 -11.34,8.858 v 26.606 c 0,4.968 4.981,8.858 11.34,8.858 h 38.223 c 6.358,0 11.34,-3.891 11.34,-8.858 V 32.207 C 64.286,27.239 59.305,23.348 52.946,23.348 Z"
|
||||
id="path2"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
d="m 24.957,14.955 c 0.768,0 1.535,-0.293 2.121,-0.879 l 3.756,-3.756 v 13.028 6 11.494 c 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3 v -11.494 -6 -13.231 l 3.959,3.959 c 0.586,0.586 1.354,0.879 2.121,0.879 0.767,0 1.535,-0.293 2.121,-0.879 1.172,-1.171 1.172,-3.071 0,-4.242 L 36.078,0.877 C 35.492,0.291 34.725,0 33.958,0 33.95,0 33.943,0 33.935,0 33.927,0 33.92,0 33.912,0 33.145,0 32.378,0.291 31.792,0.877 l -8.957,8.957 c -1.172,1.171 -1.172,3.071 0,4.242 0.587,0.586 1.354,0.879 2.122,0.879 z"
|
||||
id="path4"
|
||||
style="fill:#ffffff" />
|
||||
</g>
|
||||
<g
|
||||
id="g8"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g10"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g12"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g14"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g16"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g18"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g20"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g22"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g24"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g26"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g28"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g30"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g32"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g34"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
<g
|
||||
id="g36"
|
||||
style="fill:#ffffff">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
91
public/icons/import.svg
Normal file
91
public/icons/import.svg
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 67.671 67.671"
|
||||
style="enable-background:new 0 0 67.671 67.671;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="import.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs41" /><sodipodi:namedview
|
||||
id="namedview39"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.309749"
|
||||
inkscape:cx="33.835499"
|
||||
inkscape:cy="33.835499"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="983"
|
||||
inkscape:window-x="482"
|
||||
inkscape:window-y="768"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g6" />
|
||||
<g
|
||||
id="g6">
|
||||
<path
|
||||
d="M52.946,23.348H42.834v6h10.112c3.007,0,5.34,1.536,5.34,2.858v26.606c0,1.322-2.333,2.858-5.34,2.858H14.724 c-3.007,0-5.34-1.536-5.34-2.858V32.207c0-1.322,2.333-2.858,5.34-2.858h10.11v-6h-10.11c-6.359,0-11.34,3.891-11.34,8.858v26.606 c0,4.968,4.981,8.858,11.34,8.858h38.223c6.358,0,11.34-3.891,11.34-8.858V32.207C64.286,27.239,59.305,23.348,52.946,23.348z"
|
||||
id="path2"
|
||||
style="fill:#ffffff" />
|
||||
<path
|
||||
d="m 42.913,34.887 c -0.768,0 -1.370265,0.528017 -2.121,0.879 l -3.756,3.756 v -19.028 -6 V 3 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 v 11.494 12 13.231 l -3.959,-3.959 c -0.586,-0.586 -1.354,-0.879 -2.121,-0.879 -0.767,0 -1.535,0.293 -2.121,0.879 -1.172,1.171 -1.172,3.071 0,4.242 l 8.957,8.957 c 0.586,0.586 1.353,0.877 2.12,0.877 h 0.023 0.023 c 0.767,0 1.534,-0.291 2.12,-0.877 l 8.957,-8.957 c 1.172,-1.171 1.172,-3.071 0,-4.242 -0.587,-0.586 -1.354,-0.879 -2.122,-0.879 z"
|
||||
id="path4"
|
||||
sodipodi:nodetypes="sscccssscccssccsscssccs"
|
||||
style="fill:#ffffff" />
|
||||
</g>
|
||||
<g
|
||||
id="g8">
|
||||
</g>
|
||||
<g
|
||||
id="g10">
|
||||
</g>
|
||||
<g
|
||||
id="g12">
|
||||
</g>
|
||||
<g
|
||||
id="g14">
|
||||
</g>
|
||||
<g
|
||||
id="g16">
|
||||
</g>
|
||||
<g
|
||||
id="g18">
|
||||
</g>
|
||||
<g
|
||||
id="g20">
|
||||
</g>
|
||||
<g
|
||||
id="g22">
|
||||
</g>
|
||||
<g
|
||||
id="g24">
|
||||
</g>
|
||||
<g
|
||||
id="g26">
|
||||
</g>
|
||||
<g
|
||||
id="g28">
|
||||
</g>
|
||||
<g
|
||||
id="g30">
|
||||
</g>
|
||||
<g
|
||||
id="g32">
|
||||
</g>
|
||||
<g
|
||||
id="g34">
|
||||
</g>
|
||||
<g
|
||||
id="g36">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
public/icons/skip.svg
Normal file
1
public/icons/skip.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>
|
||||
|
After Width: | Height: | Size: 196 B |
1
public/icons/sort.svg
Normal file
1
public/icons/sort.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 201 B |
@@ -123,6 +123,14 @@ html, body {
|
||||
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
|
||||
}
|
||||
|
||||
.categoryExtraOptions {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#music_offtopic_autoSkipOnMusicVideos {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.option-group > div:last-child, .option-group > #keybind-dialog {
|
||||
border-bottom: inherit;
|
||||
}
|
||||
@@ -309,6 +317,10 @@ input[type='number'] {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
tr.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#options {
|
||||
height: 100vh;
|
||||
flex-basis: 80%;
|
||||
@@ -671,3 +683,8 @@ svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upsellButton {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -66,18 +66,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<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 data-type="toggle" data-sync="muteSegments">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
@@ -314,6 +302,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-toggle-type="reverse" data-sync="showUpsells" data-no-safari="true">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="showUpsell" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="showUpsells">
|
||||
__MSG_hideUpsells__
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="keybinds" class="option-group hidden">
|
||||
@@ -333,6 +333,16 @@
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
<div data-type="keybind-change" data-sync="nextChapterKeybind">
|
||||
<label class="optionLabel">__MSG_nextChapterKeybind__:</label>
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
<div data-type="keybind-change" data-sync="previousChapterKeybind">
|
||||
<label class="optionLabel">__MSG_previousChapterKeybind__:</label>
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="import" class="option-group hidden">
|
||||
|
||||
@@ -152,22 +152,46 @@
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh segments button
|
||||
*/
|
||||
#refreshSegmentsButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
#issueReporterImportExport {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#refreshSegmentsButton, #issueReporterImportExport button {
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
margin: 5px auto;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
}
|
||||
#refreshSegmentsButton:hover {
|
||||
|
||||
#refreshSegmentsButton:hover, #issueReporterImportExport button:hover {
|
||||
background-color: var(--sb-grey-bg-color);
|
||||
}
|
||||
|
||||
#issueReporterImportExport button {
|
||||
padding: 5px;
|
||||
margin-right: 15px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#issueReporterImportExport img {
|
||||
width: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#importSegmentsText {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
#importSegmentsMenu button {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
* <details> wrapper around each segment
|
||||
*/
|
||||
@@ -199,6 +223,15 @@
|
||||
.segmentSummary > div {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.segmentActive {
|
||||
color: #bdfffb;
|
||||
}
|
||||
|
||||
.segmentPassed {
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
/*
|
||||
* Category dot in segment
|
||||
*/
|
||||
@@ -560,3 +593,45 @@
|
||||
margin-bottom: 20px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#sponsorBlockPopupBody .u-mZ {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#sponsorBlockPopupBody .hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#issueReporterTabs {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#issueReporterTabs > span {
|
||||
padding: 2px 4px;
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
background-color: #444848;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#issueReporterTabs > span > span {
|
||||
position: relative;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#issueReporterTabs > span > span::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.1em;
|
||||
background-color: rgb(145, 0, 0);
|
||||
transition: transform 300ms;
|
||||
transform: scaleX(0);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
#issueReporterTabs > span.sbSelected > span::after {
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
|
||||
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
|
||||
<link id="sponsorBlockStyleSheet" href="shared.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body id="sponsorBlockPopupBody" style="visibility: hidden">
|
||||
@@ -34,7 +35,33 @@
|
||||
</button>
|
||||
<!-- Video Segments -->
|
||||
<div id="issueReporterContainer">
|
||||
<div id="issueReporterTabs" class="hidden">
|
||||
<span id="issueReporterTabSegments" class="sbSelected">
|
||||
<span>__MSG_SegmentsCap__</span>
|
||||
</span>
|
||||
<span id="issueReporterTabChapters">
|
||||
<span>__MSG_Chapters__</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="issueReporterTimeButtons"></div>
|
||||
<div id="issueReporterImportExport" class="hidden">
|
||||
<div id="importExportButtons">
|
||||
<button id="importSegmentsButton" title="__MSG_importSegments__" class="hidden">
|
||||
<img src="/icons/import.svg" alt="Refresh icon" id="importSegments" />
|
||||
</button>
|
||||
<button id="exportSegmentsButton" title="__MSG_exportSegments__">
|
||||
<img src="/icons/export.svg" alt="Export icon" id="exportSegments" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span id="importSegmentsMenu" class="hidden">
|
||||
<textarea id="importSegmentsText" rows="5" style="width:80%"></textarea>
|
||||
|
||||
<button id="importSegmentsSubmit" title="__MSG_importSegments__">
|
||||
__MSG_Import__
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
1
public/res/countries.json
Normal file
1
public/res/countries.json
Normal file
@@ -0,0 +1 @@
|
||||
{"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}}
|
||||
219
public/shared.css
Normal file
219
public/shared.css
Normal file
@@ -0,0 +1,219 @@
|
||||
.sponsorSkipNoticeParent {
|
||||
position: absolute;
|
||||
|
||||
bottom: 100px;
|
||||
right: var(--skip-notice-right);
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeParent, .sponsorSkipNotice {
|
||||
border-spacing: var(--skip-notice-border-horizontal) var(--skip-notice-border-vertical);
|
||||
padding-left: var(--skip-notice-padding);
|
||||
padding-right: var(--skip-notice-padding);
|
||||
|
||||
border-collapse: unset;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeParent {
|
||||
min-width: 350px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.sponsorSkipNotice {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeTableContainer {
|
||||
background-color: rgba(28, 28, 28, 0.9);
|
||||
border-radius: 5px;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.sponsorSkipNotice {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeLimitWidth {
|
||||
max-width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.sponsorSkipNotice .hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* For Cloudtube */
|
||||
.sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeFadeIn {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeFaded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeFadeOut {
|
||||
transition: opacity 3s cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
opacity: 0 !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.sponsorSkipNotice .sponsorSkipNoticeTimeLeft {
|
||||
color: #eeeeee;
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid #eeeeee;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeTimeLeft img {
|
||||
vertical-align: middle;
|
||||
height: 13px;
|
||||
|
||||
padding-top: 7.8%;
|
||||
padding-bottom: 7.8%;
|
||||
}
|
||||
|
||||
/* if two are very close to eachother */
|
||||
.secondSkipNotice {
|
||||
bottom: 290px;
|
||||
}
|
||||
|
||||
.noticeLeftIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sponsorSkipNotice .sponsorSkipNoticeUnskipSection {
|
||||
float: left;
|
||||
|
||||
border-left: 1px solid rgb(150, 150, 150);
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeButton {
|
||||
background: none;
|
||||
color: rgb(235, 235, 235);
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 13.3333px !important;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
margin-right: 10px;
|
||||
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeButton:hover {
|
||||
background-color: rgba(235, 235, 235,0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
transition: background-color 0.4s;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeFirstRow .sponsorSkipNoticeButton.sponsorSkipSmallButton {
|
||||
height: 1.3em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sponsorTimesVoteButtonsContainer {
|
||||
float: left;
|
||||
vertical-align:middle;
|
||||
padding: 2px 5px;
|
||||
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.sponsorTimesVoteButtonsContainer div{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeRightSection {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
|
||||
float: right;
|
||||
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeRightButton {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeCloseButton {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
box-sizing: unset;
|
||||
|
||||
padding: 2px 5px;
|
||||
|
||||
margin-left: 2px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeCloseButton.biggerCloseButton {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.sponsorSkipMessage {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: rgb(235, 235, 235);
|
||||
|
||||
margin-top: auto;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.sponsorSkipInfo {
|
||||
font-size: 10px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
#sponsorTimesThanksForVotingText {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#sponsorTimesThanksForVotingInfoText {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.sponsorTimesVoteButtonMessage {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.sponsorTimesInfoMessage {
|
||||
font-size: 13.3333px;
|
||||
color: rgb(235, 235, 235);
|
||||
}
|
||||
|
||||
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
||||
padding-left: 5px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
94
public/upsell/index.html
Normal file
94
public/upsell/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<title>Upsell - SponsorBlock</title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link href="styles.css" rel="stylesheet" />
|
||||
|
||||
<script src="../js/vendor.js"></script>
|
||||
<script src="../js/upsell.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="sponsorBlockPageBody">
|
||||
|
||||
<div id="title" class="titleBar">
|
||||
<img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic" />
|
||||
SponsorBlock
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="center">
|
||||
<p>
|
||||
__MSG_chaptersPage1__
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/H_mP7bpbA_c?modestbranding=1&rel=0" title="Demo Video"
|
||||
frameborder="0" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<div class="center row-item">
|
||||
<a href="https://buy.ajay.app/l/sponsorblock" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||
<div id="oneTimePurchase" class="option-button inline">
|
||||
__MSG_oneTimePurchase__
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="https://www.patreon.com/ajayyy" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||
<div class="option-button side-by-side inline">
|
||||
__MSG_joinOnPatreon__
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="center row-item">
|
||||
<input id="redeemCodeInput" class="option-text-box" type="text" placeholder="__MSG_enterLicenseKey__">
|
||||
<div id="redeemButton" class="option-button inline">
|
||||
__MSG_redeem__
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center row-item">
|
||||
<a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%3A3000%2Fapi%2FgenerateToken%2Fpatreon"
|
||||
class="option-link" target="_blank" rel="noreferrer">
|
||||
<div class="option-button inline">
|
||||
__MSG_patreonSignIn__
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="cantAfford" class="center">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
__MSG_alreadyDonated__ sponsorblock-free@ajay.app
|
||||
</div>
|
||||
|
||||
<div id="subsidizedPrice" class="center hidden">
|
||||
__MSG_selectYourCountry__
|
||||
</div>
|
||||
|
||||
<div id="subsidizedLink" class="center hidden">
|
||||
<a href="https://buy.ajay.app/l/sponsorblock/purchasing-power" class="option-link" target="_blank"
|
||||
rel="noreferrer">
|
||||
<div class="option-button inline">
|
||||
__MSG_discountLink__
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="noSubsidizedLink" class="center hidden">
|
||||
__MSG_noDiscount__
|
||||
</div>
|
||||
|
||||
</body>
|
||||
387
public/upsell/styles.css
Normal file
387
public/upsell/styles.css
Normal file
@@ -0,0 +1,387 @@
|
||||
/* Based on options page CSS */
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center p {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.row-item {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.keybind-status {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.small-description {
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.medium-description {
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.option-text-box {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.option-button {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: #c00000;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.option-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.option-link.side-by-side {
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.option-button:hover {
|
||||
background-color: #fc0303;
|
||||
}
|
||||
|
||||
.option-button.disabled {
|
||||
cursor: default;
|
||||
|
||||
background-color: #520000;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
#options {
|
||||
max-width: 60%;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.switch-container:after {
|
||||
content: attr(label-name);
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
width: max-content;
|
||||
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-label-container {
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #707070;
|
||||
}
|
||||
|
||||
.animated * {
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.animated .slider:before {
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #fc0303;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(16px);
|
||||
-ms-transform: translateX(16px);
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
/* Boilerplate CSS from https://ajay.app */
|
||||
|
||||
body {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
.projectPreview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.projectPreviewImage {
|
||||
position: absolute;
|
||||
left: -90px;
|
||||
width: 80px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.projectPreviewImageLarge {
|
||||
position: absolute;
|
||||
left: -210px;
|
||||
width: 200px;
|
||||
top: 50%;
|
||||
transform: translateY(-20%);
|
||||
}
|
||||
|
||||
.projectPreviewImageLargeRight {
|
||||
position: absolute;
|
||||
right: -210px;
|
||||
width: 200px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.createdBy {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#title {
|
||||
background-color: #636363;
|
||||
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
font-size: 50px;
|
||||
color: #212121;
|
||||
|
||||
padding: 20px;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
transition: font-size 1s;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 40px;
|
||||
color: #dad8d8;
|
||||
|
||||
padding-top: 10px;
|
||||
|
||||
transition: font-size 0.4s;
|
||||
}
|
||||
|
||||
.subtitle:hover {
|
||||
font-size: 45px;
|
||||
|
||||
transition: font-size 0.4s;
|
||||
}
|
||||
|
||||
.profilepic {
|
||||
background-color: #636363 !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.profilepiccircle {
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 20px;
|
||||
|
||||
height: 80px;
|
||||
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
height: 95px;
|
||||
|
||||
transition: height 0.2s;
|
||||
}
|
||||
|
||||
#contact,.smalllink {
|
||||
font-size: 25px;
|
||||
color: #e8e8e8;
|
||||
|
||||
text-align: center;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#contact {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p,li {
|
||||
font-size: 20px;
|
||||
color: #c4c4c4;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
p,li,code,a {
|
||||
max-width: 60%;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media screen and (orientation:portrait) {
|
||||
p,li,code,a {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.projectPreviewImage {
|
||||
position: unset;
|
||||
width: 130px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.previewImage {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#recentPostTitle {
|
||||
font-size: 30px;
|
||||
color: #dad8d8;
|
||||
}
|
||||
|
||||
#recentPostDate {
|
||||
font-size: 15px;
|
||||
color: #dad8d8;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
color: #dad8d8;
|
||||
}
|
||||
|
||||
svg {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.number-container:before {
|
||||
content: attr(label-name);
|
||||
padding-right: 4px;
|
||||
width: max-content;
|
||||
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* React styles */
|
||||
|
||||
.categoryTableElement {
|
||||
font-size: 16px;
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
.categoryTableElement > * {
|
||||
padding-right: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.optionsSelector {
|
||||
background-color: #c00000;
|
||||
color: white;
|
||||
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.categoryColorTextBox {
|
||||
width: 60px;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#subsidizedPrice {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#discountButton {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ const utils = new Utils({
|
||||
unregisterFirefoxContentScript
|
||||
});
|
||||
|
||||
const popupPort: Record<string, chrome.runtime.Port> = {};
|
||||
|
||||
// Used only on Firefox, which does not support non persistent background pages.
|
||||
const contentScriptRegistrations = {};
|
||||
|
||||
@@ -53,7 +55,7 @@ if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
|
||||
Config.configSyncListeners.push(onNavigationApiAvailableChange);
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
||||
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
|
||||
switch(request.message) {
|
||||
case "openConfig":
|
||||
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
|
||||
@@ -100,6 +102,22 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
||||
});
|
||||
return true;
|
||||
}
|
||||
case "time":
|
||||
if (sender.tab) {
|
||||
popupPort[sender.tab.id]?.postMessage(request);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === "popup") {
|
||||
chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
}, tabs => {
|
||||
popupPort[tabs[0].id] = port;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,7 +134,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
|
||||
|
||||
//generate a userID
|
||||
const newUserID = utils.generateUserID();
|
||||
const newUserID = GenericUtils.generateUserID();
|
||||
//save this UUID
|
||||
Config.config.userID = newUserID;
|
||||
|
||||
@@ -165,7 +183,7 @@ async function submitVote(type: number, UUID: string, category: string) {
|
||||
|
||||
if (userID == undefined || userID === "undefined") {
|
||||
//generate one
|
||||
userID = utils.generateUserID();
|
||||
userID = GenericUtils.generateUserID();
|
||||
Config.config.userID = userID;
|
||||
}
|
||||
|
||||
|
||||
121
src/components/ChapterVoteComponent.tsx
Normal file
121
src/components/ChapterVoteComponent.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as React from "react";
|
||||
import Config from "../config";
|
||||
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||
|
||||
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
||||
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||
import { VoteResponse } from "../messageTypes";
|
||||
import { AnimationUtils } from "../utils/animationUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
import { Tooltip } from "../render/Tooltip";
|
||||
|
||||
export interface ChapterVoteProps {
|
||||
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
|
||||
}
|
||||
|
||||
export interface ChapterVoteState {
|
||||
segment?: SponsorTime;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVoteState> {
|
||||
tooltip?: Tooltip;
|
||||
|
||||
constructor(props: ChapterVoteProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
segment: null,
|
||||
show: false
|
||||
};
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
{/* Upvote Button */}
|
||||
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
|
||||
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||
draggable="false"
|
||||
title={chrome.i18n.getMessage("upvoteButtonInfo")}
|
||||
onClick={(e) => this.vote(e, 1)}>
|
||||
<ThumbsUpSvg className="playerButtonImage"
|
||||
fill={Config.config.colorPalette.white}
|
||||
width={"inherit"} height={"inherit"} />
|
||||
</button>
|
||||
|
||||
{/* Downvote Button */}
|
||||
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
|
||||
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||
draggable="false"
|
||||
title={chrome.i18n.getMessage("reportButtonInfo")}
|
||||
onClick={(e) => {
|
||||
const chapterNode = document.querySelector(".ytp-chapter-container") as HTMLElement;
|
||||
|
||||
if (this.tooltip) {
|
||||
this.tooltip.close();
|
||||
this.tooltip = null;
|
||||
} else {
|
||||
const referenceNode = chapterNode?.parentElement?.parentElement;
|
||||
if (referenceNode) {
|
||||
const outerBounding = referenceNode.getBoundingClientRect();
|
||||
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
|
||||
|
||||
this.tooltip = new Tooltip({
|
||||
referenceNode: chapterNode?.parentElement?.parentElement,
|
||||
prependElement: chapterNode?.parentElement,
|
||||
showLogo: false,
|
||||
showGotIt: false,
|
||||
bottomOffset: `${outerBounding.height + 25}px`,
|
||||
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
|
||||
extraClass: "centeredSBTriangle",
|
||||
buttons: [
|
||||
{
|
||||
name: chrome.i18n.getMessage("incorrectVote"),
|
||||
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
|
||||
this.tooltip?.close();
|
||||
this.tooltip = null;
|
||||
})
|
||||
}, {
|
||||
name: chrome.i18n.getMessage("harmfulVote"),
|
||||
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
|
||||
this.tooltip?.close();
|
||||
this.tooltip = null;
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<ThumbsDownSvg
|
||||
className="playerButtonImage"
|
||||
fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
|
||||
width={"inherit"}
|
||||
height={"inherit"} />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise<void> {
|
||||
event.stopPropagation();
|
||||
if (this.state.segment) {
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(element ?? event.currentTarget as HTMLElement, 0.3);
|
||||
|
||||
const response = await this.props.vote(type, this.state.segment.UUID);
|
||||
await stopAnimation();
|
||||
|
||||
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
|
||||
this.setState({
|
||||
show: type === 1
|
||||
});
|
||||
} else if (response.statusCode !== 403) {
|
||||
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChapterVoteComponent;
|
||||
@@ -11,6 +11,7 @@ export interface NoticeProps {
|
||||
noticeTitle: string,
|
||||
|
||||
maxCountdownTime?: () => number,
|
||||
dontPauseCountdown?: boolean,
|
||||
amountOfPreviousNotices?: number,
|
||||
showInSecondSlot?: boolean,
|
||||
timed?: boolean,
|
||||
@@ -25,6 +26,8 @@ export interface NoticeProps {
|
||||
smaller?: boolean,
|
||||
limitWidth?: boolean,
|
||||
extraClass?: string,
|
||||
hideLogo?: boolean,
|
||||
hideRightInfo?: boolean,
|
||||
|
||||
// Callback for when this is closed
|
||||
closeListener: () => void,
|
||||
@@ -117,13 +120,15 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
{/* Left column */}
|
||||
<td className="noticeLeftIcon">
|
||||
{/* Logo */}
|
||||
{!this.props.hideLogo &&
|
||||
<img id={"sponsorSkipLogo" + this.idSuffix}
|
||||
className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
}
|
||||
|
||||
<span id={"sponsorSkipMessage" + this.idSuffix}
|
||||
style={{float: "left"}}
|
||||
style={{float: "left", marginRight: this.props.hideLogo ? "0px" : null}}
|
||||
className="sponsorSkipMessage sponsorSkipObject">
|
||||
|
||||
{this.props.noticeTitle}
|
||||
@@ -135,6 +140,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
{this.props.firstRow}
|
||||
|
||||
{/* Right column */}
|
||||
{!this.props.hideRightInfo &&
|
||||
<td className="sponsorSkipNoticeRightSection"
|
||||
style={{top: "9.32px"}}>
|
||||
|
||||
@@ -157,6 +163,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
onClick={() => this.close()}>
|
||||
</img>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
|
||||
{this.props.children}
|
||||
@@ -289,7 +296,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
}
|
||||
|
||||
pauseCountdown(): void {
|
||||
if (!this.props.timed) return;
|
||||
if (!this.props.timed || this.props.dontPauseCountdown) return;
|
||||
|
||||
//remove setInterval
|
||||
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
||||
|
||||
55
src/components/SelectorComponent.tsx
Normal file
55
src/components/SelectorComponent.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface SelectorOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SelectorProps {
|
||||
id: string;
|
||||
options: SelectorOption[];
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface SelectorState {
|
||||
|
||||
}
|
||||
|
||||
class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
|
||||
|
||||
constructor(props: SelectorProps) {
|
||||
super(props);
|
||||
|
||||
// Setup state
|
||||
this.state = {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
<div id={this.props.id}
|
||||
className="sbSelector">
|
||||
<div className="sbSelectorBackground">
|
||||
{this.getOptions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getOptions(): React.ReactElement[] {
|
||||
const result: React.ReactElement[] = [];
|
||||
for (const option of this.props.options) {
|
||||
result.push(
|
||||
<div className="sbSelectorOption"
|
||||
onClick={() => this.props.onChange(option.label)}
|
||||
key={option.label}>
|
||||
{option.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectorComponent;
|
||||
@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
||||
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||
import PencilSvg from "../svg-icons/pencil_svg";
|
||||
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
enum SkipButtonState {
|
||||
Undo, // Unskip
|
||||
@@ -540,7 +541,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
|
||||
const sponsorTimesSubmitting : SponsorTime = {
|
||||
segment: this.segments[index].segment,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||
category: this.segments[index].category,
|
||||
actionType: this.segments[index].actionType,
|
||||
source: SponsorSourceType.Local
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import Config from "../config";
|
||||
import { ActionType, Category, ContentContainer, SponsorTime } from "../types";
|
||||
import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||
import Utils from "../utils";
|
||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||
import { RectangleTooltip } from "../render/RectangleTooltip";
|
||||
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
|
||||
const utils = new Utils();
|
||||
@@ -25,16 +27,23 @@ export interface SponsorTimeEditState {
|
||||
editing: boolean;
|
||||
sponsorTimeEdits: [string, string];
|
||||
selectedCategory: Category;
|
||||
description: string;
|
||||
suggestedNames: SelectorOption[];
|
||||
chapterNameSelectorOpen: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CATEGORY = "chooseACategory";
|
||||
|
||||
const categoryNamesGrams: string[] = [].concat(...CompileConfig.categoryList.filter((name) => name !== "chapter")
|
||||
.map((name) => chrome.i18n.getMessage("category_" + name).split(/\/|\s|-/)));
|
||||
|
||||
class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> {
|
||||
|
||||
idSuffix: string;
|
||||
|
||||
categoryOptionRef: React.RefObject<HTMLSelectElement>;
|
||||
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
|
||||
descriptionOptionRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
configUpdateListener: () => void;
|
||||
|
||||
@@ -42,26 +51,35 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
// Used when selecting POI or Full
|
||||
timesBeforeChanging: number[] = [];
|
||||
fullVideoWarningShown = false;
|
||||
categoryNameWarningShown = false;
|
||||
|
||||
// For description auto-complete
|
||||
fetchingSuggestions: boolean;
|
||||
|
||||
constructor(props: SponsorTimeEditProps) {
|
||||
super(props);
|
||||
|
||||
this.categoryOptionRef = React.createRef();
|
||||
this.actionTypeOptionRef = React.createRef();
|
||||
this.descriptionOptionRef = React.createRef();
|
||||
|
||||
this.idSuffix = this.props.idSuffix;
|
||||
|
||||
this.previousSkipType = ActionType.Skip;
|
||||
|
||||
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||
this.state = {
|
||||
editing: false,
|
||||
sponsorTimeEdits: [null, null],
|
||||
selectedCategory: DEFAULT_CATEGORY as Category
|
||||
selectedCategory: DEFAULT_CATEGORY as Category,
|
||||
description: sponsorTime.description || "",
|
||||
suggestedNames: [],
|
||||
chapterNameSelectorOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
// Prevent inputs from triggering key events
|
||||
document.getElementById("sponsorTimesContainer" + this.idSuffix).addEventListener('keydown', function (event) {
|
||||
document.getElementById("sponsorTimeEditContainer" + this.idSuffix).addEventListener('keydown', function (event) {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
@@ -87,6 +105,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
render(): React.ReactElement {
|
||||
this.checkToShowFullVideoWarning();
|
||||
this.checkToShowChapterWarning();
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
textAlign: "center"
|
||||
@@ -118,8 +137,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[0]}
|
||||
onChange={(e) => {this.handleOnChange(0, e, sponsorTime, e.target.value)}}
|
||||
onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}>
|
||||
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
|
||||
</input>
|
||||
|
||||
{sponsorTime.actionType !== ActionType.Poi ? (
|
||||
@@ -133,8 +152,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[1]}
|
||||
onChange={(e) => {this.handleOnChange(1, e, sponsorTime, e.target.value)}}
|
||||
onWheel={(e) => {this.changeTimesWhenScrolling(1, e, sponsorTime)}}>
|
||||
onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
|
||||
</input>
|
||||
|
||||
<span id={"nowButton1" + this.idSuffix}
|
||||
@@ -159,15 +178,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
style={timeDisplayStyle}
|
||||
className="sponsorTimeDisplay"
|
||||
onClick={this.toggleEditTime.bind(this)}>
|
||||
{utils.getFormattedTime(segment[0], true) +
|
||||
{GenericUtils.getFormattedTime(segment[0], true) +
|
||||
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
|
||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
|
||||
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segment[1], true) : "")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div id={"sponsorTimeEditContainer" + this.idSuffix} style={style}>
|
||||
|
||||
{timeDisplay}
|
||||
|
||||
@@ -178,7 +197,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
defaultValue={sponsorTime.category}
|
||||
ref={this.categoryOptionRef}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
onChange={this.categorySelectionChange.bind(this)}>
|
||||
onChange={(event) => this.categorySelectionChange(event)}>
|
||||
{this.getCategoryOptions()}
|
||||
</select>
|
||||
|
||||
@@ -209,6 +228,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</div>
|
||||
): ""}
|
||||
|
||||
{/* Chapter Name */}
|
||||
{sponsorTime.actionType === ActionType.Chapter ? (
|
||||
<div onMouseLeave={() => this.setState({chapterNameSelectorOpen: false})}>
|
||||
<input id={"chapterName" + this.idSuffix}
|
||||
className="sponsorTimeEdit"
|
||||
ref={this.descriptionOptionRef}
|
||||
type="text"
|
||||
value={this.state.description}
|
||||
onChange={(e) => this.descriptionUpdate(e.target.value)}
|
||||
onFocus={() => this.setState({chapterNameSelectorOpen: true})}>
|
||||
</input>
|
||||
{this.state.chapterNameSelectorOpen && this.state.description &&
|
||||
<SelectorComponent
|
||||
id={"chapterNameSelector" + this.idSuffix}
|
||||
options={this.state.suggestedNames}
|
||||
onChange={(v) => this.descriptionUpdate(v)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
): ""}
|
||||
|
||||
<br/>
|
||||
|
||||
{/* Editing Tools */}
|
||||
@@ -223,7 +263,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<span id={"sponsorTimePreviewButton" + this.idSuffix}
|
||||
className="sponsorTimeEditButton"
|
||||
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
|
||||
{chrome.i18n.getMessage("preview")}
|
||||
{sponsorTime.actionType !== ActionType.Chapter ? chrome.i18n.getMessage("preview")
|
||||
: chrome.i18n.getMessage("End")}
|
||||
</span>
|
||||
): ""}
|
||||
|
||||
@@ -250,16 +291,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||
|
||||
// check if change is small engough to show tooltip
|
||||
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = utils.getFormattedTimeToSeconds(targetValue);
|
||||
const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
|
||||
const difference = Math.abs(before - after);
|
||||
if (0 < difference && difference< 0.5) this.showScrollToEditToolTip();
|
||||
if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
|
||||
|
||||
sponsorTimeEdits[index] = targetValue;
|
||||
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
|
||||
|
||||
this.setState({sponsorTimeEdits});
|
||||
this.saveEditTimes();
|
||||
this.setState({sponsorTimeEdits}, () => this.saveEditTimes());
|
||||
}
|
||||
|
||||
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
|
||||
@@ -275,7 +315,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
|
||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||
let timeAsNumber = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
if (timeAsNumber !== null && e.deltaY != 0) {
|
||||
if (e.deltaY < 0) {
|
||||
timeAsNumber += step;
|
||||
@@ -284,7 +324,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
} else {
|
||||
timeAsNumber = 0;
|
||||
}
|
||||
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
|
||||
|
||||
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
|
||||
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
|
||||
|
||||
this.setState({sponsorTimeEdits});
|
||||
@@ -294,13 +335,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
showScrollToEditToolTip(): void {
|
||||
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
|
||||
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), () => { Config.config.scrollToEditTimeUpdate = true });
|
||||
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), "scrollToEdit", () => { Config.config.scrollToEditTimeUpdate = true });
|
||||
}
|
||||
}
|
||||
|
||||
showToolTip(text: string, buttonFunction?: () => void): boolean {
|
||||
showToolTip(text: string, id: string, buttonFunction?: () => void): boolean {
|
||||
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
|
||||
if (element) {
|
||||
const htmlId = `sponsorRectangleTooltip${id + this.idSuffix}`;
|
||||
if (!document.getElementById(htmlId)) {
|
||||
new RectangleTooltip({
|
||||
text,
|
||||
referenceNode: element.parentElement,
|
||||
@@ -309,11 +352,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
bottomOffset: 0 + "px",
|
||||
leftOffset: -318 + "px",
|
||||
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
||||
htmlId: "sponsorTimesContainer" + this.idSuffix,
|
||||
htmlId,
|
||||
buttonFunction,
|
||||
fontSize: "14px",
|
||||
maxHeight: "200px"
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
@@ -328,12 +372,25 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
|
||||
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
|
||||
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"))) {
|
||||
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"), "fullVideoWarning")) {
|
||||
this.fullVideoWarningShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkToShowChapterWarning(): void {
|
||||
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||
|
||||
if (sponsorTime.actionType === ActionType.Chapter && sponsorTime.description
|
||||
&& !this.categoryNameWarningShown
|
||||
&& categoryNamesGrams.some(
|
||||
(category) => sponsorTime.description.toLowerCase().includes(category.toLowerCase()))) {
|
||||
if (this.showToolTip(chrome.i18n.getMessage("chapterNameTooltipWarning"), "chapterWarning")) {
|
||||
this.categoryNameWarningShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCategoryOptions(): React.ReactElement[] {
|
||||
const elements = [(
|
||||
<option value={DEFAULT_CATEGORY}
|
||||
@@ -343,6 +400,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
)];
|
||||
|
||||
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
|
||||
// If permission not loaded, treat it like we have permission except chapter
|
||||
const defaultBlockCategories = ["chapter"];
|
||||
const permission = Config.config.permissions[category as Category];
|
||||
if ((defaultBlockCategories.includes(category) || permission !== undefined) && !permission) continue;
|
||||
|
||||
elements.push(
|
||||
<option value={category}
|
||||
key={category}
|
||||
@@ -363,7 +425,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const chosenCategory = event.target.value as Category;
|
||||
|
||||
// See if show more categories was pressed
|
||||
if (event.target.value !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === event.target.value)) {
|
||||
if (chosenCategory !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === chosenCategory)) {
|
||||
event.target.value = DEFAULT_CATEGORY;
|
||||
|
||||
// Alert that they have to enable this category first
|
||||
@@ -464,7 +526,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
this.setState({
|
||||
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
|
||||
}, this.saveEditTimes);
|
||||
}, () => this.saveEditTimes());
|
||||
}
|
||||
|
||||
toggleEditTime(): void {
|
||||
@@ -487,16 +549,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
|
||||
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
|
||||
return [utils.getFormattedTime(sponsorTime.segment[0], true),
|
||||
utils.getFormattedTime(sponsorTime.segment[1], true)];
|
||||
return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
|
||||
GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
|
||||
}
|
||||
|
||||
saveEditTimes(): void {
|
||||
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
|
||||
if (this.state.editing) {
|
||||
const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
|
||||
// Change segment time only if the format was correct
|
||||
if (startTime !== null && endTime !== null) {
|
||||
@@ -507,8 +569,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const category = this.categoryOptionRef.current.value as Category
|
||||
sponsorTimesSubmitting[this.props.index].category = category;
|
||||
|
||||
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
|
||||
sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType);
|
||||
const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);
|
||||
sponsorTimesSubmitting[this.props.index].actionType = actionType;
|
||||
|
||||
const description = actionType === ActionType.Chapter ? this.descriptionOptionRef?.current?.value : "";
|
||||
sponsorTimesSubmitting[this.props.index].description = description;
|
||||
|
||||
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
|
||||
Config.forceSyncUpdate("unsubmittedSegments");
|
||||
@@ -530,19 +595,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
previewTime(ctrlPressed = false, shiftPressed = false): void {
|
||||
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
const index = this.props.index;
|
||||
|
||||
const skipTime = sponsorTimes[index].segment[0];
|
||||
// If segment starts at 0:00, start playback at the end of the segment
|
||||
if (skipTime === 0) {
|
||||
this.props.contentContainer().previewTime(sponsorTimes[index].segment[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
let seekTime = 2;
|
||||
if (ctrlPressed) seekTime = 0.5;
|
||||
if (shiftPressed) seekTime = 0.25;
|
||||
|
||||
this.props.contentContainer().previewTime(skipTime - (seekTime * this.props.contentContainer().v.playbackRate));
|
||||
const startTime = sponsorTimes[index].segment[0];
|
||||
const endTime = sponsorTimes[index].segment[1];
|
||||
const isChapter = sponsorTimes[index].actionType === ActionType.Chapter;
|
||||
|
||||
// If segment starts at 0:00, start playback at the end of the segment
|
||||
const skipToEndTime = startTime === 0 || isChapter;
|
||||
const skipTime = skipToEndTime ? endTime : (startTime - (seekTime * this.props.contentContainer().v.playbackRate));
|
||||
|
||||
this.props.contentContainer().previewTime(skipTime, !isChapter);
|
||||
}
|
||||
|
||||
inspectTime(): void {
|
||||
@@ -586,6 +651,41 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
}
|
||||
|
||||
descriptionUpdate(description: string): void {
|
||||
this.setState({
|
||||
description
|
||||
});
|
||||
|
||||
if (!this.fetchingSuggestions) {
|
||||
this.fetchSuggestions(description);
|
||||
}
|
||||
|
||||
this.saveEditTimes();
|
||||
}
|
||||
|
||||
async fetchSuggestions(description: string): Promise<void> {
|
||||
if (this.props.contentContainer().channelIDInfo.status !== ChannelIDStatus.Found) return;
|
||||
|
||||
this.fetchingSuggestions = true;
|
||||
const result = await utils.asyncRequestToServer("GET", "/api/chapterNames", {
|
||||
description,
|
||||
channelID: this.props.contentContainer().channelIDInfo.id
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
try {
|
||||
const names = JSON.parse(result.responseText) as {description: string}[];
|
||||
this.setState({
|
||||
suggestedNames: names.map(n => ({
|
||||
label: n.description
|
||||
}))
|
||||
});
|
||||
} catch (e) {} //eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
this.fetchingSuggestions = false;
|
||||
}
|
||||
|
||||
configUpdate(): void {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -73,12 +73,19 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
const sortButton =
|
||||
<img id={"sponsorSkipSortButton" + this.state.idSuffix}
|
||||
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipSmallButton"
|
||||
onClick={() => this.sortSegments()}
|
||||
src={chrome.extension.getURL("icons/sort.svg")}>
|
||||
</img>;
|
||||
return (
|
||||
<NoticeComponent noticeTitle={this.state.noticeTitle}
|
||||
idSuffix={this.state.idSuffix}
|
||||
ref={this.noticeRef}
|
||||
closeListener={this.cancel.bind(this)}
|
||||
zIndex={5000}>
|
||||
zIndex={5000}
|
||||
firstColumn={sortButton}>
|
||||
|
||||
{/* Text Boxes */}
|
||||
{this.getMessageBoxes()}
|
||||
@@ -198,6 +205,16 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
sortSegments(): void {
|
||||
let sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
sponsorTimesSubmitting = sponsorTimesSubmitting.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
|
||||
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
|
||||
Config.forceSyncUpdate("unsubmittedSegments");
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
categoryChangeListener(index: number, category: Category): void {
|
||||
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
|
||||
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category } from "../types";
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
import { Category } from "../../types";
|
||||
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
|
||||
|
||||
export interface CategoryChooserProps {
|
||||
@@ -1,10 +1,13 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Config from "../config"
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category, CategorySkipOption } from "../types";
|
||||
import Config from "../../config"
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
import { Category, CategorySkipOption } from "../../types";
|
||||
|
||||
import { getCategorySuffix } from "../utils/categoryUtils";
|
||||
import { getCategorySuffix } from "../../utils/categoryUtils";
|
||||
import ToggleOptionComponent, { ToggleOptionProps } from "./ToggleOptionComponent";
|
||||
import { fetchingChaptersAllowed } from "../../utils/licenseKey";
|
||||
import LockSvg from "../../svg-icons/lock_svg";
|
||||
|
||||
export interface CategorySkipOptionsProps {
|
||||
category: Category;
|
||||
@@ -15,6 +18,7 @@ export interface CategorySkipOptionsProps {
|
||||
export interface CategorySkipOptionsState {
|
||||
color: string;
|
||||
previewColor: string;
|
||||
hideChapter: boolean;
|
||||
}
|
||||
|
||||
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
|
||||
@@ -27,7 +31,14 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
this.state = {
|
||||
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
|
||||
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color,
|
||||
}
|
||||
hideChapter: true
|
||||
};
|
||||
|
||||
fetchingChaptersAllowed().then((allowed) => {
|
||||
this.setState({
|
||||
hideChapter: !allowed
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
@@ -51,12 +62,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
}
|
||||
}
|
||||
|
||||
let extraClasses = "";
|
||||
const disabled = this.props.category === "chapter" && this.state.hideChapter;
|
||||
if (disabled) {
|
||||
extraClasses += " disabled";
|
||||
|
||||
if (!Config.config.showUpsells) {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr id={this.props.category + "OptionsRow"}
|
||||
className="categoryTableElement">
|
||||
className={`categoryTableElement${extraClasses}`} >
|
||||
<td id={this.props.category + "OptionName"}
|
||||
className="categoryTableLabel">
|
||||
{disabled &&
|
||||
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
|
||||
}
|
||||
{chrome.i18n.getMessage("category_" + this.props.category)}
|
||||
</td>
|
||||
|
||||
@@ -65,21 +89,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
<select
|
||||
className="optionsSelector"
|
||||
defaultValue={defaultOption}
|
||||
disabled={disabled}
|
||||
onChange={this.skipOptionSelected.bind(this)}>
|
||||
{this.getCategorySkipOptions()}
|
||||
</select>
|
||||
</td>
|
||||
|
||||
{this.props.category !== "chapter" &&
|
||||
<td id={this.props.category + "ColorOption"}
|
||||
className="colorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
disabled={disabled}
|
||||
onChange={(event) => this.setColorState(event, false)}
|
||||
value={this.state.color} />
|
||||
</td>
|
||||
}
|
||||
|
||||
{this.props.category !== "exclusive_access" &&
|
||||
{!["chapter", "exclusive_access"].includes(this.props.category) &&
|
||||
<td id={this.props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
@@ -93,7 +121,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
</tr>
|
||||
|
||||
<tr id={this.props.category + "DescriptionRow"}
|
||||
className="small-description categoryTableDescription">
|
||||
className={`small-description categoryTableDescription${extraClasses}`}>
|
||||
<td
|
||||
colSpan={2}>
|
||||
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
|
||||
@@ -104,6 +132,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -147,7 +177,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"]
|
||||
else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
|
||||
for (const optionName of optionNames) {
|
||||
elements.push(
|
||||
@@ -184,6 +215,42 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
Config.config.barTypes = Config.config.barTypes;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
getExtraOptionComponents(category: string, extraClasses: string, disabled: boolean): JSX.Element[] {
|
||||
const result = [];
|
||||
for (const option of this.getExtraOptions(category)) {
|
||||
result.push(
|
||||
<tr key={option.configKey} className={extraClasses}>
|
||||
<td id={`${category}_${option.configKey}`} className="categoryExtraOptions">
|
||||
<ToggleOptionComponent
|
||||
configKey={option.configKey}
|
||||
label={option.label}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getExtraOptions(category: string): ToggleOptionProps[] {
|
||||
switch (category) {
|
||||
case "chapter":
|
||||
return [{
|
||||
configKey: "renderSegmentsAsChapters",
|
||||
label: chrome.i18n.getMessage("renderAsChapters"),
|
||||
}];
|
||||
case "music_offtopic":
|
||||
return [{
|
||||
configKey: "autoSkipOnMusicVideos",
|
||||
label: chrome.i18n.getMessage("autoSkipOnMusicVideos"),
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CategorySkipOptionsComponent;
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Config from "../config";
|
||||
import { Keybind } from "../types";
|
||||
import Config from "../../config";
|
||||
import { Keybind } from "../../types";
|
||||
import KeybindDialogComponent from "./KeybindDialogComponent";
|
||||
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
|
||||
import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
|
||||
|
||||
export interface KeybindProps {
|
||||
option: string;
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { ChangeEvent } from "react";
|
||||
import Config from "../config";
|
||||
import { Keybind } from "../types";
|
||||
import { keybindEquals, formatKey } from "../utils/configUtils";
|
||||
import Config from "../../config";
|
||||
import { Keybind } from "../../types";
|
||||
import { keybindEquals, formatKey } from "../../utils/configUtils";
|
||||
|
||||
export interface KeybindDialogProps {
|
||||
option: string;
|
||||
56
src/components/options/ToggleOptionComponent.tsx
Normal file
56
src/components/options/ToggleOptionComponent.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Config from "../../config";
|
||||
|
||||
export interface ToggleOptionProps {
|
||||
configKey: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ToggleOptionState {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOptionState> {
|
||||
|
||||
constructor(props: ToggleOptionProps) {
|
||||
super(props);
|
||||
|
||||
// Setup state
|
||||
this.state = {
|
||||
enabled: Config.config[props.configKey]
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<div className="switch-container">
|
||||
<label className="switch">
|
||||
<input id={this.props.configKey}
|
||||
type="checkbox"
|
||||
checked={this.state.enabled}
|
||||
disabled={this.props.disabled}
|
||||
onChange={(e) => this.clicked(e)}/>
|
||||
<span className="slider round"></span>
|
||||
</label>
|
||||
<label className="switch-label" htmlFor={this.props.configKey}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
clicked(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
Config.config[this.props.configKey] = event.target.checked;
|
||||
|
||||
this.setState({
|
||||
enabled: event.target.checked
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ToggleOptionComponent;
|
||||
@@ -3,12 +3,18 @@ import * as invidiousList from "../ci/invidiouslist.json";
|
||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
|
||||
import { keybindEquals } from "./utils/configUtils";
|
||||
|
||||
export interface Permission {
|
||||
canSubmit: boolean;
|
||||
}
|
||||
|
||||
interface SBConfig {
|
||||
userID: string,
|
||||
isVip: boolean,
|
||||
permissions: Record<Category, Permission>,
|
||||
/* Contains unsubmitted segments that the user has created. */
|
||||
unsubmittedSegments: Record<string, SponsorTime[]>,
|
||||
defaultCategory: Category,
|
||||
renderSegmentsAsChapters: boolean,
|
||||
whitelistedChannels: string[],
|
||||
forceChannelCheck: boolean,
|
||||
minutesSaved: number,
|
||||
@@ -44,6 +50,7 @@ interface SBConfig {
|
||||
allowExpirements: boolean,
|
||||
showDonationLink: boolean,
|
||||
showPopupDonationCount: number,
|
||||
showUpsells: boolean,
|
||||
donateClicked: number,
|
||||
autoHideInfoButton: boolean,
|
||||
autoSkipOnMusicVideos: boolean,
|
||||
@@ -56,6 +63,7 @@ interface SBConfig {
|
||||
categoryPillUpdate: boolean,
|
||||
darkMode: boolean,
|
||||
showCategoryGuidelines: boolean,
|
||||
chaptersAvailable: boolean,
|
||||
|
||||
// Used to cache calculated text color info
|
||||
categoryPillColors: {
|
||||
@@ -68,10 +76,19 @@ interface SBConfig {
|
||||
skipKeybind: Keybind,
|
||||
startSponsorKeybind: Keybind,
|
||||
submitKeybind: Keybind,
|
||||
nextChapterKeybind: Keybind,
|
||||
previousChapterKeybind: Keybind,
|
||||
|
||||
// What categories should be skipped
|
||||
categorySelections: CategorySelection[],
|
||||
|
||||
payments: {
|
||||
licenseKey: string,
|
||||
lastCheck: number,
|
||||
freeAccess: boolean,
|
||||
chaptersAllowed: boolean
|
||||
}
|
||||
|
||||
// Preview bar
|
||||
barTypes: {
|
||||
"preview-chooseACategory": PreviewBarOption,
|
||||
@@ -128,8 +145,10 @@ const Config: SBObject = {
|
||||
syncDefaults: {
|
||||
userID: null,
|
||||
isVip: false,
|
||||
permissions: {},
|
||||
unsubmittedSegments: {},
|
||||
defaultCategory: "chooseACategory" as Category,
|
||||
renderSegmentsAsChapters: false,
|
||||
whitelistedChannels: [],
|
||||
forceChannelCheck: false,
|
||||
minutesSaved: 0,
|
||||
@@ -165,6 +184,7 @@ const Config: SBObject = {
|
||||
allowExpirements: true,
|
||||
showDonationLink: true,
|
||||
showPopupDonationCount: 0,
|
||||
showUpsells: true,
|
||||
donateClicked: 0,
|
||||
autoHideInfoButton: true,
|
||||
autoSkipOnMusicVideos: false,
|
||||
@@ -172,6 +192,7 @@ const Config: SBObject = {
|
||||
categoryPillUpdate: false,
|
||||
darkMode: true,
|
||||
showCategoryGuidelines: true,
|
||||
chaptersAvailable: true,
|
||||
|
||||
categoryPillColors: {},
|
||||
|
||||
@@ -185,6 +206,8 @@ const Config: SBObject = {
|
||||
skipKeybind: {key: "Enter"},
|
||||
startSponsorKeybind: {key: ";"},
|
||||
submitKeybind: {key: "'"},
|
||||
nextChapterKeybind: {key: "]"},
|
||||
previousChapterKeybind: {key: "["},
|
||||
|
||||
categorySelections: [{
|
||||
name: "sponsor" as Category,
|
||||
@@ -197,6 +220,13 @@ const Config: SBObject = {
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
}],
|
||||
|
||||
payments: {
|
||||
licenseKey: null,
|
||||
lastCheck: 0,
|
||||
freeAccess: false,
|
||||
chaptersAllowed: false
|
||||
},
|
||||
|
||||
colorPalette: {
|
||||
red: "#780303",
|
||||
white: "#ffffff",
|
||||
@@ -516,6 +546,8 @@ function migrateOldSyncFormats(config: SBConfig) {
|
||||
}
|
||||
|
||||
async function setupConfig() {
|
||||
if (typeof(chrome) === "undefined") return;
|
||||
|
||||
await fetchConfig();
|
||||
addDefaults();
|
||||
const config = configProxy();
|
||||
|
||||
198
src/content.ts
198
src/content.ts
@@ -12,12 +12,14 @@ import SubmissionNotice from "./render/SubmissionNotice";
|
||||
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
import { isSafari, keybindEquals } from "./utils/configUtils";
|
||||
import { CategoryPill } from "./render/CategoryPill";
|
||||
import { AnimationUtils } from "./utils/animationUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
import { logDebug } from "./utils/logger";
|
||||
import { importTimes } from "./utils/exporter";
|
||||
import { ChapterVote } from "./render/ChapterVote";
|
||||
import { openWarningDialog } from "./utils/warnings";
|
||||
|
||||
// Hack to get the CSS loaded on permission-based sites (Invidious)
|
||||
@@ -26,7 +28,8 @@ utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
||||
//was sponsor data found when doing SponsorsLookup
|
||||
let sponsorDataFound = false;
|
||||
//the actual sponsorTimes if loaded and UUIDs associated with them
|
||||
let sponsorTimes: SponsorTime[] = null;
|
||||
let sponsorTimes: SponsorTime[] = [];
|
||||
let existingChaptersImported = false;
|
||||
//what video id are these sponsors for
|
||||
let sponsorVideoID: VideoID = null;
|
||||
// List of open skip notices
|
||||
@@ -138,7 +141,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({
|
||||
previewTime,
|
||||
videoInfo,
|
||||
getRealCurrentTime: getRealCurrentTime,
|
||||
lockedCategories
|
||||
lockedCategories,
|
||||
channelIDInfo
|
||||
});
|
||||
|
||||
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
||||
@@ -167,6 +171,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
found: sponsorDataFound,
|
||||
status: lastResponseStatus,
|
||||
sponsorTimes: sponsorTimes,
|
||||
time: video.currentTime,
|
||||
onMobileYouTube
|
||||
});
|
||||
|
||||
@@ -212,10 +217,17 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
found: sponsorDataFound,
|
||||
status: lastResponseStatus,
|
||||
sponsorTimes: sponsorTimes,
|
||||
time: video.currentTime,
|
||||
onMobileYouTube
|
||||
}));
|
||||
|
||||
return true;
|
||||
case "unskip":
|
||||
unskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), null, true);
|
||||
break;
|
||||
case "reskip":
|
||||
reskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), true);
|
||||
break;
|
||||
case "submitVote":
|
||||
vote(request.type, request.UUID).then((response) => sendResponse(response));
|
||||
return true;
|
||||
@@ -230,6 +242,31 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
case "copyToClipboard":
|
||||
navigator.clipboard.writeText(request.text);
|
||||
break;
|
||||
case "importSegments": {
|
||||
const importedSegments = importTimes(request.data, video.duration);
|
||||
let addedSegments = false;
|
||||
for (const segment of importedSegments) {
|
||||
if (!sponsorTimesSubmitting.concat(sponsorTimes ?? []).some(
|
||||
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
|
||||
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)) {
|
||||
sponsorTimesSubmitting.push(segment);
|
||||
addedSegments = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedSegments) {
|
||||
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
|
||||
Config.forceSyncUpdate("unsubmittedSegments");
|
||||
|
||||
updateEditButtonsOnPlayer();
|
||||
updateSponsorTimesSubmitting(false);
|
||||
}
|
||||
|
||||
sendResponse({
|
||||
importedSegments
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "keydown":
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: request.key,
|
||||
@@ -249,8 +286,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
|
||||
/**
|
||||
* Called when the config is updated
|
||||
*
|
||||
* @param {String} changes
|
||||
*/
|
||||
function contentConfigUpdateListener(changes: StorageChangesObject) {
|
||||
for (const key in changes) {
|
||||
@@ -276,8 +311,8 @@ function resetValues() {
|
||||
lastCheckVideoTime = -1;
|
||||
retryCount = 0;
|
||||
|
||||
//reset sponsor times
|
||||
sponsorTimes = null;
|
||||
sponsorTimes = [];
|
||||
existingChaptersImported = false;
|
||||
sponsorSkipped = [];
|
||||
|
||||
videoInfo = null;
|
||||
@@ -423,7 +458,7 @@ function createPreviewBar(): void {
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For Desktop YouTube
|
||||
selector: ".ytp-progress-bar-container",
|
||||
selector: ".ytp-progress-bar",
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For Desktop YouTube
|
||||
@@ -441,7 +476,8 @@ function createPreviewBar(): void {
|
||||
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
|
||||
|
||||
if (el) {
|
||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
||||
const chapterVote = new ChapterVote(voteAsync);
|
||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
|
||||
|
||||
updatePreviewBar();
|
||||
|
||||
@@ -507,13 +543,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
}
|
||||
|
||||
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
|
||||
|
||||
if (!video || video.paused) return;
|
||||
if (!video) return;
|
||||
if (currentTime === undefined || currentTime === null) {
|
||||
currentTime = getVirtualTime();
|
||||
}
|
||||
lastTimeFromWaitingEvent = null;
|
||||
|
||||
updateActiveSegment(currentTime);
|
||||
|
||||
if (video.paused) return;
|
||||
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
|
||||
const currentSkip = skipInfo.array[skipInfo.index];
|
||||
@@ -568,6 +606,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
if (incorrectVideoCheck(videoID, currentSkip)) return;
|
||||
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
|
||||
|
||||
if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) {
|
||||
if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
|
||||
skipToTime({
|
||||
v: video,
|
||||
@@ -597,6 +636,11 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
forcedIncludeIntersectingSegments = true;
|
||||
forcedIncludeNonIntersectingSegments = false;
|
||||
}
|
||||
} else {
|
||||
forcedSkipTime = forceVideoTime + 0.001;
|
||||
}
|
||||
} else {
|
||||
forcedSkipTime = forceVideoTime + 0.001;
|
||||
}
|
||||
|
||||
startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments);
|
||||
@@ -793,9 +837,13 @@ function setupVideoListeners() {
|
||||
lastTimeFromWaitingEvent = null;
|
||||
|
||||
startSponsorSchedule();
|
||||
} else if (video.currentTime === 0) {
|
||||
} else {
|
||||
updateActiveSegment(video.currentTime);
|
||||
|
||||
if (video.currentTime === 0) {
|
||||
lastPausedAtZero = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('ratechange', () => startSponsorSchedule());
|
||||
// Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740)
|
||||
@@ -888,7 +936,12 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
if (response?.ok) {
|
||||
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
|
||||
?.filter((video) => video.videoID === sponsorVideoID)
|
||||
?.map((video) => video.segments)[0];
|
||||
?.map((video) => video.segments)?.[0]
|
||||
?.map((segment) => ({
|
||||
...segment,
|
||||
source: SponsorSourceType.Server
|
||||
}))
|
||||
?.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
if (!recievedSegments || !recievedSegments.length) {
|
||||
// return if no video found
|
||||
retryFetch(404);
|
||||
@@ -909,6 +962,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
|
||||
const oldSegments = sponsorTimes || [];
|
||||
sponsorTimes = recievedSegments;
|
||||
existingChaptersImported = false;
|
||||
|
||||
// Hide all submissions smaller than the minimum duration
|
||||
if (Config.config.minDuration !== 0) {
|
||||
@@ -956,13 +1010,28 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
retryFetch(lastResponseStatus);
|
||||
}
|
||||
|
||||
importExistingChapters(true);
|
||||
|
||||
if (Config.config.isVip) {
|
||||
lockedCategoriesLookup();
|
||||
}
|
||||
}
|
||||
|
||||
function importExistingChapters(wait: boolean) {
|
||||
if (!existingChaptersImported) {
|
||||
GenericUtils.wait(() => video && getExistingChapters(sponsorVideoID, video.duration),
|
||||
wait ? 5000 : 0, 100, (c) => c?.length > 0).then((chapters) => {
|
||||
if (!existingChaptersImported && chapters?.length > 0) {
|
||||
sponsorTimes = (sponsorTimes ?? []).concat(...chapters).sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
existingChaptersImported = true;
|
||||
updatePreviewBar();
|
||||
}
|
||||
}).catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
}
|
||||
}
|
||||
|
||||
function getEnabledActionTypes(): ActionType[] {
|
||||
const actionTypes = [ActionType.Skip, ActionType.Poi];
|
||||
const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
|
||||
if (Config.config.muteSegments) {
|
||||
actionTypes.push(ActionType.Mute);
|
||||
}
|
||||
@@ -1000,7 +1069,8 @@ function retryFetch(errorCode: number): void {
|
||||
|
||||
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
|
||||
setTimeout(() => {
|
||||
if (sponsorVideoID && sponsorTimes?.length === 0) {
|
||||
if (sponsorVideoID && sponsorTimes?.length === 0
|
||||
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
|
||||
sponsorsLookup();
|
||||
}
|
||||
}, delay);
|
||||
@@ -1164,9 +1234,11 @@ function updatePreviewBar(): void {
|
||||
previewBarSegments.push({
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
unsubmitted: false,
|
||||
actionType: segment.actionType,
|
||||
showLarger: segment.actionType === ActionType.Poi
|
||||
unsubmitted: false,
|
||||
showLarger: segment.actionType === ActionType.Poi,
|
||||
description: segment.description,
|
||||
source: segment.source,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1175,16 +1247,21 @@ function updatePreviewBar(): void {
|
||||
previewBarSegments.push({
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
unsubmitted: true,
|
||||
actionType: segment.actionType,
|
||||
showLarger: segment.actionType === ActionType.Poi
|
||||
unsubmitted: true,
|
||||
showLarger: segment.actionType === ActionType.Poi,
|
||||
description: segment.description,
|
||||
source: segment.source
|
||||
});
|
||||
});
|
||||
|
||||
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
|
||||
updateActiveSegment(video.currentTime);
|
||||
|
||||
if (Config.config.showTimeWithSkips) {
|
||||
const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
|
||||
const skippedDuration = utils.getTimestampsDuration(previewBarSegments
|
||||
.filter(({actionType}) => actionType !== ActionType.Chapter)
|
||||
.map(({segment}) => segment));
|
||||
|
||||
showTimeWithoutSkips(skippedDuration);
|
||||
}
|
||||
@@ -1248,7 +1325,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
||||
|
||||
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
|
||||
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true);
|
||||
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true);
|
||||
|
||||
// This is an array in-case multiple segments have the exact same start time
|
||||
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
|
||||
@@ -1263,7 +1340,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
||||
|
||||
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
|
||||
getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false);
|
||||
const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false);
|
||||
|
||||
const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
|
||||
const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
|
||||
@@ -1344,7 +1421,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
|
||||
* the current time, but end after
|
||||
*/
|
||||
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
|
||||
minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
|
||||
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
|
||||
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
|
||||
|
||||
const includedTimes: ScheduledTime[] = [];
|
||||
@@ -1355,9 +1432,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|
||||
scheduledTime: sponsorTime.segment[0]
|
||||
}));
|
||||
|
||||
// Schedule at the end time to know when to unmute
|
||||
sponsorTimes.filter(sponsorTime => sponsorTime.actionType === ActionType.Mute)
|
||||
.forEach(sponsorTime => {
|
||||
// Schedule at the end time to know when to unmute and remove title from seek bar
|
||||
sponsorTimes.forEach(sponsorTime => {
|
||||
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
|
||||
possibleTimes.push({
|
||||
...sponsorTime,
|
||||
@@ -1370,8 +1446,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|
||||
if ((minimum === undefined
|
||||
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|
||||
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
|
||||
&& (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
|
||||
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
|
||||
&& possibleTimes[i].segment.length === 2
|
||||
&& possibleTimes[i].actionType !== ActionType.Poi) {
|
||||
|
||||
scheduledTimes.push(possibleTimes[i].scheduledTime);
|
||||
@@ -1535,7 +1611,7 @@ function reskipSponsorTime(segment: SponsorTime, forceSeek = false) {
|
||||
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
|
||||
|
||||
video.currentTime = segment.segment[1];
|
||||
sendTelemetryAndCount([segment], skippedTime, fullSkip);
|
||||
sendTelemetryAndCount([segment], segment.actionType !== ActionType.Chapter ? skippedTime : 0, fullSkip);
|
||||
startSponsorSchedule(true, segment.segment[1], false);
|
||||
}
|
||||
}
|
||||
@@ -1586,6 +1662,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
||||
|
||||
function shouldSkip(segment: SponsorTime): boolean {
|
||||
return (segment.actionType !== ActionType.Full
|
||||
&& segment.source !== SponsorSourceType.YouTube
|
||||
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|
||||
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||
}
|
||||
@@ -1692,7 +1769,7 @@ function startOrEndTimingNewSegment() {
|
||||
if (!isSegmentCreationInProgress()) {
|
||||
sponsorTimesSubmitting.push({
|
||||
segment: [roundedTime],
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||
category: Config.config.defaultCategory,
|
||||
actionType: ActionType.Skip,
|
||||
source: SponsorSourceType.Local
|
||||
@@ -1716,6 +1793,8 @@ function startOrEndTimingNewSegment() {
|
||||
|
||||
updateEditButtonsOnPlayer();
|
||||
updateSponsorTimesSubmitting(false);
|
||||
|
||||
importExistingChapters(false);
|
||||
}
|
||||
|
||||
function getIncompleteSegment(): SponsorTime {
|
||||
@@ -1754,9 +1833,14 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
|
||||
UUID: segmentTime.UUID,
|
||||
category: segmentTime.category,
|
||||
actionType: segmentTime.actionType,
|
||||
description: segmentTime.description,
|
||||
source: segmentTime.source
|
||||
});
|
||||
}
|
||||
|
||||
if (sponsorTimesSubmitting.length > 0) {
|
||||
importExistingChapters(true);
|
||||
}
|
||||
}
|
||||
|
||||
updatePreviewBar();
|
||||
@@ -1878,7 +1962,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
|
||||
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
|
||||
|
||||
// Don't vote for preview sponsors
|
||||
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source === SponsorSourceType.Local) return;
|
||||
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source !== SponsorSourceType.Server) return;
|
||||
|
||||
// See if the local time saved count and skip count should be saved
|
||||
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
|
||||
@@ -2025,7 +2109,7 @@ async function sendSubmitMessage() {
|
||||
} catch(e) {} // eslint-disable-line no-empty
|
||||
|
||||
// Add submissions to current sponsors list
|
||||
sponsorTimes = (sponsorTimes || []).concat(newSegments);
|
||||
sponsorTimes = (sponsorTimes || []).concat(newSegments).sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
|
||||
// Increase contribution count
|
||||
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
|
||||
@@ -2062,7 +2146,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
||||
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
for (let s = 0; s < sponsorTimes[i].segment.length; s++) {
|
||||
let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
|
||||
let timeMessage = GenericUtils.getFormattedTime(sponsorTimes[i].segment[s]);
|
||||
//if this is an end time
|
||||
if (s == 1) {
|
||||
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
|
||||
@@ -2078,6 +2162,44 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
||||
return sponsorTimesMessage;
|
||||
}
|
||||
|
||||
function updateActiveSegment(currentTime: number): void {
|
||||
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
|
||||
chrome.runtime.sendMessage({
|
||||
message: "time",
|
||||
time: currentTime
|
||||
});
|
||||
}
|
||||
|
||||
function nextChapter(): void {
|
||||
const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter)
|
||||
.sort((a, b) => a.segment[1] - b.segment[1]);
|
||||
if (chapters.length <= 0) return;
|
||||
|
||||
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
|
||||
&& time.segment[1] > video.currentTime);
|
||||
if (nextChapter !== -1) {
|
||||
reskipSponsorTime(chapters[nextChapter], true);
|
||||
} else {
|
||||
video.currentTime = video.duration;
|
||||
}
|
||||
}
|
||||
|
||||
function previousChapter(): void {
|
||||
const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter);
|
||||
if (chapters.length <= 0) return;
|
||||
|
||||
// subtract 5 seconds to allow skipping back to the previous chapter if close to start of
|
||||
// the current one
|
||||
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
|
||||
&& time.segment[0] > video.currentTime - Math.min(5, time.segment[1] - time.segment[0]));
|
||||
const previousChapter = nextChapter !== -1 ? (nextChapter - 1) : (chapters.length - 1);
|
||||
if (previousChapter !== -1) {
|
||||
unskipSponsorTime(chapters[previousChapter], null, true);
|
||||
} else {
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function addPageListeners(): void {
|
||||
const refreshListners = () => {
|
||||
if (!isVisible(video)) {
|
||||
@@ -2107,6 +2229,8 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
const skipKey = Config.config.skipKeybind;
|
||||
const startSponsorKey = Config.config.startSponsorKeybind;
|
||||
const submitKey = Config.config.submitKeybind;
|
||||
const nextChapterKey = Config.config.nextChapterKeybind;
|
||||
const previousChapterKey = Config.config.previousChapterKeybind;
|
||||
|
||||
if (keybindEquals(key, skipKey)) {
|
||||
if (activeSkipKeybindElement)
|
||||
@@ -2118,6 +2242,12 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
} else if (keybindEquals(key, submitKey)) {
|
||||
submitSponsorTimes();
|
||||
return;
|
||||
} else if (keybindEquals(key, nextChapterKey)) {
|
||||
nextChapter();
|
||||
return;
|
||||
} else if (keybindEquals(key, previousChapterKey)) {
|
||||
previousChapter();
|
||||
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)
|
||||
@@ -2188,7 +2318,7 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
display.appendChild(duration);
|
||||
}
|
||||
|
||||
const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration)
|
||||
const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration);
|
||||
|
||||
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
|
||||
}
|
||||
@@ -2207,7 +2337,7 @@ function checkForPreloadedSegment() {
|
||||
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
|
||||
sponsorTimesSubmitting.push({
|
||||
segment: segment.segment,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||
category: segment.category ? segment.category : Config.config.defaultCategory,
|
||||
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
|
||||
source: SponsorSourceType.Local
|
||||
|
||||
@@ -6,41 +6,63 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
|
||||
'use strict';
|
||||
|
||||
import Config from "../config";
|
||||
import { ActionType } from "../types";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
import { ChapterVote } from "../render/ChapterVote";
|
||||
import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
|
||||
import { partition } from "../utils/arrayUtils";
|
||||
import { shortCategoryName } from "../utils/categoryUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
const MIN_CHAPTER_SIZE = 0.003;
|
||||
|
||||
export interface PreviewBarSegment {
|
||||
segment: [number, number];
|
||||
category: string;
|
||||
unsubmitted: boolean;
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
unsubmitted: boolean;
|
||||
showLarger: boolean;
|
||||
description: string;
|
||||
source: SponsorSourceType;
|
||||
}
|
||||
|
||||
interface ChapterGroup extends SegmentContainer {
|
||||
originalDuration: number
|
||||
}
|
||||
|
||||
class PreviewBar {
|
||||
container: HTMLUListElement;
|
||||
categoryTooltip?: HTMLDivElement;
|
||||
tooltipContainer?: HTMLElement;
|
||||
categoryTooltipContainer?: HTMLElement;
|
||||
chapterTooltip?: HTMLDivElement;
|
||||
|
||||
parent: HTMLElement;
|
||||
onMobileYouTube: boolean;
|
||||
onInvidious: boolean;
|
||||
|
||||
segments: PreviewBarSegment[] = [];
|
||||
existingChapters: PreviewBarSegment[] = [];
|
||||
videoDuration = 0;
|
||||
|
||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
|
||||
// For chapter bar
|
||||
hoveredSection: HTMLElement;
|
||||
customChaptersBar: HTMLElement;
|
||||
chaptersBarSegments: PreviewBarSegment[];
|
||||
chapterVote: ChapterVote;
|
||||
originalChapterBar: HTMLElement;
|
||||
originalChapterBarBlocks: NodeListOf<HTMLElement>;
|
||||
|
||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
|
||||
if (test) return;
|
||||
this.container = document.createElement('ul');
|
||||
this.container.id = 'previewbar';
|
||||
|
||||
this.parent = parent;
|
||||
this.onMobileYouTube = onMobileYouTube;
|
||||
this.onInvidious = onInvidious;
|
||||
this.chapterVote = chapterVote;
|
||||
|
||||
this.createElement(parent);
|
||||
this.createChapterMutationObservers();
|
||||
|
||||
this.setupHoverText();
|
||||
}
|
||||
@@ -51,16 +73,19 @@ class PreviewBar {
|
||||
// Create label placeholder
|
||||
this.categoryTooltip = document.createElement("div");
|
||||
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
this.chapterTooltip = document.createElement("div");
|
||||
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
||||
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
||||
|
||||
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
|
||||
this.tooltipContainer = tooltipTextWrapper.parentElement;
|
||||
this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
|
||||
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title");
|
||||
if (!this.tooltipContainer || !titleTooltip) return;
|
||||
if (!this.categoryTooltipContainer || !titleTooltip) return;
|
||||
|
||||
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
|
||||
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
|
||||
|
||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||
if (!seekBar) return;
|
||||
@@ -76,7 +101,7 @@ class PreviewBar {
|
||||
});
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
|
||||
if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return;
|
||||
|
||||
// If the mutation observed is only for our tooltip text, ignore
|
||||
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
||||
@@ -93,7 +118,7 @@ class PreviewBar {
|
||||
const tooltipText = tooltipTextElement.textContent;
|
||||
if (tooltipText === null || tooltipText.length === 0) continue;
|
||||
|
||||
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
|
||||
timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
|
||||
|
||||
if (timeInSeconds !== null) break;
|
||||
}
|
||||
@@ -101,36 +126,32 @@ class PreviewBar {
|
||||
if (timeInSeconds === null) return;
|
||||
|
||||
// Find the segment at that location, using the shortest if multiple found
|
||||
let segment: PreviewBarSegment | null = null;
|
||||
let currentSegmentLength = Infinity;
|
||||
|
||||
for (const seg of this.segments) {//
|
||||
const segmentLength = seg.segment[1] - seg.segment[0];
|
||||
const minSize = this.getMinimumSize(seg.showLarger);
|
||||
|
||||
const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
|
||||
const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
|
||||
if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
|
||||
if (segmentLength < currentSegmentLength) {
|
||||
currentSegmentLength = segmentLength;
|
||||
segment = seg;
|
||||
}
|
||||
}
|
||||
const [normalSegments, chapterSegments] =
|
||||
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
|
||||
(segment) => segment.actionType !== ActionType.Chapter);
|
||||
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
|
||||
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
|
||||
if (mainSegment === null && secondarySegment !== null) {
|
||||
mainSegment = secondarySegment;
|
||||
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
|
||||
}
|
||||
|
||||
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
} else if (segment !== null) {
|
||||
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
|
||||
if (segment.unsubmitted) {
|
||||
this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category);
|
||||
if (mainSegment === null && secondarySegment === null) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
} else {
|
||||
this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
|
||||
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
if (mainSegment !== null && secondarySegment !== null) {
|
||||
this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
|
||||
} else {
|
||||
this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips");
|
||||
}
|
||||
|
||||
// Use the class if the timestamp text uses it to prevent overlapping
|
||||
this.setTooltipTitle(mainSegment, this.categoryTooltip);
|
||||
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
|
||||
|
||||
// Used to prevent overlapping
|
||||
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -140,6 +161,21 @@ class PreviewBar {
|
||||
});
|
||||
}
|
||||
|
||||
private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void {
|
||||
if (segment) {
|
||||
const name = segment.description || shortCategoryName(segment.category);
|
||||
if (segment.unsubmitted) {
|
||||
tooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + name;
|
||||
} else {
|
||||
tooltip.textContent = name;
|
||||
}
|
||||
|
||||
tooltip.style.removeProperty("display");
|
||||
} else {
|
||||
tooltip.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
createElement(parent: HTMLElement): void {
|
||||
this.parent = parent;
|
||||
|
||||
@@ -157,39 +193,62 @@ class PreviewBar {
|
||||
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// On the seek bar
|
||||
this.parent.prepend(this.container);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.videoDuration = 0;
|
||||
this.segments = [];
|
||||
|
||||
while (this.container.firstChild) {
|
||||
this.container.removeChild(this.container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
||||
this.segments = segments ?? [];
|
||||
this.videoDuration = videoDuration ?? 0;
|
||||
|
||||
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||
// Sometimes video duration is inaccurate, pull from accessibility info
|
||||
const ariaDuration = parseInt(progressBar?.getAttribute('aria-valuemax')) ?? 0;
|
||||
if (ariaDuration && Math.abs(ariaDuration - this.videoDuration) > 3) {
|
||||
this.videoDuration = ariaDuration;
|
||||
}
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
this.clear();
|
||||
if (!segments) return;
|
||||
if (!this.segments) return;
|
||||
|
||||
this.segments = segments;
|
||||
this.videoDuration = videoDuration;
|
||||
this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||
this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
|
||||
this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0])
|
||||
|
||||
this.segments.sort(({segment: a}, {segment: b}) => {
|
||||
const sortedSegments = this.segments.sort(({ segment: a }, { segment: b }) => {
|
||||
// Sort longer segments before short segments to make shorter segments render later
|
||||
return (b[1] - b[0]) - (a[1] - a[0]);
|
||||
}).forEach((segment) => {
|
||||
});
|
||||
for (const segment of sortedSegments) {
|
||||
const bar = this.createBar(segment);
|
||||
|
||||
this.container.appendChild(bar);
|
||||
});
|
||||
}
|
||||
|
||||
createBar({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement {
|
||||
this.createChaptersBar(this.segments.sort((a, b) => a.segment[0] - b.segment[0]));
|
||||
|
||||
const chapterChevron = this.getChapterChevron();
|
||||
if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
|
||||
&& segment.source === SponsorSourceType.YouTube)) {
|
||||
chapterChevron.style.removeProperty("display");
|
||||
} else {
|
||||
chapterChevron.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
createBar(barSegment: PreviewBarSegment): HTMLLIElement {
|
||||
const { category, unsubmitted, segment, showLarger } = barSegment;
|
||||
|
||||
const bar = document.createElement('li');
|
||||
bar.classList.add('previewbar');
|
||||
bar.innerHTML = showLarger ? ' ' : ' ';
|
||||
@@ -202,7 +261,9 @@ class PreviewBar {
|
||||
|
||||
bar.style.position = "absolute";
|
||||
const duration = Math.min(segment[1], this.videoDuration) - segment[0];
|
||||
if (duration > 0) bar.style.width = this.timeToPercentage(duration);
|
||||
if (duration > 0) {
|
||||
bar.style.width = `calc(${this.intervalToPercentage(segment[0], segment[1])}${this.chapterFilter(barSegment) ? ' - 2px' : ''})`;
|
||||
}
|
||||
|
||||
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
|
||||
bar.style.left = this.timeToPercentage(time);
|
||||
@@ -210,6 +271,413 @@ class PreviewBar {
|
||||
return bar;
|
||||
}
|
||||
|
||||
createChaptersBar(segments: PreviewBarSegment[]): void {
|
||||
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||
if (!progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) return;
|
||||
|
||||
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|
||||
|| (!Config.config.renderSegmentsAsChapters
|
||||
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|
||||
|| segment.source === SponsorSourceType.YouTube))) {
|
||||
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||
this.originalChapterBar.style.removeProperty("display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge overlapping chapters
|
||||
const filteredSegments = segments?.filter((segment) => this.chapterFilter(segment));
|
||||
const chaptersToRender = this.createChapterRenderGroups(filteredSegments).filter((segment) => this.chapterGroupFilter(segment));
|
||||
|
||||
if (chaptersToRender?.length <= 0) {
|
||||
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||
this.originalChapterBar.style.removeProperty("display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create it from cloning
|
||||
let createFromScratch = false;
|
||||
if (!this.customChaptersBar) {
|
||||
createFromScratch = true;
|
||||
this.customChaptersBar = this.originalChapterBar.cloneNode(true) as HTMLElement;
|
||||
this.customChaptersBar.classList.add("sponsorBlockChapterBar");
|
||||
}
|
||||
this.customChaptersBar.style.removeProperty("display");
|
||||
const originalSections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container");
|
||||
const originalSection = originalSections[0];
|
||||
|
||||
this.customChaptersBar = this.customChaptersBar;
|
||||
|
||||
// For switching to a video with less chapters
|
||||
if (originalSections.length > chaptersToRender.length) {
|
||||
for (let i = originalSections.length - 1; i >= chaptersToRender.length; i--) {
|
||||
this.customChaptersBar.removeChild(originalSections[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Modify it to have sections for each segment
|
||||
for (let i = 0; i < chaptersToRender.length; i++) {
|
||||
const chapter = chaptersToRender[i].segment;
|
||||
let newSection = originalSections[i] as HTMLElement;
|
||||
if (!newSection) {
|
||||
newSection = originalSection.cloneNode(true) as HTMLElement;
|
||||
|
||||
this.firstTimeSetupChapterSection(newSection);
|
||||
this.customChaptersBar.appendChild(newSection);
|
||||
}
|
||||
|
||||
this.setupChapterSection(newSection, chapter[0], chapter[1], i !== chaptersToRender.length - 1);
|
||||
}
|
||||
|
||||
// Hide old bar
|
||||
this.originalChapterBar.style.display = "none";
|
||||
|
||||
if (createFromScratch) {
|
||||
if (this.container?.parentElement === progressBar) {
|
||||
progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
|
||||
} else {
|
||||
progressBar.prepend(this.customChaptersBar);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateChapterAllMutation(this.originalChapterBar, progressBar, true);
|
||||
}
|
||||
|
||||
createChapterRenderGroups(segments: PreviewBarSegment[]): ChapterGroup[] {
|
||||
const result: ChapterGroup[] = [];
|
||||
|
||||
segments?.forEach((segment, index) => {
|
||||
const latestChapter = result[result.length - 1];
|
||||
if (latestChapter && latestChapter.segment[1] > segment.segment[0]) {
|
||||
const segmentDuration = segment.segment[1] - segment.segment[0];
|
||||
if (segment.segment[0] < latestChapter.segment[0]
|
||||
|| segmentDuration < latestChapter.originalDuration) {
|
||||
// Remove latest if it starts too late
|
||||
let latestValidChapter = latestChapter;
|
||||
const chaptersToAddBack: ChapterGroup[] = []
|
||||
while (latestValidChapter?.segment[0] >= segment.segment[0]) {
|
||||
const invalidChapter = result.pop();
|
||||
if (invalidChapter.segment[1] > segment.segment[1]) {
|
||||
if (invalidChapter.segment[0] === segment.segment[0]) {
|
||||
invalidChapter.segment[0] = segment.segment[1];
|
||||
}
|
||||
|
||||
chaptersToAddBack.push(invalidChapter);
|
||||
}
|
||||
latestValidChapter = result[result.length - 1];
|
||||
}
|
||||
|
||||
// Split the latest chapter if smaller
|
||||
result.push({
|
||||
segment: [segment.segment[0], segment.segment[1]],
|
||||
originalDuration: segmentDuration,
|
||||
});
|
||||
if (latestValidChapter?.segment[1] > segment.segment[1]) {
|
||||
result.push({
|
||||
segment: [segment.segment[1], latestValidChapter.segment[1]],
|
||||
originalDuration: latestValidChapter.originalDuration
|
||||
});
|
||||
}
|
||||
|
||||
chaptersToAddBack.reverse();
|
||||
let lastChapterChecked: number[] = segment.segment;
|
||||
for (const chapter of chaptersToAddBack) {
|
||||
if (chapter.segment[0] < lastChapterChecked[1]) {
|
||||
chapter.segment[0] = lastChapterChecked[1];
|
||||
}
|
||||
|
||||
lastChapterChecked = chapter.segment;
|
||||
}
|
||||
result.push(...chaptersToAddBack);
|
||||
if (latestValidChapter) latestValidChapter.segment[1] = segment.segment[0];
|
||||
} else {
|
||||
// Start at end of old one otherwise
|
||||
result.push({
|
||||
segment: [latestChapter.segment[1], segment.segment[1]],
|
||||
originalDuration: segmentDuration
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Add empty buffer before segment if needed
|
||||
const lastTime = latestChapter?.segment[1] || 0;
|
||||
if (segment.segment[0] > lastTime) {
|
||||
result.push({
|
||||
segment: [lastTime, segment.segment[0]],
|
||||
originalDuration: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Normal case
|
||||
const endTime = Math.min(segment.segment[1], this.videoDuration);
|
||||
result.push({
|
||||
segment: [segment.segment[0], endTime],
|
||||
originalDuration: endTime - segment.segment[0]
|
||||
});
|
||||
}
|
||||
|
||||
// Add empty buffer after segment if needed
|
||||
if (index === segments.length - 1) {
|
||||
const nextSegment = segments[index + 1];
|
||||
const nextTime = nextSegment ? nextSegment.segment[0] : this.videoDuration;
|
||||
const lastTime = result[result.length - 1]?.segment[1] || segment.segment[1];
|
||||
if (this.intervalToDecimal(lastTime, nextTime) > MIN_CHAPTER_SIZE) {
|
||||
result.push({
|
||||
segment: [lastTime, nextTime],
|
||||
originalDuration: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private setupChapterSection(section: HTMLElement, startTime: number, endTime: number, addMargin: boolean): void {
|
||||
const sizePercent = this.intervalToPercentage(startTime, endTime);
|
||||
if (addMargin) {
|
||||
section.style.marginRight = "2px";
|
||||
section.style.width = `calc(${sizePercent} - 2px)`;
|
||||
} else {
|
||||
section.style.marginRight = "0";
|
||||
section.style.width = sizePercent;
|
||||
}
|
||||
|
||||
section.setAttribute("decimal-width", String(this.intervalToDecimal(startTime, endTime)));
|
||||
}
|
||||
|
||||
private firstTimeSetupChapterSection(section: HTMLElement): void {
|
||||
section.addEventListener("mouseenter", () => {
|
||||
this.hoveredSection?.classList.remove("ytp-exp-chapter-hover-effect");
|
||||
section.classList.add("ytp-exp-chapter-hover-effect");
|
||||
this.hoveredSection = section;
|
||||
});
|
||||
}
|
||||
|
||||
private createChapterMutationObservers(): void {
|
||||
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||
const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||
if (!progressBar || !chapterBar) return;
|
||||
|
||||
const attributeObserver = new MutationObserver((mutations) => {
|
||||
const changes: Record<string, HTMLElement> = {};
|
||||
for (const mutation of mutations) {
|
||||
const currentElement = mutation.target as HTMLElement;
|
||||
if (mutation.type === "attributes"
|
||||
&& currentElement.parentElement?.classList.contains("ytp-progress-list")) {
|
||||
changes[currentElement.classList[0]] = mutation.target as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateChapterMutation(changes, progressBar);
|
||||
});
|
||||
|
||||
attributeObserver.observe(chapterBar, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class"]
|
||||
});
|
||||
|
||||
const childListObserver = new MutationObserver((mutations) => {
|
||||
const changes: Record<string, HTMLElement> = {};
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "childList") {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateChapterMutation(changes, progressBar);
|
||||
});
|
||||
|
||||
// Only direct children, no subtree
|
||||
childListObserver.observe(chapterBar, {
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
|
||||
private updateChapterAllMutation(originalChapterBar: HTMLElement, progressBar: HTMLElement, firstUpdate = false): void {
|
||||
const elements = originalChapterBar.querySelectorAll(".ytp-progress-list > *");
|
||||
const changes: Record<string, HTMLElement> = {};
|
||||
for (const element of elements) {
|
||||
changes[element.classList[0]] = element as HTMLElement;
|
||||
}
|
||||
|
||||
this.updateChapterMutation(changes, progressBar, firstUpdate);
|
||||
}
|
||||
|
||||
private updateChapterMutation(changes: Record<string, HTMLElement>, progressBar: HTMLElement, firstUpdate = false): void {
|
||||
// Go through each newly generated chapter bar and update the width based on changes array
|
||||
if (this.customChaptersBar) {
|
||||
// Width reached so far in decimal percent
|
||||
let cursor = 0;
|
||||
|
||||
const sections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container") as NodeListOf<HTMLElement>;
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
|
||||
const sectionWidthDecimal = parseFloat(section.getAttribute("decimal-width"));
|
||||
const sectionWidthDecimalNoMargin = sectionWidthDecimal - 2 / progressBar.clientWidth;
|
||||
|
||||
for (const className in changes) {
|
||||
const selector = `.${className}`
|
||||
const customChangedElement = section.querySelector(selector) as HTMLElement;
|
||||
if (customChangedElement) {
|
||||
const fullSectionWidth = i === sections.length - 1 ? sectionWidthDecimal : sectionWidthDecimalNoMargin;
|
||||
const changedElement = changes[className];
|
||||
const changedData = this.findLeftAndScale(selector, changedElement, progressBar);
|
||||
|
||||
const left = (changedData.left) / progressBar.clientWidth;
|
||||
const calculatedLeft = Math.max(0, Math.min(1, (left - cursor) / fullSectionWidth));
|
||||
if (!isNaN(left) && !isNaN(calculatedLeft)) {
|
||||
customChangedElement.style.left = `${calculatedLeft * 100}%`;
|
||||
customChangedElement.style.removeProperty("display");
|
||||
}
|
||||
|
||||
if (changedData.scale !== null) {
|
||||
const transformScale = (changedData.scale) / progressBar.clientWidth;
|
||||
|
||||
customChangedElement.style.transform =
|
||||
`scaleX(${Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft))}`;
|
||||
if (firstUpdate) {
|
||||
customChangedElement.style.transition = "none";
|
||||
setTimeout(() => customChangedElement.style.removeProperty("transition"), 50);
|
||||
}
|
||||
}
|
||||
|
||||
if (customChangedElement.className !== changedElement.className) {
|
||||
customChangedElement.className = changedElement.className;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor += sectionWidthDecimal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
|
||||
{ left: number, scale: number } {
|
||||
const sections = currentElement.parentElement.parentElement.parentElement.children;
|
||||
let currentWidth = 0;
|
||||
|
||||
let left = 0;
|
||||
let leftPosition = 0;
|
||||
|
||||
let scale = null;
|
||||
let scalePosition = 0;
|
||||
let scaleWidth = 0;
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i] as HTMLElement;
|
||||
const checkElement = section.querySelector(selector) as HTMLElement;
|
||||
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") || progressBar.clientWidth;
|
||||
const currentSectionWidth = currentSectionWidthNoMargin
|
||||
+ this.getPartialChapterSectionStyle(section, "marginRight");
|
||||
|
||||
// First check for left
|
||||
const checkLeft = parseFloat(checkElement.style.left.replace("px", ""));
|
||||
if (checkLeft !== 0) {
|
||||
left = checkLeft;
|
||||
leftPosition = currentWidth;
|
||||
}
|
||||
|
||||
// Then check for scale
|
||||
const transformMatch = checkElement.style.transform.match(/scaleX\(([0-9.]+?)\)/);
|
||||
if (transformMatch) {
|
||||
const transformScale = parseFloat(transformMatch[1]);
|
||||
if (i === sections.length - 1 || (transformScale < 1 && transformScale + checkLeft / currentSectionWidthNoMargin < 0.99999)) {
|
||||
scale = transformScale;
|
||||
scaleWidth = currentSectionWidthNoMargin;
|
||||
|
||||
if (transformScale > 0) {
|
||||
// reached the end of this section for sure, since the scale is now between 0 and 1
|
||||
// if the scale is always zero, then it will go through all sections but still return 0
|
||||
|
||||
scalePosition = currentWidth;
|
||||
if (checkLeft !== 0) {
|
||||
scalePosition += left;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentWidth += currentSectionWidth;
|
||||
}
|
||||
|
||||
return {
|
||||
left: left + leftPosition,
|
||||
scale: scale !== null ? scale * scaleWidth + scalePosition : null
|
||||
};
|
||||
}
|
||||
|
||||
private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
|
||||
const data = element.style[param];
|
||||
if (data?.includes("100%")) {
|
||||
return 0;
|
||||
} else {
|
||||
return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): void {
|
||||
if (!segments && submittingSegments?.length <= 0) return;
|
||||
|
||||
segments ??= [];
|
||||
if (submittingSegments?.length > 0) segments = segments.concat(submittingSegments);
|
||||
const activeSegments = segments.filter((segment) => {
|
||||
return segment.hidden === SponsorHideType.Visible
|
||||
&& segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
|
||||
});
|
||||
|
||||
this.setActiveSegments(activeSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the text to the chapters slot if not filled by default
|
||||
*/
|
||||
private setActiveSegments(segments: SponsorTime[]): void {
|
||||
const chaptersContainer = document.querySelector(".ytp-chapter-container") as HTMLDivElement;
|
||||
|
||||
if (chaptersContainer) {
|
||||
// TODO: Check if existing chapters exist (if big chapters menu is available?)
|
||||
|
||||
if (segments.length > 0) {
|
||||
chaptersContainer.style.removeProperty("display");
|
||||
|
||||
const chosenSegment = segments.sort((a, b) => {
|
||||
if (a.actionType === ActionType.Chapter && b.actionType !== ActionType.Chapter) {
|
||||
return -1;
|
||||
} else if (a.actionType !== ActionType.Chapter && b.actionType === ActionType.Chapter) {
|
||||
return 1;
|
||||
} else {
|
||||
return (b.segment[0] - a.segment[0]);
|
||||
}
|
||||
})[0];
|
||||
|
||||
const chapterButton = chaptersContainer.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
|
||||
chapterButton.classList.remove("ytp-chapter-container-disabled");
|
||||
chapterButton.disabled = false;
|
||||
|
||||
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
|
||||
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
|
||||
|
||||
const chapterVoteContainer = this.chapterVote.getContainer();
|
||||
if (chosenSegment.source === SponsorSourceType.Server) {
|
||||
if (!chapterButton.contains(chapterVoteContainer)) {
|
||||
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
|
||||
}
|
||||
|
||||
this.chapterVote.setVisibility(true);
|
||||
this.chapterVote.setSegment(chosenSegment);
|
||||
} else {
|
||||
this.chapterVote.setVisibility(false);
|
||||
}
|
||||
} else {
|
||||
// Hide chapters menu again
|
||||
chaptersContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.container.remove();
|
||||
|
||||
@@ -218,14 +686,66 @@ class PreviewBar {
|
||||
this.categoryTooltip = undefined;
|
||||
}
|
||||
|
||||
if (this.tooltipContainer) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
this.tooltipContainer = undefined;
|
||||
if (this.categoryTooltipContainer) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
this.categoryTooltipContainer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private chapterFilter(segment: PreviewBarSegment): boolean {
|
||||
return (Config.config.renderSegmentsAsChapters || segment.actionType === ActionType.Chapter)
|
||||
&& segment.actionType !== ActionType.Poi
|
||||
&& this.chapterGroupFilter(segment);
|
||||
}
|
||||
|
||||
private chapterGroupFilter(segment: SegmentContainer): boolean {
|
||||
return segment.segment.length === 2 && this.intervalToDecimal(segment.segment[0], segment.segment[1]) > MIN_CHAPTER_SIZE;
|
||||
}
|
||||
|
||||
intervalToPercentage(startTime: number, endTime: number) {
|
||||
return `${this.intervalToDecimal(startTime, endTime) * 100}%`;
|
||||
}
|
||||
|
||||
intervalToDecimal(startTime: number, endTime: number) {
|
||||
return (this.timeToDecimal(endTime) - this.timeToDecimal(startTime));
|
||||
}
|
||||
|
||||
timeToPercentage(time: number): string {
|
||||
return Math.min(100, time / this.videoDuration * 100) + '%';
|
||||
return `${this.timeToDecimal(time) * 100}%`
|
||||
}
|
||||
|
||||
timeToDecimal(time: number): number {
|
||||
if (this.originalChapterBarBlocks?.length > 1 && this.existingChapters.length === this.originalChapterBarBlocks?.length) {
|
||||
// Parent element to still work when display: none
|
||||
const totalPixels = this.originalChapterBar.parentElement.clientWidth;
|
||||
let pixelOffset = 0;
|
||||
let lastCheckedChapter = -1;
|
||||
for (let i = 0; i < this.originalChapterBarBlocks.length; i++) {
|
||||
const chapterElement = this.originalChapterBarBlocks[i];
|
||||
const widthPixels = parseFloat(chapterElement.style.width.replace("px", ""));
|
||||
|
||||
if (time >= this.existingChapters[i].segment[1]) {
|
||||
const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0;
|
||||
pixelOffset += widthPixels + marginPixels;
|
||||
lastCheckedChapter = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The next chapter is the one we are currently inside of
|
||||
const latestChapter = this.existingChapters[lastCheckedChapter + 1];
|
||||
if (latestChapter) {
|
||||
const latestWidth = parseFloat(this.originalChapterBarBlocks[lastCheckedChapter + 1].style.width.replace("px", ""));
|
||||
const latestChapterDuration = latestChapter.segment[1] - latestChapter.segment[0];
|
||||
|
||||
const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration;
|
||||
const sizeOfCurrentChapter = latestWidth / totalPixels;
|
||||
return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter)));
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(1, time / this.videoDuration);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -234,6 +754,31 @@ class PreviewBar {
|
||||
getMinimumSize(showLarger = false): number {
|
||||
return this.videoDuration * (showLarger ? 0.006 : 0.003);
|
||||
}
|
||||
|
||||
private getSmallestSegment(timeInSeconds: number, segments: PreviewBarSegment[]): PreviewBarSegment | null {
|
||||
let segment: PreviewBarSegment | null = null;
|
||||
let currentSegmentLength = Infinity;
|
||||
|
||||
for (const seg of segments) { //
|
||||
const segmentLength = seg.segment[1] - seg.segment[0];
|
||||
const minSize = this.getMinimumSize(seg.showLarger);
|
||||
|
||||
const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
|
||||
const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
|
||||
if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
|
||||
if (segmentLength < currentSegmentLength) {
|
||||
currentSegmentLength = segmentLength;
|
||||
segment = seg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
private getChapterChevron(): HTMLElement {
|
||||
return document.querySelector(".ytp-chapter-title-chevron");
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewBar;
|
||||
|
||||
@@ -30,6 +30,11 @@ interface IsInfoFoundMessage {
|
||||
updating: boolean;
|
||||
}
|
||||
|
||||
interface SkipMessage {
|
||||
message: "unskip" | "reskip";
|
||||
UUID: SegmentUUID;
|
||||
}
|
||||
|
||||
interface SubmitVoteMessage {
|
||||
message: "submitVote";
|
||||
type: number;
|
||||
@@ -47,6 +52,11 @@ interface CopyToClipboardMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ImportSegmentsMessage {
|
||||
message: "importSegments";
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface KeyDownMessage {
|
||||
message: "keydown";
|
||||
key: string;
|
||||
@@ -59,12 +69,13 @@ interface KeyDownMessage {
|
||||
metaKey: boolean;
|
||||
}
|
||||
|
||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | KeyDownMessage);
|
||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage);
|
||||
|
||||
export interface IsInfoFoundMessageResponse {
|
||||
found: boolean;
|
||||
status: number;
|
||||
sponsorTimes: SponsorTime[];
|
||||
time: number;
|
||||
onMobileYouTube: boolean;
|
||||
}
|
||||
|
||||
@@ -90,11 +101,23 @@ export type MessageResponse =
|
||||
| GetChannelIDResponse
|
||||
| SponsorStartResponse
|
||||
| IsChannelWhitelistedResponse
|
||||
| Record<never, never> // empty object response {}
|
||||
| VoteResponse;
|
||||
| Record<string, never> // empty object response {}
|
||||
| VoteResponse
|
||||
| ImportSegmentsResponse;
|
||||
|
||||
export interface VoteResponse {
|
||||
successType: number;
|
||||
statusCode: number;
|
||||
responseText: string;
|
||||
}
|
||||
|
||||
export interface ImportSegmentsResponse {
|
||||
importedSegments: SponsorTime[];
|
||||
}
|
||||
|
||||
export interface TimeUpdateMessage {
|
||||
message: "time";
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type PopupMessage = TimeUpdateMessage;
|
||||
|
||||
@@ -10,7 +10,7 @@ window.SB = Config;
|
||||
|
||||
import Utils from "./utils";
|
||||
import CategoryChooser from "./render/CategoryChooser";
|
||||
import KeybindComponent from "./components/KeybindComponent";
|
||||
import KeybindComponent from "./components/options/KeybindComponent";
|
||||
import { showDonationLink } from "./utils/configUtils";
|
||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||
const utils = new Utils();
|
||||
|
||||
244
src/popup.ts
244
src/popup.ts
@@ -1,12 +1,15 @@
|
||||
import Config from "./config";
|
||||
|
||||
import Utils from "./utils";
|
||||
import { SponsorTime, SponsorHideType, ActionType, StorageChangesObject } from "./types";
|
||||
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
|
||||
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject, CategorySkipOption } from "./types";
|
||||
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
|
||||
import { showDonationLink } from "./utils/configUtils";
|
||||
import { AnimationUtils } from "./utils/animationUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
import { shortCategoryName } from "./utils/categoryUtils";
|
||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||
import { exportTimes } from "./utils/exporter";
|
||||
import GenericNotice from "./render/GenericNotice";
|
||||
const utils = new Utils();
|
||||
|
||||
interface MessageListener {
|
||||
@@ -68,10 +71,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
|
||||
//the start and end time pairs (2d)
|
||||
let sponsorTimes: SponsorTime[] = [];
|
||||
let downloadedTimes: SponsorTime[] = [];
|
||||
|
||||
//current video ID of this tab
|
||||
let currentVideoID = null;
|
||||
|
||||
enum SegmentTab {
|
||||
Segments,
|
||||
Chapters
|
||||
}
|
||||
let segmentTab = SegmentTab.Segments;
|
||||
let port: chrome.runtime.Port = null;
|
||||
|
||||
const PageElements: PageElements = {};
|
||||
|
||||
[
|
||||
@@ -124,11 +135,21 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
"refreshSegmentsButton",
|
||||
"whitelistButton",
|
||||
"sbDonate",
|
||||
"issueReporterTabs",
|
||||
"issueReporterTabSegments",
|
||||
"issueReporterTabChapters",
|
||||
"sponsorTimesDonateContainer",
|
||||
"sbConsiderDonateLink",
|
||||
"sbCloseDonate",
|
||||
"sbBetaServerWarning",
|
||||
"sbCloseButton"
|
||||
"sbCloseButton",
|
||||
"issueReporterImportExport",
|
||||
"importSegmentsButton",
|
||||
"exportSegmentsButton",
|
||||
"importSegmentsMenu",
|
||||
"importSegmentsText",
|
||||
"importSegmentsSubmit"
|
||||
|
||||
].forEach(id => PageElements[id] = document.getElementById(id));
|
||||
|
||||
getSegmentsFromContentScript(false);
|
||||
@@ -162,7 +183,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
//setup click listeners
|
||||
PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
|
||||
PageElements.importSegmentsButton.addEventListener("click",
|
||||
() => PageElements.importSegmentsMenu.classList.toggle("hidden"));
|
||||
PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
|
||||
|
||||
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
|
||||
PageElements.whitelistToggle.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
@@ -215,6 +240,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
setupComPort();
|
||||
|
||||
//show proper disable skipping button
|
||||
const disableSkipping = Config.config.disableSkipping;
|
||||
if (disableSkipping != undefined && disableSkipping) {
|
||||
@@ -230,7 +257,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
PageElements.showNoticeAgain.style.display = "unset";
|
||||
}
|
||||
|
||||
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&userID=" + Config.config.userID, (res) => {
|
||||
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&value=freeChaptersAccess&userID="
|
||||
+ Config.config.userID, (res) => {
|
||||
if (res.status === 200) {
|
||||
const userInfo = JSON.parse(res.responseText);
|
||||
PageElements.usernameValue.innerText = userInfo.userName;
|
||||
@@ -259,6 +287,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
|
||||
Config.config.isVip = userInfo.vip;
|
||||
Config.config.permissions = userInfo.permissions;
|
||||
|
||||
if (userInfo.freeChaptersAccess) {
|
||||
Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess;
|
||||
Config.config.payments.freeAccess = userInfo.freeChaptersAccess;
|
||||
Config.config.payments.lastCheck = Date.now();
|
||||
Config.forceSyncUpdate("payments");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,6 +330,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
// Must be delayed so it only happens once loaded
|
||||
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
|
||||
|
||||
PageElements.issueReporterTabSegments.addEventListener("click", () => {
|
||||
PageElements.issueReporterTabSegments.classList.add("sbSelected");
|
||||
PageElements.issueReporterTabChapters.classList.remove("sbSelected");
|
||||
|
||||
segmentTab = SegmentTab.Segments;
|
||||
getSegmentsFromContentScript(true);
|
||||
});
|
||||
|
||||
PageElements.issueReporterTabChapters.addEventListener("click", () => {
|
||||
PageElements.issueReporterTabSegments.classList.remove("sbSelected");
|
||||
PageElements.issueReporterTabChapters.classList.add("sbSelected");
|
||||
|
||||
segmentTab = SegmentTab.Chapters;
|
||||
getSegmentsFromContentScript(true);
|
||||
});
|
||||
|
||||
function showDonateWidget(viewCount: number) {
|
||||
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
|
||||
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
|
||||
@@ -365,10 +417,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
PageElements.whitelistButton.classList.remove("hidden");
|
||||
PageElements.loadingIndicator.style.display = "none";
|
||||
|
||||
downloadedTimes = request.sponsorTimes ?? [];
|
||||
if (request.found) {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
|
||||
|
||||
displayDownloadedSponsorTimes(request);
|
||||
if (request.sponsorTimes) {
|
||||
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
|
||||
}
|
||||
} else if (request.status == 404 || request.status == 200) {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
||||
} else {
|
||||
@@ -441,10 +496,27 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
|
||||
//display the video times from the array at the top, in a different section
|
||||
function displayDownloadedSponsorTimes(request: { found: boolean, sponsorTimes: SponsorTime[] }) {
|
||||
if (request.sponsorTimes != undefined) {
|
||||
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
|
||||
let currentSegmentTab = segmentTab;
|
||||
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
|
||||
PageElements.issueReporterTabs.classList.add("hidden");
|
||||
currentSegmentTab = SegmentTab.Segments;
|
||||
} else {
|
||||
PageElements.issueReporterTabs.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Sort list by start time
|
||||
const segmentTimes = request.sponsorTimes
|
||||
const downloadedTimes = sponsorTimes
|
||||
.filter((segment) => {
|
||||
if (currentSegmentTab === SegmentTab.Segments) {
|
||||
return segment.actionType !== ActionType.Chapter;
|
||||
} else if (currentSegmentTab === SegmentTab.Chapters) {
|
||||
return segment.actionType === ActionType.Chapter
|
||||
&& segment.source !== SponsorSourceType.YouTube;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.segment[1] - b.segment[1])
|
||||
.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
|
||||
@@ -454,39 +526,58 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
if (downloadedTimes.length > 0) {
|
||||
PageElements.issueReporterImportExport.classList.remove("hidden");
|
||||
if (utils.getCategorySelection("chapter")?.option === CategorySkipOption.ShowOverlay) {
|
||||
PageElements.importSegmentsButton.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
PageElements.issueReporterImportExport.classList.add("hidden");
|
||||
}
|
||||
|
||||
const isVip = Config.config.isVip;
|
||||
for (let i = 0; i < segmentTimes.length; i++) {
|
||||
const UUID = segmentTimes[i].UUID;
|
||||
const locked = segmentTimes[i].locked;
|
||||
for (let i = 0; i < downloadedTimes.length; i++) {
|
||||
const UUID = downloadedTimes[i].UUID;
|
||||
const locked = downloadedTimes[i].locked;
|
||||
const category = downloadedTimes[i].category;
|
||||
const actionType = downloadedTimes[i].actionType;
|
||||
|
||||
const segmentSummary = document.createElement("summary");
|
||||
segmentSummary.className = "segmentSummary";
|
||||
segmentSummary.classList.add("segmentSummary");
|
||||
if (time >= downloadedTimes[i].segment[0]) {
|
||||
if (time < downloadedTimes[i].segment[1]) {
|
||||
segmentSummary.classList.add("segmentActive");
|
||||
} else {
|
||||
segmentSummary.classList.add("segmentPassed");
|
||||
}
|
||||
}
|
||||
|
||||
const categoryColorCircle = document.createElement("span");
|
||||
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
||||
categoryColorCircle.style.backgroundColor = Config.config.barTypes[segmentTimes[i].category]?.color;
|
||||
categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
|
||||
categoryColorCircle.classList.add("dot");
|
||||
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
||||
|
||||
let extraInfo = "";
|
||||
if (segmentTimes[i].hidden === SponsorHideType.Downvoted) {
|
||||
if (downloadedTimes[i].hidden === SponsorHideType.Downvoted) {
|
||||
//this one is downvoted
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
||||
} else if (segmentTimes[i].hidden === SponsorHideType.MinimumDuration) {
|
||||
} else if (downloadedTimes[i].hidden === SponsorHideType.MinimumDuration) {
|
||||
//this one is too short
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
||||
} else if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
} else if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
||||
}
|
||||
|
||||
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
|
||||
const name = downloadedTimes[i].description || shortCategoryName(category);
|
||||
const textNode = document.createTextNode(name + extraInfo);
|
||||
const segmentTimeFromToNode = document.createElement("div");
|
||||
if (segmentTimes[i].actionType === ActionType.Full) {
|
||||
if (downloadedTimes[i].actionType === ActionType.Full) {
|
||||
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
|
||||
} else {
|
||||
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
|
||||
(segmentTimes[i].actionType !== ActionType.Poi
|
||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
|
||||
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
|
||||
(actionType !== ActionType.Poi
|
||||
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
|
||||
: "");
|
||||
}
|
||||
|
||||
@@ -494,13 +585,12 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
|
||||
// for inline-styling purposes
|
||||
const labelContainer = document.createElement("div");
|
||||
labelContainer.appendChild(categoryColorCircle);
|
||||
if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = "summaryLabel";
|
||||
span.appendChild(textNode);
|
||||
labelContainer.appendChild(span);
|
||||
// for inline-styling purposes
|
||||
|
||||
segmentSummary.appendChild(labelContainer);
|
||||
segmentSummary.appendChild(segmentTimeFromToNode);
|
||||
@@ -542,7 +632,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
||||
hideButton.className = "voteButton";
|
||||
hideButton.title = chrome.i18n.getMessage("hideSegment");
|
||||
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
||||
} else {
|
||||
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
||||
@@ -551,12 +641,12 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
|
||||
stopAnimation();
|
||||
|
||||
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
||||
segmentTimes[i].hidden = SponsorHideType.Visible;
|
||||
downloadedTimes[i].hidden = SponsorHideType.Visible;
|
||||
} else {
|
||||
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
||||
segmentTimes[i].hidden = SponsorHideType.Hidden;
|
||||
downloadedTimes[i].hidden = SponsorHideType.Hidden;
|
||||
}
|
||||
|
||||
messageHandler.query({
|
||||
@@ -567,21 +657,30 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
tabs[0].id,
|
||||
{
|
||||
message: "hideSegment",
|
||||
type: segmentTimes[i].hidden,
|
||||
type: downloadedTimes[i].hidden,
|
||||
UUID: UUID
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const skipButton = document.createElement("img");
|
||||
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
|
||||
skipButton.className = "voteButton";
|
||||
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
|
||||
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
|
||||
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
|
||||
|
||||
//add thumbs up, thumbs down and uuid copy buttons to the container
|
||||
voteButtonsContainer.appendChild(upvoteButton);
|
||||
voteButtonsContainer.appendChild(downvoteButton);
|
||||
voteButtonsContainer.appendChild(uuidButton);
|
||||
if ((segmentTimes[i].actionType === ActionType.Skip || segmentTimes[i].actionType === ActionType.Mute)
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) {
|
||||
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
|
||||
voteButtonsContainer.appendChild(hideButton);
|
||||
}
|
||||
voteButtonsContainer.appendChild(skipButton);
|
||||
|
||||
|
||||
// Will contain request status
|
||||
const voteStatusContainer = document.createElement("div");
|
||||
@@ -601,7 +700,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
container.appendChild(votingButtons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function submitTimes() {
|
||||
if (sponsorTimes.length > 0) {
|
||||
@@ -708,6 +806,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
//this is not a YouTube video page
|
||||
function displayNoVideo() {
|
||||
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
|
||||
|
||||
PageElements.issueReporterTabs.classList.add("hidden");
|
||||
}
|
||||
|
||||
function addVoteMessage(message, UUID) {
|
||||
@@ -881,6 +981,37 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
|
||||
if (actionType === ActionType.Chapter) {
|
||||
sendMessage({
|
||||
message: "unskip",
|
||||
UUID: UUID
|
||||
});
|
||||
} else {
|
||||
sendMessage({
|
||||
message: "reskip",
|
||||
UUID: UUID
|
||||
});
|
||||
}
|
||||
|
||||
if (element) {
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
|
||||
stopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(request: Message): void {
|
||||
messageHandler.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
}, tabs => {
|
||||
messageHandler.sendMessage(
|
||||
tabs[0].id,
|
||||
request
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Should skipping be disabled (visuals stay)
|
||||
*/
|
||||
@@ -910,6 +1041,41 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function importSegments() {
|
||||
const text = (PageElements.importSegmentsText as HTMLInputElement).value;
|
||||
|
||||
await sendTabMessage({
|
||||
message: "importSegments",
|
||||
data: text
|
||||
}) as ImportSegmentsResponse;
|
||||
|
||||
PageElements.importSegmentsMenu.classList.add("hidden");
|
||||
}
|
||||
|
||||
function exportSegments() {
|
||||
copyToClipboard(exportTimes(downloadedTimes));
|
||||
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.exportSegmentsButton, 0.3);
|
||||
stopAnimation();
|
||||
new GenericNotice(null, "exportCopied", {
|
||||
title: chrome.i18n.getMessage(`CopiedExclamation`),
|
||||
timed: true,
|
||||
maxCountdownTime: () => 0.6,
|
||||
referenceNode: PageElements.exportSegmentsButton.parentElement,
|
||||
dontPauseCountdown: true,
|
||||
style: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
minWidth: 0,
|
||||
right: "30px",
|
||||
margin: "auto",
|
||||
height: "max-content"
|
||||
},
|
||||
hideLogo: true,
|
||||
hideRightInfo: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts time in minutes to 2d 5h 25.1
|
||||
* If less than 1 hour, just returns minutes
|
||||
@@ -934,6 +1100,20 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupComPort(): void {
|
||||
port = chrome.runtime.connect({ name: "popup" });
|
||||
port.onDisconnect.addListener(() => setupComPort());
|
||||
port.onMessage.addListener((msg) => onMessage(msg));
|
||||
}
|
||||
|
||||
function onMessage(msg: PopupMessage) {
|
||||
switch (msg.message) {
|
||||
case "time":
|
||||
displayDownloadedSponsorTimes(downloadedTimes, msg.time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runThePopup();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import CategoryChooserComponent from "../components/CategoryChooserComponent";
|
||||
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
|
||||
|
||||
class CategoryChooser {
|
||||
|
||||
|
||||
63
src/render/ChapterVote.tsx
Normal file
63
src/render/ChapterVote.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
|
||||
import { VoteResponse } from "../messageTypes";
|
||||
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||
|
||||
export class ChapterVote {
|
||||
container: HTMLElement;
|
||||
ref: React.RefObject<ChapterVoteComponent>;
|
||||
|
||||
unsavedState: ChapterVoteState;
|
||||
|
||||
mutationObserver?: MutationObserver;
|
||||
|
||||
constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>) {
|
||||
this.ref = React.createRef();
|
||||
|
||||
this.container = document.createElement('span');
|
||||
this.container.id = "chapterVote";
|
||||
this.container.style.height = "100%";
|
||||
|
||||
ReactDOM.render(
|
||||
<ChapterVoteComponent ref={this.ref} vote={vote} />,
|
||||
this.container
|
||||
);
|
||||
}
|
||||
|
||||
getContainer(): HTMLElement {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
setVisibility(show: boolean): void {
|
||||
const newState = {
|
||||
show,
|
||||
};
|
||||
|
||||
if (this.ref.current) {
|
||||
this.ref.current?.setState(newState);
|
||||
} else {
|
||||
this.unsavedState = newState;
|
||||
}
|
||||
}
|
||||
|
||||
async setSegment(segment: SponsorTime): Promise<void> {
|
||||
if (this.ref.current?.state?.segment !== segment) {
|
||||
const newState = {
|
||||
segment,
|
||||
show: true
|
||||
};
|
||||
|
||||
if (this.ref.current) {
|
||||
this.ref.current?.setState(newState);
|
||||
} else {
|
||||
this.unsavedState = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
import { ContentContainer } from "../types";
|
||||
import { ButtonListener, ContentContainer } from "../types";
|
||||
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
|
||||
|
||||
export interface ButtonListener {
|
||||
name: string,
|
||||
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
export interface TextBox {
|
||||
icon: string,
|
||||
text: string
|
||||
@@ -20,12 +15,17 @@ export interface TextBox {
|
||||
|
||||
export interface NoticeOptions {
|
||||
title: string,
|
||||
referenceNode?: HTMLElement,
|
||||
textBoxes?: TextBox[],
|
||||
buttons?: ButtonListener[],
|
||||
fadeIn?: boolean,
|
||||
timed?: boolean
|
||||
style?: React.CSSProperties;
|
||||
extraClass?: string;
|
||||
maxCountdownTime?: () => number;
|
||||
dontPauseCountdown?: boolean;
|
||||
hideLogo?: boolean;
|
||||
hideRightInfo?: boolean;
|
||||
}
|
||||
|
||||
export default class GenericNotice {
|
||||
@@ -42,7 +42,7 @@ export default class GenericNotice {
|
||||
|
||||
this.contentContainer = contentContainer;
|
||||
|
||||
const referenceNode = utils.findReferenceNode();
|
||||
const referenceNode = options.referenceNode ?? utils.findReferenceNode();
|
||||
|
||||
this.noticeElement = document.createElement("div");
|
||||
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
|
||||
@@ -62,6 +62,10 @@ export default class GenericNotice {
|
||||
ref={this.noticeRef}
|
||||
style={options.style}
|
||||
extraClass={options.extraClass}
|
||||
maxCountdownTime={options.maxCountdownTime}
|
||||
dontPauseCountdown={options.dontPauseCountdown}
|
||||
hideLogo={options.hideLogo}
|
||||
hideRightInfo={options.hideRightInfo}
|
||||
closeListener={() => this.close()} >
|
||||
|
||||
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
|
||||
|
||||
@@ -33,8 +33,8 @@ export class RectangleTooltip {
|
||||
props.fontSize ??= "10px";
|
||||
|
||||
this.container = document.createElement('div');
|
||||
props.htmlId ??= props.text;
|
||||
this.container.id = "sponsorRectangleTooltip" + props.htmlId;
|
||||
props.htmlId ??= "sponsorRectangleTooltip" + props.text;
|
||||
this.container.id = props.htmlId;
|
||||
this.container.style.display = "relative";
|
||||
|
||||
if (props.prependElement) {
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { ButtonListener } from "../types";
|
||||
|
||||
export interface TooltipProps {
|
||||
text: string,
|
||||
link?: string,
|
||||
referenceNode: HTMLElement,
|
||||
prependElement?: HTMLElement, // Element to append before
|
||||
bottomOffset?: string
|
||||
text?: string;
|
||||
link?: string;
|
||||
referenceNode: HTMLElement;
|
||||
prependElement?: HTMLElement; // Element to append before
|
||||
bottomOffset?: string;
|
||||
leftOffset?: string;
|
||||
rightOffset?: string;
|
||||
timeout?: number;
|
||||
opacity?: number;
|
||||
displayTriangle?: boolean;
|
||||
extraClass?: string;
|
||||
showLogo?: boolean;
|
||||
showGotIt?: boolean;
|
||||
buttons?: ButtonListener[];
|
||||
}
|
||||
|
||||
export class Tooltip {
|
||||
text: string;
|
||||
text?: string;
|
||||
container: HTMLDivElement;
|
||||
|
||||
timer: NodeJS.Timeout;
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
props.bottomOffset ??= "70px";
|
||||
props.leftOffset ??= "inherit";
|
||||
props.rightOffset ??= "inherit";
|
||||
props.opacity ??= 0.7;
|
||||
props.displayTriangle ??= true;
|
||||
props.extraClass ??= "";
|
||||
props.showLogo ??= true;
|
||||
props.showGotIt ??= true;
|
||||
this.text = props.text;
|
||||
@@ -45,14 +53,15 @@ export class Tooltip {
|
||||
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{bottom: props.bottomOffset, backgroundColor}}
|
||||
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
|
||||
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
|
||||
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
|
||||
<div>
|
||||
{props.showLogo ?
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
: null}
|
||||
{this.text ?
|
||||
<span className="sponsorSkipObject">
|
||||
{this.text + (props.link ? ". " : "")}
|
||||
{props.link ?
|
||||
@@ -64,6 +73,9 @@ export class Tooltip {
|
||||
</a>
|
||||
: null}
|
||||
</span>
|
||||
: null}
|
||||
|
||||
{this.getButtons(props.buttons)}
|
||||
</div>
|
||||
{props.showGotIt ?
|
||||
<button className="sponsorSkipObject sponsorSkipNoticeButton"
|
||||
@@ -78,6 +90,27 @@ export class Tooltip {
|
||||
)
|
||||
}
|
||||
|
||||
getButtons(buttons?: ButtonListener[]): JSX.Element[] {
|
||||
if (buttons) {
|
||||
const result: JSX.Element[] = [];
|
||||
|
||||
for (const button of buttons) {
|
||||
result.push(
|
||||
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
|
||||
key={button.name}
|
||||
onClick={(e) => button.listener(e)}>
|
||||
|
||||
{button.name}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
this.container.remove();
|
||||
|
||||
22
src/svg-icons/lock_svg.tsx
Normal file
22
src/svg-icons/lock_svg.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
|
||||
const lockSvg = ({
|
||||
fill = "#fcba03",
|
||||
className = "",
|
||||
width = "20",
|
||||
height = "20",
|
||||
onClick
|
||||
}): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={width}
|
||||
width={height}
|
||||
className={className}
|
||||
fill={fill}
|
||||
onClick={onClick} >
|
||||
<path
|
||||
d="M5.5 18q-.625 0-1.062-.438Q4 17.125 4 16.5v-8q0-.625.438-1.062Q4.875 7 5.5 7H6V5q0-1.667 1.167-2.833Q8.333 1 10 1q1.667 0 2.833 1.167Q14 3.333 14 5v2h.5q.625 0 1.062.438Q16 7.875 16 8.5v8q0 .625-.438 1.062Q15.125 18 14.5 18Zm4.5-4q.625 0 1.062-.438.438-.437.438-1.062t-.438-1.062Q10.625 11 10 11t-1.062.438Q8.5 11.875 8.5 12.5t.438 1.062Q9.375 14 10 14ZM7.5 7h5V5q0-1.042-.729-1.771Q11.042 2.5 10 2.5q-1.042 0-1.771.729Q7.5 3.958 7.5 5Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default lockSvg;
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as React from "react";
|
||||
|
||||
const thumbsDownSvg = ({
|
||||
fill = "#ffffff"
|
||||
fill = "#ffffff",
|
||||
className = "",
|
||||
width = "18",
|
||||
height = "18"
|
||||
}): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as React from "react";
|
||||
|
||||
const thumbsUpSvg = ({
|
||||
fill = "#ffffff"
|
||||
fill = "#ffffff",
|
||||
className = "",
|
||||
width = "18",
|
||||
height = "18"
|
||||
}): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
fill={fill}
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
|
||||
20
src/types.ts
20
src/types.ts
@@ -21,7 +21,8 @@ export interface ContentContainer {
|
||||
previewTime: (time: number, unpause?: boolean) => void,
|
||||
videoInfo: VideoInfo,
|
||||
getRealCurrentTime: () => number,
|
||||
lockedCategories: string[]
|
||||
lockedCategories: string[],
|
||||
channelIDInfo: ChannelIDInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ export enum SponsorHideType {
|
||||
export enum ActionType {
|
||||
Skip = "skip",
|
||||
Mute = "mute",
|
||||
Chapter = "chapter",
|
||||
Full = "full",
|
||||
Poi = "poi"
|
||||
}
|
||||
@@ -69,19 +71,24 @@ export type Category = string & { __categoryBrand: unknown };
|
||||
|
||||
export enum SponsorSourceType {
|
||||
Server = undefined,
|
||||
Local = 1
|
||||
Local = 1,
|
||||
YouTube = 2
|
||||
}
|
||||
|
||||
export interface SponsorTime {
|
||||
export interface SegmentContainer {
|
||||
segment: [number] | [number, number];
|
||||
}
|
||||
|
||||
export interface SponsorTime extends SegmentContainer {
|
||||
UUID: SegmentUUID;
|
||||
locked?: number;
|
||||
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
description?: string;
|
||||
|
||||
hidden?: SponsorHideType;
|
||||
source?: SponsorSourceType;
|
||||
source: SponsorSourceType;
|
||||
videoDuration?: number;
|
||||
}
|
||||
|
||||
@@ -231,3 +238,8 @@ export type Keybind = {
|
||||
alt?: boolean,
|
||||
shift?: boolean
|
||||
}
|
||||
|
||||
export interface ButtonListener {
|
||||
name: string,
|
||||
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
71
src/upsell.ts
Normal file
71
src/upsell.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import Config from "./config";
|
||||
import { checkLicenseKey } from "./utils/licenseKey";
|
||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||
|
||||
import * as countries from "../public/res/countries.json";
|
||||
|
||||
// This is needed, if Config is not imported before Utils, things break.
|
||||
// Probably due to cyclic dependencies
|
||||
Config.config;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
async function init() {
|
||||
localizeHtmlPage();
|
||||
|
||||
const cantAfford = document.getElementById("cantAfford");
|
||||
const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/);
|
||||
cantAfford.appendChild(document.createTextNode(cantAffordTexts[0]));
|
||||
const discountButton = document.createElement("span");
|
||||
discountButton.id = "discountButton";
|
||||
discountButton.innerText = cantAffordTexts[1];
|
||||
cantAfford.appendChild(discountButton);
|
||||
cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
|
||||
|
||||
const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
|
||||
redeemButton.addEventListener("click", async () => {
|
||||
const licenseKey = redeemButton.value;
|
||||
|
||||
if (await checkLicenseKey(licenseKey)) {
|
||||
Config.config.payments.licenseKey = licenseKey;
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
alert(chrome.i18n.getMessage("redeemSuccess"));
|
||||
} else {
|
||||
alert(chrome.i18n.getMessage("redeemFailed"));
|
||||
}
|
||||
});
|
||||
|
||||
discountButton.addEventListener("click", async () => {
|
||||
const subsidizedSection = document.getElementById("subsidizedPrice");
|
||||
subsidizedSection.classList.remove("hidden");
|
||||
|
||||
const oldSelector = document.getElementById("countrySelector");
|
||||
if (oldSelector) oldSelector.remove();
|
||||
const countrySelector = document.createElement("select");
|
||||
countrySelector.id = "countrySelector";
|
||||
countrySelector.className = "optionsSelector";
|
||||
const defaultOption = document.createElement("option");
|
||||
defaultOption.innerText = chrome.i18n.getMessage("chooseACountry");
|
||||
countrySelector.appendChild(defaultOption);
|
||||
|
||||
for (const country of Object.keys(countries)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = country;
|
||||
option.innerText = country;
|
||||
countrySelector.appendChild(option);
|
||||
}
|
||||
|
||||
countrySelector.addEventListener("change", () => {
|
||||
if (countries[countrySelector.value]?.allowed) {
|
||||
document.getElementById("subsidizedLink").classList.remove("hidden");
|
||||
document.getElementById("noSubsidizedLink").classList.add("hidden");
|
||||
} else {
|
||||
document.getElementById("subsidizedLink").classList.add("hidden");
|
||||
document.getElementById("noSubsidizedLink").classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
subsidizedSection.appendChild(countrySelector);
|
||||
});
|
||||
}
|
||||
68
src/utils.ts
68
src/utils.ts
@@ -30,7 +30,7 @@ export default class Utils {
|
||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
}
|
||||
|
||||
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
async wait<T>(condition: () => T, timeout = 5000, check = 100): Promise<T> {
|
||||
return GenericUtils.wait(condition, timeout, check);
|
||||
}
|
||||
|
||||
@@ -331,24 +331,6 @@ export default class Utils {
|
||||
return permissionRegex;
|
||||
}
|
||||
|
||||
generateUserID(length = 36): string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
const values = new Uint32Array(length);
|
||||
window.crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[values[i] % charset.length];
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a custom server
|
||||
*
|
||||
@@ -434,54 +416,6 @@ export default class Utils {
|
||||
return referenceNode;
|
||||
}
|
||||
|
||||
getFormattedTime(seconds: number, precise?: boolean): string {
|
||||
seconds = Math.max(seconds, 0);
|
||||
|
||||
const hours = Math.floor(seconds / 60 / 60);
|
||||
const minutes = Math.floor(seconds / 60) % 60;
|
||||
let minutesDisplay = String(minutes);
|
||||
let secondsNum = seconds % 60;
|
||||
if (!precise) {
|
||||
secondsNum = Math.floor(secondsNum);
|
||||
}
|
||||
|
||||
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
|
||||
|
||||
if (secondsNum < 10) {
|
||||
//add a zero
|
||||
secondsDisplay = "0" + secondsDisplay;
|
||||
}
|
||||
if (hours && minutes < 10) {
|
||||
//add a zero
|
||||
minutesDisplay = "0" + minutesDisplay;
|
||||
}
|
||||
if (isNaN(hours) || isNaN(minutes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
getFormattedTimeToSeconds(formatted: string): number | null {
|
||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||
|
||||
if (fragments === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
shortCategoryName(categoryName: string): string {
|
||||
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
||||
}
|
||||
|
||||
isContentScript(): boolean {
|
||||
return window.location.protocol === "http:" || window.location.protocol === "https:";
|
||||
}
|
||||
|
||||
6
src/utils/arrayUtils.ts
Normal file
6
src/utils/arrayUtils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function partition<T>(array: T[], filter: (element: T) => boolean): [T[], T[]] {
|
||||
const pass = [], fail = [];
|
||||
array.forEach((element) => (filter(element) ? pass : fail).push(element));
|
||||
|
||||
return [pass, fail];
|
||||
}
|
||||
@@ -41,7 +41,13 @@ export function getCategorySuffix(category: Category): string {
|
||||
return "_POI";
|
||||
} else if (category === "exclusive_access") {
|
||||
return "_full";
|
||||
} else if (category === "chapter") {
|
||||
return "_chapter";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function shortCategoryName(categoryName: string): string {
|
||||
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
||||
}
|
||||
@@ -137,5 +137,16 @@ export function getGuidelineInfo(category: Category): TextBox[] {
|
||||
icon: "icons/bolt.svg",
|
||||
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
||||
}];
|
||||
case "chapter":
|
||||
return [{
|
||||
icon: "icons/close-smaller.svg",
|
||||
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
|
||||
}, {
|
||||
icon: "icons/check-smaller.svg",
|
||||
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
|
||||
}, {
|
||||
icon: "icons/check-smaller.svg",
|
||||
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
||||
}];
|
||||
}
|
||||
}
|
||||
65
src/utils/exporter.ts
Normal file
65
src/utils/exporter.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
|
||||
import { shortCategoryName } from "./categoryUtils";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
|
||||
export function exportTimes(segments: SponsorTime[]): string {
|
||||
let result = "";
|
||||
for (const segment of segments) {
|
||||
if (![ActionType.Full, ActionType.Mute].includes(segment.actionType)
|
||||
&& segment.source !== SponsorSourceType.YouTube) {
|
||||
result += exportTime(segment) + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return result.replace(/\n$/, "");
|
||||
}
|
||||
|
||||
function exportTime(segment: SponsorTime): string {
|
||||
const name = segment.description || shortCategoryName(segment.category);
|
||||
|
||||
return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
|
||||
segment.segment[1] && segment.segment[0] !== segment.segment[1]
|
||||
? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
|
||||
}
|
||||
|
||||
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
|
||||
const lines = data.split("\n");
|
||||
const result: SponsorTime[] = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
|
||||
if (match) {
|
||||
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
|
||||
if (startTime) {
|
||||
const specialCharsMatcher = /^(?:\s+seconds?)?[:()-\s]*|(?:\s+at)?[:()-\s]+$/g
|
||||
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
|
||||
let titleRight = null;
|
||||
const split2 = line.split(match[1] || match[0]);
|
||||
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
|
||||
|
||||
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
|
||||
if (title) {
|
||||
const segment: SponsorTime = {
|
||||
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
|
||||
category: "chapter" as Category,
|
||||
actionType: ActionType.Chapter,
|
||||
description: title,
|
||||
source: SponsorSourceType.Local,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID
|
||||
};
|
||||
|
||||
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||
result[result.length - 1].segment[1] = segment.segment[0];
|
||||
}
|
||||
|
||||
result.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||
result[result.length - 1].segment[1] = videoDuration;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Function that can be used to wait for a condition before returning. */
|
||||
async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
@@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
||||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (result) {
|
||||
if (predicate ? predicate(result) : result) {
|
||||
resolve(result);
|
||||
clearInterval(interval);
|
||||
}
|
||||
@@ -21,6 +21,50 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
||||
});
|
||||
}
|
||||
|
||||
function getFormattedTimeToSeconds(formatted: string): number | null {
|
||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||
|
||||
if (fragments === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
function getFormattedTime(seconds: number, precise?: boolean): string {
|
||||
seconds = Math.max(seconds, 0);
|
||||
|
||||
const hours = Math.floor(seconds / 60 / 60);
|
||||
const minutes = Math.floor(seconds / 60) % 60;
|
||||
let minutesDisplay = String(minutes);
|
||||
let secondsNum = seconds % 60;
|
||||
if (!precise) {
|
||||
secondsNum = Math.floor(secondsNum);
|
||||
}
|
||||
|
||||
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
|
||||
|
||||
if (secondsNum < 10) {
|
||||
//add a zero
|
||||
secondsDisplay = "0" + secondsDisplay;
|
||||
}
|
||||
if (hours && minutes < 10) {
|
||||
//add a zero
|
||||
minutesDisplay = "0" + minutesDisplay;
|
||||
}
|
||||
if (isNaN(hours) || isNaN(minutes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error message in a nice string
|
||||
*
|
||||
@@ -85,10 +129,31 @@ function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): str
|
||||
return url;
|
||||
}
|
||||
|
||||
function generateUserID(length = 36): string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
if (window.crypto && window.crypto.getRandomValues) {
|
||||
const values = new Uint32Array(length);
|
||||
window.crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[values[i] % charset.length];
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export const GenericUtils = {
|
||||
wait,
|
||||
getFormattedTime,
|
||||
getFormattedTimeToSeconds,
|
||||
getErrorMessage,
|
||||
getLuminance,
|
||||
generateUserID,
|
||||
indexesOf,
|
||||
objectToURI
|
||||
}
|
||||
65
src/utils/licenseKey.ts
Normal file
65
src/utils/licenseKey.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import Config from "../config";
|
||||
import Utils from "../utils";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
|
||||
const utils = new Utils();
|
||||
|
||||
export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
|
||||
const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
|
||||
licenseKey
|
||||
});
|
||||
|
||||
try {
|
||||
if (result.ok && JSON.parse(result.responseText).allowed) {
|
||||
Config.config.payments.chaptersAllowed = true;
|
||||
Config.config.payments.lastCheck = Date.now();
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (e) { } //eslint-disable-line no-empty
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function fetchingChaptersAllowed(): Promise<boolean> {
|
||||
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//more than 14 days
|
||||
if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
|
||||
const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
|
||||
|
||||
if (!Config.config.payments.chaptersAllowed) {
|
||||
return licensePromise;
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.config.payments.chaptersAllowed) return true;
|
||||
|
||||
if (Config.config.payments.lastCheck === 0) {
|
||||
// Check for free access if no license key, and it is the first time
|
||||
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
value: "freeChaptersAccess",
|
||||
userID: Config.config.userID
|
||||
});
|
||||
|
||||
try {
|
||||
if (result.ok) {
|
||||
const userInfo = JSON.parse(result.responseText);
|
||||
|
||||
Config.config.payments.lastCheck = Date.now();
|
||||
if (userInfo.freeChaptersAccess) {
|
||||
Config.config.payments.freeAccess = true;
|
||||
Config.config.payments.chaptersAllowed = true;
|
||||
Config.forceSyncUpdate("payments");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) { } //eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export function getControls(): HTMLElement | false {
|
||||
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
|
||||
export function getControls(): HTMLElement {
|
||||
const controlsSelectors = [
|
||||
// YouTube
|
||||
".ytp-right-controls",
|
||||
@@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isVisible(element: HTMLElement): boolean {
|
||||
@@ -63,6 +66,44 @@ export function getHashParams(): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
|
||||
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
|
||||
|
||||
const chapters: SponsorTime[] = [];
|
||||
if (chaptersBox) {
|
||||
let lastSegment: SponsorTime = null;
|
||||
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
|
||||
for (const link of links) {
|
||||
const timeElement = link.querySelector("#time") as HTMLElement;
|
||||
const description = link.querySelector("#details h4") as HTMLElement;
|
||||
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
|
||||
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
|
||||
|
||||
if (lastSegment) {
|
||||
lastSegment.segment[1] = time;
|
||||
chapters.push(lastSegment);
|
||||
}
|
||||
|
||||
lastSegment = {
|
||||
segment: [time, null],
|
||||
category: "chapter" as Category,
|
||||
actionType: ActionType.Chapter,
|
||||
description: description.innerText,
|
||||
source: SponsorSourceType.YouTube,
|
||||
UUID: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSegment) {
|
||||
lastSegment.segment[1] = duration;
|
||||
chapters.push(lastSegment);
|
||||
}
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
||||
|
||||
export function localizeHtmlPage(): void {
|
||||
//Localize by replacing __MSG_***__ meta tags
|
||||
const localizedTitle = getLocalizedMessage(document.title);
|
||||
|
||||
241
test/exporter.test.ts
Normal file
241
test/exporter.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../src/types";
|
||||
import { exportTimes, importTimes } from "../src/utils/exporter";
|
||||
|
||||
describe("Export segments", () => {
|
||||
it("Some segments", () => {
|
||||
const segments: SponsorTime[] = [{
|
||||
segment: [0, 10],
|
||||
category: "chapter" as Category,
|
||||
actionType: ActionType.Chapter,
|
||||
description: "Chapter 1",
|
||||
source: SponsorSourceType.Server,
|
||||
UUID: "1" as SegmentUUID
|
||||
}, {
|
||||
segment: [20, 20],
|
||||
category: "poi_highlight" as Category,
|
||||
actionType: ActionType.Poi,
|
||||
description: "Highlight",
|
||||
source: SponsorSourceType.Server,
|
||||
UUID: "2" as SegmentUUID
|
||||
}, {
|
||||
segment: [30, 40],
|
||||
category: "sponsor" as Category,
|
||||
actionType: ActionType.Skip,
|
||||
description: "Sponsor", // Force a description since chrome is not defined
|
||||
source: SponsorSourceType.Server,
|
||||
UUID: "3" as SegmentUUID
|
||||
}, {
|
||||
segment: [50, 60],
|
||||
category: "selfpromo" as Category,
|
||||
actionType: ActionType.Mute,
|
||||
description: "Selfpromo",
|
||||
source: SponsorSourceType.Server,
|
||||
UUID: "4" as SegmentUUID
|
||||
}, {
|
||||
segment: [0, 0],
|
||||
category: "selfpromo" as Category,
|
||||
actionType: ActionType.Full,
|
||||
description: "Selfpromo",
|
||||
source: SponsorSourceType.Server,
|
||||
UUID: "5" as SegmentUUID
|
||||
}, {
|
||||
segment: [80, 90],
|
||||
category: "interaction" as Category,
|
||||
actionType: ActionType.Skip,
|
||||
description: "Interaction",
|
||||
source: SponsorSourceType.YouTube,
|
||||
UUID: "6" as SegmentUUID
|
||||
}];
|
||||
|
||||
const result = exportTimes(segments);
|
||||
|
||||
expect(result).toBe(
|
||||
"0:00.000 - 0:10.000 Chapter 1\n" +
|
||||
"0:20.000 Highlight\n" +
|
||||
"0:30.000 - 0:40.000 Sponsor"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Import segments", () => {
|
||||
it("1:20 to 1:21 thing", () => {
|
||||
const input = ` 1:20 to 1:21 thing
|
||||
1:25 to 1:28 another thing`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 81],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 88],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("thing 1:20 to 1:21", () => {
|
||||
const input = ` thing 1:20 to 1:21
|
||||
another thing 1:25 to 1:28 ext`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 81],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 88],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("1:20 - 1:21 thing", () => {
|
||||
const input = ` 1:20 - 1:21 thing
|
||||
1:25 - 1:28 another thing`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 81],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 88],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("1:20 1:21 thing", () => {
|
||||
const input = ` 1:20 1:21 thing
|
||||
1:25 1:28 another thing`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 81],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 88],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("1:20 thing", () => {
|
||||
const input = ` 1:20 thing
|
||||
1:25 another thing`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 85],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("1:20: thing", () => {
|
||||
const input = ` 1:20: thing
|
||||
1:25: another thing`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 85],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("1:20 (thing)", () => {
|
||||
const input = ` 1:20 (thing)
|
||||
1:25 (another thing)`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 85],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("thing 1:20", () => {
|
||||
const input = ` thing 1:20
|
||||
another thing 1:25`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 85],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("thing at 1:20", () => {
|
||||
const input = ` thing at 1:20
|
||||
another thing at 1:25`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [80, 85],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [85, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("thing at 1s", () => {
|
||||
const input = ` thing at 1s
|
||||
another thing at 5s`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [1, 5],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [5, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
|
||||
it("thing at 1 second", () => {
|
||||
const input = ` thing at 1 second
|
||||
another thing at 5 seconds`;
|
||||
|
||||
const result = importTimes(input, 120);
|
||||
expect(result).toMatchObject([{
|
||||
segment: [1, 5],
|
||||
description: "thing",
|
||||
category: "chapter" as Category
|
||||
}, {
|
||||
segment: [5, 120],
|
||||
description: "another thing",
|
||||
category: "chapter" as Category
|
||||
}]);
|
||||
});
|
||||
});
|
||||
665
test/previewBar.test.ts
Normal file
665
test/previewBar.test.ts
Normal file
@@ -0,0 +1,665 @@
|
||||
import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
|
||||
|
||||
describe("createChapterRenderGroups", () => {
|
||||
let previewBar: PreviewBar;
|
||||
beforeEach(() => {
|
||||
previewBar = new PreviewBar(null, null, null, null, true);
|
||||
})
|
||||
|
||||
it("Two unrelated times", () => {
|
||||
previewBar.videoDuration = 315;
|
||||
const groups = previewBar.createChapterRenderGroups([{
|
||||
segment: [2, 30],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}, {
|
||||
segment: [50, 80],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}] as PreviewBarSegment[]);
|
||||
|
||||
expect(groups).toStrictEqual([{
|
||||
segment: [0, 2],
|
||||
originalDuration: 0
|
||||
}, {
|
||||
segment: [2, 30],
|
||||
originalDuration: 30 - 2
|
||||
}, {
|
||||
segment: [30, 50],
|
||||
originalDuration: 0
|
||||
}, {
|
||||
segment: [50, 80],
|
||||
originalDuration: 80 - 50
|
||||
}, {
|
||||
segment: [80, 315],
|
||||
originalDuration: 0
|
||||
}]);
|
||||
});
|
||||
|
||||
it("Small time in bigger time", () => {
|
||||
previewBar.videoDuration = 315;
|
||||
const groups = previewBar.createChapterRenderGroups([{
|
||||
segment: [2.52, 30],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}, {
|
||||
segment: [20, 25],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}] as PreviewBarSegment[]);
|
||||
|
||||
expect(groups).toStrictEqual([{
|
||||
segment: [0, 2.52],
|
||||
originalDuration: 0
|
||||
}, {
|
||||
segment: [2.52, 20],
|
||||
originalDuration: 30 - 2.52
|
||||
}, {
|
||||
segment: [20, 25],
|
||||
originalDuration: 25 - 20
|
||||
}, {
|
||||
segment: [25, 30],
|
||||
originalDuration: 30 - 2.52
|
||||
}, {
|
||||
segment: [30, 315],
|
||||
originalDuration: 0
|
||||
}]);
|
||||
});
|
||||
|
||||
it("Same start time", () => {
|
||||
previewBar.videoDuration = 315;
|
||||
const groups = previewBar.createChapterRenderGroups([{
|
||||
segment: [2.52, 30],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}, {
|
||||
segment: [2.52, 40],
|
||||
category: "sponsor",
|
||||
unsubmitted: false,
|
||||
showLarger: false,
|
||||
description: ""
|
||||
}] as PreviewBarSegment[]);
|
||||
|
||||
expect(groups).toStrictEqual([{
|
||||
segment: [0, 2.52],
|
||||
originalDuration: 0
|
||||
}, {
|
||||
segment: [2.52, 30],
|
||||
originalDuration: 30 - 2.52
|
||||
}, {
|
||||
segment: [30, 40],
|
||||
originalDuration: 40 - 2.52
|
||||
}, {
|
||||
segment: [40, 315],
|
||||
originalDuration: 0
|
||||
}]);
|
||||
});
|
||||
|
||||
it("Lots of overlapping segments", () => {
|
||||
previewBar.videoDuration = 315.061;
|
||||
const groups = previewBar.createChapterRenderGroups([
|
||||
{
|
||||
"category": "chapter",
|
||||
"segment": [
|
||||
0,
|
||||
49.977
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||
"description": "Start of video"
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
2.926,
|
||||
5
|
||||
],
|
||||
"locked": 1,
|
||||
"votes": 2,
|
||||
"videoDuration": 316,
|
||||
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "chapter",
|
||||
"segment": [
|
||||
14.487,
|
||||
37.133
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||
"description": "Subset of start"
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
23.450537,
|
||||
34.486084
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": -1,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "interaction",
|
||||
"segment": [
|
||||
50.015343,
|
||||
56.775314
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "b2a85e8cdfbf21dd504babbcaca7f751b55a5a2df8179c1a83a121d0f5d56c0e",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
62.51888,
|
||||
74.33331
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": -1,
|
||||
"videoDuration": 316,
|
||||
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
88.71328,
|
||||
96.05933
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "6c08c092db2b7a31210717cc1f2652e7e97d032e03c82b029a27c81cead1f90c",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
101.50703,
|
||||
115.088326
|
||||
],
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "2db207ad4b7a535a548fab293f4567bf97373997e67aadb47df8f91b673f6e53",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
122.211845,
|
||||
137.42178
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 1,
|
||||
"videoDuration": 0,
|
||||
"userID": "0312cbfa514d9d2dfb737816dc45f52aba7c23f0a3f0911273a6993b2cb57fcc",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
144.08913,
|
||||
160.14084
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": -1,
|
||||
"videoDuration": 316,
|
||||
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
164.22084,
|
||||
170.98082
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "845c4377060d5801f5324f8e1be1e8373bfd9addcf6c68fc5a3c038111b506a3",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
180.56674,
|
||||
189.16516
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": -1,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "7c6b015687db7800c05756a0fd226fd8d101f5a1e1bfb1e5d97c440331fd6cb7",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
204.10468,
|
||||
211.87865
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "3472e8ee00b5da957377ae32d59ddd3095c2b634c2c0c970dfabfb81d143699f",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
214.92064,
|
||||
222.0186
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 0,
|
||||
"userID": "62a00dffb344d27de7adf8ea32801c2fc0580087dc8d282837923e4bda6a1745",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
233.0754,
|
||||
244.56734
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": -1,
|
||||
"videoDuration": 315,
|
||||
"userID": "dcf7fb0a6c071d5a93273ebcbecaee566e0ff98181057a36ed855e9b92bf25ea",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
260.64053,
|
||||
269.35938
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
288.686,
|
||||
301.96
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"category": "sponsor",
|
||||
"segment": [
|
||||
288.686,
|
||||
295
|
||||
],
|
||||
"locked": 0,
|
||||
"votes": 0,
|
||||
"videoDuration": 315.061,
|
||||
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||
"description": ""
|
||||
}] as unknown as PreviewBarSegment[]);
|
||||
|
||||
expect(groups).toStrictEqual([
|
||||
{
|
||||
"segment": [
|
||||
0,
|
||||
2.926
|
||||
],
|
||||
"originalDuration": 49.977
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
2.926,
|
||||
5
|
||||
],
|
||||
"originalDuration": 2.074
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
5,
|
||||
14.487
|
||||
],
|
||||
"originalDuration": 49.977
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
14.487,
|
||||
23.450537
|
||||
],
|
||||
"originalDuration": 22.646
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
23.450537,
|
||||
34.486084
|
||||
],
|
||||
"originalDuration": 11.035546999999998
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
34.486084,
|
||||
37.133
|
||||
],
|
||||
"originalDuration": 22.646
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
37.133,
|
||||
49.977
|
||||
],
|
||||
"originalDuration": 49.977
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
49.977,
|
||||
50.015343
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
50.015343,
|
||||
56.775314
|
||||
],
|
||||
"originalDuration": 6.759971
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
56.775314,
|
||||
62.51888
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
62.51888,
|
||||
74.33331
|
||||
],
|
||||
"originalDuration": 11.814429999999994
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
74.33331,
|
||||
88.71328
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
88.71328,
|
||||
96.05933
|
||||
],
|
||||
"originalDuration": 7.346050000000005
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
96.05933,
|
||||
101.50703
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
101.50703,
|
||||
115.088326
|
||||
],
|
||||
"originalDuration": 13.581295999999995
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
115.088326,
|
||||
122.211845
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
122.211845,
|
||||
137.42178
|
||||
],
|
||||
"originalDuration": 15.209935000000016
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
137.42178,
|
||||
144.08913
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
144.08913,
|
||||
160.14084
|
||||
],
|
||||
"originalDuration": 16.051709999999986
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
160.14084,
|
||||
164.22084
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
164.22084,
|
||||
170.98082
|
||||
],
|
||||
"originalDuration": 6.759979999999985
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
170.98082,
|
||||
180.56674
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
180.56674,
|
||||
189.16516
|
||||
],
|
||||
"originalDuration": 8.598419999999976
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
189.16516,
|
||||
204.10468
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
204.10468,
|
||||
211.87865
|
||||
],
|
||||
"originalDuration": 7.773969999999991
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
211.87865,
|
||||
214.92064
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
214.92064,
|
||||
222.0186
|
||||
],
|
||||
"originalDuration": 7.0979600000000005
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
222.0186,
|
||||
233.0754
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
233.0754,
|
||||
244.56734
|
||||
],
|
||||
"originalDuration": 11.49194
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
244.56734,
|
||||
260.64053
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
260.64053,
|
||||
269.35938
|
||||
],
|
||||
"originalDuration": 8.718849999999975
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
269.35938,
|
||||
288.686
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
288.686,
|
||||
295
|
||||
],
|
||||
"originalDuration": 6.314000000000021
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
295,
|
||||
301.96
|
||||
],
|
||||
"originalDuration": 13.274000000000001
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
301.96,
|
||||
315.061
|
||||
],
|
||||
"originalDuration": 0
|
||||
}
|
||||
]);
|
||||
})
|
||||
|
||||
it("Multiple overlapping", () => {
|
||||
previewBar.videoDuration = 3615.161;
|
||||
const groups = previewBar.createChapterRenderGroups([{
|
||||
"segment": [
|
||||
160,
|
||||
2797.323
|
||||
],
|
||||
"category": "chooseACategory",
|
||||
"unsubmitted": true,
|
||||
"showLarger": false,
|
||||
},{
|
||||
"segment": [
|
||||
169,
|
||||
3432.255
|
||||
],
|
||||
"category": "chooseACategory",
|
||||
"unsubmitted": true,
|
||||
"showLarger": false,
|
||||
},{
|
||||
"segment": [
|
||||
169,
|
||||
3412.413
|
||||
],
|
||||
"category": "chooseACategory",
|
||||
"unsubmitted": true,
|
||||
"showLarger": false,
|
||||
},{
|
||||
"segment": [
|
||||
1594.92,
|
||||
1674.286
|
||||
],
|
||||
"category": "sponsor",
|
||||
"unsubmitted": false,
|
||||
"showLarger": false,
|
||||
}
|
||||
] as unknown as PreviewBarSegment[]);
|
||||
|
||||
expect(groups).toStrictEqual([
|
||||
{
|
||||
"segment": [
|
||||
0,
|
||||
160
|
||||
],
|
||||
"originalDuration": 0
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
160,
|
||||
169
|
||||
],
|
||||
"originalDuration": 2637.323
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
169,
|
||||
1594.92
|
||||
],
|
||||
"originalDuration": 3243.413
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
1594.92,
|
||||
1674.286
|
||||
],
|
||||
"originalDuration": 79.36599999999999
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
1674.286,
|
||||
3412.413
|
||||
],
|
||||
"originalDuration": 3243.413
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
3412.413,
|
||||
3432.255
|
||||
],
|
||||
"originalDuration": 3263.255
|
||||
},
|
||||
{
|
||||
"segment": [
|
||||
3432.255,
|
||||
3615.161
|
||||
],
|
||||
"originalDuration": 0
|
||||
}
|
||||
]);
|
||||
});
|
||||
})
|
||||
@@ -31,7 +31,8 @@ module.exports = env => ({
|
||||
content: path.join(__dirname, srcDir + 'content.ts'),
|
||||
options: path.join(__dirname, srcDir + 'options.ts'),
|
||||
help: path.join(__dirname, srcDir + 'help.ts'),
|
||||
permissions: path.join(__dirname, srcDir + 'permissions.ts')
|
||||
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
|
||||
upsell: path.join(__dirname, srcDir + 'upsell.ts')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, '../dist/js'),
|
||||
|
||||
Reference in New Issue
Block a user