mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-06 11:37:02 +03:00
@@ -2,7 +2,7 @@
|
|||||||
"serverAddress": "https://sponsor.ajay.app",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
1089
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
|||||||
"eslint-plugin-react": "^7.30.1",
|
"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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
106
public/icons/export.svg
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 67.671 67.671"
|
||||||
|
style="enable-background:new 0 0 67.671 67.671;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="export.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs41" /><sodipodi:namedview
|
||||||
|
id="namedview39"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="9.309749"
|
||||||
|
inkscape:cx="33.835499"
|
||||||
|
inkscape:cy="16.649214"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="731"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Capa_1" />
|
||||||
|
<g
|
||||||
|
id="g6"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
<path
|
||||||
|
d="M 52.946,23.348 H 42.834 v 6 h 10.112 c 3.007,0 5.34,1.536 5.34,2.858 v 26.606 c 0,1.322 -2.333,2.858 -5.34,2.858 H 14.724 c -3.007,0 -5.34,-1.536 -5.34,-2.858 V 32.207 c 0,-1.322 2.333,-2.858 5.34,-2.858 h 10.11 v -6 h -10.11 c -6.359,0 -11.34,3.891 -11.34,8.858 v 26.606 c 0,4.968 4.981,8.858 11.34,8.858 h 38.223 c 6.358,0 11.34,-3.891 11.34,-8.858 V 32.207 C 64.286,27.239 59.305,23.348 52.946,23.348 Z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="m 24.957,14.955 c 0.768,0 1.535,-0.293 2.121,-0.879 l 3.756,-3.756 v 13.028 6 11.494 c 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3 v -11.494 -6 -13.231 l 3.959,3.959 c 0.586,0.586 1.354,0.879 2.121,0.879 0.767,0 1.535,-0.293 2.121,-0.879 1.172,-1.171 1.172,-3.071 0,-4.242 L 36.078,0.877 C 35.492,0.291 34.725,0 33.958,0 33.95,0 33.943,0 33.935,0 33.927,0 33.92,0 33.912,0 33.145,0 32.378,0.291 31.792,0.877 l -8.957,8.957 c -1.172,1.171 -1.172,3.071 0,4.242 0.587,0.586 1.354,0.879 2.122,0.879 z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g16"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g18"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g20"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g22"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g24"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g26"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g28"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g30"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g32"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g34"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g36"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
91
public/icons/import.svg
Normal file
91
public/icons/import.svg
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 67.671 67.671"
|
||||||
|
style="enable-background:new 0 0 67.671 67.671;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="import.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs41" /><sodipodi:namedview
|
||||||
|
id="namedview39"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="9.309749"
|
||||||
|
inkscape:cx="33.835499"
|
||||||
|
inkscape:cy="33.835499"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="983"
|
||||||
|
inkscape:window-x="482"
|
||||||
|
inkscape:window-y="768"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g6" />
|
||||||
|
<g
|
||||||
|
id="g6">
|
||||||
|
<path
|
||||||
|
d="M52.946,23.348H42.834v6h10.112c3.007,0,5.34,1.536,5.34,2.858v26.606c0,1.322-2.333,2.858-5.34,2.858H14.724 c-3.007,0-5.34-1.536-5.34-2.858V32.207c0-1.322,2.333-2.858,5.34-2.858h10.11v-6h-10.11c-6.359,0-11.34,3.891-11.34,8.858v26.606 c0,4.968,4.981,8.858,11.34,8.858h38.223c6.358,0,11.34-3.891,11.34-8.858V32.207C64.286,27.239,59.305,23.348,52.946,23.348z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="m 42.913,34.887 c -0.768,0 -1.370265,0.528017 -2.121,0.879 l -3.756,3.756 v -19.028 -6 V 3 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 v 11.494 12 13.231 l -3.959,-3.959 c -0.586,-0.586 -1.354,-0.879 -2.121,-0.879 -0.767,0 -1.535,0.293 -2.121,0.879 -1.172,1.171 -1.172,3.071 0,4.242 l 8.957,8.957 c 0.586,0.586 1.353,0.877 2.12,0.877 h 0.023 0.023 c 0.767,0 1.534,-0.291 2.12,-0.877 l 8.957,-8.957 c 1.172,-1.171 1.172,-3.071 0,-4.242 -0.587,-0.586 -1.354,-0.879 -2.122,-0.879 z"
|
||||||
|
id="path4"
|
||||||
|
sodipodi:nodetypes="sscccssscccssccsscssccs"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g16">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g18">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g20">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g22">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g24">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g26">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g28">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g30">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g32">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g34">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g36">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
public/icons/skip.svg
Normal file
1
public/icons/skip.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 196 B |
1
public/icons/sort.svg
Normal file
1
public/icons/sort.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 201 B |
@@ -123,6 +123,14 @@ html, body {
|
|||||||
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
|
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;
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
1
public/res/countries.json
Normal file
1
public/res/countries.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}}
|
||||||
219
public/shared.css
Normal file
219
public/shared.css
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
.sponsorSkipNoticeParent {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: 100px;
|
||||||
|
right: var(--skip-notice-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeParent, .sponsorSkipNotice {
|
||||||
|
border-spacing: var(--skip-notice-border-horizontal) var(--skip-notice-border-vertical);
|
||||||
|
padding-left: var(--skip-notice-padding);
|
||||||
|
padding-right: var(--skip-notice-padding);
|
||||||
|
|
||||||
|
border-collapse: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeParent {
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeTableContainer {
|
||||||
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice {
|
||||||
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeLimitWidth {
|
||||||
|
max-width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Cloudtube */
|
||||||
|
.sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFadeIn {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFaded {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFadeOut {
|
||||||
|
transition: opacity 3s cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||||
|
opacity: 0 !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .sponsorSkipNoticeTimeLeft {
|
||||||
|
color: #eeeeee;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeTimeLeft img {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 13px;
|
||||||
|
|
||||||
|
padding-top: 7.8%;
|
||||||
|
padding-bottom: 7.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if two are very close to eachother */
|
||||||
|
.secondSkipNotice {
|
||||||
|
bottom: 290px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noticeLeftIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .sponsorSkipNoticeUnskipSection {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
border-left: 1px solid rgb(150, 150, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeButton {
|
||||||
|
background: none;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
border: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13.3333px !important;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeButton:hover {
|
||||||
|
background-color: rgba(235, 235, 235,0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
transition: background-color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFirstRow .sponsorSkipNoticeButton.sponsorSkipSmallButton {
|
||||||
|
height: 1.3em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonsContainer {
|
||||||
|
float: left;
|
||||||
|
vertical-align:middle;
|
||||||
|
padding: 2px 5px;
|
||||||
|
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonsContainer div{
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeRightSection {
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeRightButton {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeCloseButton {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
box-sizing: unset;
|
||||||
|
|
||||||
|
padding: 2px 5px;
|
||||||
|
|
||||||
|
margin-left: 2px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeCloseButton.biggerCloseButton {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipMessage {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
|
||||||
|
margin-top: auto;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipInfo {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sponsorTimesThanksForVotingText {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sponsorTimesThanksForVotingInfoText {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonMessage {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesInfoMessage {
|
||||||
|
font-size: 13.3333px;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
94
public/upsell/index.html
Normal file
94
public/upsell/index.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Upsell - SponsorBlock</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<link href="styles.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="../js/vendor.js"></script>
|
||||||
|
<script src="../js/upsell.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="sponsorBlockPageBody">
|
||||||
|
|
||||||
|
<div id="title" class="titleBar">
|
||||||
|
<img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic" />
|
||||||
|
SponsorBlock
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<p>
|
||||||
|
__MSG_chaptersPage1__
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/H_mP7bpbA_c?modestbranding=1&rel=0" title="Demo Video"
|
||||||
|
frameborder="0" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<a href="https://buy.ajay.app/l/sponsorblock" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||||
|
<div id="oneTimePurchase" class="option-button inline">
|
||||||
|
__MSG_oneTimePurchase__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://www.patreon.com/ajayyy" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||||
|
<div class="option-button side-by-side inline">
|
||||||
|
__MSG_joinOnPatreon__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<input id="redeemCodeInput" class="option-text-box" type="text" placeholder="__MSG_enterLicenseKey__">
|
||||||
|
<div id="redeemButton" class="option-button inline">
|
||||||
|
__MSG_redeem__
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%3A3000%2Fapi%2FgenerateToken%2Fpatreon"
|
||||||
|
class="option-link" target="_blank" rel="noreferrer">
|
||||||
|
<div class="option-button inline">
|
||||||
|
__MSG_patreonSignIn__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cantAfford" class="center">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
__MSG_alreadyDonated__ sponsorblock-free@ajay.app
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="subsidizedPrice" class="center hidden">
|
||||||
|
__MSG_selectYourCountry__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="subsidizedLink" class="center hidden">
|
||||||
|
<a href="https://buy.ajay.app/l/sponsorblock/purchasing-power" class="option-link" target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
<div class="option-button inline">
|
||||||
|
__MSG_discountLink__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noSubsidizedLink" class="center hidden">
|
||||||
|
__MSG_noDiscount__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
387
public/upsell/styles.css
Normal file
387
public/upsell/styles.css
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
/* Based on options page CSS */
|
||||||
|
html {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center p {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-item {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keybind-status {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text-box {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: #c00000;
|
||||||
|
padding: 10px;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-link.side-by-side {
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button:hover {
|
||||||
|
background-color: #fc0303;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button.disabled {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
background-color: #520000;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
#options {
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: left;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container:after {
|
||||||
|
content: attr(label-name);
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label-container {
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated * {
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated .slider:before {
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #fc0303;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(16px);
|
||||||
|
-ms-transform: translateX(16px);
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Boilerplate CSS from https://ajay.app */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreview {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImage {
|
||||||
|
position: absolute;
|
||||||
|
left: -90px;
|
||||||
|
width: 80px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImageLarge {
|
||||||
|
position: absolute;
|
||||||
|
left: -210px;
|
||||||
|
width: 200px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImageLargeRight {
|
||||||
|
position: absolute;
|
||||||
|
right: -210px;
|
||||||
|
width: 200px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.createdBy {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
background-color: #636363;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
font-size: 50px;
|
||||||
|
color: #212121;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
transition: font-size 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #dad8d8;
|
||||||
|
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
transition: font-size 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle:hover {
|
||||||
|
font-size: 45px;
|
||||||
|
|
||||||
|
transition: font-size 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilepic {
|
||||||
|
background-color: #636363 !important;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilepiccircle {
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
height: 80px;
|
||||||
|
|
||||||
|
transition: height 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
height: 95px;
|
||||||
|
|
||||||
|
transition: height 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact,.smalllink {
|
||||||
|
font-size: 25px;
|
||||||
|
color: #e8e8e8;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,li {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #c4c4c4;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,li,code,a {
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: left;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation:portrait) {
|
||||||
|
p,li,code,a {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImage {
|
||||||
|
position: unset;
|
||||||
|
width: 130px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewImage {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recentPostTitle {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recentPostDate {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3,h4,h5,h6 {
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-container:before {
|
||||||
|
content: attr(label-name);
|
||||||
|
padding-right: 4px;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* React styles */
|
||||||
|
|
||||||
|
.categoryTableElement {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryTableElement > * {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionsSelector {
|
||||||
|
background-color: #c00000;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryColorTextBox {
|
||||||
|
width: 60px;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subsidizedPrice {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#discountButton {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ const utils = new Utils({
|
|||||||
unregisterFirefoxContentScript
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
src/components/ChapterVoteComponent.tsx
Normal file
121
src/components/ChapterVoteComponent.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Config from "../config";
|
||||||
|
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||||
|
|
||||||
|
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
||||||
|
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||||
|
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||||
|
import { VoteResponse } from "../messageTypes";
|
||||||
|
import { AnimationUtils } from "../utils/animationUtils";
|
||||||
|
import { GenericUtils } from "../utils/genericUtils";
|
||||||
|
import { Tooltip } from "../render/Tooltip";
|
||||||
|
|
||||||
|
export interface ChapterVoteProps {
|
||||||
|
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChapterVoteState {
|
||||||
|
segment?: SponsorTime;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVoteState> {
|
||||||
|
tooltip?: Tooltip;
|
||||||
|
|
||||||
|
constructor(props: ChapterVoteProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
segment: null,
|
||||||
|
show: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Upvote Button */}
|
||||||
|
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
|
||||||
|
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||||
|
draggable="false"
|
||||||
|
title={chrome.i18n.getMessage("upvoteButtonInfo")}
|
||||||
|
onClick={(e) => this.vote(e, 1)}>
|
||||||
|
<ThumbsUpSvg className="playerButtonImage"
|
||||||
|
fill={Config.config.colorPalette.white}
|
||||||
|
width={"inherit"} height={"inherit"} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Downvote Button */}
|
||||||
|
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
|
||||||
|
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||||
|
draggable="false"
|
||||||
|
title={chrome.i18n.getMessage("reportButtonInfo")}
|
||||||
|
onClick={(e) => {
|
||||||
|
const chapterNode = document.querySelector(".ytp-chapter-container") as HTMLElement;
|
||||||
|
|
||||||
|
if (this.tooltip) {
|
||||||
|
this.tooltip.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
} else {
|
||||||
|
const referenceNode = chapterNode?.parentElement?.parentElement;
|
||||||
|
if (referenceNode) {
|
||||||
|
const outerBounding = referenceNode.getBoundingClientRect();
|
||||||
|
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip = new Tooltip({
|
||||||
|
referenceNode: chapterNode?.parentElement?.parentElement,
|
||||||
|
prependElement: chapterNode?.parentElement,
|
||||||
|
showLogo: false,
|
||||||
|
showGotIt: false,
|
||||||
|
bottomOffset: `${outerBounding.height + 25}px`,
|
||||||
|
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
|
||||||
|
extraClass: "centeredSBTriangle",
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: chrome.i18n.getMessage("incorrectVote"),
|
||||||
|
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
|
||||||
|
this.tooltip?.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
name: chrome.i18n.getMessage("harmfulVote"),
|
||||||
|
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
|
||||||
|
this.tooltip?.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ThumbsDownSvg
|
||||||
|
className="playerButtonImage"
|
||||||
|
fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
|
||||||
|
width={"inherit"}
|
||||||
|
height={"inherit"} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise<void> {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (this.state.segment) {
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(element ?? event.currentTarget as HTMLElement, 0.3);
|
||||||
|
|
||||||
|
const response = await this.props.vote(type, this.state.segment.UUID);
|
||||||
|
await stopAnimation();
|
||||||
|
|
||||||
|
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
|
||||||
|
this.setState({
|
||||||
|
show: type === 1
|
||||||
|
});
|
||||||
|
} else if (response.statusCode !== 403) {
|
||||||
|
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChapterVoteComponent;
|
||||||
@@ -11,6 +11,7 @@ export interface NoticeProps {
|
|||||||
noticeTitle: string,
|
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);
|
||||||
|
|||||||
55
src/components/SelectorComponent.tsx
Normal file
55
src/components/SelectorComponent.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface SelectorOption {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorProps {
|
||||||
|
id: string;
|
||||||
|
options: SelectorOption[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
|
||||||
|
|
||||||
|
constructor(props: SelectorProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div id={this.props.id}
|
||||||
|
className="sbSelector">
|
||||||
|
<div className="sbSelectorBackground">
|
||||||
|
{this.getOptions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): React.ReactElement[] {
|
||||||
|
const result: React.ReactElement[] = [];
|
||||||
|
for (const option of this.props.options) {
|
||||||
|
result.push(
|
||||||
|
<div className="sbSelectorOption"
|
||||||
|
onClick={() => this.props.onChange(option.label)}
|
||||||
|
key={option.label}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectorComponent;
|
||||||
@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
|||||||
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
import 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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
56
src/components/options/ToggleOptionComponent.tsx
Normal file
56
src/components/options/ToggleOptionComponent.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import Config from "../../config";
|
||||||
|
|
||||||
|
export interface ToggleOptionProps {
|
||||||
|
configKey: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleOptionState {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOptionState> {
|
||||||
|
|
||||||
|
constructor(props: ToggleOptionProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
enabled: Config.config[props.configKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="switch-container">
|
||||||
|
<label className="switch">
|
||||||
|
<input id={this.props.configKey}
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.enabled}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onChange={(e) => this.clicked(e)}/>
|
||||||
|
<span className="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<label className="switch-label" htmlFor={this.props.configKey}>
|
||||||
|
{this.props.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
|
Config.config[this.props.configKey] = event.target.checked;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
enabled: event.target.checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleOptionComponent;
|
||||||
@@ -3,12 +3,18 @@ import * as invidiousList from "../ci/invidiouslist.json";
|
|||||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
|
import { 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();
|
||||||
|
|||||||
254
src/content.ts
254
src/content.ts
@@ -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
|
||||||
|
|||||||
@@ -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 ? ' ' : ' ';
|
bar.innerHTML = showLarger ? ' ' : ' ';
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
464
src/popup.ts
464
src/popup.ts
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
63
src/render/ChapterVote.tsx
Normal file
63
src/render/ChapterVote.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
|
||||||
|
import { VoteResponse } from "../messageTypes";
|
||||||
|
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||||
|
|
||||||
|
export class ChapterVote {
|
||||||
|
container: HTMLElement;
|
||||||
|
ref: React.RefObject<ChapterVoteComponent>;
|
||||||
|
|
||||||
|
unsavedState: ChapterVoteState;
|
||||||
|
|
||||||
|
mutationObserver?: MutationObserver;
|
||||||
|
|
||||||
|
constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>) {
|
||||||
|
this.ref = React.createRef();
|
||||||
|
|
||||||
|
this.container = document.createElement('span');
|
||||||
|
this.container.id = "chapterVote";
|
||||||
|
this.container.style.height = "100%";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<ChapterVoteComponent ref={this.ref} vote={vote} />,
|
||||||
|
this.container
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContainer(): HTMLElement {
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
|
this.container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisibility(show: boolean): void {
|
||||||
|
const newState = {
|
||||||
|
show,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.ref.current) {
|
||||||
|
this.ref.current?.setState(newState);
|
||||||
|
} else {
|
||||||
|
this.unsavedState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSegment(segment: SponsorTime): Promise<void> {
|
||||||
|
if (this.ref.current?.state?.segment !== segment) {
|
||||||
|
const newState = {
|
||||||
|
segment,
|
||||||
|
show: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.ref.current) {
|
||||||
|
this.ref.current?.setState(newState);
|
||||||
|
} else {
|
||||||
|
this.unsavedState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
|
|||||||
import Utils from "../utils";
|
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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
22
src/svg-icons/lock_svg.tsx
Normal file
22
src/svg-icons/lock_svg.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const lockSvg = ({
|
||||||
|
fill = "#fcba03",
|
||||||
|
className = "",
|
||||||
|
width = "20",
|
||||||
|
height = "20",
|
||||||
|
onClick
|
||||||
|
}): JSX.Element => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={width}
|
||||||
|
width={height}
|
||||||
|
className={className}
|
||||||
|
fill={fill}
|
||||||
|
onClick={onClick} >
|
||||||
|
<path
|
||||||
|
d="M5.5 18q-.625 0-1.062-.438Q4 17.125 4 16.5v-8q0-.625.438-1.062Q4.875 7 5.5 7H6V5q0-1.667 1.167-2.833Q8.333 1 10 1q1.667 0 2.833 1.167Q14 3.333 14 5v2h.5q.625 0 1.062.438Q16 7.875 16 8.5v8q0 .625-.438 1.062Q15.125 18 14.5 18Zm4.5-4q.625 0 1.062-.438.438-.437.438-1.062t-.438-1.062Q10.625 11 10 11t-1.062.438Q8.5 11.875 8.5 12.5t.438 1.062Q9.375 14 10 14ZM7.5 7h5V5q0-1.042-.729-1.771Q11.042 2.5 10 2.5q-1.042 0-1.771.729Q7.5 3.958 7.5 5Z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default lockSvg;
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import * as React from "react";
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
20
src/types.ts
20
src/types.ts
@@ -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
71
src/upsell.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Config from "./config";
|
||||||
|
import { checkLicenseKey } from "./utils/licenseKey";
|
||||||
|
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||||
|
|
||||||
|
import * as countries from "../public/res/countries.json";
|
||||||
|
|
||||||
|
// This is needed, if Config is not imported before Utils, things break.
|
||||||
|
// Probably due to cyclic dependencies
|
||||||
|
Config.config;
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
localizeHtmlPage();
|
||||||
|
|
||||||
|
const cantAfford = document.getElementById("cantAfford");
|
||||||
|
const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/);
|
||||||
|
cantAfford.appendChild(document.createTextNode(cantAffordTexts[0]));
|
||||||
|
const discountButton = document.createElement("span");
|
||||||
|
discountButton.id = "discountButton";
|
||||||
|
discountButton.innerText = cantAffordTexts[1];
|
||||||
|
cantAfford.appendChild(discountButton);
|
||||||
|
cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
|
||||||
|
|
||||||
|
const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
|
||||||
|
redeemButton.addEventListener("click", async () => {
|
||||||
|
const licenseKey = redeemButton.value;
|
||||||
|
|
||||||
|
if (await checkLicenseKey(licenseKey)) {
|
||||||
|
Config.config.payments.licenseKey = licenseKey;
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
alert(chrome.i18n.getMessage("redeemSuccess"));
|
||||||
|
} else {
|
||||||
|
alert(chrome.i18n.getMessage("redeemFailed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
discountButton.addEventListener("click", async () => {
|
||||||
|
const subsidizedSection = document.getElementById("subsidizedPrice");
|
||||||
|
subsidizedSection.classList.remove("hidden");
|
||||||
|
|
||||||
|
const oldSelector = document.getElementById("countrySelector");
|
||||||
|
if (oldSelector) oldSelector.remove();
|
||||||
|
const countrySelector = document.createElement("select");
|
||||||
|
countrySelector.id = "countrySelector";
|
||||||
|
countrySelector.className = "optionsSelector";
|
||||||
|
const defaultOption = document.createElement("option");
|
||||||
|
defaultOption.innerText = chrome.i18n.getMessage("chooseACountry");
|
||||||
|
countrySelector.appendChild(defaultOption);
|
||||||
|
|
||||||
|
for (const country of Object.keys(countries)) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = country;
|
||||||
|
option.innerText = country;
|
||||||
|
countrySelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
countrySelector.addEventListener("change", () => {
|
||||||
|
if (countries[countrySelector.value]?.allowed) {
|
||||||
|
document.getElementById("subsidizedLink").classList.remove("hidden");
|
||||||
|
document.getElementById("noSubsidizedLink").classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
document.getElementById("subsidizedLink").classList.add("hidden");
|
||||||
|
document.getElementById("noSubsidizedLink").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subsidizedSection.appendChild(countrySelector);
|
||||||
|
});
|
||||||
|
}
|
||||||
68
src/utils.ts
68
src/utils.ts
@@ -30,7 +30,7 @@ export default class Utils {
|
|||||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
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
6
src/utils/arrayUtils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function partition<T>(array: T[], filter: (element: T) => boolean): [T[], T[]] {
|
||||||
|
const pass = [], fail = [];
|
||||||
|
array.forEach((element) => (filter(element) ? pass : fail).push(element));
|
||||||
|
|
||||||
|
return [pass, fail];
|
||||||
|
}
|
||||||
@@ -41,7 +41,13 @@ export function getCategorySuffix(category: Category): string {
|
|||||||
return "_POI";
|
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);
|
||||||
}
|
}
|
||||||
@@ -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
65
src/utils/exporter.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
|
||||||
|
import { shortCategoryName } from "./categoryUtils";
|
||||||
|
import { GenericUtils } from "./genericUtils";
|
||||||
|
|
||||||
|
export function exportTimes(segments: SponsorTime[]): string {
|
||||||
|
let result = "";
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (![ActionType.Full, ActionType.Mute].includes(segment.actionType)
|
||||||
|
&& segment.source !== SponsorSourceType.YouTube) {
|
||||||
|
result += exportTime(segment) + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.replace(/\n$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTime(segment: SponsorTime): string {
|
||||||
|
const name = segment.description || shortCategoryName(segment.category);
|
||||||
|
|
||||||
|
return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
|
||||||
|
segment.segment[1] && segment.segment[0] !== segment.segment[1]
|
||||||
|
? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const result: SponsorTime[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
|
||||||
|
if (match) {
|
||||||
|
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
|
||||||
|
if (startTime) {
|
||||||
|
const specialCharsMatcher = /^(?:\s+seconds?)?[:()-\s]*|(?:\s+at)?[:()-\s]+$/g
|
||||||
|
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
|
||||||
|
let titleRight = null;
|
||||||
|
const split2 = line.split(match[1] || match[0]);
|
||||||
|
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
|
||||||
|
|
||||||
|
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
|
||||||
|
if (title) {
|
||||||
|
const segment: SponsorTime = {
|
||||||
|
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
|
||||||
|
category: "chapter" as Category,
|
||||||
|
actionType: ActionType.Chapter,
|
||||||
|
description: title,
|
||||||
|
source: SponsorSourceType.Local,
|
||||||
|
UUID: GenericUtils.generateUserID() as SegmentUUID
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||||
|
result[result.length - 1].segment[1] = segment.segment[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||||
|
result[result.length - 1].segment[1] = videoDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Function that can be used to wait for a condition before returning. */
|
/** 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
65
src/utils/licenseKey.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import Config from "../config";
|
||||||
|
import Utils from "../utils";
|
||||||
|
import * as CompileConfig from "../../config.json";
|
||||||
|
|
||||||
|
const utils = new Utils();
|
||||||
|
|
||||||
|
export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
|
||||||
|
licenseKey
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (result.ok && JSON.parse(result.responseText).allowed) {
|
||||||
|
Config.config.payments.chaptersAllowed = true;
|
||||||
|
Config.config.payments.lastCheck = Date.now();
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { } //eslint-disable-line no-empty
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchingChaptersAllowed(): Promise<boolean> {
|
||||||
|
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//more than 14 days
|
||||||
|
if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
|
||||||
|
const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
|
||||||
|
|
||||||
|
if (!Config.config.payments.chaptersAllowed) {
|
||||||
|
return licensePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.config.payments.chaptersAllowed) return true;
|
||||||
|
|
||||||
|
if (Config.config.payments.lastCheck === 0) {
|
||||||
|
// Check for free access if no license key, and it is the first time
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||||
|
value: "freeChaptersAccess",
|
||||||
|
userID: Config.config.userID
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (result.ok) {
|
||||||
|
const userInfo = JSON.parse(result.responseText);
|
||||||
|
|
||||||
|
Config.config.payments.lastCheck = Date.now();
|
||||||
|
if (userInfo.freeChaptersAccess) {
|
||||||
|
Config.config.payments.freeAccess = true;
|
||||||
|
Config.config.payments.chaptersAllowed = true;
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { } //eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export function getControls(): HTMLElement | false {
|
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||||
|
import { GenericUtils } from "./genericUtils";
|
||||||
|
|
||||||
|
export function getControls(): HTMLElement {
|
||||||
const controlsSelectors = [
|
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
241
test/exporter.test.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../src/types";
|
||||||
|
import { exportTimes, importTimes } from "../src/utils/exporter";
|
||||||
|
|
||||||
|
describe("Export segments", () => {
|
||||||
|
it("Some segments", () => {
|
||||||
|
const segments: SponsorTime[] = [{
|
||||||
|
segment: [0, 10],
|
||||||
|
category: "chapter" as Category,
|
||||||
|
actionType: ActionType.Chapter,
|
||||||
|
description: "Chapter 1",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "1" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [20, 20],
|
||||||
|
category: "poi_highlight" as Category,
|
||||||
|
actionType: ActionType.Poi,
|
||||||
|
description: "Highlight",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "2" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [30, 40],
|
||||||
|
category: "sponsor" as Category,
|
||||||
|
actionType: ActionType.Skip,
|
||||||
|
description: "Sponsor", // Force a description since chrome is not defined
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "3" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [50, 60],
|
||||||
|
category: "selfpromo" as Category,
|
||||||
|
actionType: ActionType.Mute,
|
||||||
|
description: "Selfpromo",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "4" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [0, 0],
|
||||||
|
category: "selfpromo" as Category,
|
||||||
|
actionType: ActionType.Full,
|
||||||
|
description: "Selfpromo",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "5" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [80, 90],
|
||||||
|
category: "interaction" as Category,
|
||||||
|
actionType: ActionType.Skip,
|
||||||
|
description: "Interaction",
|
||||||
|
source: SponsorSourceType.YouTube,
|
||||||
|
UUID: "6" as SegmentUUID
|
||||||
|
}];
|
||||||
|
|
||||||
|
const result = exportTimes(segments);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
"0:00.000 - 0:10.000 Chapter 1\n" +
|
||||||
|
"0:20.000 Highlight\n" +
|
||||||
|
"0:30.000 - 0:40.000 Sponsor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Import segments", () => {
|
||||||
|
it("1:20 to 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 to 1:21 thing
|
||||||
|
1:25 to 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing 1:20 to 1:21", () => {
|
||||||
|
const input = ` thing 1:20 to 1:21
|
||||||
|
another thing 1:25 to 1:28 ext`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 - 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 - 1:21 thing
|
||||||
|
1:25 - 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 1:21 thing
|
||||||
|
1:25 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 thing", () => {
|
||||||
|
const input = ` 1:20 thing
|
||||||
|
1:25 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20: thing", () => {
|
||||||
|
const input = ` 1:20: thing
|
||||||
|
1:25: another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 (thing)", () => {
|
||||||
|
const input = ` 1:20 (thing)
|
||||||
|
1:25 (another thing)`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing 1:20", () => {
|
||||||
|
const input = ` thing 1:20
|
||||||
|
another thing 1:25`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1:20", () => {
|
||||||
|
const input = ` thing at 1:20
|
||||||
|
another thing at 1:25`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1s", () => {
|
||||||
|
const input = ` thing at 1s
|
||||||
|
another thing at 5s`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [1, 5],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [5, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1 second", () => {
|
||||||
|
const input = ` thing at 1 second
|
||||||
|
another thing at 5 seconds`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [1, 5],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [5, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
665
test/previewBar.test.ts
Normal file
665
test/previewBar.test.ts
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
|
||||||
|
|
||||||
|
describe("createChapterRenderGroups", () => {
|
||||||
|
let previewBar: PreviewBar;
|
||||||
|
beforeEach(() => {
|
||||||
|
previewBar = new PreviewBar(null, null, null, null, true);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Two unrelated times", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [50, 80],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2, 30],
|
||||||
|
originalDuration: 30 - 2
|
||||||
|
}, {
|
||||||
|
segment: [30, 50],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [50, 80],
|
||||||
|
originalDuration: 80 - 50
|
||||||
|
}, {
|
||||||
|
segment: [80, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Small time in bigger time", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2.52, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [20, 25],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2.52],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 20],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [20, 25],
|
||||||
|
originalDuration: 25 - 20
|
||||||
|
}, {
|
||||||
|
segment: [25, 30],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [30, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Same start time", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2.52, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 40],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2.52],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 30],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [30, 40],
|
||||||
|
originalDuration: 40 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [40, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Lots of overlapping segments", () => {
|
||||||
|
previewBar.videoDuration = 315.061;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([
|
||||||
|
{
|
||||||
|
"category": "chapter",
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
49.977
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||||
|
"description": "Start of video"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
2.926,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"locked": 1,
|
||||||
|
"votes": 2,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "chapter",
|
||||||
|
"segment": [
|
||||||
|
14.487,
|
||||||
|
37.133
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||||
|
"description": "Subset of start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
23.450537,
|
||||||
|
34.486084
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "interaction",
|
||||||
|
"segment": [
|
||||||
|
50.015343,
|
||||||
|
56.775314
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b2a85e8cdfbf21dd504babbcaca7f751b55a5a2df8179c1a83a121d0f5d56c0e",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
62.51888,
|
||||||
|
74.33331
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
88.71328,
|
||||||
|
96.05933
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "6c08c092db2b7a31210717cc1f2652e7e97d032e03c82b029a27c81cead1f90c",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
101.50703,
|
||||||
|
115.088326
|
||||||
|
],
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "2db207ad4b7a535a548fab293f4567bf97373997e67aadb47df8f91b673f6e53",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
122.211845,
|
||||||
|
137.42178
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 1,
|
||||||
|
"videoDuration": 0,
|
||||||
|
"userID": "0312cbfa514d9d2dfb737816dc45f52aba7c23f0a3f0911273a6993b2cb57fcc",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
144.08913,
|
||||||
|
160.14084
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
164.22084,
|
||||||
|
170.98082
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "845c4377060d5801f5324f8e1be1e8373bfd9addcf6c68fc5a3c038111b506a3",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
180.56674,
|
||||||
|
189.16516
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "7c6b015687db7800c05756a0fd226fd8d101f5a1e1bfb1e5d97c440331fd6cb7",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
204.10468,
|
||||||
|
211.87865
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "3472e8ee00b5da957377ae32d59ddd3095c2b634c2c0c970dfabfb81d143699f",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
214.92064,
|
||||||
|
222.0186
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 0,
|
||||||
|
"userID": "62a00dffb344d27de7adf8ea32801c2fc0580087dc8d282837923e4bda6a1745",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
233.0754,
|
||||||
|
244.56734
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315,
|
||||||
|
"userID": "dcf7fb0a6c071d5a93273ebcbecaee566e0ff98181057a36ed855e9b92bf25ea",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
260.64053,
|
||||||
|
269.35938
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
301.96
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
295
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
}] as unknown as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
2.926
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
2.926,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"originalDuration": 2.074
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
5,
|
||||||
|
14.487
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
14.487,
|
||||||
|
23.450537
|
||||||
|
],
|
||||||
|
"originalDuration": 22.646
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
23.450537,
|
||||||
|
34.486084
|
||||||
|
],
|
||||||
|
"originalDuration": 11.035546999999998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
34.486084,
|
||||||
|
37.133
|
||||||
|
],
|
||||||
|
"originalDuration": 22.646
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
37.133,
|
||||||
|
49.977
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
49.977,
|
||||||
|
50.015343
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
50.015343,
|
||||||
|
56.775314
|
||||||
|
],
|
||||||
|
"originalDuration": 6.759971
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
56.775314,
|
||||||
|
62.51888
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
62.51888,
|
||||||
|
74.33331
|
||||||
|
],
|
||||||
|
"originalDuration": 11.814429999999994
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
74.33331,
|
||||||
|
88.71328
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
88.71328,
|
||||||
|
96.05933
|
||||||
|
],
|
||||||
|
"originalDuration": 7.346050000000005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
96.05933,
|
||||||
|
101.50703
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
101.50703,
|
||||||
|
115.088326
|
||||||
|
],
|
||||||
|
"originalDuration": 13.581295999999995
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
115.088326,
|
||||||
|
122.211845
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
122.211845,
|
||||||
|
137.42178
|
||||||
|
],
|
||||||
|
"originalDuration": 15.209935000000016
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
137.42178,
|
||||||
|
144.08913
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
144.08913,
|
||||||
|
160.14084
|
||||||
|
],
|
||||||
|
"originalDuration": 16.051709999999986
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
160.14084,
|
||||||
|
164.22084
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
164.22084,
|
||||||
|
170.98082
|
||||||
|
],
|
||||||
|
"originalDuration": 6.759979999999985
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
170.98082,
|
||||||
|
180.56674
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
180.56674,
|
||||||
|
189.16516
|
||||||
|
],
|
||||||
|
"originalDuration": 8.598419999999976
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
189.16516,
|
||||||
|
204.10468
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
204.10468,
|
||||||
|
211.87865
|
||||||
|
],
|
||||||
|
"originalDuration": 7.773969999999991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
211.87865,
|
||||||
|
214.92064
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
214.92064,
|
||||||
|
222.0186
|
||||||
|
],
|
||||||
|
"originalDuration": 7.0979600000000005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
222.0186,
|
||||||
|
233.0754
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
233.0754,
|
||||||
|
244.56734
|
||||||
|
],
|
||||||
|
"originalDuration": 11.49194
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
244.56734,
|
||||||
|
260.64053
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
260.64053,
|
||||||
|
269.35938
|
||||||
|
],
|
||||||
|
"originalDuration": 8.718849999999975
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
269.35938,
|
||||||
|
288.686
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
295
|
||||||
|
],
|
||||||
|
"originalDuration": 6.314000000000021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
295,
|
||||||
|
301.96
|
||||||
|
],
|
||||||
|
"originalDuration": 13.274000000000001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
301.96,
|
||||||
|
315.061
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Multiple overlapping", () => {
|
||||||
|
previewBar.videoDuration = 3615.161;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
"segment": [
|
||||||
|
160,
|
||||||
|
2797.323
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
3432.255
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
3412.413
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
1594.92,
|
||||||
|
1674.286
|
||||||
|
],
|
||||||
|
"category": "sponsor",
|
||||||
|
"unsubmitted": false,
|
||||||
|
"showLarger": false,
|
||||||
|
}
|
||||||
|
] as unknown as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
160,
|
||||||
|
169
|
||||||
|
],
|
||||||
|
"originalDuration": 2637.323
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
1594.92
|
||||||
|
],
|
||||||
|
"originalDuration": 3243.413
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
1594.92,
|
||||||
|
1674.286
|
||||||
|
],
|
||||||
|
"originalDuration": 79.36599999999999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
1674.286,
|
||||||
|
3412.413
|
||||||
|
],
|
||||||
|
"originalDuration": 3243.413
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
3412.413,
|
||||||
|
3432.255
|
||||||
|
],
|
||||||
|
"originalDuration": 3263.255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
3432.255,
|
||||||
|
3615.161
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -31,7 +31,8 @@ module.exports = env => ({
|
|||||||
content: path.join(__dirname, srcDir + 'content.ts'),
|
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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user