Compare commits

..

29 Commits

Author SHA1 Message Date
Ajay Ramachandran
4f0f8655f4 Merge pull request #1425 from mchangrh/contentScriptRebase
rebase document script out of videoInfo
2022-09-02 15:20:40 -04:00
Ajay Ramachandran
668f6856d1 bump version 2022-09-02 15:20:25 -04:00
Ajay
c8e2bb0c13 Auto update hidden categories when redeemed 2022-09-02 14:38:49 -04:00
Ajay
39ed7ea83c Fix license code box 2022-09-02 14:26:41 -04:00
Ajay
f1b2ff801a Fix redirect uri 2022-09-02 13:50:29 -04:00
Ajay Ramachandran
1d9c3a8b80 fix typo 2022-09-02 04:55:17 -04:00
Ajay
29c6151fe3 Ensure channel id is defined before declaring it found 2022-09-02 01:30:13 -04:00
Ajay
1377be9915 Use events for channel id and fallback to current system
Also fix formatting
2022-09-02 01:30:12 -04:00
Michael C
c479a601cd rebase document script out of videoInfo #1312 2022-09-02 01:30:09 -04:00
Ajay Ramachandran
f66a4d25bf Merge pull request #1446 from mini-bomba/clearUnsubmittedSegments
Add a section in options for unsubmitted segments
2022-09-02 00:17:57 -04:00
Ajay
9c7d153f15 Move segment export to backup page and improve margins 2022-09-02 00:15:05 -04:00
mini-bomba
bbea534781 Add "Export segments as URL" option the unsubmitted videos section 2022-09-01 23:03:25 -04:00
mini-bomba
df2586e76d Load segment description from hashparams 2022-09-01 23:03:25 -04:00
mini-bomba
59093cdf21 Move new react components to components/options/, following latest changes 2022-09-01 23:03:25 -04:00
mini-bomba
5f6307041a Add an Export Segments button to the unsubmitted segments list 2022-09-01 23:03:25 -04:00
mini-bomba
26f2143247 Don't force-sync unsubmitted segments when clear confirm prompt is cancelled 2022-09-01 23:03:25 -04:00
mini-bomba
bd292ff886 Split unsubmittedSegmentCounts string into many to account for singular/plural forms of nouns
hopefully with enough context for translators to properly translate...
2022-09-01 23:03:25 -04:00
mini-bomba
9915d46ad4 Add a section in options for unsubmitted segments 2022-09-01 23:03:25 -04:00
Ajay
2b5a02e068 Make required segments thicker in preview bar 2022-09-01 21:35:58 -04:00
Ajay
1f68f512fa Fix linting error 2022-09-01 20:04:53 -04:00
Ajay
d18f7c6195 Make limited width option better 2022-09-01 17:10:01 -04:00
Ajay
015ac7d46e Fix test breaking due to chrome.* api 2022-09-01 16:52:10 -04:00
Ajay
6631dfdea3 Also check license status for submitting chapter 2022-09-01 16:44:02 -04:00
Ajay
212fbb83fe Add tooltip to sort segments 2022-09-01 16:33:34 -04:00
Ajay
9e08d6012c Fix export/import not appearing without segments and without chapter enabled 2022-09-01 16:32:23 -04:00
Ajay
69c0fe1caf Make importer import non chapters too 2022-09-01 16:25:43 -04:00
Ajay
fcecd1163d Improve locked category display 2022-09-01 16:10:57 -04:00
Ajay
29ea112b4f Move hyphen so it is not treated as a range 2022-09-01 16:07:29 -04:00
Ajay Ramachandran
2b96fd5f57 Merge pull request #1001 from ajayyy/chapters
Chapters
2022-09-01 15:24:35 -04:00
26 changed files with 662 additions and 111 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "4.7.1",
"version": "5.0",
"default_locale": "en",
"description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",
@@ -69,7 +69,8 @@
"icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html",
"content.css"
"content.css",
"js/document.js"
],
"permissions": [
"storage",

View File

@@ -125,6 +125,9 @@
"SubmitTimes": {
"message": "Submit Segments"
},
"sortSegments": {
"message": "Sort Segments"
},
"submitCheck": {
"message": "Are you sure you want to submit this?"
},
@@ -1105,7 +1108,7 @@
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Reedem Successful!"
"message": "Redeem Successful!"
},
"redeemFailed": {
"message": "License key is invalid"
@@ -1150,5 +1153,60 @@
},
"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"
},
"unsubmittedSegmentCounts": {
"message": "You currently have {0} on {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "You currently have no unsubmitted segments",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "unsubmitted segment",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "unsubmitted segments",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Clear all segments",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Are you sure you want to clear all your unsubmitted segments?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Show segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Hide segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "Video ID",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Segment Count",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Actions",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Share as URL"
}
}

View File

@@ -39,6 +39,10 @@
height: 100%;
}
.previewbar.requiredSegment {
transform: scaleY(3)
}
/* Make sure settings are upfront */
.ytp-settings-menu {
z-index: 6000 !important;

View File

@@ -317,6 +317,10 @@ input[type='number'] {
color: grey;
}
.disabled .slider {
cursor: default;
}
tr.disabled {
opacity: 0.3;
}
@@ -358,6 +362,10 @@ tr.disabled {
padding: 4px;
}
.switch-label {
width: inherit;
}
.switch {
position: relative;
display: inline-block;

View File

@@ -362,6 +362,8 @@
</div>
</div>
</div>
<div data-type="react-UnsubmittedVideosComponent"></div>
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
<h2>__MSG_exportOptions__</h2>
@@ -490,7 +492,7 @@
<div class="small-description">__MSG_copyDebugInformationOptions__</div>
</div>
<div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
<div class="switch-container">
<label class="switch">

View File

@@ -44,12 +44,12 @@
</span>
</div>
<div id="issueReporterTimeButtons"></div>
<div id="issueReporterImportExport" class="hidden">
<div id="issueReporterImportExport">
<div id="importExportButtons">
<button id="importSegmentsButton" title="__MSG_importSegments__" class="hidden">
<button id="importSegmentsButton" title="__MSG_importSegments__">
<img src="/icons/import.svg" alt="Refresh icon" id="importSegments" />
</button>
<button id="exportSegmentsButton" title="__MSG_exportSegments__">
<button id="exportSegmentsButton" class="hidden" title="__MSG_exportSegments__">
<img src="/icons/export.svg" alt="Export icon" id="exportSegments" />
</button>
</div>

View File

@@ -58,7 +58,7 @@
</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"
<a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%2Fapi%2FgenerateToken%2Fpatreon"
class="option-link" target="_blank" rel="noreferrer">
<div class="option-button inline">
__MSG_patreonSignIn__

View File

@@ -7,6 +7,7 @@ import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { RectangleTooltip } from "../render/RectangleTooltip";
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
import { GenericUtils } from "../utils/genericUtils";
import { noRefreshFetchingChaptersAllowed } from "../utils/licenseKey";
const utils = new Utils();
@@ -402,7 +403,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
// If permission not loaded, treat it like we have permission except chapter
const defaultBlockCategories = ["chapter"];
const permission = Config.config.permissions[category as Category];
const permission = Config.config.permissions[category as Category] && (category !== "chapter" || noRefreshFetchingChaptersAllowed());
if ((defaultBlockCategories.includes(category) || permission !== undefined) && !permission) continue;
elements.push(

View File

@@ -77,6 +77,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
<img id={"sponsorSkipSortButton" + this.state.idSuffix}
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipSmallButton"
onClick={() => this.sortSegments()}
title={chrome.i18n.getMessage("sortSegments")}
src={chrome.extension.getURL("icons/sort.svg")}>
</img>;
return (

View File

@@ -38,10 +38,21 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
this.setState({
hideChapter: !allowed
});
})
});
}
render(): React.ReactElement {
if (this.state.hideChapter) {
// Ensure force update refreshes this
fetchingChaptersAllowed().then((allowed) => {
if (allowed) {
this.setState({
hideChapter: !allowed
});
}
});
}
let defaultOption = "disable";
// Set the default opton properly
for (const categorySelection of Config.config.categorySelections) {
@@ -78,9 +89,6 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
className={`categoryTableElement${extraClasses}`} >
<td id={this.props.category + "OptionName"}
className="categoryTableLabel">
{disabled &&
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
}
{chrome.i18n.getMessage("category_" + this.props.category)}
</td>
@@ -93,6 +101,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
onChange={this.skipOptionSelected.bind(this)}>
{this.getCategorySkipOptions()}
</select>
{disabled &&
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
}
</td>
{this.props.category !== "chapter" &&
@@ -226,6 +238,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
configKey={option.configKey}
label={option.label}
disabled={disabled}
style={{width: "inherit"}}
/>
</td>
</tr>

View File

@@ -6,6 +6,7 @@ export interface ToggleOptionProps {
configKey: string;
label: string;
disabled?: boolean;
style?: React.CSSProperties;
}
export interface ToggleOptionState {
@@ -26,7 +27,7 @@ class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOpt
render(): React.ReactElement {
return (
<div>
<div className="switch-container">
<div className="switch-container" style={this.props.style}>
<label className="switch">
<input id={this.props.configKey}
type="checkbox"

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListItem from "./UnsubmittedVideoListItem";
export interface UnsubmittedVideoListProps {
}
export interface UnsubmittedVideoListState {
}
class UnsubmittedVideoListComponent extends React.Component<UnsubmittedVideoListProps, UnsubmittedVideoListState> {
constructor(props: UnsubmittedVideoListProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
// Render nothing if there are no unsubmitted segments
if (Object.keys(Config.config.unsubmittedSegments).length == 0)
return <></>;
return (
<table id="unsubmittedVideosList"
className="categoryChooserTable"
style={{marginTop: "10px"}} >
<tbody>
{/* Headers */}
<tr id="UnsubmittedVideosListHeader"
className="categoryTableElement categoryTableHeader">
<th id="UnsubmittedVideoID">
{chrome.i18n.getMessage("videoID")}
</th>
<th id="UnsubmittedSegmentCount">
{chrome.i18n.getMessage("segmentCount")}
</th>
<th id="UnsubmittedVideoActions">
{chrome.i18n.getMessage("actions")}
</th>
</tr>
{this.getUnsubmittedVideos()}
</tbody>
</table>
);
}
getUnsubmittedVideos(): JSX.Element[] {
const elements: JSX.Element[] = [];
for (const videoID of Object.keys(Config.config.unsubmittedSegments)) {
elements.push(
<UnsubmittedVideoListItem videoID={videoID} key={videoID}>
</UnsubmittedVideoListItem>
);
}
return elements;
}
}
export default UnsubmittedVideoListComponent;

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import Config from "../../config";
import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
export interface UnsubmittedVideosListItemProps {
videoID: string;
}
export interface UnsubmittedVideosListItemState {
}
class UnsubmittedVideoListItem extends React.Component<UnsubmittedVideosListItemProps, UnsubmittedVideosListItemState> {
constructor(props: UnsubmittedVideosListItemProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
const segmentCount = Config.config.unsubmittedSegments[this.props.videoID]?.length ?? 0;
return (
<>
<tr id={this.props.videoID + "UnsubmittedSegmentsRow"}
className="categoryTableElement">
<td id={this.props.videoID + "UnsubmittedVideoID"}
className="categoryTableLabel">
<a href={`https://youtu.be/${this.props.videoID}`}
target="_blank" rel="noreferrer">
{this.props.videoID}
</a>
</td>
<td id={this.props.videoID + "UnsubmittedSegmentCount"}>
{segmentCount}
</td>
<td id={this.props.videoID + "UnsubmittedVideoActions"}>
<div id={this.props.videoID + "ExportSegmentsAction"}
className="option-button inline low-profile"
onClick={this.exportSegments.bind(this)}>
{chrome.i18n.getMessage("exportSegments")}
</div>
{" "}
<div id={this.props.videoID + "ExportSegmentsAsURLAction"}
className="option-button inline low-profile"
onClick={this.exportSegmentsAsURL.bind(this)}>
{chrome.i18n.getMessage("exportSegmentsAsURL")}
</div>
{" "}
<div id={this.props.videoID + "ClearSegmentsAction"}
className="option-button inline low-profile"
onClick={this.clearSegments.bind(this)}>
{chrome.i18n.getMessage("clearTimes")}
</div>
</td>
</tr>
</>
);
}
clearSegments(): void {
if (confirm(chrome.i18n.getMessage("clearThis"))) {
delete Config.config.unsubmittedSegments[this.props.videoID];
Config.forceSyncUpdate("unsubmittedSegments");
}
}
exportSegments(): void {
this.copyToClipboard(exportTimes(Config.config.unsubmittedSegments[this.props.videoID]));
}
exportSegmentsAsURL(): void {
this.copyToClipboard(`https://youtube.com/watch?v=${this.props.videoID}${exportTimesAsHashParam(Config.config.unsubmittedSegments[this.props.videoID])}`)
}
copyToClipboard(text: string): void {
navigator.clipboard.writeText(text)
.then(() => {
alert(chrome.i18n.getMessage("CopiedExclamation"));
})
.catch(() => {
alert(chrome.i18n.getMessage("copyDebugInformationFailed"));
});
}
}
export default UnsubmittedVideoListItem;

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListComponent from "./UnsubmittedVideoListComponent";
export interface UnsubmittedVideosProps {
}
export interface UnsubmittedVideosState {
tableVisible: boolean,
}
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {
constructor(props: UnsubmittedVideosProps) {
super(props);
this.state = {
tableVisible: false,
};
}
render(): React.ReactElement {
const videoCount = Object.keys(Config.config.unsubmittedSegments).length;
const segmentCount = Object.values(Config.config.unsubmittedSegments).reduce((acc: number, vid: Array<unknown>) => acc + vid.length, 0);
return <>
<div style={{marginBottom: "10px"}}>
{segmentCount == 0 ?
chrome.i18n.getMessage("unsubmittedSegmentCountsZero") :
chrome.i18n.getMessage("unsubmittedSegmentCounts")
.replace("{0}", `${segmentCount} ${chrome.i18n.getMessage("unsubmittedSegments" + (segmentCount == 1 ? "Singular" : "Plural"))}`)
.replace("{1}", `${videoCount} ${chrome.i18n.getMessage("videos" + (videoCount == 1 ? "Singular" : "Plural"))}`)
}
</div>
{videoCount > 0 && <div className="option-button inline" onClick={() => this.setState({tableVisible: !this.state.tableVisible})}>
{chrome.i18n.getMessage(this.state.tableVisible ? "hideUnsubmittedSegments" : "showUnsubmittedSegments")}
</div>}
{" "}
<div className="option-button inline" onClick={this.clearAllSegments}>
{chrome.i18n.getMessage("clearUnsubmittedSegments")}
</div>
{this.state.tableVisible && <UnsubmittedVideoListComponent/>}
</>;
}
clearAllSegments(): void {
if (confirm(chrome.i18n.getMessage("clearUnsubmittedSegmentsConfirm")))
Config.config.unsubmittedSegments = {};
}
}
export default UnsubmittedVideosComponent;

View File

@@ -1,11 +1,27 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime, HashedValue } from "./types";
import { ContentContainer, Keybind } from "./types";
import {
ActionType,
Category,
CategorySkipOption,
ChannelIDInfo,
ChannelIDStatus,
ContentContainer,
HashedValue,
Keybind,
ScheduledTime,
SegmentUUID,
SkipToTimeParams,
SponsorHideType,
SponsorSourceType,
SponsorTime,
StorageChangesObject,
ToggleSkippable,
VideoID,
VideoInfo,
PageType
} from "./types";
import Utils from "./utils";
const utils = new Utils();
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
@@ -22,6 +38,8 @@ import { importTimes } from "./utils/exporter";
import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings";
const utils = new Utils();
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -38,6 +56,10 @@ let activeSkipKeybindElement: ToggleSkippable = null;
// JSON video info
let videoInfo: VideoInfo = null;
// Page Type - browse/watch etc...
let pageType: PageType;
// if video is live or premiere
let isLivePremiere: boolean
// The channel this video is about
let channelIDInfo: ChannelIDInfo;
// Locked Categories in this tab, like: ["sponsor","intro","outro"]
@@ -73,7 +95,7 @@ let onInvidious: boolean;
let onMobileYouTube: boolean;
//the video id of the last preview bar update
let lastPreviewBarUpdate;
let lastPreviewBarUpdate: VideoID;
// Is the video currently being switched
let switchingVideos = null;
@@ -247,8 +269,9 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
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)) {
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)
&& (segment.category !== "chapter" || utils.getCategorySelection("chapter"))) {
sponsorTimesSubmitting.push(segment);
addedSegments = true;
}
@@ -316,12 +339,14 @@ function resetValues() {
sponsorSkipped = [];
videoInfo = null;
pageType = null;
channelWhitelisted = false;
channelIDInfo = {
status: ChannelIDStatus.Fetching,
id: null
};
lockedCategories = [];
isLivePremiere = false;
//empty the preview bar
if (previewBar !== null) {
@@ -560,7 +585,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
const videoID = sponsorVideoID;
const skipBuffer = 0.003;
if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
&& timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) {
video.muted = false;
videoMuted = false;
@@ -627,8 +652,8 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
});
}
}
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|| currentSkip.actionType === ActionType.Mute) {
forcedSkipTime = skipTime[0] + 0.001;
} else {
@@ -671,7 +696,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}, 1);
} else {
logDebug(`Starting timeout to skip ${video.currentTime} to skip at ${skipTime[0]}`);
// Schedule for right before to be more precise than normal timeout
currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 150));
}
@@ -691,8 +716,8 @@ function getVirtualTime(): number {
}
function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean {
const checkFunction = (segment) => segment.actionType === ActionType.Mute
&& segment.segment[0] <= currentTime
const checkFunction = (segment) => segment.actionType === ActionType.Mute
&& segment.segment[0] <= currentTime
&& (segment.segment[1] > currentTime || (includeOverlap && segment.segment[1] + 0.02 > currentTime));
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
}
@@ -772,7 +797,7 @@ function setupVideoListeners() {
// If it is not the first event, then the only way to get to 0 is if there is a seek event
// This check makes sure that changing the video resolution doesn't cause the extension to think it
// gone back to the begining
if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA
if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA
&& video.currentTime === 0) return;
updateVirtualTime();
@@ -803,7 +828,7 @@ function setupVideoListeners() {
video.addEventListener('playing', () => {
updateVirtualTime();
lastPausedAtZero = false;
if (startedWaiting) {
startedWaiting = false;
logDebug(`[SB] Playing event after buffering: ${Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
@@ -1061,7 +1086,7 @@ function retryFetch(errorCode: number): void {
sponsorDataFound = false;
if (errorCode !== 404 && retryCount > 1) {
// Too many errors (50x), give up
// Too many errors (50x), give up
return;
}
@@ -1069,7 +1094,7 @@ function retryFetch(errorCode: number): void {
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
setTimeout(() => {
if (sponsorVideoID && sponsorTimes?.length === 0
if (sponsorVideoID && sponsorTimes?.length === 0
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
sponsorsLookup();
}
@@ -1139,6 +1164,8 @@ function startSkipScheduleCheckingForStartSponsors() {
function getYouTubeVideoID(document: Document, url?: string): string | boolean {
url ||= document.URL;
// pageType shortcut
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument()
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
// skip to document and don't hide if on /embed/
@@ -1146,17 +1173,19 @@ function getYouTubeVideoID(document: Document, url?: string): string | boolean {
// skip to URL if matches youtube watch or invidious or matches youtube pattern
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
// skip to document if matches pattern
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument();
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(true, PageType.Channel);
// not sure, try URL then document
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
}
function getYouTubeVideoIDFromDocument(hideIcon = true): string | boolean {
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string | boolean {
// get ID from document (channel trailer / embedded playlist)
const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']");
const videoURL = element?.getAttribute("href");
if (videoURL) {
onInvidious = hideIcon;
// if href found, hint was correct
pageType = pageHint;
return getYouTubeVideoIDFromURL(videoURL);
} else {
return false;
@@ -1226,6 +1255,7 @@ function updatePreviewBar(): void {
if (video === null) return;
const hashParams = getHashParams();
const previewBarSegments: PreviewBarSegment[] = [];
if (sponsorTimes) {
sponsorTimes.forEach((segment) => {
@@ -1239,6 +1269,7 @@ function updatePreviewBar(): void {
showLarger: segment.actionType === ActionType.Poi,
description: segment.description,
source: segment.source,
requiredSegment: hashParams.requiredSegment && segment.UUID === hashParams.requiredSegment
});
});
}
@@ -1274,25 +1305,29 @@ function updatePreviewBar(): void {
async function whitelistCheck() {
const whitelistedChannels = Config.config.whitelistedChannels;
const getChannelID = () =>
(document.querySelector("a.ytd-video-owner-renderer") // YouTube
?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed
?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious
?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube
?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1];
try {
await utils.wait(() => !!getChannelID(), 6000, 20);
await utils.wait(() => channelIDInfo.status === ChannelIDStatus.Found, 6000, 20);
channelIDInfo = {
status: ChannelIDStatus.Found,
id: getChannelID().match(/^\/?([^\s/]+)/)[0]
};
// If found, continue on, it was set by the listener
} catch (e) {
channelIDInfo = {
status: ChannelIDStatus.Failed,
id: null
};
// Try fallback
const channelIDFallback = (document.querySelector("a.ytd-video-owner-renderer") // YouTube
?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed
?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious
?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube
?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1];
if (channelIDFallback) {
channelIDInfo = {
status: ChannelIDStatus.Found,
id: channelIDFallback
};
} else {
channelIDInfo = {
status: ChannelIDStatus.Failed,
id: null
};
}
}
//see if this is a whitelisted channel
@@ -1331,7 +1366,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
// Find auto skipping segments if possible, sort by duration otherwise
const minSponsorTimeIndex = minSponsorTimeIndexes.sort(
(a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b]))
(a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b]))
|| (submittedArray[a].segment[1] - submittedArray[a].segment[0]) - (submittedArray[b].segment[1] - submittedArray[b].segment[0])))[0] ?? -1;
// Store extra indexes for the non-auto skipping segments if others occur at the exact same start time
const extraIndexes = minSponsorTimeIndexes.filter((i) => i !== minSponsorTimeIndex && autoSkipSorter(submittedArray[i]) !== 0);
@@ -1445,7 +1480,7 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
for (let i = 0; i < possibleTimes.length; i++) {
if ((minimum === undefined
|| ((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)))
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
&& possibleTimes[i].segment.length === 2
&& possibleTimes[i].actionType !== ActionType.Poi) {
@@ -1573,13 +1608,13 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, unskipTime: number, startReskip: boolean) {
for (const skipNotice of skipNotices) {
if (skippingSegments.length === skipNotice.segments.length
if (skippingSegments.length === skipNotice.segments.length
&& skippingSegments.every((segment) => skipNotice.segments.some((s) => s.UUID === segment.UUID))) {
// Skip notice already exists
return;
}
}
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime, startReskip);
if (onMobileYouTube || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
skipNotices.push(newSkipNotice);
@@ -1593,7 +1628,7 @@ function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forc
video.muted = false;
videoMuted = false;
}
if (forceSeek || segment.actionType === ActionType.Skip) {
//add a tiny bit of time to make sure it is not skipped again
video.currentTime = unskipTime ?? segment.segment[0] + 0.001;
@@ -1711,7 +1746,7 @@ function updateEditButtonsOnPlayer(): void {
// Don't try to update the buttons if we aren't on a YouTube video page
if (!sponsorVideoID || onMobileYouTube) return;
const buttonsEnabled = !Config.config.hideVideoPlayerControls && !onInvidious;
const buttonsEnabled = !(Config.config.hideVideoPlayerControls || onInvidious);
let creatingSegment = false;
let submitButtonVisible = false;
@@ -1866,10 +1901,10 @@ function openInfoMenu() {
//hide info button
if (playerButtons.info) playerButtons.info.button.style.display = "none";
const popup = document.createElement("div");
popup.id = "sponsorBlockPopupContainer";
const frame = document.createElement("iframe");
frame.width = "374";
frame.height = "500";
@@ -1888,7 +1923,7 @@ function openInfoMenu() {
//old youtube theme
parentNode = document.getElementById("watch7-sidebar-contents");
}
parentNode.insertBefore(popup, parentNode.firstChild);
}
@@ -2049,7 +2084,7 @@ function submitSponsorTimes() {
//called after all the checks have been made that it's okay to do so
async function sendSubmitMessage() {
// Block if submitting on a running livestream or premiere
if (isVisible(document.querySelector(".ytp-live-badge"))) {
if (isLivePremiere || isVisible(document.querySelector(".ytp-live-badge"))) {
alert(chrome.i18n.getMessage("liveOrPremiere"));
return;
}
@@ -2162,6 +2197,36 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage;
}
function windowListenerHandler(event: MessageEvent): void {
const data = event.data;
const dataType = data.type;
if (data.source !== "sponsorblock") return;
if (dataType === "navigation") {
sponsorVideoID = data.videoID;
pageType = data.pageType;
if (data.channelID) {
channelIDInfo = {
id: data.channelID,
status: ChannelIDStatus.Found
};
}
} else if (dataType === "ad") {
if (isAdPlaying != data.playing) {
isAdPlaying = data.playing
updatePreviewBar();
updateVisibilityOfPlayerControlsButton();
}
} else if (dataType === "data") {
if (data.video !== sponsorVideoID) {
sponsorVideoID = data.videoID;
videoIDChange(sponsorVideoID);
}
isLivePremiere = data.isLive || data.isPremiere
}
}
function updateActiveSegment(currentTime: number): void {
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
chrome.runtime.sendMessage({
@@ -2175,7 +2240,7 @@ function nextChapter(): void {
.sort((a, b) => a.segment[1] - b.segment[1]);
if (chapters.length <= 0) return;
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
&& time.segment[1] > video.currentTime);
if (nextChapter !== -1) {
reskipSponsorTime(chapters[nextChapter], true);
@@ -2190,7 +2255,7 @@ function previousChapter(): void {
// 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
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) {
@@ -2207,7 +2272,14 @@ function addPageListeners(): void {
}
};
// inject into document
const docScript = document.createElement("script");
docScript.src = chrome.runtime.getURL("js/document.js");
(document.head || document.documentElement).appendChild(docScript);
document.addEventListener("yt-navigate-start", resetValues);
document.addEventListener("yt-navigate-finish", refreshListners);
window.addEventListener("message", windowListenerHandler);
}
function addHotkeyListener(): void {
@@ -2317,7 +2389,7 @@ function showTimeWithoutSkips(skippedDuration: number): void {
display.appendChild(duration);
}
const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration);
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
@@ -2340,6 +2412,7 @@ function checkForPreloadedSegment() {
UUID: GenericUtils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
description: segment.description ?? "",
source: SponsorSourceType.Local
});
@@ -2359,7 +2432,7 @@ function checkForPreloadedSegment() {
const navigationApiAvailable = "navigation" in window;
if (navigationApiAvailable) {
// TODO: Remove type cast once type declarations are updated
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) =>
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) =>
videoIDChange(getYouTubeVideoID(document, (e as unknown as Record<string, Record<string, string>>).destination.url)));
}

80
src/document.ts Normal file
View File

@@ -0,0 +1,80 @@
/*
Content script are run in an isolated DOM so it is not possible to access some key details that are sanitized when passed cross-dom
This script is used to get the details from the page and make them available for the content script by being injected directly into the page
*/
import { PageType } from "./types";
interface StartMessage {
type: "navigation",
pageType: PageType
videoID: string | null,
}
interface FinishMessage extends StartMessage {
channelID: string,
channelTitle: string
}
interface AdMessage {
type: "ad",
playing: boolean
}
interface VideoData {
type: "data",
videoID: string,
isLive: boolean,
isPremiere: boolean
}
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;
// global playerClient - too difficult to type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let playerClient: any;
const sendMessage = (message: WindowMessage): void => {
window.postMessage({ source: "sponsorblock", ...message }, "/");
}
function setupPlayerClient(e: CustomEvent): void {
if (playerClient) return; // early exit if already defined
playerClient = e.detail;
sendVideoData(); // send playerData after setup
e.detail.addEventListener('onAdStart', () => sendMessage({ type: "ad", playing: true } as AdMessage));
e.detail.addEventListener('onAdFinish', () => sendMessage({ type: "ad", playing: false } as AdMessage));
}
document.addEventListener("yt-player-updated", setupPlayerClient);
document.addEventListener("yt-navigate-start", navigationStartSend);
document.addEventListener("yt-navigate-finish", navigateFinishSend);
function navigationParser(event: CustomEvent): StartMessage {
const pageType: PageType = event.detail.pageType;
const result: StartMessage = { type: "navigation", pageType, videoID: null };
if (pageType === "shorts" || pageType === "watch") {
const endpoint = event.detail.endpoint
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
}
return result;
}
function navigationStartSend(event: CustomEvent): void {
sendMessage(navigationParser(event) as StartMessage);
}
function navigateFinishSend(event: CustomEvent): void {
sendVideoData(); // arrived at new video, send video data
const videoDetails = event.detail?.response?.playerResponse?.videoDetails;
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
}
function sendVideoData(): void {
if (!playerClient) return;
const { video_id, isLive, isPremiere } = playerClient.getVideoData();
sendMessage({ type: "data", videoID: video_id, isLive, isPremiere } as VideoData);
}

View File

@@ -23,6 +23,7 @@ export interface PreviewBarSegment {
showLarger: boolean;
description: string;
source: SponsorSourceType;
requiredSegment?: boolean;
}
interface ChapterGroup extends SegmentContainer {
@@ -251,6 +252,7 @@ class PreviewBar {
const bar = document.createElement('li');
bar.classList.add('previewbar');
if (barSegment.requiredSegment) bar.classList.add("requiredSegment");
bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;';
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;

View File

@@ -10,12 +10,17 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
import UnsubmittedVideos from "./render/UnsubmittedVideos";
import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { StorageChangesObject } from "./types";
const utils = new Utils();
let embed = false;
const categoryChoosers: CategoryChooser[] = [];
const unsubmittedVideos: UnsubmittedVideos[] = [];
window.addEventListener('DOMContentLoaded', init);
async function init() {
@@ -103,7 +108,7 @@ async function init() {
// Add click listener
checkbox.addEventListener("click", async () => {
// Confirm if required
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
&& !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = !checkbox.checked;
return;
@@ -120,7 +125,7 @@ async function init() {
if (!checkbox.checked) {
// Enable the notice
Config.config["dontShowNotice"] = false;
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
showNoticeSwitch.checked = true;
}
@@ -162,7 +167,7 @@ async function init() {
}
case "text-change": {
const textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box");
const textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set");
textChangeInput.value = Config.config[option];
@@ -290,7 +295,10 @@ async function init() {
break;
}
case "react-CategoryChooserComponent":
new CategoryChooser(optionsElements[i]);
categoryChoosers.push(new CategoryChooser(optionsElements[i]));
break;
case "react-UnsubmittedVideosComponent":
unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i]));
break;
}
}
@@ -338,8 +346,8 @@ function createStickyHeader() {
/**
* Handle special cases where an option shouldn't show
*
* @param {String} element
*
* @param {String} element
*/
async function shouldHideOption(element: Element): Promise<boolean> {
return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
@@ -348,10 +356,8 @@ async function shouldHideOption(element: Element): Promise<boolean> {
/**
* Called when the config is updated
*
* @param {String} element
*/
function optionsConfigUpdateListener() {
function optionsConfigUpdateListener(changes: StorageChangesObject) {
const optionsContainer = document.getElementById("options");
const optionsElements = optionsContainer.querySelectorAll("*");
@@ -359,14 +365,25 @@ function optionsConfigUpdateListener() {
switch (optionsElements[i].getAttribute("data-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
break;
}
}
if (changes.categorySelections || changes.payments) {
for (const chooser of categoryChoosers) {
chooser.update();
}
} else if (changes.unsubmittedSegments) {
for (const chooser of unsubmittedVideos) {
chooser.update();
}
}
}
/**
* Will set display elements to the proper text
*
* @param element
*
* @param element
*/
function updateDisplayElement(element: HTMLElement) {
const displayOption = element.getAttribute("data-sync")
@@ -393,9 +410,9 @@ function updateDisplayElement(element: HTMLElement) {
/**
* Initializes the option to add Invidious instances
*
* @param element
* @param option
*
* @param element
* @param option
*/
function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
@@ -447,9 +464,9 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
/**
* Run when the invidious button is being initialized
*
* @param checkbox
* @param option
*
* @param checkbox
* @param option
*/
function invidiousInit(checkbox: HTMLInputElement, option: string) {
utils.containsInvidiousPermission().then((result) => {
@@ -463,9 +480,9 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
/**
* Run whenever the invidious checkbox is clicked
*
* @param checkbox
* @param option
*
* @param checkbox
* @param option
*/
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
@@ -474,8 +491,8 @@ async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Pro
/**
* Will trigger the textbox to appear to be able to change an option's text.
*
* @param element
*
* @param element
*/
function activatePrivateTextChange(element: HTMLElement) {
const button = element.querySelector(".trigger-button");
@@ -492,7 +509,7 @@ function activatePrivateTextChange(element: HTMLElement) {
element.querySelector(".option-hidden-section").classList.remove("hidden");
return;
}
let result = Config.config[option];
// See if anything extra must be done
switch (option) {
@@ -503,7 +520,7 @@ function activatePrivateTextChange(element: HTMLElement) {
}
textBox.value = result;
const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => {
setTextOption(option, element, textBox.value);
@@ -532,7 +549,7 @@ function activatePrivateTextChange(element: HTMLElement) {
/**
* Function to run when a textbox change is submitted
*
*
* @param option data-sync value
* @param element main container div
* @param value new text
@@ -542,7 +559,7 @@ async function setTextOption(option: string, element: HTMLElement, value: string
const confirmMessage = element.getAttribute("data-confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
// See if anything extra must be done
switch (option) {
case "*":
@@ -554,13 +571,13 @@ async function setTextOption(option: string, element: HTMLElement, value: string
if (newConfig.supportInvidious) {
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
checkbox.checked = true;
await invidiousOnClick(checkbox, "supportInvidious");
}
window.location.reload();
} catch (e) {
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
}
@@ -603,7 +620,7 @@ function uploadConfig(e) {
/**
* Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue.
*
*
* @param input Input server address
*/
function validateServerAddress(input: string): string {
@@ -637,7 +654,7 @@ function copyDebugOutputToClipboard() {
// Sanitise sensitive user config values
delete output.config.userID;
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
? "Default server address" : "Custom server address";
output.config.invidiousInstances = output.config.invidiousInstances.length;
output.config.whitelistedChannels = output.config.whitelistedChannels.length;
@@ -654,4 +671,4 @@ function copyDebugOutputToClipboard() {
function isIncognitoAllowed(): Promise<boolean> {
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
}
}

View File

@@ -1,7 +1,7 @@
import Config from "./config";
import Utils from "./utils";
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject, CategorySkipOption } from "./types";
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
@@ -527,12 +527,9 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
if (downloadedTimes.length > 0) {
PageElements.issueReporterImportExport.classList.remove("hidden");
if (utils.getCategorySelection("chapter")?.option === CategorySkipOption.ShowOverlay) {
PageElements.importSegmentsButton.classList.remove("hidden");
}
PageElements.exportSegmentsButton.classList.remove("hidden");
} else {
PageElements.issueReporterImportExport.classList.add("hidden");
PageElements.exportSegmentsButton.classList.add("hidden");
}
const isVip = Config.config.isVip;

View File

@@ -4,12 +4,20 @@ import CategoryChooserComponent from "../components/options/CategoryChooserCompo
class CategoryChooser {
ref: React.RefObject<CategoryChooserComponent>;
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<CategoryChooserComponent/>,
<CategoryChooserComponent ref={this.ref} />,
element
);
}
update(): void {
this.ref.current?.forceUpdate();
}
}
export default CategoryChooser;

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
class UnsubmittedVideos {
ref: React.RefObject<UnsubmittedVideosComponent>;
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<UnsubmittedVideosComponent ref={this.ref} />,
element
);
}
update(): void {
this.ref.current?.forceUpdate();
}
}
export default UnsubmittedVideos;

View File

@@ -239,6 +239,14 @@ export type Keybind = {
shift?: boolean
}
export enum PageType {
Shorts = "shorts",
Watch = "watch",
Search = "search",
Browse = "browse",
Channel = "channel"
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void

View File

@@ -23,8 +23,9 @@ async function init() {
cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
const redeemInput = document.getElementById("redeemCodeInput") as HTMLInputElement;
redeemButton.addEventListener("click", async () => {
const licenseKey = redeemButton.value;
const licenseKey = redeemInput.value;
if (await checkLicenseKey(licenseKey)) {
Config.config.payments.licenseKey = licenseKey;

View File

@@ -1,6 +1,15 @@
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
import { shortCategoryName } from "./categoryUtils";
import { GenericUtils } from "./genericUtils";
import * as CompileConfig from "../../config.json";
const inTest = typeof chrome === "undefined";
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
.map((code) => ({
code,
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
}));
export function exportTimes(segments: SponsorTime[]): string {
let result = "";
@@ -30,7 +39,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
if (startTime) {
const specialCharsMatcher = /^(?:\s+seconds?)?[:()-\s]*|(?:\s+at)?[:()-\s]+$/g
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]);
@@ -38,10 +47,12 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
if (title) {
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
category: "chapter" as Category,
actionType: ActionType.Chapter,
category: determinedCategory ?? ("chapter" as Category),
actionType: determinedCategory ? ActionType.Skip : ActionType.Chapter,
description: title,
source: SponsorSourceType.Local,
UUID: GenericUtils.generateUserID() as SegmentUUID
@@ -62,4 +73,15 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
}
return result;
}
}
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
const hashparamSegments = segments.map(segment => ({
actionType: segment.actionType,
category: segment.category,
segment: segment.segment,
...(segment.description ? {description: segment.description} : {}) // don't include the description param if empty
}));
return `#segments=${JSON.stringify(hashparamSegments)}`;
}

View File

@@ -22,6 +22,13 @@ export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
return false
}
/**
* The other one also tried refreshing, so returns a promise
*/
export function noRefreshFetchingChaptersAllowed(): boolean {
return Config.config.payments.chaptersAllowed || CompileConfig["freeChapterAccess"];
}
export async function fetchingChaptersAllowed(): Promise<boolean> {
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
return true;

View File

@@ -29,9 +29,10 @@ module.exports = env => ({
popup: path.join(__dirname, srcDir + 'popup.ts'),
background: path.join(__dirname, srcDir + 'background.ts'),
content: path.join(__dirname, srcDir + 'content.ts'),
options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.ts'),
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.ts'),
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
document: path.join(__dirname, srcDir + 'document.ts'),
upsell: path.join(__dirname, srcDir + 'upsell.ts')
},
output: {