Merge pull request #1001 from ajayyy/chapters

Chapters
This commit is contained in:
Ajay Ramachandran
2022-09-01 15:24:35 -04:00
committed by GitHub
57 changed files with 5448 additions and 560 deletions

View File

@@ -2,7 +2,7 @@
"serverAddress": "https://sponsor.ajay.app", "serverAddress": "https://sponsor.ajay.app",
"testingServerAddress": "https://sponsor.ajay.app/test", "testingServerAddress": "https://sponsor.ajay.app/test",
"serverAddressComment": "This specifies the default SponsorBlock server to connect to", "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": { "categorySupport": {
"sponsor": ["skip", "mute", "full"], "sponsor": ["skip", "mute", "full"],
"selfpromo": ["skip", "mute", "full"], "selfpromo": ["skip", "mute", "full"],
@@ -13,7 +13,8 @@
"preview": ["skip", "mute"], "preview": ["skip", "mute"],
"filler": ["skip", "mute"], "filler": ["skip", "mute"],
"music_offtopic": ["skip"], "music_offtopic": ["skip"],
"poi_highlight": ["poi"] "poi_highlight": ["poi"],
"chapter": ["chapter"]
}, },
"wikiLinks": { "wikiLinks": {
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor", "sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
@@ -27,6 +28,7 @@
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section", "music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight", "poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines", "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"
} }
} }

View File

@@ -18,6 +18,7 @@
], ],
"css": [ "css": [
"content.css", "content.css",
"shared.css",
"./libs/Source+Sans+Pro.css", "./libs/Source+Sans+Pro.css",
"popup.css" "popup.css"
] ]
@@ -48,9 +49,11 @@
"icons/beep.ogg", "icons/beep.ogg",
"icons/pause.svg", "icons/pause.svg",
"icons/stop.svg", "icons/stop.svg",
"icons/skip.svg",
"icons/heart.svg", "icons/heart.svg",
"icons/visible.svg", "icons/visible.svg",
"icons/not_visible.svg", "icons/not_visible.svg",
"icons/sort.svg",
"icons/money.svg", "icons/money.svg",
"icons/segway.png", "icons/segway.png",
"icons/close-smaller.svg", "icons/close-smaller.svg",
@@ -61,6 +64,8 @@
"icons/bolt.svg", "icons/bolt.svg",
"icons/stopwatch.svg", "icons/stopwatch.svg",
"icons/music-note.svg", "icons/music-note.svg",
"icons/import.svg",
"icons/export.svg",
"icons/PlayerInfoIconSponsorBlocker.svg", "icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg", "icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html", "popup.html",

1089
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"fork-ts-checker-webpack-plugin": "^7.2.13", "fork-ts-checker-webpack-plugin": "^7.2.13",
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"schema-utils": "^4.0.0", "schema-utils": "^4.0.0",
"selenium-webdriver": "^4.3.1", "selenium-webdriver": "^4.3.1",

View File

@@ -25,6 +25,16 @@
"Segments": { "Segments": {
"message": "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": { "upvoteButtonInfo": {
"message": "Upvote this submission" "message": "Upvote this submission"
}, },
@@ -289,6 +299,14 @@
"message": "Submit segments", "message": "Submit segments",
"description": "Keybind label" "description": "Keybind label"
}, },
"nextChapterKeybind": {
"message": "Next chapter",
"description": "Keybind label"
},
"previousChapterKeybind": {
"message": "Previous chapter",
"description": "Keybind label"
},
"keybindDescription": { "keybindDescription": {
"message": "Select a key by typing it and choose any modifier keys you wish to use." "message": "Select a key by typing it and choose any modifier keys you wish to use."
}, },
@@ -545,6 +563,10 @@
"message": "to", "message": "to",
"description": "Used between segments. Example: 1:20 to 1:30" "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": { "generic_guideline1": {
"message": "Include segue transitions" "message": "Include segue transitions"
}, },
@@ -696,6 +718,21 @@
"category_poi_highlight_guideline3": { "category_poi_highlight_guideline3": {
"message": "Can skip to the title or thumbnail" "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": { "category_livestream_messages": {
"message": "Livestream: Donation/Message Readings" "message": "Livestream: Donation/Message Readings"
}, },
@@ -726,6 +763,9 @@
"showOverlay_full": { "showOverlay_full": {
"message": "Show Label" "message": "Show Label"
}, },
"showOverlay_chapter": {
"message": "Show Chapters"
},
"autoSkipOnMusicVideos": { "autoSkipOnMusicVideos": {
"message": "Auto skip all segments when there is a non-music segment" "message": "Auto skip all segments when there is a non-music segment"
}, },
@@ -781,6 +821,10 @@
"bracketEnd": { "bracketEnd": {
"message": "(End)" "message": "(End)"
}, },
"End": {
"message": "End",
"description": "Button that skips to the end of a segment"
},
"hiddenDueToDownvote": { "hiddenDueToDownvote": {
"message": "hidden: downvote" "message": "hidden: downvote"
}, },
@@ -821,6 +865,13 @@
"downvoteDescription": { "downvoteDescription": {
"message": "Incorrect/Wrong Timing" "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": { "incorrectCategory": {
"message": "Change Category" "message": "Change Category"
}, },
@@ -856,6 +907,9 @@
"categoryPillTitleText": { "categoryPillTitleText": {
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate" "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": { "experiementOptOut": {
"message": "Opt-out of all future experiments", "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." "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": { "confirmResetToDefault": {
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone." "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"
} }
} }

View File

@@ -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 { .hidden {
display: none; display: none;
} }
@@ -12,7 +21,7 @@
height: 100%; height: 100%;
transform: scaleY(0.6) translateY(-30%) translateY(1.5px); 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); transition: transform .1s cubic-bezier(0,0,0.2,1);
} }
@@ -45,23 +54,48 @@
transform: translateY(-1em) !important; transform: translateY(-1em) !important;
} }
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
transform: translateY(-2em) !important;
}
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible { .ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
transform: translateY(-2em) !important; 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 { #movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
transform: translateY(1em) !important; 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 { .ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
transform: translateY(0.5em) !important; 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 { .ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
display: block !important; display: block !important;
transform: translateY(1em) !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 { .popup {
@@ -88,6 +122,16 @@
vertical-align: top; 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 { .autoHiding {
overflow: visible !important; overflow: visible !important;
} }
@@ -113,8 +157,8 @@
.sponsorSkipObject { .sponsorSkipObject {
font-family: Roboto, Arial, Helvetica, sans-serif; font-family: Roboto, Arial, Helvetica, sans-serif;
margin-left: 2px; margin-left: var(--skip-notice-margin);
margin-right: 2px; margin-right: var(--skip-notice-margin);
} }
.sponsorSkipLogo { .sponsorSkipLogo {
@@ -145,7 +189,7 @@
position: absolute; position: absolute;
right: 5px; right: 5px;
bottom: 100px; bottom: 100px;
right: 10px; right: var(--skip-notice-right);
} }
.sponsorSkipNoticeParent { .sponsorSkipNoticeParent {
@@ -525,7 +569,7 @@ input::-webkit-inner-spin-button {
margin-bottom: 5px; margin-bottom: 5px;
background-color: rgba(28, 28, 28, 0.9); 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; color: white;
border-width: 3px; border-width: 3px;
padding: 3px; padding: 3px;
@@ -536,6 +580,45 @@ input::-webkit-inner-spin-button {
color: white; 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 { .helpButton {
height: 25px; height: 25px;
cursor: pointer; cursor: pointer;
@@ -623,6 +706,11 @@ input::-webkit-inner-spin-button {
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent; border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
} }
.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
left: 50%;
right: 50%;
}
.sponsorBlockLockedColor { .sponsorBlockLockedColor {
color: #ffc83d; color: #ffc83d;
} }

106
public/icons/export.svg Normal file
View 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
View 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
View 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
View 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

View File

@@ -123,6 +123,14 @@ html, body {
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1; 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 { .option-group > div:last-child, .option-group > #keybind-dialog {
border-bottom: inherit; border-bottom: inherit;
} }
@@ -309,6 +317,10 @@ input[type='number'] {
color: grey; color: grey;
} }
tr.disabled {
opacity: 0.3;
}
#options { #options {
height: 100vh; height: 100vh;
flex-basis: 80%; flex-basis: 80%;
@@ -670,4 +682,9 @@ svg {
#options > div { #options > div {
max-width: 100%; max-width: 100%;
} }
}
.upsellButton {
cursor: pointer;
vertical-align: middle;
} }

View File

@@ -66,18 +66,6 @@
</div> </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 data-type="toggle" data-sync="muteSegments">
<div class="switch-container"> <div class="switch-container">
<label class="switch"> <label class="switch">
@@ -314,6 +302,18 @@
</div> </div>
</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>
<div id="keybinds" class="option-group hidden"> <div id="keybinds" class="option-group hidden">
@@ -333,6 +333,16 @@
<div class="inline"></div> <div class="inline"></div>
</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>
<div id="import" class="option-group hidden"> <div id="import" class="option-group hidden">

View File

@@ -152,22 +152,46 @@
margin: 8px; margin: 8px;
} }
/*
* Refresh segments button
*/
#refreshSegmentsButton { #refreshSegmentsButton {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 5px;
margin: 5px auto;
}
#issueReporterImportExport {
position: relative;
}
#refreshSegmentsButton, #issueReporterImportExport button {
background: transparent; background: transparent;
border-radius: 50%; border-radius: 50%;
margin: 5px auto;
border: none; border: none;
padding: 5px;
} }
#refreshSegmentsButton:hover {
#refreshSegmentsButton:hover, #issueReporterImportExport button:hover {
background-color: var(--sb-grey-bg-color); 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 * <details> wrapper around each segment
*/ */
@@ -199,6 +223,15 @@
.segmentSummary > div { .segmentSummary > div {
text-align: left; text-align: left;
} }
.segmentActive {
color: #bdfffb;
}
.segmentPassed {
color: #adadad;
}
/* /*
* Category dot in segment * Category dot in segment
*/ */
@@ -560,3 +593,45 @@
margin-bottom: 20px; margin-bottom: 20px;
padding: 5px; 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);
}

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet"> <link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet"> <link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
<link id="sponsorBlockStyleSheet" href="shared.css" rel="stylesheet">
</head> </head>
<body id="sponsorBlockPopupBody" style="visibility: hidden"> <body id="sponsorBlockPopupBody" style="visibility: hidden">
@@ -34,7 +35,33 @@
</button> </button>
<!-- Video Segments --> <!-- Video Segments -->
<div id="issueReporterContainer"> <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="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>
</div> </div>

View 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
View 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
View 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
View 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;
}

View File

@@ -14,6 +14,8 @@ const utils = new Utils({
unregisterFirefoxContentScript unregisterFirefoxContentScript
}); });
const popupPort: Record<string, chrome.runtime.Port> = {};
// Used only on Firefox, which does not support non persistent background pages. // Used only on Firefox, which does not support non persistent background pages.
const contentScriptRegistrations = {}; const contentScriptRegistrations = {};
@@ -53,7 +55,7 @@ if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
Config.configSyncListeners.push(onNavigationApiAvailableChange); Config.configSyncListeners.push(onNavigationApiAvailableChange);
} }
chrome.runtime.onMessage.addListener(function (request, _, callback) { chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) { switch(request.message) {
case "openConfig": case "openConfig":
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))}); chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
@@ -100,9 +102,25 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
}); });
return true; 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;
});
}
});
//add help page on install //add help page on install
chrome.runtime.onInstalled.addListener(function () { chrome.runtime.onInstalled.addListener(function () {
// This let's the config sync to run fully before checking. // This let's the config sync to run fully before checking.
@@ -116,7 +134,7 @@ chrome.runtime.onInstalled.addListener(function () {
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")}); chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
//generate a userID //generate a userID
const newUserID = utils.generateUserID(); const newUserID = GenericUtils.generateUserID();
//save this UUID //save this UUID
Config.config.userID = newUserID; Config.config.userID = newUserID;
@@ -165,7 +183,7 @@ async function submitVote(type: number, UUID: string, category: string) {
if (userID == undefined || userID === "undefined") { if (userID == undefined || userID === "undefined") {
//generate one //generate one
userID = utils.generateUserID(); userID = GenericUtils.generateUserID();
Config.config.userID = userID; Config.config.userID = userID;
} }

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

View File

@@ -11,6 +11,7 @@ export interface NoticeProps {
noticeTitle: string, noticeTitle: string,
maxCountdownTime?: () => number, maxCountdownTime?: () => number,
dontPauseCountdown?: boolean,
amountOfPreviousNotices?: number, amountOfPreviousNotices?: number,
showInSecondSlot?: boolean, showInSecondSlot?: boolean,
timed?: boolean, timed?: boolean,
@@ -25,6 +26,8 @@ export interface NoticeProps {
smaller?: boolean, smaller?: boolean,
limitWidth?: boolean, limitWidth?: boolean,
extraClass?: string, extraClass?: string,
hideLogo?: boolean,
hideRightInfo?: boolean,
// Callback for when this is closed // Callback for when this is closed
closeListener: () => void, closeListener: () => void,
@@ -117,13 +120,15 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
{/* Left column */} {/* Left column */}
<td className="noticeLeftIcon"> <td className="noticeLeftIcon">
{/* Logo */} {/* Logo */}
<img id={"sponsorSkipLogo" + this.idSuffix} {!this.props.hideLogo &&
className="sponsorSkipLogo sponsorSkipObject" <img id={"sponsorSkipLogo" + this.idSuffix}
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}> className="sponsorSkipLogo sponsorSkipObject"
</img> src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
}
<span id={"sponsorSkipMessage" + this.idSuffix} <span id={"sponsorSkipMessage" + this.idSuffix}
style={{float: "left"}} style={{float: "left", marginRight: this.props.hideLogo ? "0px" : null}}
className="sponsorSkipMessage sponsorSkipObject"> className="sponsorSkipMessage sponsorSkipObject">
{this.props.noticeTitle} {this.props.noticeTitle}
@@ -135,28 +140,30 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
{this.props.firstRow} {this.props.firstRow}
{/* Right column */} {/* Right column */}
<td className="sponsorSkipNoticeRightSection" {!this.props.hideRightInfo &&
style={{top: "9.32px"}}> <td className="sponsorSkipNoticeRightSection"
style={{top: "9.32px"}}>
{/* Time left */}
{this.props.timed ? (
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
onClick={() => this.toggleManualPause()}
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
{this.getCountdownElements()}
</span>
) : ""}
{/* Time left */}
{this.props.timed ? (
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
onClick={() => this.toggleManualPause()}
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
{this.getCountdownElements()} {/* Close button */}
<img src={chrome.extension.getURL("icons/close.png")}
</span> className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
) : ""} + (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
onClick={() => this.close()}>
</img>
{/* Close button */} </td>
<img src={chrome.extension.getURL("icons/close.png")} }
className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
+ (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
onClick={() => this.close()}>
</img>
</td>
</tr> </tr>
{this.props.children} {this.props.children}
@@ -289,7 +296,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
} }
pauseCountdown(): void { pauseCountdown(): void {
if (!this.props.timed) return; if (!this.props.timed || this.props.dontPauseCountdown) return;
//remove setInterval //remove setInterval
if (this.countdownInterval) clearInterval(this.countdownInterval); if (this.countdownInterval) clearInterval(this.countdownInterval);

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

View File

@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import PencilSvg from "../svg-icons/pencil_svg"; import PencilSvg from "../svg-icons/pencil_svg";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils"; import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { GenericUtils } from "../utils/genericUtils";
enum SkipButtonState { enum SkipButtonState {
Undo, // Unskip Undo, // Unskip
@@ -540,7 +541,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
const sponsorVideoID = this.props.contentContainer().sponsorVideoID; const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
const sponsorTimesSubmitting : SponsorTime = { const sponsorTimesSubmitting : SponsorTime = {
segment: this.segments[index].segment, segment: this.segments[index].segment,
UUID: utils.generateUserID() as SegmentUUID, UUID: GenericUtils.generateUserID() as SegmentUUID,
category: this.segments[index].category, category: this.segments[index].category,
actionType: this.segments[index].actionType, actionType: this.segments[index].actionType,
source: SponsorSourceType.Local source: SponsorSourceType.Local

View File

@@ -1,10 +1,12 @@
import * as React from "react"; import * as React from "react";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Config from "../config"; import Config from "../config";
import { ActionType, Category, ContentContainer, SponsorTime } from "../types"; import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
import Utils from "../utils"; import Utils from "../utils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { RectangleTooltip } from "../render/RectangleTooltip"; import { RectangleTooltip } from "../render/RectangleTooltip";
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
import { GenericUtils } from "../utils/genericUtils";
const utils = new Utils(); const utils = new Utils();
@@ -25,16 +27,23 @@ export interface SponsorTimeEditState {
editing: boolean; editing: boolean;
sponsorTimeEdits: [string, string]; sponsorTimeEdits: [string, string];
selectedCategory: Category; selectedCategory: Category;
description: string;
suggestedNames: SelectorOption[];
chapterNameSelectorOpen: boolean;
} }
const DEFAULT_CATEGORY = "chooseACategory"; 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> { class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> {
idSuffix: string; idSuffix: string;
categoryOptionRef: React.RefObject<HTMLSelectElement>; categoryOptionRef: React.RefObject<HTMLSelectElement>;
actionTypeOptionRef: React.RefObject<HTMLSelectElement>; actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
descriptionOptionRef: React.RefObject<HTMLInputElement>;
configUpdateListener: () => void; configUpdateListener: () => void;
@@ -42,26 +51,35 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// Used when selecting POI or Full // Used when selecting POI or Full
timesBeforeChanging: number[] = []; timesBeforeChanging: number[] = [];
fullVideoWarningShown = false; fullVideoWarningShown = false;
categoryNameWarningShown = false;
// For description auto-complete
fetchingSuggestions: boolean;
constructor(props: SponsorTimeEditProps) { constructor(props: SponsorTimeEditProps) {
super(props); super(props);
this.categoryOptionRef = React.createRef(); this.categoryOptionRef = React.createRef();
this.actionTypeOptionRef = React.createRef(); this.actionTypeOptionRef = React.createRef();
this.descriptionOptionRef = React.createRef();
this.idSuffix = this.props.idSuffix; this.idSuffix = this.props.idSuffix;
this.previousSkipType = ActionType.Skip; this.previousSkipType = ActionType.Skip;
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
this.state = { this.state = {
editing: false, editing: false,
sponsorTimeEdits: [null, null], sponsorTimeEdits: [null, null],
selectedCategory: DEFAULT_CATEGORY as Category selectedCategory: DEFAULT_CATEGORY as Category,
description: sponsorTime.description || "",
suggestedNames: [],
chapterNameSelectorOpen: false
}; };
} }
componentDidMount(): void { componentDidMount(): void {
// Prevent inputs from triggering key events // 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(); event.stopPropagation();
}); });
@@ -87,6 +105,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
render(): React.ReactElement { render(): React.ReactElement {
this.checkToShowFullVideoWarning(); this.checkToShowFullVideoWarning();
this.checkToShowChapterWarning();
const style: React.CSSProperties = { const style: React.CSSProperties = {
textAlign: "center" textAlign: "center"
@@ -118,8 +137,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
type="text" type="text"
style={{color: "inherit", backgroundColor: "inherit"}} style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[0]} value={this.state.sponsorTimeEdits[0]}
onChange={(e) => {this.handleOnChange(0, e, sponsorTime, e.target.value)}} onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}> onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
</input> </input>
{sponsorTime.actionType !== ActionType.Poi ? ( {sponsorTime.actionType !== ActionType.Poi ? (
@@ -133,8 +152,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
type="text" type="text"
style={{color: "inherit", backgroundColor: "inherit"}} style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[1]} value={this.state.sponsorTimeEdits[1]}
onChange={(e) => {this.handleOnChange(1, e, sponsorTime, e.target.value)}} onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
onWheel={(e) => {this.changeTimesWhenScrolling(1, e, sponsorTime)}}> onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
</input> </input>
<span id={"nowButton1" + this.idSuffix} <span id={"nowButton1" + this.idSuffix}
@@ -159,15 +178,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style={timeDisplayStyle} style={timeDisplayStyle}
className="sponsorTimeDisplay" className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}> onClick={this.toggleEditTime.bind(this)}>
{utils.getFormattedTime(segment[0], true) + {GenericUtils.getFormattedTime(segment[0], true) +
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi) ((!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> </div>
); );
} }
return ( return (
<div style={style}> <div id={"sponsorTimeEditContainer" + this.idSuffix} style={style}>
{timeDisplay} {timeDisplay}
@@ -178,7 +197,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
defaultValue={sponsorTime.category} defaultValue={sponsorTime.category}
ref={this.categoryOptionRef} ref={this.categoryOptionRef}
style={{color: "inherit", backgroundColor: "inherit"}} style={{color: "inherit", backgroundColor: "inherit"}}
onChange={this.categorySelectionChange.bind(this)}> onChange={(event) => this.categorySelectionChange(event)}>
{this.getCategoryOptions()} {this.getCategoryOptions()}
</select> </select>
@@ -209,6 +228,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</div> </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/> <br/>
{/* Editing Tools */} {/* Editing Tools */}
@@ -223,7 +263,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<span id={"sponsorTimePreviewButton" + this.idSuffix} <span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton" className="sponsorTimeEditButton"
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}> 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> </span>
): ""} ): ""}
@@ -250,16 +291,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const sponsorTimeEdits = this.state.sponsorTimeEdits; const sponsorTimeEdits = this.state.sponsorTimeEdits;
// check if change is small engough to show tooltip // check if change is small engough to show tooltip
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]); const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
const after = utils.getFormattedTimeToSeconds(targetValue); const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
const difference = Math.abs(before - after); 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; sponsorTimeEdits[index] = targetValue;
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue; if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
this.setState({sponsorTimeEdits}); this.setState({sponsorTimeEdits}, () => this.saveEditTimes());
this.saveEditTimes();
} }
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void { 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; 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 (timeAsNumber !== null && e.deltaY != 0) {
if (e.deltaY < 0) { if (e.deltaY < 0) {
timeAsNumber += step; timeAsNumber += step;
@@ -284,7 +324,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
} else { } else {
timeAsNumber = 0; timeAsNumber = 0;
} }
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0]; if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
this.setState({sponsorTimeEdits}); this.setState({sponsorTimeEdits});
@@ -294,26 +335,29 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
showScrollToEditToolTip(): void { showScrollToEditToolTip(): void {
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) { 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); const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
if (element) { if (element) {
new RectangleTooltip({ const htmlId = `sponsorRectangleTooltip${id + this.idSuffix}`;
text, if (!document.getElementById(htmlId)) {
referenceNode: element.parentElement, new RectangleTooltip({
prependElement: element, text,
timeout: 15, referenceNode: element.parentElement,
bottomOffset: 0 + "px", prependElement: element,
leftOffset: -318 + "px", timeout: 15,
backgroundColor: "rgba(28, 28, 28, 1.0)", bottomOffset: 0 + "px",
htmlId: "sponsorTimesContainer" + this.idSuffix, leftOffset: -318 + "px",
buttonFunction, backgroundColor: "rgba(28, 28, 28, 1.0)",
fontSize: "14px", htmlId,
maxHeight: "200px" buttonFunction,
}); fontSize: "14px",
maxHeight: "200px"
});
}
return true; return true;
} else { } else {
@@ -328,12 +372,25 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (videoPercentage > 0.6 && !this.fullVideoWarningShown if (videoPercentage > 0.6 && !this.fullVideoWarningShown
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) { && (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; 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[] { getCategoryOptions(): React.ReactElement[] {
const elements = [( const elements = [(
<option value={DEFAULT_CATEGORY} <option value={DEFAULT_CATEGORY}
@@ -343,6 +400,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
)]; )];
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) { 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( elements.push(
<option value={category} <option value={category}
key={category} key={category}
@@ -363,7 +425,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const chosenCategory = event.target.value as Category; const chosenCategory = event.target.value as Category;
// See if show more categories was pressed // 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; event.target.value = DEFAULT_CATEGORY;
// Alert that they have to enable this category first // Alert that they have to enable this category first
@@ -464,7 +526,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.setState({ this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime) sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
}, this.saveEditTimes); }, () => this.saveEditTimes());
} }
toggleEditTime(): void { 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 */ /** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] { getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
return [utils.getFormattedTime(sponsorTime.segment[0], true), return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
utils.getFormattedTime(sponsorTime.segment[1], true)]; GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
} }
saveEditTimes(): void { saveEditTimes(): void {
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting; const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
if (this.state.editing) { if (this.state.editing) {
const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]); const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]); const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
// Change segment time only if the format was correct // Change segment time only if the format was correct
if (startTime !== null && endTime !== null) { if (startTime !== null && endTime !== null) {
@@ -507,8 +569,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const category = this.categoryOptionRef.current.value as Category const category = this.categoryOptionRef.current.value as Category
sponsorTimesSubmitting[this.props.index].category = category; sponsorTimesSubmitting[this.props.index].category = category;
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType; const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);
sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType); 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.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments"); Config.forceSyncUpdate("unsubmittedSegments");
@@ -530,19 +595,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
previewTime(ctrlPressed = false, shiftPressed = false): void { previewTime(ctrlPressed = false, shiftPressed = false): void {
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting; const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
const index = this.props.index; 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; let seekTime = 2;
if (ctrlPressed) seekTime = 0.5; if (ctrlPressed) seekTime = 0.5;
if (shiftPressed) seekTime = 0.25; 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 { 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 { configUpdate(): void {
this.forceUpdate(); this.forceUpdate();
} }

View File

@@ -73,12 +73,19 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
} }
render(): React.ReactElement { 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 ( return (
<NoticeComponent noticeTitle={this.state.noticeTitle} <NoticeComponent noticeTitle={this.state.noticeTitle}
idSuffix={this.state.idSuffix} idSuffix={this.state.idSuffix}
ref={this.noticeRef} ref={this.noticeRef}
closeListener={this.cancel.bind(this)} closeListener={this.cancel.bind(this)}
zIndex={5000}> zIndex={5000}
firstColumn={sortButton}>
{/* Text Boxes */} {/* Text Boxes */}
{this.getMessageBoxes()} {this.getMessageBoxes()}
@@ -198,6 +205,16 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
this.cancel(); 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 { categoryChangeListener(index: number, category: Category): void {
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth; const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines if (category !== "chooseACategory" && Config.config.showCategoryGuidelines

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../../config.json";
import { Category } from "../types"; import { Category } from "../../types";
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent"; import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
export interface CategoryChooserProps { export interface CategoryChooserProps {

View File

@@ -1,10 +1,13 @@
import * as React from "react"; import * as React from "react";
import Config from "../config" import Config from "../../config"
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../../config.json";
import { Category, CategorySkipOption } from "../types"; 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 { export interface CategorySkipOptionsProps {
category: Category; category: Category;
@@ -15,6 +18,7 @@ export interface CategorySkipOptionsProps {
export interface CategorySkipOptionsState { export interface CategorySkipOptionsState {
color: string; color: string;
previewColor: string; previewColor: string;
hideChapter: boolean;
} }
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> { class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
@@ -27,7 +31,14 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
this.state = { this.state = {
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color, color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + 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 { 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 ( return (
<> <>
<tr id={this.props.category + "OptionsRow"} <tr id={this.props.category + "OptionsRow"}
className="categoryTableElement"> className={`categoryTableElement${extraClasses}`} >
<td id={this.props.category + "OptionName"} <td id={this.props.category + "OptionName"}
className="categoryTableLabel"> className="categoryTableLabel">
{disabled &&
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
}
{chrome.i18n.getMessage("category_" + this.props.category)} {chrome.i18n.getMessage("category_" + this.props.category)}
</td> </td>
@@ -65,21 +89,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
<select <select
className="optionsSelector" className="optionsSelector"
defaultValue={defaultOption} defaultValue={defaultOption}
disabled={disabled}
onChange={this.skipOptionSelected.bind(this)}> onChange={this.skipOptionSelected.bind(this)}>
{this.getCategorySkipOptions()} {this.getCategorySkipOptions()}
</select> </select>
</td> </td>
<td id={this.props.category + "ColorOption"} {this.props.category !== "chapter" &&
className="colorOption"> <td id={this.props.category + "ColorOption"}
<input className="colorOption">
className="categoryColorTextBox option-text-box" <input
type="color" className="categoryColorTextBox option-text-box"
onChange={(event) => this.setColorState(event, false)} type="color"
value={this.state.color} /> disabled={disabled}
</td> 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"} <td id={this.props.category + "PreviewColorOption"}
className="previewColorOption"> className="previewColorOption">
<input <input
@@ -93,7 +121,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</tr> </tr>
<tr id={this.props.category + "DescriptionRow"} <tr id={this.props.category + "DescriptionRow"}
className="small-description categoryTableDescription"> className={`small-description categoryTableDescription${extraClasses}`}>
<td <td
colSpan={2}> colSpan={2}>
{chrome.i18n.getMessage("category_" + this.props.category + "_description")} {chrome.i18n.getMessage("category_" + this.props.category + "_description")}
@@ -103,6 +131,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</a> </a>
</td> </td>
</tr> </tr>
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
</> </>
); );
@@ -147,7 +177,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
const elements: JSX.Element[] = []; const elements: JSX.Element[] = [];
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"]; 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) { for (const optionName of optionNames) {
elements.push( elements.push(
@@ -184,6 +215,42 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
Config.config.barTypes = Config.config.barTypes; Config.config.barTypes = Config.config.barTypes;
}, 50); }, 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; export default CategorySkipOptionsComponent;

View File

@@ -1,9 +1,9 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import Config from "../config"; import Config from "../../config";
import { Keybind } from "../types"; import { Keybind } from "../../types";
import KeybindDialogComponent from "./KeybindDialogComponent"; import KeybindDialogComponent from "./KeybindDialogComponent";
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils"; import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
export interface KeybindProps { export interface KeybindProps {
option: string; option: string;

View File

@@ -1,8 +1,8 @@
import * as React from "react"; import * as React from "react";
import { ChangeEvent } from "react"; import { ChangeEvent } from "react";
import Config from "../config"; import Config from "../../config";
import { Keybind } from "../types"; import { Keybind } from "../../types";
import { keybindEquals, formatKey } from "../utils/configUtils"; import { keybindEquals, formatKey } from "../../utils/configUtils";
export interface KeybindDialogProps { export interface KeybindDialogProps {
option: string; option: string;

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

View File

@@ -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 { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { keybindEquals } from "./utils/configUtils"; import { keybindEquals } from "./utils/configUtils";
export interface Permission {
canSubmit: boolean;
}
interface SBConfig { interface SBConfig {
userID: string, userID: string,
isVip: boolean, isVip: boolean,
permissions: Record<Category, Permission>,
/* Contains unsubmitted segments that the user has created. */ /* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>, unsubmittedSegments: Record<string, SponsorTime[]>,
defaultCategory: Category, defaultCategory: Category,
renderSegmentsAsChapters: boolean,
whitelistedChannels: string[], whitelistedChannels: string[],
forceChannelCheck: boolean, forceChannelCheck: boolean,
minutesSaved: number, minutesSaved: number,
@@ -44,6 +50,7 @@ interface SBConfig {
allowExpirements: boolean, allowExpirements: boolean,
showDonationLink: boolean, showDonationLink: boolean,
showPopupDonationCount: number, showPopupDonationCount: number,
showUpsells: boolean,
donateClicked: number, donateClicked: number,
autoHideInfoButton: boolean, autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean, autoSkipOnMusicVideos: boolean,
@@ -56,6 +63,7 @@ interface SBConfig {
categoryPillUpdate: boolean, categoryPillUpdate: boolean,
darkMode: boolean, darkMode: boolean,
showCategoryGuidelines: boolean, showCategoryGuidelines: boolean,
chaptersAvailable: boolean,
// Used to cache calculated text color info // Used to cache calculated text color info
categoryPillColors: { categoryPillColors: {
@@ -68,10 +76,19 @@ interface SBConfig {
skipKeybind: Keybind, skipKeybind: Keybind,
startSponsorKeybind: Keybind, startSponsorKeybind: Keybind,
submitKeybind: Keybind, submitKeybind: Keybind,
nextChapterKeybind: Keybind,
previousChapterKeybind: Keybind,
// What categories should be skipped // What categories should be skipped
categorySelections: CategorySelection[], categorySelections: CategorySelection[],
payments: {
licenseKey: string,
lastCheck: number,
freeAccess: boolean,
chaptersAllowed: boolean
}
// Preview bar // Preview bar
barTypes: { barTypes: {
"preview-chooseACategory": PreviewBarOption, "preview-chooseACategory": PreviewBarOption,
@@ -128,8 +145,10 @@ const Config: SBObject = {
syncDefaults: { syncDefaults: {
userID: null, userID: null,
isVip: false, isVip: false,
permissions: {},
unsubmittedSegments: {}, unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category, defaultCategory: "chooseACategory" as Category,
renderSegmentsAsChapters: false,
whitelistedChannels: [], whitelistedChannels: [],
forceChannelCheck: false, forceChannelCheck: false,
minutesSaved: 0, minutesSaved: 0,
@@ -165,6 +184,7 @@ const Config: SBObject = {
allowExpirements: true, allowExpirements: true,
showDonationLink: true, showDonationLink: true,
showPopupDonationCount: 0, showPopupDonationCount: 0,
showUpsells: true,
donateClicked: 0, donateClicked: 0,
autoHideInfoButton: true, autoHideInfoButton: true,
autoSkipOnMusicVideos: false, autoSkipOnMusicVideos: false,
@@ -172,6 +192,7 @@ const Config: SBObject = {
categoryPillUpdate: false, categoryPillUpdate: false,
darkMode: true, darkMode: true,
showCategoryGuidelines: true, showCategoryGuidelines: true,
chaptersAvailable: true,
categoryPillColors: {}, categoryPillColors: {},
@@ -185,6 +206,8 @@ const Config: SBObject = {
skipKeybind: {key: "Enter"}, skipKeybind: {key: "Enter"},
startSponsorKeybind: {key: ";"}, startSponsorKeybind: {key: ";"},
submitKeybind: {key: "'"}, submitKeybind: {key: "'"},
nextChapterKeybind: {key: "]"},
previousChapterKeybind: {key: "["},
categorySelections: [{ categorySelections: [{
name: "sponsor" as Category, name: "sponsor" as Category,
@@ -197,6 +220,13 @@ const Config: SBObject = {
option: CategorySkipOption.ShowOverlay option: CategorySkipOption.ShowOverlay
}], }],
payments: {
licenseKey: null,
lastCheck: 0,
freeAccess: false,
chaptersAllowed: false
},
colorPalette: { colorPalette: {
red: "#780303", red: "#780303",
white: "#ffffff", white: "#ffffff",
@@ -516,6 +546,8 @@ function migrateOldSyncFormats(config: SBConfig) {
} }
async function setupConfig() { async function setupConfig() {
if (typeof(chrome) === "undefined") return;
await fetchConfig(); await fetchConfig();
addDefaults(); addDefaults();
const config = configProxy(); const config = configProxy();

View File

@@ -12,12 +12,14 @@ import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse, VoteResponse } from "./messageTypes"; import { Message, MessageResponse, VoteResponse } from "./messageTypes";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser"; import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils"; import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
import { isSafari, keybindEquals } from "./utils/configUtils"; import { isSafari, keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill"; import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils"; import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils"; import { GenericUtils } from "./utils/genericUtils";
import { logDebug } from "./utils/logger"; import { logDebug } from "./utils/logger";
import { importTimes } from "./utils/exporter";
import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings"; import { openWarningDialog } from "./utils/warnings";
// Hack to get the CSS loaded on permission-based sites (Invidious) // 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 //was sponsor data found when doing SponsorsLookup
let sponsorDataFound = false; let sponsorDataFound = false;
//the actual sponsorTimes if loaded and UUIDs associated with them //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 //what video id are these sponsors for
let sponsorVideoID: VideoID = null; let sponsorVideoID: VideoID = null;
// List of open skip notices // List of open skip notices
@@ -138,7 +141,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({
previewTime, previewTime,
videoInfo, videoInfo,
getRealCurrentTime: getRealCurrentTime, getRealCurrentTime: getRealCurrentTime,
lockedCategories lockedCategories,
channelIDInfo
}); });
// value determining when to count segment as skipped and send telemetry to server (percent based) // 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, found: sponsorDataFound,
status: lastResponseStatus, status: lastResponseStatus,
sponsorTimes: sponsorTimes, sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube onMobileYouTube
}); });
@@ -212,10 +217,17 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
found: sponsorDataFound, found: sponsorDataFound,
status: lastResponseStatus, status: lastResponseStatus,
sponsorTimes: sponsorTimes, sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube onMobileYouTube
})); }));
return true; 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": case "submitVote":
vote(request.type, request.UUID).then((response) => sendResponse(response)); vote(request.type, request.UUID).then((response) => sendResponse(response));
return true; return true;
@@ -230,6 +242,31 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
case "copyToClipboard": case "copyToClipboard":
navigator.clipboard.writeText(request.text); navigator.clipboard.writeText(request.text);
break; 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": case "keydown":
document.dispatchEvent(new KeyboardEvent('keydown', { document.dispatchEvent(new KeyboardEvent('keydown', {
key: request.key, key: request.key,
@@ -249,8 +286,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
/** /**
* Called when the config is updated * Called when the config is updated
*
* @param {String} changes
*/ */
function contentConfigUpdateListener(changes: StorageChangesObject) { function contentConfigUpdateListener(changes: StorageChangesObject) {
for (const key in changes) { for (const key in changes) {
@@ -276,8 +311,8 @@ function resetValues() {
lastCheckVideoTime = -1; lastCheckVideoTime = -1;
retryCount = 0; retryCount = 0;
//reset sponsor times sponsorTimes = [];
sponsorTimes = null; existingChaptersImported = false;
sponsorSkipped = []; sponsorSkipped = [];
videoInfo = null; videoInfo = null;
@@ -423,7 +458,7 @@ function createPreviewBar(): void {
isVisibleCheck: true isVisibleCheck: true
}, { }, {
// For Desktop YouTube // For Desktop YouTube
selector: ".ytp-progress-bar-container", selector: ".ytp-progress-bar",
isVisibleCheck: true isVisibleCheck: true
}, { }, {
// For Desktop YouTube // For Desktop YouTube
@@ -441,7 +476,8 @@ function createPreviewBar(): void {
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0]; const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
if (el) { if (el) {
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious); const chapterVote = new ChapterVote(voteAsync);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
updatePreviewBar(); updatePreviewBar();
@@ -507,13 +543,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
} }
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`); logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
if (!video) return;
if (!video || video.paused) return;
if (currentTime === undefined || currentTime === null) { if (currentTime === undefined || currentTime === null) {
currentTime = getVirtualTime(); currentTime = getVirtualTime();
} }
lastTimeFromWaitingEvent = null; lastTimeFromWaitingEvent = null;
updateActiveSegment(currentTime);
if (video.paused) return;
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments); const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
const currentSkip = skipInfo.array[skipInfo.index]; const currentSkip = skipInfo.array[skipInfo.index];
@@ -568,35 +606,41 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (incorrectVideoCheck(videoID, currentSkip)) return; if (incorrectVideoCheck(videoID, currentSkip)) return;
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime()); forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) { if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) {
skipToTime({ if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
v: video, skipToTime({
skipTime, v: video,
skippingSegments, skipTime,
openNotice: skipInfo.openNotice skippingSegments,
}); openNotice: skipInfo.openNotice
});
// These are segments that start at the exact same time but need seperate notices // These are segments that start at the exact same time but need seperate notices
for (const extra of skipInfo.extraIndexes) { for (const extra of skipInfo.extraIndexes) {
const extraSkip = skipInfo.array[extra]; const extraSkip = skipInfo.array[extra];
if (shouldSkip(extraSkip)) { if (shouldSkip(extraSkip)) {
skipToTime({ skipToTime({
v: video, v: video,
skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]], skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
skippingSegments: [extraSkip], skippingSegments: [extraSkip],
openNotice: skipInfo.openNotice openNotice: skipInfo.openNotice
}); });
}
}
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) {
forcedSkipTime = skipTime[0] + 0.001;
} else {
forcedSkipTime = skipTime[1];
forcedIncludeIntersectingSegments = true;
forcedIncludeNonIntersectingSegments = false;
} }
}
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) {
forcedSkipTime = skipTime[0] + 0.001;
} else { } else {
forcedSkipTime = skipTime[1]; forcedSkipTime = forceVideoTime + 0.001;
forcedIncludeIntersectingSegments = true;
forcedIncludeNonIntersectingSegments = false;
} }
} else {
forcedSkipTime = forceVideoTime + 0.001;
} }
startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments); startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments);
@@ -793,8 +837,12 @@ function setupVideoListeners() {
lastTimeFromWaitingEvent = null; lastTimeFromWaitingEvent = null;
startSponsorSchedule(); startSponsorSchedule();
} else if (video.currentTime === 0) { } else {
lastPausedAtZero = true; updateActiveSegment(video.currentTime);
if (video.currentTime === 0) {
lastPausedAtZero = true;
}
} }
}); });
video.addEventListener('ratechange', () => startSponsorSchedule()); video.addEventListener('ratechange', () => startSponsorSchedule());
@@ -888,7 +936,12 @@ async function sponsorsLookup(keepOldSubmissions = true) {
if (response?.ok) { if (response?.ok) {
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText) const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === sponsorVideoID) ?.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) { if (!recievedSegments || !recievedSegments.length) {
// return if no video found // return if no video found
retryFetch(404); retryFetch(404);
@@ -909,6 +962,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
const oldSegments = sponsorTimes || []; const oldSegments = sponsorTimes || [];
sponsorTimes = recievedSegments; sponsorTimes = recievedSegments;
existingChaptersImported = false;
// Hide all submissions smaller than the minimum duration // Hide all submissions smaller than the minimum duration
if (Config.config.minDuration !== 0) { if (Config.config.minDuration !== 0) {
@@ -956,13 +1010,28 @@ async function sponsorsLookup(keepOldSubmissions = true) {
retryFetch(lastResponseStatus); retryFetch(lastResponseStatus);
} }
importExistingChapters(true);
if (Config.config.isVip) { if (Config.config.isVip) {
lockedCategoriesLookup(); 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[] { function getEnabledActionTypes(): ActionType[] {
const actionTypes = [ActionType.Skip, ActionType.Poi]; const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
if (Config.config.muteSegments) { if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute); 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); const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
setTimeout(() => { setTimeout(() => {
if (sponsorVideoID && sponsorTimes?.length === 0) { if (sponsorVideoID && sponsorTimes?.length === 0
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
sponsorsLookup(); sponsorsLookup();
} }
}, delay); }, delay);
@@ -1164,9 +1234,11 @@ function updatePreviewBar(): void {
previewBarSegments.push({ previewBarSegments.push({
segment: segment.segment as [number, number], segment: segment.segment as [number, number],
category: segment.category, category: segment.category,
unsubmitted: false,
actionType: segment.actionType, 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({ previewBarSegments.push({
segment: segment.segment as [number, number], segment: segment.segment as [number, number],
category: segment.category, category: segment.category,
unsubmitted: true,
actionType: segment.actionType, 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) previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
updateActiveSegment(video.currentTime);
if (Config.config.showTimeWithSkips) { 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); showTimeWithoutSkips(skippedDuration);
} }
@@ -1248,7 +1325,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); 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 // This is an array in-case multiple segments have the exact same start time
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime)); const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
@@ -1263,7 +1340,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); 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 minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex); const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
@@ -1344,7 +1421,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
* the current time, but end after * the current time, but end after
*/ */
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean, 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: []}; if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
const includedTimes: ScheduledTime[] = []; const includedTimes: ScheduledTime[] = [];
@@ -1355,9 +1432,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
scheduledTime: sponsorTime.segment[0] scheduledTime: sponsorTime.segment[0]
})); }));
// Schedule at the end time to know when to unmute // Schedule at the end time to know when to unmute and remove title from seek bar
sponsorTimes.filter(sponsorTime => sponsorTime.actionType === ActionType.Mute) sponsorTimes.forEach(sponsorTime => {
.forEach(sponsorTime => {
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) { if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
possibleTimes.push({ possibleTimes.push({
...sponsorTime, ...sponsorTime,
@@ -1369,9 +1445,9 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
for (let i = 0; i < possibleTimes.length; i++) { for (let i = 0; i < possibleTimes.length; i++) {
if ((minimum === undefined if ((minimum === undefined
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum) || ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum))) || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
&& (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible) && (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
&& possibleTimes[i].segment.length === 2
&& possibleTimes[i].actionType !== ActionType.Poi) { && possibleTimes[i].actionType !== ActionType.Poi) {
scheduledTimes.push(possibleTimes[i].scheduledTime); scheduledTimes.push(possibleTimes[i].scheduledTime);
@@ -1535,7 +1611,7 @@ function reskipSponsorTime(segment: SponsorTime, forceSeek = false) {
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
video.currentTime = segment.segment[1]; video.currentTime = segment.segment[1];
sendTelemetryAndCount([segment], skippedTime, fullSkip); sendTelemetryAndCount([segment], segment.actionType !== ActionType.Chapter ? skippedTime : 0, fullSkip);
startSponsorSchedule(true, segment.segment[1], false); startSponsorSchedule(true, segment.segment[1], false);
} }
} }
@@ -1586,6 +1662,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
function shouldSkip(segment: SponsorTime): boolean { function shouldSkip(segment: SponsorTime): boolean {
return (segment.actionType !== ActionType.Full return (segment.actionType !== ActionType.Full
&& segment.source !== SponsorSourceType.YouTube
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")); || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
} }
@@ -1692,7 +1769,7 @@ function startOrEndTimingNewSegment() {
if (!isSegmentCreationInProgress()) { if (!isSegmentCreationInProgress()) {
sponsorTimesSubmitting.push({ sponsorTimesSubmitting.push({
segment: [roundedTime], segment: [roundedTime],
UUID: utils.generateUserID() as SegmentUUID, UUID: GenericUtils.generateUserID() as SegmentUUID,
category: Config.config.defaultCategory, category: Config.config.defaultCategory,
actionType: ActionType.Skip, actionType: ActionType.Skip,
source: SponsorSourceType.Local source: SponsorSourceType.Local
@@ -1716,6 +1793,8 @@ function startOrEndTimingNewSegment() {
updateEditButtonsOnPlayer(); updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false); updateSponsorTimesSubmitting(false);
importExistingChapters(false);
} }
function getIncompleteSegment(): SponsorTime { function getIncompleteSegment(): SponsorTime {
@@ -1754,9 +1833,14 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
UUID: segmentTime.UUID, UUID: segmentTime.UUID,
category: segmentTime.category, category: segmentTime.category,
actionType: segmentTime.actionType, actionType: segmentTime.actionType,
description: segmentTime.description,
source: segmentTime.source source: segmentTime.source
}); });
} }
if (sponsorTimesSubmitting.length > 0) {
importExistingChapters(true);
}
} }
updatePreviewBar(); updatePreviewBar();
@@ -1878,7 +1962,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID); const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
// Don't vote for preview sponsors // 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 // See if the local time saved count and skip count should be saved
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) { if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
@@ -2025,7 +2109,7 @@ async function sendSubmitMessage() {
} catch(e) {} // eslint-disable-line no-empty } catch(e) {} // eslint-disable-line no-empty
// Add submissions to current sponsors list // 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 // Increase contribution count
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length; 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 i = 0; i < sponsorTimes.length; i++) {
for (let s = 0; s < sponsorTimes[i].segment.length; s++) { 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 this is an end time
if (s == 1) { if (s == 1) {
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage; timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
@@ -2078,6 +2162,44 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage; 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 { function addPageListeners(): void {
const refreshListners = () => { const refreshListners = () => {
if (!isVisible(video)) { if (!isVisible(video)) {
@@ -2107,6 +2229,8 @@ function hotkeyListener(e: KeyboardEvent): void {
const skipKey = Config.config.skipKeybind; const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind; const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind; const submitKey = Config.config.submitKeybind;
const nextChapterKey = Config.config.nextChapterKeybind;
const previousChapterKey = Config.config.previousChapterKeybind;
if (keybindEquals(key, skipKey)) { if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement) if (activeSkipKeybindElement)
@@ -2118,6 +2242,12 @@ function hotkeyListener(e: KeyboardEvent): void {
} else if (keybindEquals(key, submitKey)) { } else if (keybindEquals(key, submitKey)) {
submitSponsorTimes(); submitSponsorTimes();
return; 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) //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)
@@ -2187,8 +2317,8 @@ function showTimeWithoutSkips(skippedDuration: number): void {
display.appendChild(duration); display.appendChild(duration);
} }
const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration) const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration);
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")"; 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])) { if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
sponsorTimesSubmitting.push({ sponsorTimesSubmitting.push({
segment: segment.segment, segment: segment.segment,
UUID: utils.generateUserID() as SegmentUUID, UUID: GenericUtils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory, category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip, actionType: segment.actionType ? segment.actionType : ActionType.Skip,
source: SponsorSourceType.Local source: SponsorSourceType.Local

View File

@@ -6,41 +6,63 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
'use strict'; 'use strict';
import Config from "../config"; import Config from "../config";
import { ActionType } from "../types"; import { ChapterVote } from "../render/ChapterVote";
import Utils from "../utils"; import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
const utils = new Utils(); import { partition } from "../utils/arrayUtils";
import { shortCategoryName } from "../utils/categoryUtils";
import { GenericUtils } from "../utils/genericUtils";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003;
export interface PreviewBarSegment { export interface PreviewBarSegment {
segment: [number, number]; segment: [number, number];
category: string; category: Category;
unsubmitted: boolean;
actionType: ActionType; actionType: ActionType;
unsubmitted: boolean;
showLarger: boolean; showLarger: boolean;
description: string;
source: SponsorSourceType;
}
interface ChapterGroup extends SegmentContainer {
originalDuration: number
} }
class PreviewBar { class PreviewBar {
container: HTMLUListElement; container: HTMLUListElement;
categoryTooltip?: HTMLDivElement; categoryTooltip?: HTMLDivElement;
tooltipContainer?: HTMLElement; categoryTooltipContainer?: HTMLElement;
chapterTooltip?: HTMLDivElement;
parent: HTMLElement; parent: HTMLElement;
onMobileYouTube: boolean; onMobileYouTube: boolean;
onInvidious: boolean; onInvidious: boolean;
segments: PreviewBarSegment[] = []; segments: PreviewBarSegment[] = [];
existingChapters: PreviewBarSegment[] = [];
videoDuration = 0; 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 = document.createElement('ul');
this.container.id = 'previewbar'; this.container.id = 'previewbar';
this.parent = parent; this.parent = parent;
this.onMobileYouTube = onMobileYouTube; this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious; this.onInvidious = onInvidious;
this.chapterVote = chapterVote;
this.createElement(parent); this.createElement(parent);
this.createChapterMutationObservers();
this.setupHoverText(); this.setupHoverText();
} }
@@ -51,16 +73,19 @@ class PreviewBar {
// Create label placeholder // Create label placeholder
this.categoryTooltip = document.createElement("div"); this.categoryTooltip = document.createElement("div");
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip"; 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"); const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return; if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init // 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"); 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.categoryTooltip, titleTooltip.nextSibling);
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
const seekBar = document.querySelector(".ytp-progress-bar-container"); const seekBar = document.querySelector(".ytp-progress-bar-container");
if (!seekBar) return; if (!seekBar) return;
@@ -76,7 +101,7 @@ class PreviewBar {
}); });
const observer = new MutationObserver((mutations) => { 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 the mutation observed is only for our tooltip text, ignore
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) { if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
@@ -93,7 +118,7 @@ class PreviewBar {
const tooltipText = tooltipTextElement.textContent; const tooltipText = tooltipTextElement.textContent;
if (tooltipText === null || tooltipText.length === 0) continue; if (tooltipText === null || tooltipText.length === 0) continue;
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText); timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
if (timeInSeconds !== null) break; if (timeInSeconds !== null) break;
} }
@@ -101,36 +126,32 @@ class PreviewBar {
if (timeInSeconds === null) return; if (timeInSeconds === null) return;
// Find the segment at that location, using the shortest if multiple found // Find the segment at that location, using the shortest if multiple found
let segment: PreviewBarSegment | null = null; const [normalSegments, chapterSegments] =
let currentSegmentLength = Infinity; partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
(segment) => segment.actionType !== ActionType.Chapter);
for (const seg of this.segments) {// let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
const segmentLength = seg.segment[1] - seg.segment[0]; let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
const minSize = this.getMinimumSize(seg.showLarger); if (mainSegment === null && secondarySegment !== null) {
mainSegment = secondarySegment;
const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]); secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
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;
}
}
} }
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) { if (mainSegment === null && secondarySegment === null) {
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS); this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
} else if (segment !== null) { } else {
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS); this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
if (mainSegment !== null && secondarySegment !== null) {
if (segment.unsubmitted) { this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category);
} else { } else {
this.categoryTooltip.textContent = utils.shortCategoryName(segment.category); 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.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 { createElement(parent: HTMLElement): void {
this.parent = parent; this.parent = parent;
@@ -148,7 +184,7 @@ class PreviewBar {
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)"; parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
parent.style.opacity = "1"; parent.style.opacity = "1";
} }
this.container.style.transform = "none"; this.container.style.transform = "none";
} else if (!this.onInvidious) { } else if (!this.onInvidious) {
// Hover listener // Hover listener
@@ -157,39 +193,62 @@ class PreviewBar {
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered")); this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
} }
// On the seek bar // On the seek bar
this.parent.prepend(this.container); this.parent.prepend(this.container);
} }
clear(): void { clear(): void {
this.videoDuration = 0;
this.segments = [];
while (this.container.firstChild) { while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild); this.container.removeChild(this.container.firstChild);
} }
} }
set(segments: PreviewBarSegment[], videoDuration: number): void { 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(); this.clear();
if (!segments) return; if (!this.segments) return;
this.segments = segments; this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
this.videoDuration = videoDuration; 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 // Sort longer segments before short segments to make shorter segments render later
return (b[1] - b[0]) - (a[1] - a[0]); return (b[1] - b[0]) - (a[1] - a[0]);
}).forEach((segment) => { });
for (const segment of sortedSegments) {
const bar = this.createBar(segment); const bar = this.createBar(segment);
this.container.appendChild(bar); this.container.appendChild(bar);
}); }
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({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement { createBar(barSegment: PreviewBarSegment): HTMLLIElement {
const { category, unsubmitted, segment, showLarger } = barSegment;
const bar = document.createElement('li'); const bar = document.createElement('li');
bar.classList.add('previewbar'); bar.classList.add('previewbar');
bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;'; bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;';
@@ -202,7 +261,9 @@ class PreviewBar {
bar.style.position = "absolute"; bar.style.position = "absolute";
const duration = Math.min(segment[1], this.videoDuration) - segment[0]; 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]; const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
bar.style.left = this.timeToPercentage(time); bar.style.left = this.timeToPercentage(time);
@@ -210,6 +271,413 @@ class PreviewBar {
return bar; 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 { remove(): void {
this.container.remove(); this.container.remove();
@@ -218,14 +686,66 @@ class PreviewBar {
this.categoryTooltip = undefined; this.categoryTooltip = undefined;
} }
if (this.tooltipContainer) { if (this.categoryTooltipContainer) {
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS); this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
this.tooltipContainer = undefined; 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 { 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 { getMinimumSize(showLarger = false): number {
return this.videoDuration * (showLarger ? 0.006 : 0.003); 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; export default PreviewBar;

View File

@@ -30,6 +30,11 @@ interface IsInfoFoundMessage {
updating: boolean; updating: boolean;
} }
interface SkipMessage {
message: "unskip" | "reskip";
UUID: SegmentUUID;
}
interface SubmitVoteMessage { interface SubmitVoteMessage {
message: "submitVote"; message: "submitVote";
type: number; type: number;
@@ -47,6 +52,11 @@ interface CopyToClipboardMessage {
text: string; text: string;
} }
interface ImportSegmentsMessage {
message: "importSegments";
data: string;
}
interface KeyDownMessage { interface KeyDownMessage {
message: "keydown"; message: "keydown";
key: string; key: string;
@@ -59,12 +69,13 @@ interface KeyDownMessage {
metaKey: boolean; 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 { export interface IsInfoFoundMessageResponse {
found: boolean; found: boolean;
status: number; status: number;
sponsorTimes: SponsorTime[]; sponsorTimes: SponsorTime[];
time: number;
onMobileYouTube: boolean; onMobileYouTube: boolean;
} }
@@ -90,11 +101,23 @@ export type MessageResponse =
| GetChannelIDResponse | GetChannelIDResponse
| SponsorStartResponse | SponsorStartResponse
| IsChannelWhitelistedResponse | IsChannelWhitelistedResponse
| Record<never, never> // empty object response {} | Record<string, never> // empty object response {}
| VoteResponse; | VoteResponse
| ImportSegmentsResponse;
export interface VoteResponse { export interface VoteResponse {
successType: number; successType: number;
statusCode: number; statusCode: number;
responseText: string; responseText: string;
} }
export interface ImportSegmentsResponse {
importedSegments: SponsorTime[];
}
export interface TimeUpdateMessage {
message: "time";
time: number;
}
export type PopupMessage = TimeUpdateMessage;

View File

@@ -10,7 +10,7 @@ window.SB = Config;
import Utils from "./utils"; import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser"; import CategoryChooser from "./render/CategoryChooser";
import KeybindComponent from "./components/KeybindComponent"; import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils"; import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils"; import { localizeHtmlPage } from "./utils/pageUtils";
const utils = new Utils(); const utils = new Utils();

View File

@@ -1,12 +1,15 @@
import Config from "./config"; import Config from "./config";
import Utils from "./utils"; import Utils from "./utils";
import { SponsorTime, SponsorHideType, ActionType, StorageChangesObject } from "./types"; import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject, CategorySkipOption } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes"; import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils"; import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils"; import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils"; import { GenericUtils } from "./utils/genericUtils";
import { shortCategoryName } from "./utils/categoryUtils";
import { localizeHtmlPage } from "./utils/pageUtils"; import { localizeHtmlPage } from "./utils/pageUtils";
import { exportTimes } from "./utils/exporter";
import GenericNotice from "./render/GenericNotice";
const utils = new Utils(); const utils = new Utils();
interface MessageListener { interface MessageListener {
@@ -68,10 +71,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//the start and end time pairs (2d) //the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = []; let sponsorTimes: SponsorTime[] = [];
let downloadedTimes: SponsorTime[] = [];
//current video ID of this tab //current video ID of this tab
let currentVideoID = null; let currentVideoID = null;
enum SegmentTab {
Segments,
Chapters
}
let segmentTab = SegmentTab.Segments;
let port: chrome.runtime.Port = null;
const PageElements: PageElements = {}; const PageElements: PageElements = {};
[ [
@@ -124,11 +135,21 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
"refreshSegmentsButton", "refreshSegmentsButton",
"whitelistButton", "whitelistButton",
"sbDonate", "sbDonate",
"issueReporterTabs",
"issueReporterTabSegments",
"issueReporterTabChapters",
"sponsorTimesDonateContainer", "sponsorTimesDonateContainer",
"sbConsiderDonateLink", "sbConsiderDonateLink",
"sbCloseDonate", "sbCloseDonate",
"sbBetaServerWarning", "sbBetaServerWarning",
"sbCloseButton" "sbCloseButton",
"issueReporterImportExport",
"importSegmentsButton",
"exportSegmentsButton",
"importSegmentsMenu",
"importSegmentsText",
"importSegmentsSubmit"
].forEach(id => PageElements[id] = document.getElementById(id)); ].forEach(id => PageElements[id] = document.getElementById(id));
getSegmentsFromContentScript(false); 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.sponsorStart.addEventListener("click", sendSponsorStartMessage);
PageElements.whitelistToggle.addEventListener("change", function () { PageElements.whitelistToggle.addEventListener("change", function () {
if (this.checked) { if (this.checked) {
@@ -215,6 +240,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}); });
} }
setupComPort();
//show proper disable skipping button //show proper disable skipping button
const disableSkipping = Config.config.disableSkipping; const disableSkipping = Config.config.disableSkipping;
if (disableSkipping != undefined && disableSkipping) { if (disableSkipping != undefined && disableSkipping) {
@@ -230,7 +257,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "unset"; 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) { if (res.status === 200) {
const userInfo = JSON.parse(res.responseText); const userInfo = JSON.parse(res.responseText);
PageElements.usernameValue.innerText = userInfo.userName; PageElements.usernameValue.innerText = userInfo.userName;
@@ -259,6 +287,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
} }
Config.config.isVip = userInfo.vip; 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 // Must be delayed so it only happens once loaded
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250); 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) { function showDonateWidget(viewCount: number) {
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5 if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) { && 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.whitelistButton.classList.remove("hidden");
PageElements.loadingIndicator.style.display = "none"; PageElements.loadingIndicator.style.display = "none";
downloadedTimes = request.sponsorTimes ?? [];
if (request.found) { if (request.found) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound"); 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) { } else if (request.status == 404 || request.status == 200) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404"); PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
} else { } else {
@@ -441,165 +496,208 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
} }
//display the video times from the array at the top, in a different section //display the video times from the array at the top, in a different section
function displayDownloadedSponsorTimes(request: { found: boolean, sponsorTimes: SponsorTime[] }) { function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
if (request.sponsorTimes != undefined) { let currentSegmentTab = segmentTab;
// Sort list by start time if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
const segmentTimes = request.sponsorTimes PageElements.issueReporterTabs.classList.add("hidden");
.sort((a, b) => a.segment[1] - b.segment[1]) currentSegmentTab = SegmentTab.Segments;
.sort((a, b) => a.segment[0] - b.segment[0]); } else {
PageElements.issueReporterTabs.classList.remove("hidden");
}
//add them as buttons to the issue reporting container // Sort list by start time
const container = document.getElementById("issueReporterTimeButtons"); const downloadedTimes = sponsorTimes
while (container.firstChild) { .filter((segment) => {
container.removeChild(container.firstChild); 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]);
//add them as buttons to the issue reporting container
const container = document.getElementById("issueReporterTimeButtons");
while (container.firstChild) {
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 < 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.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 isVip = Config.config.isVip; const categoryColorCircle = document.createElement("span");
for (let i = 0; i < segmentTimes.length; i++) { categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
const UUID = segmentTimes[i].UUID; categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
const locked = segmentTimes[i].locked; categoryColorCircle.classList.add("dot");
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
const segmentSummary = document.createElement("summary"); let extraInfo = "";
segmentSummary.className = "segmentSummary"; if (downloadedTimes[i].hidden === SponsorHideType.Downvoted) {
//this one is downvoted
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
} else if (downloadedTimes[i].hidden === SponsorHideType.MinimumDuration) {
//this one is too short
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
} else if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
}
const categoryColorCircle = document.createElement("span"); const name = downloadedTimes[i].description || shortCategoryName(category);
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID; const textNode = document.createTextNode(name + extraInfo);
categoryColorCircle.style.backgroundColor = Config.config.barTypes[segmentTimes[i].category]?.color; const segmentTimeFromToNode = document.createElement("div");
categoryColorCircle.classList.add("dot"); if (downloadedTimes[i].actionType === ActionType.Full) {
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle"); segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
(actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
: "");
}
let extraInfo = ""; segmentTimeFromToNode.style.margin = "5px";
if (segmentTimes[i].hidden === SponsorHideType.Downvoted) {
//this one is downvoted
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
} else if (segmentTimes[i].hidden === SponsorHideType.MinimumDuration) {
//this one is too short
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
} else if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
}
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo); // for inline-styling purposes
const segmentTimeFromToNode = document.createElement("div"); const labelContainer = document.createElement("div");
if (segmentTimes[i].actionType === ActionType.Full) { if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
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.style.margin = "5px"; const span = document.createElement('span');
span.className = "summaryLabel";
span.appendChild(textNode);
labelContainer.appendChild(span);
// for inline-styling purposes segmentSummary.appendChild(labelContainer);
const labelContainer = document.createElement("div"); segmentSummary.appendChild(segmentTimeFromToNode);
labelContainer.appendChild(categoryColorCircle);
const span = document.createElement('span'); const votingButtons = document.createElement("details");
span.className = "summaryLabel"; votingButtons.classList.add("votingButtons");
span.appendChild(textNode);
labelContainer.appendChild(span);
// for inline-styling purposes
segmentSummary.appendChild(labelContainer); //thumbs up and down buttons
segmentSummary.appendChild(segmentTimeFromToNode); const voteButtonsContainer = document.createElement("div");
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
const votingButtons = document.createElement("details"); const upvoteButton = document.createElement("img");
votingButtons.classList.add("votingButtons"); upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
upvoteButton.className = "voteButton";
upvoteButton.title = chrome.i18n.getMessage("upvote");
upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
upvoteButton.addEventListener("click", () => vote(1, UUID));
//thumbs up and down buttons const downvoteButton = document.createElement("img");
const voteButtonsContainer = document.createElement("div"); downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID; downvoteButton.className = "voteButton";
voteButtonsContainer.classList.add("sbVoteButtonsContainer"); downvoteButton.title = chrome.i18n.getMessage("downvote");
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
downvoteButton.addEventListener("click", () => vote(0, UUID));
const upvoteButton = document.createElement("img"); const uuidButton = document.createElement("img");
upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID; uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
upvoteButton.className = "voteButton"; uuidButton.className = "voteButton";
upvoteButton.title = chrome.i18n.getMessage("upvote"); uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg"); uuidButton.title = chrome.i18n.getMessage("copySegmentID");
upvoteButton.addEventListener("click", () => vote(1, UUID)); uuidButton.addEventListener("click", () => {
copyToClipboard(UUID);
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
stopAnimation();
});
const downvoteButton = document.createElement("img"); const hideButton = document.createElement("img");
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID; hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
downvoteButton.className = "voteButton"; hideButton.className = "voteButton";
downvoteButton.title = chrome.i18n.getMessage("downvote"); hideButton.title = chrome.i18n.getMessage("hideSegment");
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg"); if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
downvoteButton.addEventListener("click", () => vote(0, UUID)); hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
} else {
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
}
hideButton.addEventListener("click", () => {
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
stopAnimation();
const uuidButton = document.createElement("img"); if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
uuidButton.className = "voteButton";
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
uuidButton.title = chrome.i18n.getMessage("copySegmentID");
uuidButton.addEventListener("click", () => {
copyToClipboard(UUID);
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
stopAnimation();
});
const hideButton = document.createElement("img");
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
hideButton.className = "voteButton";
hideButton.title = chrome.i18n.getMessage("hideSegment");
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
} else {
hideButton.src = chrome.runtime.getURL("icons/visible.svg"); hideButton.src = chrome.runtime.getURL("icons/visible.svg");
downloadedTimes[i].hidden = SponsorHideType.Visible;
} else {
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
downloadedTimes[i].hidden = SponsorHideType.Hidden;
} }
hideButton.addEventListener("click", () => {
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
stopAnimation();
if (segmentTimes[i].hidden === SponsorHideType.Hidden) { messageHandler.query({
hideButton.src = chrome.runtime.getURL("icons/visible.svg"); active: true,
segmentTimes[i].hidden = SponsorHideType.Visible; currentWindow: true
} else { }, tabs => {
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg"); messageHandler.sendMessage(
segmentTimes[i].hidden = SponsorHideType.Hidden; tabs[0].id,
} {
message: "hideSegment",
messageHandler.query({ type: downloadedTimes[i].hidden,
active: true, UUID: UUID
currentWindow: true }
}, tabs => { );
messageHandler.sendMessage(
tabs[0].id,
{
message: "hideSegment",
type: segmentTimes[i].hidden,
UUID: UUID
}
);
});
}); });
});
//add thumbs up, thumbs down and uuid copy buttons to the container const skipButton = document.createElement("img");
voteButtonsContainer.appendChild(upvoteButton); skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
voteButtonsContainer.appendChild(downvoteButton); skipButton.className = "voteButton";
voteButtonsContainer.appendChild(uuidButton); skipButton.src = chrome.runtime.getURL("icons/skip.svg");
if ((segmentTimes[i].actionType === ActionType.Skip || segmentTimes[i].actionType === ActionType.Mute) skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) { votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
voteButtonsContainer.appendChild(hideButton);
}
// Will contain request status //add thumbs up, thumbs down and uuid copy buttons to the container
const voteStatusContainer = document.createElement("div"); voteButtonsContainer.appendChild(upvoteButton);
voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID; voteButtonsContainer.appendChild(downvoteButton);
voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer"); voteButtonsContainer.appendChild(uuidButton);
voteStatusContainer.style.display = "none"; if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
const thanksForVotingText = document.createElement("div"); voteButtonsContainer.appendChild(hideButton);
thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
voteStatusContainer.appendChild(thanksForVotingText);
votingButtons.append(segmentSummary);
votingButtons.append(voteButtonsContainer);
votingButtons.append(voteStatusContainer);
container.appendChild(votingButtons);
} }
voteButtonsContainer.appendChild(skipButton);
// Will contain request status
const voteStatusContainer = document.createElement("div");
voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
voteStatusContainer.style.display = "none";
const thanksForVotingText = document.createElement("div");
thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
voteStatusContainer.appendChild(thanksForVotingText);
votingButtons.append(segmentSummary);
votingButtons.append(voteButtonsContainer);
votingButtons.append(voteStatusContainer);
container.appendChild(votingButtons);
} }
} }
@@ -708,6 +806,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//this is not a YouTube video page //this is not a YouTube video page
function displayNoVideo() { function displayNoVideo() {
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID"); document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
PageElements.issueReporterTabs.classList.add("hidden");
} }
function addVoteMessage(message, UUID) { 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) * 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 * Converts time in minutes to 2d 5h 25.1
* If less than 1 hour, just returns minutes * 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(); runThePopup();

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import CategoryChooserComponent from "../components/CategoryChooserComponent"; import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
class CategoryChooser { class CategoryChooser {

View 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;
}
}
}
}

View File

@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils"; import Utils from "../utils";
const utils = new Utils(); const utils = new Utils();
import { ContentContainer } from "../types"; import { ButtonListener, ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
export interface TextBox { export interface TextBox {
icon: string, icon: string,
text: string text: string
@@ -20,12 +15,17 @@ export interface TextBox {
export interface NoticeOptions { export interface NoticeOptions {
title: string, title: string,
referenceNode?: HTMLElement,
textBoxes?: TextBox[], textBoxes?: TextBox[],
buttons?: ButtonListener[], buttons?: ButtonListener[],
fadeIn?: boolean, fadeIn?: boolean,
timed?: boolean timed?: boolean
style?: React.CSSProperties; style?: React.CSSProperties;
extraClass?: string; extraClass?: string;
maxCountdownTime?: () => number;
dontPauseCountdown?: boolean;
hideLogo?: boolean;
hideRightInfo?: boolean;
} }
export default class GenericNotice { export default class GenericNotice {
@@ -42,7 +42,7 @@ export default class GenericNotice {
this.contentContainer = contentContainer; this.contentContainer = contentContainer;
const referenceNode = utils.findReferenceNode(); const referenceNode = options.referenceNode ?? utils.findReferenceNode();
this.noticeElement = document.createElement("div"); this.noticeElement = document.createElement("div");
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix; this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
@@ -62,6 +62,10 @@ export default class GenericNotice {
ref={this.noticeRef} ref={this.noticeRef}
style={options.style} style={options.style}
extraClass={options.extraClass} extraClass={options.extraClass}
maxCountdownTime={options.maxCountdownTime}
dontPauseCountdown={options.dontPauseCountdown}
hideLogo={options.hideLogo}
hideRightInfo={options.hideRightInfo}
closeListener={() => this.close()} > closeListener={() => this.close()} >
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix} <tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}

View File

@@ -33,8 +33,8 @@ export class RectangleTooltip {
props.fontSize ??= "10px"; props.fontSize ??= "10px";
this.container = document.createElement('div'); this.container = document.createElement('div');
props.htmlId ??= props.text; props.htmlId ??= "sponsorRectangleTooltip" + props.text;
this.container.id = "sponsorRectangleTooltip" + props.htmlId; this.container.id = props.htmlId;
this.container.style.display = "relative"; this.container.style.display = "relative";
if (props.prependElement) { if (props.prependElement) {

View File

@@ -1,29 +1,37 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { ButtonListener } from "../types";
export interface TooltipProps { export interface TooltipProps {
text: string, text?: string;
link?: string, link?: string;
referenceNode: HTMLElement, referenceNode: HTMLElement;
prependElement?: HTMLElement, // Element to append before prependElement?: HTMLElement; // Element to append before
bottomOffset?: string bottomOffset?: string;
leftOffset?: string;
rightOffset?: string;
timeout?: number; timeout?: number;
opacity?: number; opacity?: number;
displayTriangle?: boolean; displayTriangle?: boolean;
extraClass?: string;
showLogo?: boolean; showLogo?: boolean;
showGotIt?: boolean; showGotIt?: boolean;
buttons?: ButtonListener[];
} }
export class Tooltip { export class Tooltip {
text: string; text?: string;
container: HTMLDivElement; container: HTMLDivElement;
timer: NodeJS.Timeout; timer: NodeJS.Timeout;
constructor(props: TooltipProps) { constructor(props: TooltipProps) {
props.bottomOffset ??= "70px"; props.bottomOffset ??= "70px";
props.leftOffset ??= "inherit";
props.rightOffset ??= "inherit";
props.opacity ??= 0.7; props.opacity ??= 0.7;
props.displayTriangle ??= true; props.displayTriangle ??= true;
props.extraClass ??= "";
props.showLogo ??= true; props.showLogo ??= true;
props.showGotIt ??= true; props.showGotIt ??= true;
this.text = props.text; this.text = props.text;
@@ -45,25 +53,29 @@ export class Tooltip {
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`; const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render( ReactDOM.render(
<div style={{bottom: props.bottomOffset, backgroundColor}} <div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} > className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div> <div>
{props.showLogo ? {props.showLogo ?
<img className="sponsorSkipLogo sponsorSkipObject" <img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}> src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img> </img>
: null} : null}
<span className="sponsorSkipObject"> {this.text ?
{this.text + (props.link ? ". " : "")} <span className="sponsorSkipObject">
{props.link ? {this.text + (props.link ? ". " : "")}
<a style={{textDecoration: "underline"}} {props.link ?
target="_blank" <a style={{textDecoration: "underline"}}
rel="noopener noreferrer" target="_blank"
href={props.link}> rel="noopener noreferrer"
{chrome.i18n.getMessage("LearnMore")} href={props.link}>
</a> {chrome.i18n.getMessage("LearnMore")}
: null} </a>
</span> : null}
</span>
: null}
{this.getButtons(props.buttons)}
</div> </div>
{props.showGotIt ? {props.showGotIt ?
<button className="sponsorSkipObject sponsorSkipNoticeButton" <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 { close(): void {
ReactDOM.unmountComponentAtNode(this.container); ReactDOM.unmountComponentAtNode(this.container);
this.container.remove(); this.container.remove();

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

View File

@@ -1,13 +1,17 @@
import * as React from "react"; import * as React from "react";
const thumbsDownSvg = ({ const thumbsDownSvg = ({
fill = "#ffffff" fill = "#ffffff",
className = "",
width = "18",
height = "18"
}): JSX.Element => ( }): JSX.Element => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="18" width={width}
height="18" height={height}
fill={fill} fill={fill}
className={className}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path

View File

@@ -1,13 +1,17 @@
import * as React from "react"; import * as React from "react";
const thumbsUpSvg = ({ const thumbsUpSvg = ({
fill = "#ffffff" fill = "#ffffff",
className = "",
width = "18",
height = "18"
}): JSX.Element => ( }): JSX.Element => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill={fill} fill={fill}
width={width}
height={height}
className={className}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path

View File

@@ -21,7 +21,8 @@ export interface ContentContainer {
previewTime: (time: number, unpause?: boolean) => void, previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo, videoInfo: VideoInfo,
getRealCurrentTime: () => number, getRealCurrentTime: () => number,
lockedCategories: string[] lockedCategories: string[],
channelIDInfo: ChannelIDInfo
} }
} }
@@ -58,6 +59,7 @@ export enum SponsorHideType {
export enum ActionType { export enum ActionType {
Skip = "skip", Skip = "skip",
Mute = "mute", Mute = "mute",
Chapter = "chapter",
Full = "full", Full = "full",
Poi = "poi" Poi = "poi"
} }
@@ -69,19 +71,24 @@ export type Category = string & { __categoryBrand: unknown };
export enum SponsorSourceType { export enum SponsorSourceType {
Server = undefined, Server = undefined,
Local = 1 Local = 1,
YouTube = 2
} }
export interface SponsorTime { export interface SegmentContainer {
segment: [number] | [number, number]; segment: [number] | [number, number];
}
export interface SponsorTime extends SegmentContainer {
UUID: SegmentUUID; UUID: SegmentUUID;
locked?: number; locked?: number;
category: Category; category: Category;
actionType: ActionType; actionType: ActionType;
description?: string;
hidden?: SponsorHideType; hidden?: SponsorHideType;
source?: SponsorSourceType; source: SponsorSourceType;
videoDuration?: number; videoDuration?: number;
} }
@@ -230,4 +237,9 @@ export type Keybind = {
ctrl?: boolean, ctrl?: boolean,
alt?: boolean, alt?: boolean,
shift?: boolean shift?: boolean
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
} }

71
src/upsell.ts Normal file
View 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);
});
}

View File

@@ -30,7 +30,7 @@ export default class Utils {
this.backgroundScriptContainer = backgroundScriptContainer; 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); return GenericUtils.wait(condition, timeout, check);
} }
@@ -331,24 +331,6 @@ export default class Utils {
return permissionRegex; 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 * Sends a request to a custom server
* *
@@ -434,54 +416,6 @@ export default class Utils {
return referenceNode; 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 { isContentScript(): boolean {
return window.location.protocol === "http:" || window.location.protocol === "https:"; return window.location.protocol === "http:" || window.location.protocol === "https:";
} }

6
src/utils/arrayUtils.ts Normal file
View 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];
}

View File

@@ -41,7 +41,13 @@ export function getCategorySuffix(category: Category): string {
return "_POI"; return "_POI";
} else if (category === "exclusive_access") { } else if (category === "exclusive_access") {
return "_full"; return "_full";
} else if (category === "chapter") {
return "_chapter";
} else { } else {
return ""; return "";
} }
}
export function shortCategoryName(categoryName: string): string {
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
} }

View File

@@ -137,5 +137,16 @@ export function getGuidelineInfo(category: Category): TextBox[] {
icon: "icons/bolt.svg", icon: "icons/bolt.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`) 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
View 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;
}

View File

@@ -1,5 +1,5 @@
/** Function that can be used to wait for a condition before returning. */ /** 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) => { return await new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
clearInterval(interval); clearInterval(interval);
@@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
const intervalCheck = () => { const intervalCheck = () => {
const result = condition(); const result = condition();
if (result) { if (predicate ? predicate(result) : result) {
resolve(result); resolve(result);
clearInterval(interval); 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 * Gets the error message in a nice string
* *
@@ -85,10 +129,31 @@ function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): str
return url; 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 = { export const GenericUtils = {
wait, wait,
getFormattedTime,
getFormattedTimeToSeconds,
getErrorMessage, getErrorMessage,
getLuminance, getLuminance,
generateUserID,
indexesOf, indexesOf,
objectToURI objectToURI
} }

65
src/utils/licenseKey.ts Normal file
View 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;
}

View File

@@ -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 = [ const controlsSelectors = [
// YouTube // YouTube
".ytp-right-controls", ".ytp-right-controls",
@@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
} }
} }
return false; return null;
} }
export function isVisible(element: HTMLElement): boolean { export function isVisible(element: HTMLElement): boolean {
@@ -63,6 +66,44 @@ export function getHashParams(): Record<string, unknown> {
return {}; 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 { export function localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags //Localize by replacing __MSG_***__ meta tags
const localizedTitle = getLocalizedMessage(document.title); const localizedTitle = getLocalizedMessage(document.title);

241
test/exporter.test.ts Normal file
View 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
View 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
}
]);
});
})

View File

@@ -31,7 +31,8 @@ module.exports = env => ({
content: path.join(__dirname, srcDir + 'content.ts'), content: path.join(__dirname, srcDir + 'content.ts'),
options: path.join(__dirname, srcDir + 'options.ts'), options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.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: { output: {
path: path.join(__dirname, '../dist/js'), path: path.join(__dirname, '../dist/js'),