Merge pull request #690 from ajayyy/improvements

Highlight category
This commit is contained in:
Ajay Ramachandran
2021-08-19 13:23:26 -04:00
committed by GitHub
28 changed files with 21129 additions and 433 deletions

View File

@@ -2,5 +2,5 @@
"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", "interaction", "intro", "outro", "preview", "music_offtopic"] "categoryList": ["sponsor", "selfpromo", "interaction", "poi_highlight", "intro", "outro", "preview", "music_offtopic"]
} }

View File

@@ -4,6 +4,7 @@
"version": "2.2", "version": "2.2",
"default_locale": "en", "default_locale": "en",
"description": "__MSG_Description__", "description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",
"content_scripts": [{ "content_scripts": [{
"run_at": "document_start", "run_at": "document_start",
"matches": [ "matches": [
@@ -40,8 +41,11 @@
"icons/help.svg", "icons/help.svg",
"icons/report.png", "icons/report.png",
"icons/close.png", "icons/close.png",
"icons/skipIcon.svg",
"icons/refresh.svg", "icons/refresh.svg",
"icons/beep.ogg", "icons/beep.ogg",
"icons/pause.svg",
"icons/stop.svg",
"icons/PlayerInfoIconSponsorBlocker.svg", "icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg", "icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html", "popup.html",

20154
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-preset-env": "^1.7.0", "babel-preset-env": "^1.7.0",
"concurrently": "^5.1.0", "concurrently": "^5.1.0",
"react": "^16.12.0", "react": "^17.0.2",
"react-dom": "^16.12.0" "react-dom": "^17.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/chrome": "0.0.91", "@types/chrome": "0.0.91",

View File

@@ -284,8 +284,17 @@
"skip_category": { "skip_category": {
"message": "Skip {0}?" "message": "Skip {0}?"
}, },
"skip_to_category": {
"message": "Skip to {0}?",
"description": "Used for skipping to things (Skip to Highlight)"
},
"skipped": { "skipped": {
"message": "Skipped" "message": "{0} Skipped",
"description": "Example: Sponsor Skipped"
},
"skipped_to_category": {
"message": "Skipped to {0}",
"description": "Used for skipping to things (Skipped to Highlight)"
}, },
"disableAutoSkip": { "disableAutoSkip": {
"message": "Disable Auto Skip" "message": "Disable Auto Skip"
@@ -347,9 +356,6 @@
"createdBy": { "createdBy": {
"message": "Created By" "message": "Created By"
}, },
"autoSkip": {
"message": "Auto Skip"
},
"showSkipNotice": { "showSkipNotice": {
"message": "Show Notice After A Segment Is Skipped" "message": "Show Notice After A Segment Is Skipped"
}, },
@@ -544,14 +550,20 @@
"category_music_offtopic_short": { "category_music_offtopic_short": {
"message": "Non-Music" "message": "Non-Music"
}, },
"category_poi_highlight": {
"message": "Highlight"
},
"category_poi_highlight_description": {
"message": "The part of the video that most people are looking for. Similar to \"Video starts at x\" comments."
},
"category_livestream_messages": { "category_livestream_messages": {
"message": "Livestream: Donation/Message Readings" "message": "Livestream: Donation/Message Readings"
}, },
"category_livestream_messages_short": { "category_livestream_messages_short": {
"message": "Message Reading" "message": "Message Reading"
}, },
"disable": { "autoSkip": {
"message": "Disable" "message": "Auto Skip"
}, },
"manualSkip": { "manualSkip": {
"message": "Manual Skip" "message": "Manual Skip"
@@ -559,6 +571,18 @@
"showOverlay": { "showOverlay": {
"message": "Show In Seek Bar" "message": "Show In Seek Bar"
}, },
"disable": {
"message": "Disable"
},
"autoSkip_POI": {
"message": "Auto skip to the start"
},
"manualSkip_POI": {
"message": "Ask when video loads"
},
"showOverlay_POI": {
"message": "Show In Seek Bar"
},
"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"
}, },

View File

@@ -106,12 +106,10 @@
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from { opacity: 0; }
to { opacity: 1; }
} }
@keyframes fadeOut { @keyframes fadeOut {
from { opacity: 1; } to { opacity: 0; }
to { opacity: 0; }
} }
.sponsorBlockSpacer { .sponsorBlockSpacer {
@@ -131,17 +129,18 @@
right: 10px; right: 10px;
} }
.sponsorSkipNotice { .sponsorSkipNoticeParent {
min-width: 350px;
max-width: 50%;
background-color: rgba(28, 28, 28, 0.9);
position: absolute; position: absolute;
right: 5px;
bottom: 100px; bottom: 100px;
right: 10px; right: 10px;
}
.sponsorSkipNoticeParent, .sponsorSkipNotice {
min-width: 350px;
max-width: 50%;
border-radius: 5px; border-radius: 5px;
border-spacing: 5px 10px; border-spacing: 5px 10px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
@@ -149,17 +148,34 @@
border-collapse: unset; border-collapse: unset;
} }
.sponsorSkipNotice {
min-width: 350px;
background-color: rgba(28, 28, 28, 0.9);
transition: all 0.1s ease-out;
}
.sponsorSkipNotice .hidden {
display: none;
}
/* For Cloudtube */ /* For Cloudtube */
.sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th { .sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th {
border: none; border: none;
} }
.sponsorSkipNoticeFadeIn { .sponsorSkipNoticeFadeIn {
animation: fadeIn 0.5s; animation: fadeIn 0.5s ease-out;
}
.sponsorSkipNoticeFaded {
opacity: 0.5;
} }
.sponsorSkipNoticeFadeOut { .sponsorSkipNoticeFadeOut {
animation: fadeOut 3s cubic-bezier(0.55, 0.055, 0.675, 0.19); transition: opacity 3s cubic-bezier(0.55, 0.055, 0.675, 0.19);
opacity: 0 !important;
animation: none !important;
} }
.sponsorSkipNotice .sponsorSkipNoticeTimeLeft { .sponsorSkipNotice .sponsorSkipNoticeTimeLeft {
@@ -169,14 +185,28 @@
padding: 2px 5px; padding: 2px 5px;
font-size: 12px; font-size: 12px;
display: flex;
align-items: center;
border: 1px solid #eeeeee; 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 */ /* if two are very close to eachother */
.secondSkipNotice { .secondSkipNotice {
bottom: 250px; bottom: 250px;
}
transition: bottom 0.2s; .noticeLeftIcon {
display: flex;
align-items: center;
} }
.sponsorSkipNotice .sponsorSkipNoticeUnskipSection { .sponsorSkipNotice .sponsorSkipNoticeUnskipSection {
@@ -220,7 +250,9 @@
float: right; float: right;
margin-right: 5px; margin-right: 10px;
display: flex;
align-items: center;
} }
.sponsorSkipNoticeRightButton { .sponsorSkipNoticeRightButton {
@@ -243,7 +275,9 @@
font-weight: bold; font-weight: bold;
color: rgb(235, 235, 235); color: rgb(235, 235, 235);
margin-top: auto;
display: inline-block; display: inline-block;
margin-right: 10px;
} }
.sponsorSkipInfo { .sponsorSkipInfo {
@@ -467,4 +501,21 @@ input::-webkit-inner-spin-button {
.sbChatClose { .sbChatClose {
height: 14px; height: 14px;
cursor: pointer; cursor: pointer;
}
.skipButtonControlBarContainer {
cursor: pointer;
display: flex;
}
.skipButtonControlBarContainer.hidden {
display: none !important;
}
#sbSkipIconControlBarImage {
height: 60%;
top: 0px;
bottom: 0px;
display: block;
margin: auto;
} }

View File

@@ -22,13 +22,13 @@
Thanks for installing SponsorBlock. By using this extension, you agree to the <a href="https://gist.github.com/ajayyy/aa9f8ded2b573d4f73a3ffa0ef74f796">Privacy Policy</a> and <a href="https://gist.github.com/ajayyy/9e8100f069348e0bc062641f34d6af12">Terms of Use</a>. Thanks for installing SponsorBlock. By using this extension, you agree to the <a href="https://gist.github.com/ajayyy/aa9f8ded2b573d4f73a3ffa0ef74f796">Privacy Policy</a> and <a href="https://gist.github.com/ajayyy/9e8100f069348e0bc062641f34d6af12">Terms of Use</a>.
</p> </p>
<p class="projectPreview"> <p>
Come contribute, make some suggestions and help out on <a href="https://discord.gg/QnmVMpU">Discord</a> or on <a href="https://matrix.to/#/#sponsor:ajay.app?via=ajay.app&via=matrix.org&via=mozilla.org">Matrix</a>. Come contribute, make some suggestions and help out on <a href="https://discord.gg/QnmVMpU">Discord</a> or on <a href="https://matrix.to/#/#sponsor:ajay.app?via=ajay.app&via=matrix.org&via=mozilla.org">Matrix</a>.
</p> </p>
<p style="margin-bottom: 0" class="bigText center">Please review the options below</p> <p style="margin-bottom: 0; margin-top: 0" class="bigText center">Please review the options below</p>
<p> <p class="smallText">
Many features are disabled by default. If you want to skip intros, outros, use Invidious, etc., enable them below. Many features are disabled by default. If you want to skip intros, outros, use Invidious, etc., enable them below.
You can also hide/show UI elements. You can also hide/show UI elements.
</p> </p>
@@ -88,7 +88,7 @@
<h1>Can I get a copy of the Database? What happens if you disappear?</h1> <h1>Can I get a copy of the Database? What happens if you disappear?</h1>
<p> <p>
The database is public and available at <a href="https://sponsor.ajay.app/database">https://sponsor.ajay.app/database</a> and the source code is freely available. So, even if something happens to me, your submissions are not lost. The database is public and available at <a href="https://sponsor.ajay.app/database">https://sponsor.ajay.app/database</a>. The source code is freely available. So, even if something happens to me, your submissions are not lost.
</p> </p>
<h1>News and how it is made</h1> <h1>News and how it is made</h1>

View File

@@ -129,8 +129,11 @@ a {
text-decoration: none; text-decoration: none;
} }
p,li {
font-size: 16px;
}
p,li,a { p,li,a {
font-size: 20px;
color: #c4c4c4; color: #c4c4c4;
} }

View File

@@ -41,12 +41,13 @@
id="namedview18" id="namedview18"
showgrid="false" showgrid="false"
inkscape:zoom="0.83098592" inkscape:zoom="0.83098592"
inkscape:cx="-238.41697" inkscape:cx="220.07455"
inkscape:cy="258.22009" inkscape:cy="308.76246"
inkscape:window-x="477" inkscape:window-x="477"
inkscape:window-y="961" inkscape:window-y="961"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg16" /> inkscape:current-layer="svg16"
inkscape:pagecheckerboard="true" />
<defs <defs
id="defs4"> id="defs4">
<style <style

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

59
public/icons/pause.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg6"
sodipodi:docname="pause.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="730"
inkscape:window-height="480"
id="namedview8"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"
id="path4"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

71
public/icons/skipIcon.svg Normal file
View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 565.15 568"
version="1.1"
id="svg16"
sodipodi:docname="skipIcon.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="D:\Dell Data\_Projects\_____SponsorSkip\ignored\svg\SponsorBlocker4.png"
inkscape:export-xdpi="43.436523"
inkscape:export-ydpi="43.436523">
<metadata
id="metadata20">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>LogoSponsorBlocker2</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1001"
id="namedview18"
showgrid="false"
inkscape:zoom="1.6619718"
inkscape:cx="316.31071"
inkscape:cy="330.01409"
inkscape:window-x="477"
inkscape:window-y="961"
inkscape:window-maximized="1"
inkscape:current-layer="svg16"
inkscape:pagecheckerboard="true" />
<defs
id="defs4">
<style
id="style2">.cls-1{fill:red;}.cls-2{fill:#fff;}</style>
</defs>
<title
id="title6">LogoSponsorBlocker2</title>
<path
class="cls-1"
d="m 282.58,568 a 65,65 0 0 1 -34.14,-9.66 C 95.41,463.94 2.54,300.46 0,121 a 64.91,64.91 0 0 1 34,-58.09 522.56,522.56 0 0 1 497.16,0 64.91,64.91 0 0 1 34,58.12 c -2.53,179.43 -95.4,342.91 -248.42,437.3 A 65,65 0 0 1 282.58,568 Z m 0,-548.31 A 502.24,502.24 0 0 0 43.4,80.22 45.27,45.27 0 0 0 19.7,120.75 c 2.44,172.67 91.81,330 239.07,420.83 a 46.19,46.19 0 0 0 47.61,0 C 453.64,450.73 543,293.42 545.45,120.75 A 45.26,45.26 0 0 0 521.75,80.21 502.26,502.26 0 0 0 282.58,19.69 Z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
style="fill:#ffffff"
d="M 284.70508 42.693359 A 479.9 479.9 0 0 0 54.369141 100.41992 A 22.53 22.53 0 0 0 42.669922 120.41992 C 45.069922 290.25992 135.67008 438.63977 270.83008 522.00977 A 22.48 22.48 0 0 0 294.32031 522.00977 C 429.48031 438.63977 520.08047 290.25992 522.48047 120.41992 A 22.53 22.53 0 0 0 510.7793 100.41992 A 479.9 479.9 0 0 0 284.70508 42.693359 z M 188.7168 140.07227 L 312.64844 264.00586 L 188.7168 387.9375 L 159.5918 358.8125 L 254.19336 264.00586 L 159.5918 169.19727 L 188.7168 140.07227 z M 305.625 140.07227 L 429.55859 264.00586 L 305.625 387.9375 L 276.50195 358.8125 L 371.10352 264.00586 L 276.50195 169.19727 L 305.625 140.07227 z "
id="path10" />
<g
id="g825"
transform="translate(-3.86549,36.564644)" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

59
public/icons/stop.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg6"
sodipodi:docname="stop.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="730"
inkscape:window-height="480"
id="namedview8"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M6 6h12v12H6z"
id="path4"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -343,6 +343,9 @@ svg {
.categoryTableElement > * { .categoryTableElement > * {
padding-right: 15px; padding-right: 15px;
}
.categoryTableDescription > * {
padding-bottom: 15px; padding-bottom: 15px;
} }

View File

@@ -32,7 +32,7 @@ chrome.tabs.onUpdated.addListener(function(tabId) {
chrome.runtime.onMessage.addListener(function (request, sender, callback) { chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) { switch(request.message) {
case "openConfig": case "openConfig":
chrome.runtime.openOptionsPage(); chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
return; return;
case "openHelp": case "openHelp":
chrome.tabs.create({url: chrome.runtime.getURL('help/index_en.html')}); chrome.tabs.create({url: chrome.runtime.getURL('help/index_en.html')});

View File

@@ -1,6 +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 CategorySkipOptionsComponent from "./CategorySkipOptionsComponent"; import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
export interface CategoryChooserProps { export interface CategoryChooserProps {
@@ -61,7 +62,7 @@ class CategoryChooserComponent extends React.Component<CategoryChooserProps, Cat
for (const category of CompileConfig.categoryList) { for (const category of CompileConfig.categoryList) {
elements.push( elements.push(
<CategorySkipOptionsComponent category={category} <CategorySkipOptionsComponent category={category as Category}
key={category}> key={category}>
</CategorySkipOptionsComponent> </CategorySkipOptionsComponent>
); );

View File

@@ -1,10 +1,14 @@
import * as React from "react"; import * as React from "react";
import Config from "../config" import Config from "../config"
import { CategorySkipOption } from "../types"; import { Category, CategorySkipOption } from "../types";
import Utils from "../utils";
import { getCategoryActionType } from "../utils/categoryUtils";
const utils = new Utils();
export interface CategorySkipOptionsProps { export interface CategorySkipOptionsProps {
category: string; category: Category;
defaultColor?: string; defaultColor?: string;
defaultPreviewColor?: string; defaultPreviewColor?: string;
} }
@@ -87,7 +91,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</tr> </tr>
<tr id={this.props.category + "DescriptionRow"} <tr id={this.props.category + "DescriptionRow"}
className="small-description"> className="small-description categoryTableDescription">
<td <td
colSpan={2}> colSpan={2}>
{chrome.i18n.getMessage("category_" + this.props.category + "_description")} {chrome.i18n.getMessage("category_" + this.props.category + "_description")}
@@ -149,10 +153,13 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
const optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"]; const optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
console.log(getCategoryActionType(this.props.category))
for (const optionName of optionNames) { for (const optionName of optionNames) {
elements.push( elements.push(
<option key={optionName} value={optionName}> <option key={optionName} value={optionName}>
{chrome.i18n.getMessage(optionName)} {chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategoryActionType(this.props.category)
: optionName)}
</option> </option>
); );
} }

View File

@@ -1,20 +1,34 @@
import * as React from "react"; import * as React from "react";
import Config from "../config"; import Config from "../config";
enum CountdownMode {
Timer,
Paused,
Stopped
}
export interface NoticeProps { export interface NoticeProps {
noticeTitle: string, noticeTitle: string,
maxCountdownTime?: () => number, maxCountdownTime?: () => number,
amountOfPreviousNotices?: number, amountOfPreviousNotices?: number,
showInSecondSlot?: boolean,
timed?: boolean, timed?: boolean,
idSuffix?: string, idSuffix?: string,
videoSpeed?: () => number, videoSpeed?: () => number,
fadeIn?: boolean, fadeIn?: boolean,
startFaded?: boolean,
firstColumn?: React.ReactElement,
firstRow?: React.ReactElement,
bottomRow?: React.ReactElement[],
smaller?: boolean,
// Callback for when this is closed // Callback for when this is closed
closeListener: () => void, closeListener: () => void,
onMouseEnter?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void,
zIndex?: number, zIndex?: number,
style?: React.CSSProperties style?: React.CSSProperties
@@ -26,8 +40,11 @@ export interface NoticeState {
maxCountdownTime: () => number, maxCountdownTime: () => number,
countdownTime: number, countdownTime: number,
countdownText: string, countdownMode: CountdownMode,
countdownManuallyPaused: boolean,
mouseHovering: boolean;
startFaded: boolean;
} }
class NoticeComponent extends React.Component<NoticeProps, NoticeState> { class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
@@ -61,8 +78,10 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
//the countdown until this notice closes //the countdown until this notice closes
countdownTime: maxCountdownTime(), countdownTime: maxCountdownTime(),
countdownText: null, countdownMode: CountdownMode.Timer,
countdownManuallyPaused: false mouseHovering: false,
startFaded: this.props.startFaded ?? false
} }
} }
@@ -77,80 +96,149 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
} }
return ( return (
<table id={"sponsorSkipNotice" + this.idSuffix} <div id={"sponsorSkipNotice" + this.idSuffix}
className={"sponsorSkipObject sponsorSkipNotice" className={"sponsorSkipObject sponsorSkipNoticeParent"
+ (this.props.showInSecondSlot ? " secondSkipNotice" : "")}
onMouseEnter={(e) => this.onMouseEnter(e) }
onMouseLeave={() => this.timerMouseLeave()}
style={noticeStyle} >
<table className={"sponsorSkipObject sponsorSkipNotice"
+ (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "") + (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "")
+ (this.amountOfPreviousNotices > 0 ? " secondSkipNotice" : "")} + (this.state.startFaded ? " sponsorSkipNoticeFaded" : "") } >
style={noticeStyle} <tbody>
onMouseEnter={() => this.timerMouseEnter()}
onMouseLeave={() => this.timerMouseLeave()}>
<tbody>
{/* First row */} {/* First row */}
<tr id={"sponsorSkipNoticeFirstRow" + this.idSuffix}> <tr id={"sponsorSkipNoticeFirstRow" + this.idSuffix}>
{/* Left column */} {/* Left column */}
<td> <td className="noticeLeftIcon">
{/* Logo */} {/* Logo */}
<img id={"sponsorSkipLogo" + this.idSuffix} <img id={"sponsorSkipLogo" + this.idSuffix}
className="sponsorSkipLogo sponsorSkipObject" className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}> src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img> </img>
<span id={"sponsorSkipMessage" + this.idSuffix} <span id={"sponsorSkipMessage" + this.idSuffix}
style={{float: "left"}} style={{float: "left"}}
className="sponsorSkipMessage sponsorSkipObject"> className="sponsorSkipMessage sponsorSkipObject">
{this.state.noticeTitle} {this.state.noticeTitle}
</span>
</td>
{/* Right column */}
<td className="sponsorSkipNoticeRightSection"
style={{top: "11px"}}>
{/* Time left */}
{this.props.timed ? (
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
onClick={() => this.toggleManualPause()}
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
{this.state.countdownText || (this.state.countdownTime + "s")}
</span> </span>
) : ""}
{/* Close button */} {this.props.firstColumn}
<img src={chrome.extension.getURL("icons/close.png")} </td>
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
onClick={() => this.close()}>
</img>
</td>
</tr>
{this.props.children} {this.props.firstRow}
</tbody> {/* Right column */}
</table> <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>
) : ""}
{/* Close button */}
<img src={chrome.extension.getURL("icons/close.png")}
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
onClick={() => this.close()}>
</img>
</td>
</tr>
{this.props.children}
{!this.props.smaller && this.props.bottomRow ?
this.props.bottomRow
: null}
</tbody>
</table>
{/* Add as a hidden table to keep the height constant */}
{this.props.smaller && this.props.bottomRow ?
<table style={{visibility: "hidden", paddingTop: "14px"}}>
<tbody>
{this.props.bottomRow}
</tbody>
</table>
: null}
</div>
); );
} }
getCountdownElements(): React.ReactElement[] {
return [(
<span
id={"skipNoticeTimerText" + this.idSuffix}
key="skipNoticeTimerText"
className={this.state.countdownMode !== CountdownMode.Timer ? "hidden" : ""} >
{this.state.countdownTime + "s"}
</span>
),(
<img
id={"skipNoticeTimerPaused" + this.idSuffix}
key="skipNoticeTimerPaused"
className={this.state.countdownMode !== CountdownMode.Paused ? "hidden" : ""}
src={chrome.runtime.getURL("icons/pause.svg")}
alt={chrome.i18n.getMessage("paused")} />
),(
<img
id={"skipNoticeTimerStopped" + this.idSuffix}
key="skipNoticeTimerStopped"
className={this.state.countdownMode !== CountdownMode.Stopped ? "hidden" : ""}
src={chrome.runtime.getURL("icons/stop.svg")}
alt={chrome.i18n.getMessage("manualPaused")} />
)];
}
onMouseEnter(event: React.MouseEvent<HTMLElement, MouseEvent>): void {
if (this.props.onMouseEnter) this.props.onMouseEnter(event);
this.fadedMouseEnter();
this.timerMouseEnter();
}
fadedMouseEnter(): void {
if (this.state.startFaded) {
this.setState({
startFaded: false
});
}
}
timerMouseEnter(): void { timerMouseEnter(): void {
if (this.state.countdownManuallyPaused) return; if (this.state.countdownMode === CountdownMode.Stopped) return;
this.pauseCountdown(); this.pauseCountdown();
this.setState({
mouseHovering: true
});
} }
timerMouseLeave(): void { timerMouseLeave(): void {
if (this.state.countdownManuallyPaused) return; if (this.state.countdownMode === CountdownMode.Stopped) return;
this.startCountdown(); this.startCountdown();
this.setState({
mouseHovering: false
});
} }
toggleManualPause(): void { toggleManualPause(): void {
this.setState({ this.setState({
countdownManuallyPaused: !this.state.countdownManuallyPaused countdownMode: this.state.countdownMode === CountdownMode.Stopped ? CountdownMode.Timer : CountdownMode.Stopped
}, () => { }, () => {
if (this.state.countdownManuallyPaused) { if (this.state.countdownMode === CountdownMode.Stopped || this.state.mouseHovering) {
this.pauseCountdown(); this.pauseCountdown();
} else { } else {
this.startCountdown(); this.startCountdown();
@@ -207,7 +295,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
//reset countdown and inform the user //reset countdown and inform the user
this.setState({ this.setState({
countdownTime: this.state.maxCountdownTime(), countdownTime: this.state.maxCountdownTime(),
countdownText: this.state.countdownManuallyPaused ? chrome.i18n.getMessage("manualPaused") : chrome.i18n.getMessage("paused") countdownMode: this.state.countdownMode === CountdownMode.Timer ? CountdownMode.Paused : this.state.countdownMode
}); });
this.removeFadeAnimation(); this.removeFadeAnimation();
@@ -221,7 +309,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
this.setState({ this.setState({
countdownTime: this.state.maxCountdownTime(), countdownTime: this.state.maxCountdownTime(),
countdownText: null countdownMode: CountdownMode.Timer
}); });
this.setupInterval(); this.setupInterval();
@@ -243,7 +331,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
this.setState({ this.setState({
countdownTime: this.state.maxCountdownTime(), countdownTime: this.state.maxCountdownTime(),
countdownText: null countdownMode: CountdownMode.Timer
}); });
this.removeFadeAnimation(); this.removeFadeAnimation();

View File

@@ -1,10 +1,12 @@
import * as React from "react"; import * as React from "react";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Config from "../config" import Config from "../config"
import { ContentContainer, SponsorHideType, SponsorTime } from "../types"; import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime } from "../types";
import NoticeComponent from "./NoticeComponent"; import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils";
export enum SkipNoticeAction { export enum SkipNoticeAction {
None, None,
Upvote, Upvote,
@@ -20,7 +22,11 @@ export interface SkipNoticeProps {
// Contains functions and variables from the content script needed by the skip notice // Contains functions and variables from the content script needed by the skip notice
contentContainer: ContentContainer; contentContainer: ContentContainer;
closeListener: () => void closeListener: () => void;
showKeybindHint?: boolean;
smaller: boolean;
unskipTime?: number;
} }
export interface SkipNoticeState { export interface SkipNoticeState {
@@ -41,6 +47,10 @@ export interface SkipNoticeState {
thanksForVotingText?: string; //null until the voting buttons should be hidden thanksForVotingText?: string; //null until the voting buttons should be hidden
actionState?: SkipNoticeAction; actionState?: SkipNoticeAction;
showKeybindHint?: boolean;
smaller?: boolean;
} }
class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> { class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> {
@@ -50,6 +60,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
contentContainer: ContentContainer; contentContainer: ContentContainer;
amountOfPreviousNotices: number; amountOfPreviousNotices: number;
showInSecondSlot: boolean;
audio: HTMLAudioElement; audio: HTMLAudioElement;
idSuffix: string; idSuffix: string;
@@ -70,15 +81,12 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.contentContainer = props.contentContainer; this.contentContainer = props.contentContainer;
this.audio = null; this.audio = null;
const categoryName = chrome.i18n.getMessage(this.segments.length > 1 ? "multipleSegments" const noticeTitle = getSkippingText(this.segments, this.props.autoSkip);
: "category_" + this.segments[0].category + "_short") || chrome.i18n.getMessage("category_" + this.segments[0].category);
let noticeTitle = categoryName + " " + chrome.i18n.getMessage("skipped");
if (!this.autoSkip) {
noticeTitle = chrome.i18n.getMessage("skip_category").replace("{0}", categoryName);
}
//add notice const previousSkipNotices = document.getElementsByClassName("sponsorSkipNoticeParent");
this.amountOfPreviousNotices = document.getElementsByClassName("sponsorSkipNotice").length; this.amountOfPreviousNotices = previousSkipNotices.length;
// If there is at least one already in the first slot
this.showInSecondSlot = previousSkipNotices.length > 0 && [...previousSkipNotices].some(notice => !notice.classList.contains("secondSkipNotice"));
// Sort segments // Sort segments
if (this.segments.length > 1) { if (this.segments.length > 1) {
@@ -109,7 +117,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
choosingCategory: false, choosingCategory: false,
thanksForVotingText: null, thanksForVotingText: null,
actionState: SkipNoticeAction.None actionState: SkipNoticeAction.None,
showKeybindHint: this.props.showKeybindHint ?? true,
smaller: this.props.smaller ?? false
} }
if (!this.autoSkip) { if (!this.autoSkip) {
@@ -132,145 +144,172 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
noticeStyle.transform = "scale(0.8) translate(10%, 10%)"; noticeStyle.transform = "scale(0.8) translate(10%, 10%)";
} }
// If it started out as smaller, always keep the
// skip button there
const firstColumn = this.props.smaller ? (
this.getSkipButton()
) : null;
return ( return (
<NoticeComponent noticeTitle={this.state.noticeTitle} <NoticeComponent noticeTitle={this.state.noticeTitle}
amountOfPreviousNotices={this.amountOfPreviousNotices} amountOfPreviousNotices={this.amountOfPreviousNotices}
showInSecondSlot={this.showInSecondSlot}
idSuffix={this.idSuffix} idSuffix={this.idSuffix}
fadeIn={true} fadeIn={true}
startFaded={false}
timed={true} timed={true}
maxCountdownTime={this.state.maxCountdownTime} maxCountdownTime={this.state.maxCountdownTime}
videoSpeed={() => this.contentContainer().v?.playbackRate} videoSpeed={() => this.contentContainer().v?.playbackRate}
style={noticeStyle} style={noticeStyle}
ref={this.noticeRef} ref={this.noticeRef}
closeListener={() => this.closeListener()}> closeListener={() => this.closeListener()}
smaller={this.state.smaller}
firstColumn={firstColumn}
bottomRow={[...this.getMessageBoxes(), ...this.getBottomRow() ]}
onMouseEnter={() => this.onMouseEnter() } >
{(Config.config.audioNotificationOnSkip) && <audio ref={(source) => { this.audio = source; }}> {(Config.config.audioNotificationOnSkip) && <audio ref={(source) => { this.audio = source; }}>
<source src={chrome.extension.getURL("icons/beep.ogg")} type="audio/ogg"></source> <source src={chrome.extension.getURL("icons/beep.ogg")} type="audio/ogg"></source>
</audio>} </audio>}
</NoticeComponent>
);
}
{/* Text Boxes */} getBottomRow(): JSX.Element[] {
{this.getMessageBoxes()} return [
/* Bottom Row */
{/* Bottom Row */} (<tr id={"sponsorSkipNoticeSecondRow" + this.idSuffix}
<tr id={"sponsorSkipNoticeSecondRow" + this.idSuffix}> key={0}>
{/* Vote Button Container */} {/* Vote Button Container */}
{!this.state.thanksForVotingText ? {!this.state.thanksForVotingText ?
<td id={"sponsorTimesVoteButtonsContainer" + this.idSuffix} <td id={"sponsorTimesVoteButtonsContainer" + this.idSuffix}
className="sponsorTimesVoteButtonsContainer"> className="sponsorTimesVoteButtonsContainer">
{/* Upvote Button */} {/* Upvote Button */}
<img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix} <img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix}
className="sponsorSkipObject voteButton" className="sponsorSkipObject voteButton"
style={{marginRight: "10px"}} style={{marginRight: "10px"}}
src={chrome.extension.getURL("icons/thumbs_up.svg")} src={chrome.extension.getURL("icons/thumbs_up.svg")}
title={chrome.i18n.getMessage("upvoteButtonInfo")} title={chrome.i18n.getMessage("upvoteButtonInfo")}
onClick={() => this.prepAction(SkipNoticeAction.Upvote)}> onClick={() => this.prepAction(SkipNoticeAction.Upvote)}>
</img> </img>
{/* Report Button */} {/* Report Button */}
<img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix} <img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix}
className="sponsorSkipObject voteButton" className="sponsorSkipObject voteButton"
src={chrome.extension.getURL("icons/thumbs_down.svg")} src={chrome.extension.getURL("icons/thumbs_down.svg")}
title={chrome.i18n.getMessage("reportButtonInfo")} title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={() => this.adjustDownvotingState(true)}> onClick={() => this.adjustDownvotingState(true)}>
</img> </img>
</td> </td>
: :
<td id={"sponsorTimesVoteButtonInfoMessage" + this.idSuffix} <td id={"sponsorTimesVoteButtonInfoMessage" + this.idSuffix}
className="sponsorTimesInfoMessage sponsorTimesVoteButtonMessage" className="sponsorTimesInfoMessage sponsorTimesVoteButtonMessage"
style={{marginRight: "10px"}}> style={{marginRight: "10px"}}>
{this.state.thanksForVotingText} {this.state.thanksForVotingText}
</td> </td>
} }
{/* Unskip Button */} {/* Unskip/Skip Button */}
<td className="sponsorSkipNoticeUnskipSection"> {!this.props.smaller ? this.getSkipButton() : null}
<button id={"sponsorSkipUnskipButton" + this.idSuffix}
className="sponsorSkipObject sponsorSkipNoticeButton"
style={{marginLeft: "4px"}}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.unskipText + " (" + Config.config.skipKeybind + ")"} {/* Never show button if autoSkip is enabled */}
{!this.autoSkip ? "" :
<td className="sponsorSkipNoticeRightSection"
key={1}>
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
onClick={this.contentContainer().dontShowNoticeAgain}>
{chrome.i18n.getMessage("Hide")}
</button>
</td>
}
</tr>),
/* Downvote Options Row */
(this.state.downvoting &&
<tr id={"sponsorSkipNoticeDownvoteOptionsRow" + this.idSuffix}
key={2}>
<td id={"sponsorTimesDownvoteOptionsContainer" + this.idSuffix}>
{/* Normal downvote */}
<button className="sponsorSkipObject sponsorSkipNoticeButton"
onClick={() => this.prepAction(SkipNoticeAction.Downvote)}>
{chrome.i18n.getMessage("downvoteDescription")}
</button>
{/* Category vote */}
<button className="sponsorSkipObject sponsorSkipNoticeButton"
onClick={() => this.openCategoryChooser()}>
{chrome.i18n.getMessage("incorrectCategory")}
</button> </button>
</td> </td>
{/* Never show button if autoSkip is enabled */}
{!this.autoSkip ? "" :
<td className="sponsorSkipNoticeRightSection">
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
onClick={this.contentContainer().dontShowNoticeAgain}>
{chrome.i18n.getMessage("Hide")}
</button>
</td>
}
</tr> </tr>
),
{/* Downvote Options Row */} /* Category Chooser Row */
{this.state.downvoting && (this.state.choosingCategory &&
<tr id={"sponsorSkipNoticeDownvoteOptionsRow" + this.idSuffix}> <tr id={"sponsorSkipNoticeCategoryChooserRow" + this.idSuffix}
<td id={"sponsorTimesDownvoteOptionsContainer" + this.idSuffix}> key={3}>
<td>
{/* Category Selector */}
<select id={"sponsorTimeCategories" + this.idSuffix}
className="sponsorTimeCategories"
defaultValue={this.segments[0].category} //Just default to the first segment, as we don't know which they'll choose
ref={this.categoryOptionRef}>
{/* Normal downvote */} {this.getCategoryOptions()}
</select>
{/* Submit Button */}
{this.segments.length === 1 &&
<button className="sponsorSkipObject sponsorSkipNoticeButton" <button className="sponsorSkipObject sponsorSkipNoticeButton"
onClick={() => this.prepAction(SkipNoticeAction.Downvote)}> onClick={() => this.prepAction(SkipNoticeAction.CategoryVote)}>
{chrome.i18n.getMessage("downvoteDescription")}
{chrome.i18n.getMessage("submit")}
</button> </button>
}
</td>
</tr>
),
{/* Category vote */} /* Segment Chooser Row */
<button className="sponsorSkipObject sponsorSkipNoticeButton" (this.state.actionState !== SkipNoticeAction.None &&
onClick={() => this.openCategoryChooser()}> <tr id={"sponsorSkipNoticeSubmissionOptionsRow" + this.idSuffix}
key={4}>
<td id={"sponsorTimesSubmissionOptionsContainer" + this.idSuffix}>
{this.getSubmissionChooser()}
</td>
</tr>
)
];
}
{chrome.i18n.getMessage("incorrectCategory")} getSkipButton(): JSX.Element {
</button> if (this.segments.length > 1
</td> || getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI
|| this.props.unskipTime) {
return (
<span className="sponsorSkipNoticeUnskipSection">
<button id={"sponsorSkipUnskipButton" + this.idSuffix}
className="sponsorSkipObject sponsorSkipNoticeButton"
style={{marginLeft: "4px"}}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
</tr> {this.state.unskipText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")}
} </button>
</span>
{/* Category Chooser Row */} );
{this.state.choosingCategory && }
<tr id={"sponsorSkipNoticeCategoryChooserRow" + this.idSuffix}>
<td>
{/* Category Selector */}
<select id={"sponsorTimeCategories" + this.idSuffix}
className="sponsorTimeCategories"
defaultValue={this.segments[0].category} //Just default to the first segment, as we don't know which they'll choose
ref={this.categoryOptionRef}>
{this.getCategoryOptions()}
</select>
{/* Submit Button */}
{this.segments.length === 1 &&
<button className="sponsorSkipObject sponsorSkipNoticeButton"
onClick={() => this.prepAction(SkipNoticeAction.CategoryVote)}>
{chrome.i18n.getMessage("submit")}
</button>
}
</td>
</tr>
}
{/* Segment Chooser Row */}
{this.state.actionState !== SkipNoticeAction.None &&
<tr id={"sponsorSkipNoticeSubmissionOptionsRow" + this.idSuffix}>
<td id={"sponsorTimesSubmissionOptionsContainer" + this.idSuffix}>
{this.getSubmissionChooser()}
</td>
</tr>
}
</NoticeComponent>
);
} }
getSubmissionChooser(): JSX.Element[] { getSubmissionChooser(): JSX.Element[] {
@@ -289,6 +328,14 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
return elements; return elements;
} }
onMouseEnter(): void {
if (this.state.smaller) {
this.setState({
smaller: false
});
}
}
prepAction(action: SkipNoticeAction): void { prepAction(action: SkipNoticeAction): void {
if (this.segments.length === 1) { if (this.segments.length === 1) {
this.performAction(0, action); this.performAction(0, action);
@@ -299,14 +346,15 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
} }
getMessageBoxes(): JSX.Element[] | JSX.Element { getMessageBoxes(): JSX.Element[] {
if (this.state.messages.length === 0) { if (this.state.messages.length === 0) {
// Add a spacer if there is no text // Add a spacer if there is no text
return ( return [
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix} <tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
className="sponsorBlockSpacer"> className="sponsorBlockSpacer"
key={"messageBoxSpacer"}>
</tr> </tr>
); ];
} }
const elements: JSX.Element[] = []; const elements: JSX.Element[] = [];
@@ -344,7 +392,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.contentContainer().vote(0, this.segments[index].UUID, undefined, this); this.contentContainer().vote(0, this.segments[index].UUID, undefined, this);
break; break;
case SkipNoticeAction.CategoryVote: case SkipNoticeAction.CategoryVote:
this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value, this) this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this)
break; break;
case SkipNoticeAction.Unskip: case SkipNoticeAction.Unskip:
this.state.unskipCallback(index); this.state.unskipCallback(index);
@@ -391,7 +439,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
getCategoryOptions(): React.ReactElement[] { getCategoryOptions(): React.ReactElement[] {
const elements = []; const elements = [];
for (const category of CompileConfig.categoryList) { const categories = CompileConfig.categoryList.filter((cat => getCategoryActionType(cat as Category) === CategoryActionType.Skippable));
for (const category of categories) {
elements.push( elements.push(
<option value={category} <option value={category}
key={category}> key={category}>
@@ -404,7 +453,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
unskip(index: number): void { unskip(index: number): void {
this.contentContainer().unskipSponsorTime(this.segments[index]); this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime);
this.unskippedMode(index, chrome.i18n.getMessage("reskip")); this.unskippedMode(index, chrome.i18n.getMessage("reskip"));
} }
@@ -418,12 +467,14 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
} }
getUnskippedModeInfo(index: number, buttonText: string): SkipNoticeState { getUnskippedModeInfo(index: number, buttonText: string): SkipNoticeState {
const maxCountdownTime = () => { const changeCountdown = getCategoryActionType(this.segments[index].category) === CategoryActionType.Skippable;
const maxCountdownTime = changeCountdown ? () => {
const sponsorTime = this.segments[index]; const sponsorTime = this.segments[index];
const duration = Math.round((sponsorTime.segment[1] - this.contentContainer().v.currentTime) * (1 / this.contentContainer().v.playbackRate)); const duration = Math.round((sponsorTime.segment[1] - this.contentContainer().v.currentTime) * (1 / this.contentContainer().v.playbackRate));
return Math.max(duration, Config.config.skipNoticeDuration); return Math.max(duration, Config.config.skipNoticeDuration);
}; } : this.state.maxCountdownTime;
return { return {
unskipText: buttonText, unskipText: buttonText,
@@ -456,7 +507,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
}); });
} }
afterVote(segment: SponsorTime, type: number, category: string): void { afterVote(segment: SponsorTime, type: number, category: Category): void {
this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); this.addVoteButtonInfo(chrome.i18n.getMessage("voted"));
if (type === 0) { if (type === 0) {

View File

@@ -4,8 +4,9 @@ import Config from "../config";
import * as CompileConfig from "../../config.json"; import * as CompileConfig from "../../config.json";
import Utils from "../utils"; import Utils from "../utils";
import { ContentContainer, SponsorTime } from "../types"; import { Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { getCategoryActionType } from "../utils/categoryUtils";
const utils = new Utils(); const utils = new Utils();
export interface SponsorTimeEditProps { export interface SponsorTimeEditProps {
@@ -16,6 +17,7 @@ export interface SponsorTimeEditProps {
contentContainer: ContentContainer, contentContainer: ContentContainer,
submissionNotice: SubmissionNoticeComponent; submissionNotice: SubmissionNoticeComponent;
categoryList?: Category[];
} }
export interface SponsorTimeEditState { export interface SponsorTimeEditState {
@@ -106,43 +108,47 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
onChange={(e) => { onChange={(e) => {
const sponsorTimeEdits = this.state.sponsorTimeEdits; const sponsorTimeEdits = this.state.sponsorTimeEdits;
sponsorTimeEdits[0] = e.target.value; sponsorTimeEdits[0] = e.target.value;
if (getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTimeEdits[1] = e.target.value;
this.setState({sponsorTimeEdits}); this.setState({sponsorTimeEdits});
this.saveEditTimes(); this.saveEditTimes();
}}> }}>
</input> </input>
<span> {getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable ? (
{" " + chrome.i18n.getMessage("to") + " "} <span>
</span> <span>
{" " + chrome.i18n.getMessage("to") + " "}
</span>
<input id={"submittingTime1" + this.idSuffix} <input id={"submittingTime1" + this.idSuffix}
className="sponsorTimeEdit sponsorTimeEditInput" className="sponsorTimeEdit sponsorTimeEditInput"
ref={oldYouTubeDarkStyles} ref={oldYouTubeDarkStyles}
type="text" type="text"
value={this.state.sponsorTimeEdits[1]} value={this.state.sponsorTimeEdits[1]}
onChange={(e) => { onChange={(e) => {
const sponsorTimeEdits = this.state.sponsorTimeEdits; const sponsorTimeEdits = this.state.sponsorTimeEdits;
sponsorTimeEdits[1] = e.target.value; sponsorTimeEdits[1] = e.target.value;
this.setState({sponsorTimeEdits}); this.setState({sponsorTimeEdits});
this.saveEditTimes(); this.saveEditTimes();
}}> }}>
</input> </input>
<span id={"nowButton1" + this.idSuffix} <span id={"nowButton1" + this.idSuffix}
className="sponsorNowButton" className="sponsorNowButton"
onClick={() => this.setTimeToNow(1)}> onClick={() => this.setTimeToNow(1)}>
{chrome.i18n.getMessage("bracketNow")} {chrome.i18n.getMessage("bracketNow")}
</span> </span>
<span id={"endButton" + this.idSuffix} <span id={"endButton" + this.idSuffix}
className="sponsorNowButton" className="sponsorNowButton"
onClick={() => this.setTimeToEnd()}> onClick={() => this.setTimeToEnd()}>
{chrome.i18n.getMessage("bracketEnd")} {chrome.i18n.getMessage("bracketEnd")}
</span> </span>
</span>
): ""}
</div> </div>
); );
} else { } else {
@@ -151,7 +157,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
className="sponsorTimeDisplay" className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}> onClick={this.toggleEditTime.bind(this)}>
{utils.getFormattedTime(segment[0], true) + {utils.getFormattedTime(segment[0], true) +
((!isNaN(segment[1])) ? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")} ((!isNaN(segment[1]) && getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable)
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
</div> </div>
); );
} }
@@ -191,7 +198,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
{chrome.i18n.getMessage("delete")} {chrome.i18n.getMessage("delete")}
</span> </span>
{(!isNaN(segment[1])) ? ( {(!isNaN(segment[1]) && getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable) ? (
<span id={"sponsorTimePreviewButton" + this.idSuffix} <span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton" className="sponsorTimeEditButton"
onClick={this.previewTime.bind(this)}> onClick={this.previewTime.bind(this)}>
@@ -226,7 +233,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</option> </option>
)]; )];
for (const category of CompileConfig.categoryList) { for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
elements.push( elements.push(
<option value={category} <option value={category}
key={category}> key={category}>
@@ -248,11 +255,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst") if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst")
.replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) { .replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) {
// Open options page // Open options page
chrome.runtime.sendMessage({"message": "openConfig"}); chrome.runtime.sendMessage({message: "openConfig", hash: chosenCategory + "OptionsName"});
} }
return; return;
} }
if (getCategoryActionType(event.target.value as Category) === CategoryActionType.POI) {
this.setTimeTo(1, null);
this.props.contentContainer().updateEditButtonsOnPlayer();
}
this.saveEditTimes(); this.saveEditTimes();
} }
@@ -265,11 +277,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.setTimeTo(1, this.props.contentContainer().v.duration); this.setTimeTo(1, this.props.contentContainer().v.duration);
} }
/**
* @param index
* @param time If null, will set time to the first index's time
*/
setTimeTo(index: number, time: number): void { setTimeTo(index: number, time: number): void {
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index]; const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
if (time === null) time = sponsorTime.segment[0];
sponsorTime.segment[index] = sponsorTime.segment[index] = time;
time; if (getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTime.segment[1] = time;
this.setState({ this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime) sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
@@ -313,7 +330,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
} }
} }
sponsorTimesSubmitting[this.props.index].category = this.categoryOptionRef.current.value; sponsorTimesSubmitting[this.props.index].category = this.categoryOptionRef.current.value as Category;
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting); Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);

View File

@@ -12,7 +12,7 @@ export interface SubmissionNoticeProps {
callback: () => unknown; callback: () => unknown;
closeListener: () => void closeListener: () => void;
} }
export interface SubmissionNoticeeState { export interface SubmissionNoticeeState {

View File

@@ -1,14 +1,11 @@
import * as CompileConfig from "../config.json"; import * as CompileConfig from "../config.json";
import { CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types"; import { Category, CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types";
import Utils from "./utils";
const utils = new Utils();
interface SBConfig { interface SBConfig {
userID: string, userID: string,
/** Contains unsubmitted segments that the user has created. */ /** Contains unsubmitted segments that the user has created. */
segmentTimes: SBMap<string, SponsorTime[]>, segmentTimes: SBMap<string, SponsorTime[]>,
defaultCategory: string, defaultCategory: Category,
whitelistedChannels: string[], whitelistedChannels: string[],
forceChannelCheck: boolean, forceChannelCheck: boolean,
skipKeybind: string, skipKeybind: string,
@@ -63,6 +60,8 @@ interface SBConfig {
"preview-preview": PreviewBarOption, "preview-preview": PreviewBarOption,
"music_offtopic": PreviewBarOption, "music_offtopic": PreviewBarOption,
"preview-music_offtopic": PreviewBarOption, "preview-music_offtopic": PreviewBarOption,
"poi_highlight": PreviewBarOption,
"preview-poi_highlight": PreviewBarOption,
} }
} }
@@ -148,7 +147,7 @@ const Config: SBObject = {
defaults: { defaults: {
userID: null, userID: null,
segmentTimes: new SBMap("segmentTimes"), segmentTimes: new SBMap("segmentTimes"),
defaultCategory: "chooseACategory", defaultCategory: "chooseACategory" as Category,
whitelistedChannels: [], whitelistedChannels: [],
forceChannelCheck: false, forceChannelCheck: false,
skipKeybind: "Enter", skipKeybind: "Enter",
@@ -184,7 +183,7 @@ const Config: SBObject = {
autoSkipOnMusicVideos: false, autoSkipOnMusicVideos: false,
categorySelections: [{ categorySelections: [{
name: "sponsor", name: "sponsor" as Category,
option: CategorySkipOption.AutoSkip option: CategorySkipOption.AutoSkip
}], }],
@@ -249,6 +248,14 @@ const Config: SBObject = {
"preview-music_offtopic": { "preview-music_offtopic": {
color: "#a6634a", color: "#a6634a",
opacity: "0.7" opacity: "0.7"
},
"poi_highlight": {
color: "#ff1684",
opacity: "0.7"
},
"preview-poi_highlight": {
color: "#9b044c",
opacity: "0.7"
} }
} }
}, },
@@ -345,6 +352,17 @@ function fetchConfig(): Promise<void> {
} }
function migrateOldFormats(config: SBConfig) { function migrateOldFormats(config: SBConfig) {
if (!config["highlightCategoryAdded"] && !config.categorySelections.some((s) => s.name === "poi_highlight")) {
config["highlightCategoryAdded"] = true;
config.categorySelections.push({
name: "poi_highlight" as Category,
option: CategorySkipOption.ManualSkip
});
config.categorySelections = config.categorySelections;
}
if (config["askAboutUnlistedVideos"]) { if (config["askAboutUnlistedVideos"]) {
chrome.storage.sync.remove("askAboutUnlistedVideos"); chrome.storage.sync.remove("askAboutUnlistedVideos");
} }
@@ -361,25 +379,6 @@ function migrateOldFormats(config: SBConfig) {
} }
} }
// Adding preview category
if (!config["previewCategoryUpdate"]) {
config["previewCategoryUpdate"] = true;
for (const selection of config.categorySelections) {
if (selection.name === "intro"
&& selection.option === CategorySkipOption.AutoSkip || selection.option === CategorySkipOption.ManualSkip) {
// Add a default skip option for preview category
config.categorySelections.push({
name: "preview",
option: CategorySkipOption.ManualSkip
});
// Ensure it gets updated
config.categorySelections = config.categorySelections;
break;
}
}
}
if (config["disableAutoSkip"]) { if (config["disableAutoSkip"]) {
for (const selection of config.categorySelections) { for (const selection of config.categorySelections) {
if (selection.name === "sponsor") { if (selection.name === "sponsor") {
@@ -390,100 +389,6 @@ function migrateOldFormats(config: SBConfig) {
} }
} }
// Auto vote removal
if (config["autoUpvote"]) {
chrome.storage.sync.remove("autoUpvote");
}
// mobileUpdateShowCount removal
if (config["mobileUpdateShowCount"] !== undefined) {
chrome.storage.sync.remove("mobileUpdateShowCount");
}
// categoryUpdateShowCount removal
if (config["categoryUpdateShowCount"] !== undefined) {
chrome.storage.sync.remove("categoryUpdateShowCount");
}
// Channel URLS
if (config.whitelistedChannels.length > 0 &&
(config.whitelistedChannels[0] == null || config.whitelistedChannels[0].includes("/"))) {
const channelURLFixer = async() => {
const newChannelList: string[] = [];
for (const item of config.whitelistedChannels) {
if (item != null) {
if (item.includes("/channel/")) {
newChannelList.push(item.split("/")[2]);
} else if (item.includes("/user/") && utils.isContentScript()) {
// Replace channel URL with channelID
const response = await utils.asyncRequestToCustomServer("GET", "https://sponsor.ajay.app/invidious/api/v1/channels/" + item.split("/")[2] + "?fields=authorId");
if (response.ok) {
newChannelList.push((JSON.parse(response.responseText)).authorId);
} else {
// Add it at the beginning so it gets converted later
newChannelList.unshift(item);
}
} else if (item.includes("/user/")) {
// Add it at the beginning so it gets converted later (The API can only be called in the content script due to CORS issues)
newChannelList.unshift(item);
} else {
newChannelList.push(item);
}
}
}
config.whitelistedChannels = newChannelList;
}
channelURLFixer();
}
// Check if off-topic category needs to be removed
for (let i = 0; i < config.categorySelections.length; i++) {
if (config.categorySelections[i].name === "offtopic") {
config.categorySelections.splice(i, 1);
// Call set listener
config.categorySelections = config.categorySelections;
break;
}
}
// Migrate old "sponsorTimes"
if (config["sponsorTimes"]) {
let jsonData: unknown = config["sponsorTimes"];
// Check if data is stored in the old format for SBMap (a JSON string)
if (typeof jsonData === "string") {
try {
jsonData = JSON.parse(jsonData);
} catch(e) {
// Continue normally (out of this if statement)
}
}
// Otherwise junk data
if (Array.isArray(jsonData)) {
const oldMap = new Map(jsonData);
oldMap.forEach((sponsorTimes: [number, number][], key) => {
const segmentTimes: SponsorTime[] = [];
for (const segment of sponsorTimes) {
segmentTimes.push({
segment: segment,
category: "sponsor",
UUID: null
});
}
config.segmentTimes.rawSet(key, segmentTimes);
});
config.segmentTimes.update();
}
chrome.storage.sync.remove("sponsorTimes");
}
// Remove some old unused options // Remove some old unused options
if (config["sponsorVideoID"] !== undefined) { if (config["sponsorVideoID"] !== undefined) {
chrome.storage.sync.remove("sponsorVideoID"); chrome.storage.sync.remove("sponsorVideoID");

View File

@@ -1,6 +1,5 @@
import Config from "./config"; import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable } from "./types";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType } from "./types";
import { ContentContainer } from "./types"; import { ContentContainer } from "./types";
import Utils from "./utils"; import Utils from "./utils";
@@ -14,6 +13,8 @@ import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice"; import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse } from "./messageTypes"; import { Message, MessageResponse } from "./messageTypes";
import * as Chat from "./js-components/chat"; import * as Chat from "./js-components/chat";
import { getCategoryActionType } from "./utils/categoryUtils";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
// Hack to get the CSS loaded on permission-based sites (Invidious) // Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS); utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -26,6 +27,7 @@ let sponsorTimes: SponsorTime[] = null;
let sponsorVideoID: VideoID = null; let sponsorVideoID: VideoID = null;
// List of open skip notices // List of open skip notices
const skipNotices: SkipNotice[] = []; const skipNotices: SkipNotice[] = [];
let activeSkipKeybindElement: ToggleSkippable = null;
// JSON video info // JSON video info
let videoInfo: VideoInfo = null; let videoInfo: VideoInfo = null;
@@ -69,6 +71,7 @@ let channelWhitelisted = false;
// create preview bar // create preview bar
let previewBar: PreviewBar = null; let previewBar: PreviewBar = null;
let skipButtonControlBar: SkipButtonControlBar = null;
/** Element containing the player controls on the YouTube player. */ /** Element containing the player controls on the YouTube player. */
let controls: HTMLElement | null = null; let controls: HTMLElement | null = null;
@@ -449,7 +452,12 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (incorrectVideoCheck(videoID, currentSkip)) return; if (incorrectVideoCheck(videoID, currentSkip)) return;
if (video.currentTime >= skipTime[0] && video.currentTime < skipTime[1]) { if (video.currentTime >= skipTime[0] && video.currentTime < skipTime[1]) {
skipToTime(video, skipTime, skippingSegments, skipInfo.openNotice); skipToTime({
v: video,
skipTime,
skippingSegments,
openNotice: skipInfo.openNotice
});
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip) { if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip) {
forcedSkipTime = skipTime[0] + 0.001; forcedSkipTime = skipTime[0] + 0.001;
@@ -511,6 +519,7 @@ function refreshVideoAttachments() {
videosWithEventListeners.push(video); videosWithEventListeners.push(video);
setupVideoListeners(); setupVideoListeners();
setupSkipButtonControlBar();
} }
} }
} }
@@ -562,6 +571,22 @@ function setupVideoListeners() {
startSponsorSchedule(); startSponsorSchedule();
} }
if (!Config.config.dontShowNotice) {
const currentPoiSegment = sponsorTimes.find((segment) =>
getCategoryActionType(segment.category) === CategoryActionType.POI &&
video.currentTime - segment.segment[0] > 0 &&
video.currentTime - segment.segment[0] < video.duration * 0.006); // Approximate size on preview bar
if (currentPoiSegment && !skipNotices.some((notice) => notice.segments.some((s) => s.UUID === currentPoiSegment.UUID))) {
skipToTime({
v: video,
skipTime: currentPoiSegment.segment,
skippingSegments: [currentPoiSegment],
openNotice: true,
forceAutoSkip: true
});
}
}
}); });
video.addEventListener('ratechange', () => startSponsorSchedule()); video.addEventListener('ratechange', () => startSponsorSchedule());
// Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740) // Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740)
@@ -578,6 +603,22 @@ function setupVideoListeners() {
} }
} }
function setupSkipButtonControlBar() {
if (!skipButtonControlBar) {
skipButtonControlBar = new SkipButtonControlBar({
skip: (segment) => skipToTime({
v: video,
skipTime: segment.segment,
skippingSegments: [segment],
openNotice: true,
forceAutoSkip: true
})
});
}
skipButtonControlBar.attachToPage();
}
async function sponsorsLookup(id: string, keepOldSubmissions = true) { async function sponsorsLookup(id: string, keepOldSubmissions = true) {
if (!video) refreshVideoAttachments(); if (!video) refreshVideoAttachments();
//there is still no video here //there is still no video here
@@ -707,24 +748,47 @@ function retryFetch(): void {
function startSkipScheduleCheckingForStartSponsors() { function startSkipScheduleCheckingForStartSponsors() {
if (!switchingVideos) { if (!switchingVideos) {
// See if there are any starting sponsors // See if there are any starting sponsors
let startingSponsor = -1; let startingSegmentTime = -1;
let startingSegment: SponsorTime = null;
for (const time of sponsorTimes) { for (const time of sponsorTimes) {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSponsor && time.segment[1] > video.currentTime) { if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
startingSponsor = time.segment[0]; && getCategoryActionType(time.category) === CategoryActionType.Skippable) {
startingSegmentTime = time.segment[0];
startingSegment = time;
break; break;
} }
} }
if (startingSponsor === -1) { if (startingSegmentTime === -1) {
for (const time of sponsorTimesSubmitting) { for (const time of sponsorTimesSubmitting) {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSponsor && time.segment[1] > video.currentTime) { if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
startingSponsor = time.segment[0]; && getCategoryActionType(time.category) === CategoryActionType.Skippable) {
startingSegmentTime = time.segment[0];
startingSegment = time;
break; break;
} }
} }
} }
if (startingSponsor !== -1) { // For highlight category
startSponsorSchedule(undefined, startingSponsor); const poiSegments = sponsorTimes
.filter((time) => time.segment[1] > video.currentTime && getCategoryActionType(time.category) === CategoryActionType.POI)
.sort((a, b) => b.segment[0] - a.segment[0]);
for (const time of poiSegments) {
const skipOption = utils.getCategorySelection(time.category)?.option;
if (skipOption !== CategorySkipOption.ShowOverlay) {
skipToTime({
v: video,
skipTime: time.segment,
skippingSegments: [time],
openNotice: true,
unskipTime: video.currentTime
});
if (skipOption === CategorySkipOption.AutoSkip) break;
}
}
if (startingSegmentTime !== -1) {
startSponsorSchedule(undefined, startingSegmentTime);
} else { } else {
startSponsorSchedule(); startSponsorSchedule();
} }
@@ -811,7 +875,6 @@ function updatePreviewBar(): void {
if (video === null) return; if (video === null) return;
const previewBarSegments: PreviewBarSegment[] = []; const previewBarSegments: PreviewBarSegment[] = [];
if (sponsorTimes) { if (sponsorTimes) {
sponsorTimes.forEach((segment) => { sponsorTimes.forEach((segment) => {
if (segment.hidden !== SponsorHideType.Visible) return; if (segment.hidden !== SponsorHideType.Visible) return;
@@ -820,6 +883,7 @@ function updatePreviewBar(): void {
segment: segment.segment as [number, number], segment: segment.segment as [number, number],
category: segment.category, category: segment.category,
unsubmitted: false, unsubmitted: false,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
}); });
}); });
} }
@@ -829,6 +893,7 @@ function updatePreviewBar(): void {
segment: segment.segment as [number, number], segment: segment.segment as [number, number],
category: segment.category, category: segment.category,
unsubmitted: true, unsubmitted: true,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
}); });
}); });
@@ -977,7 +1042,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|| ((includeNonIntersectingSegments && sponsorTimes[i].segment[0] >= minimum) || ((includeNonIntersectingSegments && sponsorTimes[i].segment[0] >= minimum)
|| (includeIntersectingSegments && sponsorTimes[i].segment[0] < minimum && sponsorTimes[i].segment[1] > minimum))) || (includeIntersectingSegments && sponsorTimes[i].segment[0] < minimum && sponsorTimes[i].segment[1] > minimum)))
&& (!onlySkippableSponsors || utils.getCategorySelection(sponsorTimes[i].category).option !== CategorySkipOption.ShowOverlay) && (!onlySkippableSponsors || utils.getCategorySelection(sponsorTimes[i].category).option !== CategorySkipOption.ShowOverlay)
&& (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible)) { && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible)
&& getCategoryActionType(sponsorTimes[i].category) === CategoryActionType.Skippable) {
startTimes.push(sponsorTimes[i].segment[0]); startTimes.push(sponsorTimes[i].segment[0]);
} }
@@ -1021,9 +1087,9 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped:
} }
//skip from the start time to the end time for a certain index sponsor time //skip from the start time to the end time for a certain index sponsor time
function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: SponsorTime[], openNotice: boolean) { function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, unskipTime}: SkipToTimeParams): void {
// There will only be one submission if it is manual skip // There will only be one submission if it is manual skip
const autoSkip: boolean = shouldAutoSkip(skippingSegments[0]); const autoSkip: boolean = forceAutoSkip || shouldAutoSkip(skippingSegments[0]);
if ((autoSkip || sponsorTimesSubmitting.includes(skippingSegments[0])) && v.currentTime !== skipTime[1]) { if ((autoSkip || sponsorTimesSubmitting.includes(skippingSegments[0])) && v.currentTime !== skipTime[1]) {
// Fix for looped videos not working when skipping to the end #426 // Fix for looped videos not working when skipping to the end #426
@@ -1035,10 +1101,23 @@ function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: S
} }
} }
if (openNotice) { if (!autoSkip
//send out the message saying that a sponsor message was skipped && skippingSegments.length === 1
if (!Config.config.dontShowNotice || !autoSkip) { && getCategoryActionType(skippingSegments[0].category) === CategoryActionType.POI) {
skipNotices.push(new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer)); skipButtonControlBar.enable(skippingSegments[0]);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar;
} else {
if (openNotice) {
//send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) {
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime);
skipNotices.push(newSkipNotice);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = newSkipNotice;
}
} }
} }
@@ -1046,11 +1125,10 @@ function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: S
if (autoSkip) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true); if (autoSkip) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
} }
function unskipSponsorTime(segment: SponsorTime) { function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null) {
if (sponsorTimes != null) { //add a tiny bit of time to make sure it is not skipped again
//add a tiny bit of time to make sure it is not skipped again console.log(unskipTime)
video.currentTime = segment.segment[0] + 0.001; video.currentTime = unskipTime ?? segment.segment[0] + 0.001;
}
} }
function reskipSponsorTime(segment: SponsorTime) { function reskipSponsorTime(segment: SponsorTime) {
@@ -1189,7 +1267,7 @@ function updateEditButtonsOnPlayer(): void {
creatingSegment = isSegmentCreationInProgress(); creatingSegment = isSegmentCreationInProgress();
// Show only if there are any segments to submit // Show only if there are any segments to submit
submitButtonVisible = sponsorTimesSubmitting.length > 1 || (sponsorTimesSubmitting.length > 0 && !creatingSegment); submitButtonVisible = sponsorTimesSubmitting.length > 0;
// Show only if there are any segments to delete // Show only if there are any segments to delete
deleteButtonVisible = sponsorTimesSubmitting.length > 1 || (sponsorTimesSubmitting.length > 0 && !creatingSegment); deleteButtonVisible = sponsorTimesSubmitting.length > 1 || (sponsorTimesSubmitting.length > 0 && !creatingSegment);
@@ -1427,7 +1505,7 @@ function clearSponsorTimes() {
} }
//if skipNotice is null, it will not affect the UI //if skipNotice is null, it will not affect the UI
function vote(type: number, UUID: string, category?: string, skipNotice?: SkipNoticeComponent) { function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) {
if (skipNotice !== null && skipNotice !== undefined) { if (skipNotice !== null && skipNotice !== undefined) {
//add loading info //add loading info
skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading")) skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading"))
@@ -1631,9 +1709,8 @@ function hotkeyListener(e: KeyboardEvent): void {
switch (key) { switch (key) {
case skipKey: case skipKey:
if (skipNotices.length > 0) { if (activeSkipKeybindElement) {
const latestSkipNotice = skipNotices[skipNotices.length - 1]; activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
latestSkipNotice.toggleSkip.call(latestSkipNotice);
} }
break; break;
case startSponsorKey: case startSponsorKey:

View File

@@ -15,6 +15,7 @@ export interface PreviewBarSegment {
segment: [number, number]; segment: [number, number];
category: string; category: string;
unsubmitted: boolean; unsubmitted: boolean;
showLarger: boolean;
} }
class PreviewBar { class PreviewBar {
@@ -102,9 +103,10 @@ class PreviewBar {
let currentSegmentLength = Infinity; let currentSegmentLength = Infinity;
for (const seg of this.segments) { for (const seg of this.segments) {
if (seg.segment[0] <= timeInSeconds && seg.segment[1] > timeInSeconds) { const segmentLength = seg.segment[1] - seg.segment[0];
const segmentLength = seg.segment[1] - seg.segment[0]; const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
const endTime = segmentLength !== 0 ? seg.segment[1] : Math.ceil(seg.segment[1]);
if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
if (segmentLength < currentSegmentLength) { if (segmentLength < currentSegmentLength) {
currentSegmentLength = segmentLength; currentSegmentLength = segmentLength;
segment = seg; segment = seg;
@@ -181,10 +183,10 @@ class PreviewBar {
}); });
} }
createBar({category, unsubmitted, segment}: PreviewBarSegment): HTMLLIElement { createBar({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement {
const bar = document.createElement('li'); const bar = document.createElement('li');
bar.classList.add('previewbar'); bar.classList.add('previewbar');
bar.innerHTML = '&nbsp;'; bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;';
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category; const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
bar.setAttribute('sponsorblock-category', fullCategoryName); bar.setAttribute('sponsorblock-category', fullCategoryName);
@@ -193,7 +195,7 @@ class PreviewBar {
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[fullCategoryName]?.opacity; if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[fullCategoryName]?.opacity;
bar.style.position = "absolute"; bar.style.position = "absolute";
bar.style.width = this.timeToPercentage(segment[1] - segment[0]); if (segment[1] - segment[0] > 0) bar.style.width = this.timeToPercentage(segment[1] - segment[0]);
bar.style.left = this.timeToPercentage(segment[0]); bar.style.left = this.timeToPercentage(segment[0]);
return bar; return bar;

View File

@@ -0,0 +1,94 @@
import Config from "../config";
import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
export interface SkipButtonControlBarProps {
skip: (segment: SponsorTime) => void;
}
export class SkipButtonControlBar {
container: HTMLElement;
skipIcon: HTMLImageElement;
textContainer: HTMLElement;
chapterText: HTMLElement;
segment: SponsorTime;
showKeybindHint = true;
timeout: NodeJS.Timeout;
skip: (segment: SponsorTime) => void;
constructor(props: SkipButtonControlBarProps) {
this.skip = props.skip;
this.container = document.createElement("div");
this.container.classList.add("skipButtonControlBarContainer");
this.container.classList.add("hidden");
this.skipIcon = document.createElement("img");
this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg");
this.skipIcon.classList.add("ytp-button");
this.skipIcon.id = "sbSkipIconControlBarImage";
this.textContainer = document.createElement("div");
this.container.appendChild(this.skipIcon);
this.container.appendChild(this.textContainer);
this.container.addEventListener("click", () => this.toggleSkip());
this.container.addEventListener("mouseenter", () => this.stopTimer());
this.container.addEventListener("mouseleave", () => this.startTimer());
}
attachToPage(): void {
const leftControlsContainer = document.querySelector(".ytp-left-controls");
this.chapterText = document.querySelector(".ytp-chapter-container");
if (!leftControlsContainer.contains(this.container)) {
leftControlsContainer.insertBefore(this.container, this.chapterText);
}
}
enable(segment: SponsorTime): void {
this.segment = segment;
this.refreshText();
this.startTimer();
}
refreshText(): void {
if (this.segment) {
this.chapterText?.classList?.add("hidden");
this.container.classList.remove("hidden");
this.textContainer.innerText = getSkippingText([this.segment], false) + (this.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "");
}
}
setShowKeybindHint(show: boolean): void {
this.showKeybindHint = show;
this.refreshText();
}
stopTimer(): void {
if (this.timeout) clearTimeout(this.timeout);
}
startTimer(): void {
this.stopTimer();
this.timeout = setTimeout(() => this.disable(), Config.config.skipNoticeDuration * 1000);
}
disable(): void {
this.container.classList.add("hidden");
this.chapterText?.classList?.remove("hidden");
}
toggleSkip(): void {
this.skip(this.segment);
this.disable();
}
}

View File

@@ -17,7 +17,7 @@ class SkipNotice {
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>; skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer) { constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, unskipTime: number = null) {
this.skipNoticeRef = React.createRef(); this.skipNoticeRef = React.createRef();
this.segments = segments; this.segments = segments;
@@ -44,11 +44,19 @@ class SkipNotice {
autoSkip={autoSkip} autoSkip={autoSkip}
contentContainer={contentContainer} contentContainer={contentContainer}
ref={this.skipNoticeRef} ref={this.skipNoticeRef}
closeListener={() => this.close()} />, closeListener={() => this.close()}
smaller={true}
unskipTime={unskipTime} />,
this.noticeElement this.noticeElement
); );
} }
setShowKeybindHint(value: boolean): void {
this.skipNoticeRef.current.setState({
showKeybindHint: value
});
}
close(): void { close(): void {
ReactDOM.unmountComponentAtNode(this.noticeElement); ReactDOM.unmountComponentAtNode(this.noticeElement);

View File

@@ -4,9 +4,9 @@ import SkipNotice from "./render/SkipNotice";
export interface ContentContainer { export interface ContentContainer {
(): { (): {
vote: (type: number, UUID: string, category?: string, skipNotice?: SkipNoticeComponent) => void, vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void,
dontShowNoticeAgain: () => void, dontShowNoticeAgain: () => void,
unskipSponsorTime: (segment: SponsorTime) => void, unskipSponsorTime: (segment: SponsorTime, unskipTime: number) => void,
sponsorTimes: SponsorTime[], sponsorTimes: SponsorTime[],
sponsorTimesSubmitting: SponsorTime[], sponsorTimesSubmitting: SponsorTime[],
skipNotices: SkipNotice[], skipNotices: SkipNotice[],
@@ -41,7 +41,7 @@ export enum CategorySkipOption {
} }
export interface CategorySelection { export interface CategorySelection {
name: string; name: Category;
option: CategorySkipOption option: CategorySkipOption
} }
@@ -51,6 +51,14 @@ export enum SponsorHideType {
MinimumDuration MinimumDuration
} }
export enum CategoryActionType {
Skippable = "", // Strings are used to find proper language configs
POI = "_POI"
}
export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type Category = string & { __categoryBrand: unknown };
export enum SponsorSourceType { export enum SponsorSourceType {
Server = undefined, Server = undefined,
Local = 1 Local = 1
@@ -58,9 +66,9 @@ export enum SponsorSourceType {
export interface SponsorTime { export interface SponsorTime {
segment: [number] | [number, number]; segment: [number] | [number, number];
UUID: string; UUID: SegmentUUID;
category: string; category: Category;
hidden?: SponsorHideType; hidden?: SponsorHideType;
source?: SponsorSourceType; source?: SponsorSourceType;
@@ -151,7 +159,7 @@ export interface VideoInfo {
isUnlisted: boolean, isUnlisted: boolean,
hasYpcMetadata: boolean, hasYpcMetadata: boolean,
viewCount: string, viewCount: string,
category: string, category: Category,
publishDate: string, publishDate: string,
ownerChannelName: string, ownerChannelName: string,
uploadDate: string, uploadDate: string,
@@ -177,4 +185,18 @@ export enum ChannelIDStatus {
export interface ChannelIDInfo { export interface ChannelIDInfo {
id: string, id: string,
status: ChannelIDStatus status: ChannelIDStatus
}
export interface SkipToTimeParams {
v: HTMLVideoElement,
skipTime: number[],
skippingSegments: SponsorTime[],
openNotice: boolean,
forceAutoSkip?: boolean,
unskipTime?: number
}
export interface ToggleSkippable {
toggleSkip: () => void;
setShowKeybindHint: (show: boolean) => void;
} }

View File

@@ -356,7 +356,7 @@ export default class Utils {
}, (response) => { }, (response) => {
resolve(response); resolve(response);
}); });
}) });
} }
/** /**

View File

@@ -0,0 +1,23 @@
import { Category, CategoryActionType, SponsorTime } from "../types";
export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): string {
const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments"
: "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category);
if (autoSkip) {
const messageId = getCategoryActionType(segments[0].category) === CategoryActionType.Skippable
? "skipped" : "skipped_to_category";
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
} else {
const messageId = getCategoryActionType(segments[0].category) === CategoryActionType.Skippable
? "skip_category" : "skip_to_category";
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
}
}
export function getCategoryActionType(category: Category): CategoryActionType {
if (category.startsWith("poi_")) {
return CategoryActionType.POI;
} else {
return CategoryActionType.Skippable;
}
}