mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-12 14:37:23 +03:00
Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters
This commit is contained in:
@@ -125,7 +125,7 @@ chrome.runtime.onConnect.addListener((port) => {
|
||||
chrome.runtime.onInstalled.addListener(function () {
|
||||
// This let's the config sync to run fully before checking.
|
||||
// This is required on Firefox
|
||||
setTimeout(function() {
|
||||
setTimeout(async () => {
|
||||
const userID = Config.config.userID;
|
||||
|
||||
// If there is no userID, then it is the first install.
|
||||
@@ -141,6 +141,12 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
// Don't show update notification
|
||||
Config.config.categoryPillUpdate = true;
|
||||
}
|
||||
|
||||
if (Config.config.supportInvidious) {
|
||||
if (!(await utils.containsInvidiousPermission())) {
|
||||
chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")});
|
||||
}
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
@@ -224,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
|
||||
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
|
||||
// If GET, convert JSON to parameters
|
||||
if (type.toLowerCase() === "get") {
|
||||
url = utils.objectToURI(url, data, true);
|
||||
url = GenericUtils.objectToURI(url, data, true);
|
||||
|
||||
data = null;
|
||||
}
|
||||
|
||||
@@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
|
||||
: null}
|
||||
|
||||
<span>
|
||||
{this.props.text}
|
||||
{this.getTextElements(this.props.text)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
private getTextElements(text: string): Array<string | React.ReactElement> {
|
||||
const elements: Array<string | React.ReactElement> = [];
|
||||
const textParts = text.split(/(?=\s+)/);
|
||||
for (const textPart of textParts) {
|
||||
if (textPart.match(/^\s*http/)) {
|
||||
elements.push(
|
||||
<a href={textPart} target="_blank" rel="noreferrer">
|
||||
{textPart}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
elements.push(textPart);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoticeTextSelectionComponent;
|
||||
@@ -115,14 +115,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
style.marginTop = "15px";
|
||||
}
|
||||
|
||||
// This method is required to get !important
|
||||
// https://stackoverflow.com/a/45669262/1985387
|
||||
const oldYouTubeDarkStyles = (node) => {
|
||||
if (node) {
|
||||
node.style.setProperty("color", "black", "important");
|
||||
node.style.setProperty("text-shadow", "none", "important");
|
||||
}
|
||||
};
|
||||
// Create time display
|
||||
let timeDisplay: JSX.Element;
|
||||
const timeDisplayStyle: React.CSSProperties = {};
|
||||
@@ -142,8 +134,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</span>
|
||||
<input id={"submittingTime0" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
ref={oldYouTubeDarkStyles}
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[0]}
|
||||
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
|
||||
@@ -157,8 +149,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
<input id={"submittingTime1" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
ref={oldYouTubeDarkStyles}
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[1]}
|
||||
onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
|
||||
@@ -204,6 +196,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
className="sponsorTimeEditSelector sponsorTimeCategories"
|
||||
defaultValue={sponsorTime.category}
|
||||
ref={this.categoryOptionRef}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
onChange={(event) => this.categorySelectionChange(event)}>
|
||||
{this.getCategoryOptions()}
|
||||
</select>
|
||||
@@ -227,6 +220,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||
defaultValue={sponsorTime.actionType}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
ref={this.actionTypeOptionRef}
|
||||
onChange={(e) => this.actionTypeSelectionChange(e)}>
|
||||
{this.getActionTypeOptions(sponsorTime)}
|
||||
|
||||
@@ -116,10 +116,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
let option: CategorySkipOption;
|
||||
|
||||
this.removeCurrentCategorySelection();
|
||||
|
||||
switch (event.target.value) {
|
||||
case "disable":
|
||||
case "disable":
|
||||
Config.config.categorySelections = Config.config.categorySelections.filter(
|
||||
categorySelection => categorySelection.name !== this.props.category);
|
||||
return;
|
||||
case "showOverlay":
|
||||
option = CategorySkipOption.ShowOverlay;
|
||||
@@ -135,28 +135,17 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
break;
|
||||
}
|
||||
|
||||
Config.config.categorySelections.push({
|
||||
name: this.props.category,
|
||||
option: option
|
||||
});
|
||||
|
||||
// Forces the Proxy to send this to the chrome storage API
|
||||
Config.config.categorySelections = Config.config.categorySelections;
|
||||
}
|
||||
|
||||
/** Removes this category from the config list of category selections */
|
||||
removeCurrentCategorySelection(): void {
|
||||
// Remove it if it exists
|
||||
for (let i = 0; i < Config.config.categorySelections.length; i++) {
|
||||
if (Config.config.categorySelections[i].name === this.props.category) {
|
||||
Config.config.categorySelections.splice(i, 1);
|
||||
|
||||
// Forces the Proxy to send this to the chrome storage API
|
||||
Config.config.categorySelections = Config.config.categorySelections;
|
||||
|
||||
break;
|
||||
}
|
||||
const existingSelection = Config.config.categorySelections.find(selection => selection.name === this.props.category);
|
||||
if (existingSelection) {
|
||||
existingSelection.option = option;
|
||||
} else {
|
||||
Config.config.categorySelections.push({
|
||||
name: this.props.category,
|
||||
option: option
|
||||
});
|
||||
}
|
||||
|
||||
Config.forceSyncUpdate("categorySelections");
|
||||
}
|
||||
|
||||
getCategorySkipOptions(): JSX.Element[] {
|
||||
|
||||
221
src/content.ts
221
src/content.ts
@@ -10,7 +10,6 @@ import SkipNotice from "./render/SkipNotice";
|
||||
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
||||
import SubmissionNotice from "./render/SubmissionNotice";
|
||||
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
||||
import * as Chat from "./js-components/chat";
|
||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
@@ -20,6 +19,7 @@ import { AnimationUtils } from "./utils/animationUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
import { logDebug } from "./utils/logger";
|
||||
import { importTimes } from "./utils/exporter";
|
||||
import { openWarningDialog } from "./utils/warnings";
|
||||
|
||||
// Hack to get the CSS loaded on permission-based sites (Invidious)
|
||||
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
||||
@@ -62,13 +62,14 @@ let sponsorSkipped: boolean[] = [];
|
||||
let video: HTMLVideoElement;
|
||||
let videoMuted = false; // Has it been attempted to be muted
|
||||
let videoMutationObserver: MutationObserver = null;
|
||||
let waitingForNewVideo = false;
|
||||
// List of videos that have had event listeners added to them
|
||||
const videosWithEventListeners: HTMLVideoElement[] = [];
|
||||
const controlsWithEventListeners: HTMLElement[] = []
|
||||
|
||||
// This misleading variable name will be fixed soon
|
||||
let onInvidious;
|
||||
let onMobileYouTube;
|
||||
let onInvidious: boolean;
|
||||
let onMobileYouTube: boolean;
|
||||
|
||||
//the video id of the last preview bar update
|
||||
let lastPreviewBarUpdate;
|
||||
@@ -76,9 +77,6 @@ let lastPreviewBarUpdate;
|
||||
// Is the video currently being switched
|
||||
let switchingVideos = null;
|
||||
|
||||
// Made true every videoID change
|
||||
let firstEvent = false;
|
||||
|
||||
// Used by the play and playing listeners to make sure two aren't
|
||||
// called at the same time
|
||||
let lastCheckTime = 0;
|
||||
@@ -102,7 +100,8 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage
|
||||
// Direct Links after the config is loaded
|
||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
||||
// wait for hover preview to appear, and refresh attachments if ever found
|
||||
window.addEventListener("DOMContentLoaded", () => utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()));
|
||||
utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments())
|
||||
utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document)).then())
|
||||
addPageListeners();
|
||||
addHotkeyListener();
|
||||
|
||||
@@ -119,6 +118,9 @@ let submissionNotice: SubmissionNotice = null;
|
||||
// If there is an advert playing (or about to be played), this is true
|
||||
let isAdPlaying = false;
|
||||
|
||||
let lastResponseStatus: number;
|
||||
let retryCount = 0;
|
||||
|
||||
// Contains all of the functions and variables needed by the skip notice
|
||||
const skipNoticeContentContainer: ContentContainer = () => ({
|
||||
vote,
|
||||
@@ -166,6 +168,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
//send the sponsor times along with if it's found
|
||||
sendResponse({
|
||||
found: sponsorDataFound,
|
||||
status: lastResponseStatus,
|
||||
sponsorTimes: sponsorTimes,
|
||||
time: video.currentTime,
|
||||
onMobileYouTube
|
||||
@@ -206,8 +209,12 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
submitSponsorTimes();
|
||||
break;
|
||||
case "refreshSegments":
|
||||
// update video on refresh if videoID invalid
|
||||
if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document));
|
||||
// fetch segments
|
||||
sponsorsLookup(false).then(() => sendResponse({
|
||||
found: sponsorDataFound,
|
||||
status: lastResponseStatus,
|
||||
sponsorTimes: sponsorTimes,
|
||||
time: video.currentTime,
|
||||
onMobileYouTube
|
||||
@@ -301,6 +308,7 @@ if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
|
||||
function resetValues() {
|
||||
lastCheckTime = 0;
|
||||
lastCheckVideoTime = -1;
|
||||
retryCount = 0;
|
||||
|
||||
sponsorTimes = [];
|
||||
existingChaptersImported = false;
|
||||
@@ -330,8 +338,6 @@ function resetValues() {
|
||||
logDebug("Setting switching videos to true (reset data)");
|
||||
}
|
||||
|
||||
firstEvent = true;
|
||||
|
||||
// Reset advert playing flag
|
||||
isAdPlaying = false;
|
||||
|
||||
@@ -343,7 +349,7 @@ function resetValues() {
|
||||
categoryPill?.setVisibility(false);
|
||||
}
|
||||
|
||||
async function videoIDChange(id) {
|
||||
async function videoIDChange(id): Promise<void> {
|
||||
//if the id has not changed return unless the video element has changed
|
||||
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
|
||||
|
||||
@@ -447,7 +453,7 @@ function createPreviewBar(): void {
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For new mobile YouTube (#1287)
|
||||
selector: ".ytm-progress-bar",
|
||||
selector: ".progress-bar-line",
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For Desktop YouTube
|
||||
@@ -527,6 +533,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure we are on the correct video
|
||||
const newVideoID = getYouTubeVideoID(document);
|
||||
if (newVideoID !== sponsorVideoID) {
|
||||
videoIDChange(newVideoID);
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
|
||||
if (!video) return;
|
||||
if (currentTime === undefined || currentTime === null) {
|
||||
@@ -537,7 +550,16 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
updateActiveSegment(currentTime);
|
||||
|
||||
if (video.paused) return;
|
||||
if (videoMuted && !inMuteSegment(currentTime)) {
|
||||
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
|
||||
const currentSkip = skipInfo.array[skipInfo.index];
|
||||
const skipTime: number[] = [currentSkip?.scheduledTime, skipInfo.array[skipInfo.endIndex]?.segment[1]];
|
||||
const timeUntilSponsor = skipTime?.[0] - currentTime;
|
||||
const videoID = sponsorVideoID;
|
||||
const skipBuffer = 0.003;
|
||||
|
||||
if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
|
||||
&& timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) {
|
||||
video.muted = false;
|
||||
videoMuted = false;
|
||||
|
||||
@@ -547,22 +569,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
}
|
||||
}
|
||||
|
||||
logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
|
||||
if (skipInfo.index === -1) return;
|
||||
|
||||
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
|
||||
return;
|
||||
}
|
||||
|
||||
if (incorrectVideoCheck()) return;
|
||||
|
||||
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
|
||||
logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
|
||||
if (skipInfo.index === -1) return;
|
||||
|
||||
const currentSkip = skipInfo.array[skipInfo.index];
|
||||
const skipTime: number[] = [currentSkip.scheduledTime, skipInfo.array[skipInfo.endIndex].segment[1]];
|
||||
const timeUntilSponsor = skipTime[0] - currentTime;
|
||||
const videoID = sponsorVideoID;
|
||||
|
||||
// Find all indexes in between the start and end
|
||||
let skippingSegments = [skipInfo.array[skipInfo.index]];
|
||||
if (skipInfo.index !== skipInfo.endIndex) {
|
||||
@@ -576,7 +591,11 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
}
|
||||
}
|
||||
|
||||
const skipBuffer = 0.003;
|
||||
logDebug(`Next step in starting skipping: ${!shouldSkip(currentSkip)}, ${!sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)}`);
|
||||
|
||||
// Don't skip if this category should not be skipped
|
||||
if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return;
|
||||
|
||||
const skippingFunction = (forceVideoTime?: number) => {
|
||||
let forcedSkipTime: number = null;
|
||||
let forcedIncludeIntersectingSegments = false;
|
||||
@@ -593,6 +612,19 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
||||
skippingSegments,
|
||||
openNotice: skipInfo.openNotice
|
||||
});
|
||||
|
||||
// These are segments that start at the exact same time but need seperate notices
|
||||
for (const extra of skipInfo.extraIndexes) {
|
||||
const extraSkip = skipInfo.array[extra];
|
||||
if (shouldSkip(extraSkip)) {
|
||||
skipToTime({
|
||||
v: video,
|
||||
skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
|
||||
skippingSegments: [extraSkip],
|
||||
openNotice: skipInfo.openNotice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|
||||
|| currentSkip.actionType === ActionType.Mute) {
|
||||
@@ -649,15 +681,17 @@ function getVirtualTime(): number {
|
||||
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
|
||||
|
||||
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
|
||||
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6) {
|
||||
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
|
||||
return virtualTime;
|
||||
} else {
|
||||
return video.currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
function inMuteSegment(currentTime: number): boolean {
|
||||
const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
|
||||
function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -695,27 +729,30 @@ function setupVideoMutationListener() {
|
||||
});
|
||||
}
|
||||
|
||||
function refreshVideoAttachments() {
|
||||
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
|
||||
if (newVideo && newVideo !== video) {
|
||||
video = newVideo;
|
||||
async function refreshVideoAttachments(): Promise<void> {
|
||||
if (waitingForNewVideo) return;
|
||||
|
||||
if (!videosWithEventListeners.includes(video)) {
|
||||
videosWithEventListeners.push(video);
|
||||
waitingForNewVideo = true;
|
||||
const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement;
|
||||
waitingForNewVideo = false;
|
||||
|
||||
setupVideoListeners();
|
||||
setupSkipButtonControlBar();
|
||||
setupCategoryPill();
|
||||
}
|
||||
video = newVideo;
|
||||
if (!videosWithEventListeners.includes(video)) {
|
||||
videosWithEventListeners.push(video);
|
||||
|
||||
// Create a new bar in the new video element
|
||||
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
||||
previewBar.remove();
|
||||
previewBar = null;
|
||||
|
||||
createPreviewBar();
|
||||
}
|
||||
setupVideoListeners();
|
||||
setupSkipButtonControlBar();
|
||||
setupCategoryPill();
|
||||
}
|
||||
|
||||
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
||||
previewBar.remove();
|
||||
previewBar = null;
|
||||
|
||||
createPreviewBar();
|
||||
}
|
||||
|
||||
videoIDChange(getYouTubeVideoID(document));
|
||||
}
|
||||
|
||||
function setupVideoListeners() {
|
||||
@@ -727,17 +764,18 @@ function setupVideoListeners() {
|
||||
switchingVideos = false;
|
||||
|
||||
let startedWaiting = false;
|
||||
let lastPausedAtZero = true;
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
// 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 (!firstEvent && video.currentTime === 0) return;
|
||||
firstEvent = false;
|
||||
if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA
|
||||
&& video.currentTime === 0) return;
|
||||
|
||||
updateVirtualTime();
|
||||
|
||||
if (switchingVideos) {
|
||||
if (switchingVideos || lastPausedAtZero) {
|
||||
switchingVideos = false;
|
||||
logDebug("Setting switching videos to false");
|
||||
|
||||
@@ -745,6 +783,8 @@ function setupVideoListeners() {
|
||||
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
|
||||
}
|
||||
|
||||
lastPausedAtZero = false;
|
||||
|
||||
// Check if an ad is playing
|
||||
updateAdFlag();
|
||||
|
||||
@@ -760,6 +800,7 @@ function setupVideoListeners() {
|
||||
});
|
||||
video.addEventListener('playing', () => {
|
||||
updateVirtualTime();
|
||||
lastPausedAtZero = false;
|
||||
|
||||
if (startedWaiting) {
|
||||
startedWaiting = false;
|
||||
@@ -767,6 +808,14 @@ function setupVideoListeners() {
|
||||
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`);
|
||||
}
|
||||
|
||||
if (switchingVideos) {
|
||||
switchingVideos = false;
|
||||
logDebug("Setting switching videos to false");
|
||||
|
||||
// If already segments loaded before video, retry to skip starting segments
|
||||
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
|
||||
}
|
||||
|
||||
// Make sure it doesn't get double called with the play event
|
||||
if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
|
||||
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) {
|
||||
@@ -788,6 +837,10 @@ function setupVideoListeners() {
|
||||
startSponsorSchedule();
|
||||
} else {
|
||||
updateActiveSegment(video.currentTime);
|
||||
|
||||
if (video.currentTime === 0) {
|
||||
lastPausedAtZero = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('ratechange', () => startSponsorSchedule());
|
||||
@@ -867,7 +920,6 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
const hashParams = getHashParams();
|
||||
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
|
||||
|
||||
// Check for hashPrefix setting
|
||||
const hashPrefix = (await utils.getHash(sponsorVideoID, 1)).slice(0, 4) as VideoID & HashedValue;
|
||||
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
|
||||
categories,
|
||||
@@ -876,6 +928,9 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
...extraRequestData
|
||||
});
|
||||
|
||||
// store last response status
|
||||
lastResponseStatus = response?.status;
|
||||
|
||||
if (response?.ok) {
|
||||
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
|
||||
?.filter((video) => video.videoID === sponsorVideoID)
|
||||
@@ -887,7 +942,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
?.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
if (!recievedSegments || !recievedSegments.length) {
|
||||
// return if no video found
|
||||
retryFetch();
|
||||
retryFetch(404);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -949,8 +1004,8 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
||||
//otherwise the listener can handle it
|
||||
updatePreviewBar();
|
||||
}
|
||||
} else if (response?.status === 404) {
|
||||
retryFetch();
|
||||
} else {
|
||||
retryFetch(lastResponseStatus);
|
||||
}
|
||||
|
||||
importExistingChapters(true);
|
||||
@@ -999,17 +1054,24 @@ async function lockedCategoriesLookup(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function retryFetch(): void {
|
||||
function retryFetch(errorCode: number): void {
|
||||
if (!Config.config.refetchWhenNotFound) return;
|
||||
|
||||
sponsorDataFound = false;
|
||||
|
||||
if (errorCode !== 404 && retryCount > 1) {
|
||||
// Too many errors (50x), give up
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
|
||||
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
|
||||
setTimeout(() => {
|
||||
if (sponsorVideoID && sponsorTimes?.length === 0
|
||||
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
|
||||
sponsorsLookup();
|
||||
}
|
||||
}, 10000 + Math.random() * 30000);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1073,28 +1135,29 @@ function startSkipScheduleCheckingForStartSponsors() {
|
||||
}
|
||||
}
|
||||
|
||||
function getYouTubeVideoID(document: Document): string | boolean {
|
||||
const url = document.URL;
|
||||
function getYouTubeVideoID(document: Document, url?: string): string | boolean {
|
||||
url ||= document.URL;
|
||||
// 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/
|
||||
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false);
|
||||
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false);
|
||||
// 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(document);
|
||||
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument();
|
||||
// not sure, try URL then document
|
||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false);
|
||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
|
||||
}
|
||||
|
||||
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
|
||||
function getYouTubeVideoIDFromDocument(hideIcon = true): string | boolean {
|
||||
// get ID from document (channel trailer / embedded playlist)
|
||||
const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
|
||||
const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']");
|
||||
const videoURL = element?.getAttribute("href");
|
||||
if (videoURL) {
|
||||
onInvidious = hideIcon;
|
||||
return getYouTubeVideoIDFromURL(videoURL);
|
||||
} else {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1241,13 +1304,33 @@ async function whitelistCheck() {
|
||||
* Returns info about the next upcoming sponsor skip
|
||||
*/
|
||||
function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean):
|
||||
{array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} {
|
||||
{array: ScheduledTime[], index: number, endIndex: number, extraIndexes: number[], openNotice: boolean} {
|
||||
|
||||
const autoSkipSorter = (segment: ScheduledTime) => {
|
||||
const skipOption = utils.getCategorySelection(segment.category)?.option;
|
||||
if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment))
|
||||
&& segment.actionType === ActionType.Skip) {
|
||||
return 0;
|
||||
} else if (skipOption !== CategorySkipOption.ShowOverlay) {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
|
||||
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true);
|
||||
|
||||
const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime));
|
||||
// This is an array in-case multiple segments have the exact same start time
|
||||
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]))
|
||||
|| (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);
|
||||
|
||||
const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex);
|
||||
|
||||
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
|
||||
@@ -1263,6 +1346,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
||||
array: submittedArray,
|
||||
index: minSponsorTimeIndex,
|
||||
endIndex: endTimeIndex,
|
||||
extraIndexes, // Segments at same time that need seperate notices
|
||||
openNotice: true
|
||||
};
|
||||
} else {
|
||||
@@ -1270,6 +1354,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
||||
array: unsubmittedArray,
|
||||
index: minUnsubmittedSponsorTimeIndex,
|
||||
endIndex: previewEndTimeIndex,
|
||||
extraIndexes: [], // No manual things for unsubmitted
|
||||
openNotice: false
|
||||
};
|
||||
}
|
||||
@@ -1855,10 +1940,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
|
||||
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
|
||||
} else if (response.successType == -1) {
|
||||
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
|
||||
skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
|
||||
Chat.openWarningChat(response.responseText);
|
||||
skipNotice.closeListener.call(skipNotice);
|
||||
}, chrome.i18n.getMessage("voteRejectedWarning"));
|
||||
openWarningDialog(skipNoticeContentContainer);
|
||||
} else {
|
||||
skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
|
||||
}
|
||||
@@ -2046,7 +2128,7 @@ async function sendSubmitMessage() {
|
||||
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg");
|
||||
|
||||
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
|
||||
Chat.openWarningChat(response.responseText);
|
||||
openWarningDialog(skipNoticeContentContainer);
|
||||
} else {
|
||||
alert(GenericUtils.getErrorMessage(response.status, response.responseText));
|
||||
}
|
||||
@@ -2272,7 +2354,8 @@ 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", () => videoIDChange(getYouTubeVideoID(document)));
|
||||
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) =>
|
||||
videoIDChange(getYouTubeVideoID(document, (e as unknown as Record<string, Record<string, string>>).destination.url)));
|
||||
}
|
||||
|
||||
// Record availability of Navigation API
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import Config from "../config";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
export interface ChatConfig {
|
||||
displayName: string,
|
||||
composerInitialValue?: string,
|
||||
customDescription?: string
|
||||
}
|
||||
|
||||
export function openChat(config: ChatConfig): void {
|
||||
const chat = document.createElement("div");
|
||||
chat.classList.add("sbChatNotice");
|
||||
chat.style.zIndex = "2000";
|
||||
|
||||
const iframe= document.createElement("iframe");
|
||||
iframe.src = "https://chat.sponsor.ajay.app/#" + utils.objectToURI("", config, false);
|
||||
chat.appendChild(iframe);
|
||||
|
||||
const closeButton = document.createElement("img");
|
||||
closeButton.classList.add("sbChatClose");
|
||||
closeButton.src = chrome.extension.getURL("icons/close.png");
|
||||
closeButton.addEventListener("click", () => {
|
||||
chat.remove();
|
||||
closeButton.remove();
|
||||
});
|
||||
chat.appendChild(closeButton);
|
||||
|
||||
const referenceNode = utils.findReferenceNode();
|
||||
referenceNode.prepend(chat);
|
||||
}
|
||||
|
||||
export async function openWarningChat(warningMessage: string): Promise<void> {
|
||||
const warningReasonMatch = warningMessage.match(/Warning reason: '(.+)'/);
|
||||
alert(chrome.i18n.getMessage("warningChatInfo") + `\n\n${warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``}`);
|
||||
|
||||
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
|
||||
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
|
||||
const publicUserID = await utils.getHash(Config.config.userID);
|
||||
|
||||
openChat({
|
||||
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`,
|
||||
composerInitialValue: `I got a warning and confirm I [REMOVE THIS CAPITAL TEXT TO CONFIRM] reread the guidelines.` +
|
||||
warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``,
|
||||
customDescription: chrome.i18n.getMessage("warningChatInfo")
|
||||
});
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoF
|
||||
|
||||
export interface IsInfoFoundMessageResponse {
|
||||
found: boolean;
|
||||
status: number;
|
||||
sponsorTimes: SponsorTime[];
|
||||
time: number;
|
||||
onMobileYouTube: boolean;
|
||||
@@ -100,7 +101,7 @@ export type MessageResponse =
|
||||
| GetChannelIDResponse
|
||||
| SponsorStartResponse
|
||||
| IsChannelWhitelistedResponse
|
||||
| Record<string, never>
|
||||
| Record<string, never> // empty object response {}
|
||||
| VoteResponse
|
||||
| ImportSegmentsResponse;
|
||||
|
||||
|
||||
@@ -452,13 +452,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
||||
* @param option
|
||||
*/
|
||||
function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
||||
let permissions = ["declarativeContent"];
|
||||
if (utils.isFirefox()) permissions = [];
|
||||
|
||||
chrome.permissions.contains({
|
||||
origins: utils.getPermissionRegex(),
|
||||
permissions: permissions
|
||||
}, function (result) {
|
||||
utils.containsInvidiousPermission().then((result) => {
|
||||
if (result != checkbox.checked) {
|
||||
Config.config[option] = result;
|
||||
|
||||
@@ -474,22 +468,8 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
||||
* @param option
|
||||
*/
|
||||
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (checkbox.checked) {
|
||||
utils.setupExtraSitePermissions(function (granted) {
|
||||
if (!granted) {
|
||||
Config.config[option] = false;
|
||||
checkbox.checked = false;
|
||||
} else {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
utils.removeExtraSiteRegistration();
|
||||
}
|
||||
});
|
||||
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
|
||||
checkbox.checked = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,8 +578,9 @@ async function setTextOption(option: string, element: HTMLElement, value: string
|
||||
function downloadConfig() {
|
||||
const file = document.createElement("a");
|
||||
const jsonData = JSON.parse(JSON.stringify(Config.cachedSyncConfig));
|
||||
file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData)));
|
||||
file.setAttribute("download", "SponsorBlockConfig.json");
|
||||
const dateTimeString = new Date().toJSON().replace("T", "_").replace(/:/g, ".").replace(/.\d+Z/g, "")
|
||||
file.setAttribute("href", `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(jsonData))}`);
|
||||
file.setAttribute("download", `SponsorBlockConfig_${dateTimeString}.json`);
|
||||
document.body.append(file);
|
||||
file.click();
|
||||
file.remove();
|
||||
@@ -673,4 +654,4 @@ function copyDebugOutputToClipboard() {
|
||||
|
||||
function isIncognitoAllowed(): Promise<boolean> {
|
||||
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init);
|
||||
async function init() {
|
||||
localizeHtmlPage();
|
||||
|
||||
const domains = document.location.hash.replace("#", "").split(",");
|
||||
|
||||
const acceptButton = document.getElementById("acceptPermissionButton");
|
||||
acceptButton.addEventListener("click", () => {
|
||||
chrome.permissions.request({
|
||||
origins: utils.getPermissionRegex(domains),
|
||||
permissions: []
|
||||
}, (granted) => {
|
||||
if (granted) {
|
||||
utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => {
|
||||
Config.config.supportInvidious = enabled;
|
||||
|
||||
if (enabled) {
|
||||
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
|
||||
|
||||
Config.config.ytInfoPermissionGranted = true;
|
||||
|
||||
chrome.tabs.getCurrent((tab) => {
|
||||
chrome.tabs.remove(tab.id);
|
||||
});
|
||||
window.close();
|
||||
} else {
|
||||
alert(chrome.i18n.getMessage("permissionRequestFailed"));
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -415,8 +415,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
if (request.sponsorTimes) {
|
||||
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
|
||||
}
|
||||
} else {
|
||||
} else if (request.status == 404 || request.status == 200) {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
||||
} else {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,7 +666,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
voteButtonsContainer.appendChild(upvoteButton);
|
||||
voteButtonsContainer.appendChild(downvoteButton);
|
||||
voteButtonsContainer.appendChild(uuidButton);
|
||||
if (downloadedTimes[i].actionType === ActionType.Skip
|
||||
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
|
||||
voteButtonsContainer.appendChild(hideButton);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,13 @@ export default class GenericNotice {
|
||||
hideRightInfo={options.hideRightInfo}
|
||||
closeListener={() => this.close()} >
|
||||
|
||||
{this.getMessageBox(this.idSuffix, options.textBoxes)}
|
||||
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
|
||||
className="sponsorTimeMessagesRow"
|
||||
style={{maxHeight: (this.contentContainer().v.offsetHeight - 200) + "px"}}>
|
||||
<td style={{width: "100%"}}>
|
||||
{this.getMessageBoxes(this.idSuffix, options.textBoxes)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
|
||||
className="sponsorBlockSpacer">
|
||||
@@ -90,7 +96,7 @@ export default class GenericNotice {
|
||||
);
|
||||
}
|
||||
|
||||
getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
|
||||
getMessageBoxes(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
|
||||
if (textBoxes) {
|
||||
const result = [];
|
||||
for (let i = 0; i < textBoxes.length; i++) {
|
||||
|
||||
123
src/utils.ts
123
src/utils.ts
@@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config";
|
||||
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
|
||||
|
||||
import * as CompileConfig from "../config.json";
|
||||
import { findValidElementFromSelector } from "./utils/pageUtils";
|
||||
import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
|
||||
export default class Utils {
|
||||
@@ -22,8 +22,9 @@ export default class Utils {
|
||||
];
|
||||
|
||||
/* Used for waitForElement */
|
||||
waitingMutationObserver:MutationObserver = null;
|
||||
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
|
||||
creatingWaitingMutationObserver = false;
|
||||
waitingMutationObserver: MutationObserver = null;
|
||||
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
|
||||
|
||||
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
|
||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
@@ -34,40 +35,66 @@ export default class Utils {
|
||||
}
|
||||
|
||||
/* Uses a mutation observer to wait asynchronously */
|
||||
async waitForElement(selector: string): Promise<Element> {
|
||||
async waitForElement(selector: string, visibleCheck = false): Promise<Element> {
|
||||
return await new Promise((resolve) => {
|
||||
const initialElement = this.getElement(selector, visibleCheck);
|
||||
if (initialElement) {
|
||||
resolve(initialElement);
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitingElements.push({
|
||||
selector,
|
||||
visibleCheck,
|
||||
callback: resolve
|
||||
});
|
||||
|
||||
if (!this.waitingMutationObserver) {
|
||||
this.waitingMutationObserver = new MutationObserver(() => {
|
||||
const foundSelectors = [];
|
||||
for (const { selector, callback } of this.waitingElements) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
callback(element);
|
||||
foundSelectors.push(selector);
|
||||
}
|
||||
}
|
||||
if (!this.creatingWaitingMutationObserver) {
|
||||
this.creatingWaitingMutationObserver = true;
|
||||
|
||||
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
|
||||
|
||||
if (this.waitingElements.length === 0) {
|
||||
this.waitingMutationObserver.disconnect();
|
||||
this.waitingMutationObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.waitingMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
if (document.body) {
|
||||
this.setupWaitingMutationListener();
|
||||
} else {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
this.setupWaitingMutationListener();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupWaitingMutationListener(): void {
|
||||
if (!this.waitingMutationObserver) {
|
||||
this.waitingMutationObserver = new MutationObserver(() => {
|
||||
const foundSelectors = [];
|
||||
for (const { selector, visibleCheck, callback } of this.waitingElements) {
|
||||
const element = this.getElement(selector, visibleCheck);
|
||||
if (element) {
|
||||
callback(element);
|
||||
foundSelectors.push(selector);
|
||||
}
|
||||
}
|
||||
|
||||
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
|
||||
|
||||
if (this.waitingElements.length === 0) {
|
||||
this.waitingMutationObserver.disconnect();
|
||||
this.waitingMutationObserver = null;
|
||||
this.creatingWaitingMutationObserver = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.waitingMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getElement(selector: string, visibleCheck: boolean) {
|
||||
return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector);
|
||||
}
|
||||
|
||||
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.permissions.contains(permissions, resolve)
|
||||
@@ -183,6 +210,37 @@ export default class Utils {
|
||||
});
|
||||
}
|
||||
|
||||
applyInvidiousPermissions(enable: boolean, option = "supportInvidious"): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
if (enable) {
|
||||
this.setupExtraSitePermissions((granted) => {
|
||||
if (!granted) {
|
||||
Config.config[option] = false;
|
||||
}
|
||||
|
||||
resolve(granted);
|
||||
});
|
||||
} else {
|
||||
this.removeExtraSiteRegistration();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
containsInvidiousPermission(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
let permissions = ["declarativeContent"];
|
||||
if (this.isFirefox()) permissions = [];
|
||||
|
||||
chrome.permissions.contains({
|
||||
origins: this.getPermissionRegex(),
|
||||
permissions: permissions
|
||||
}, function (result) {
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
|
||||
*/
|
||||
@@ -358,19 +416,6 @@ export default class Utils {
|
||||
return referenceNode;
|
||||
}
|
||||
|
||||
objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
|
||||
let counter = 0;
|
||||
for (const key in data) {
|
||||
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
|
||||
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
|
||||
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
isContentScript(): boolean {
|
||||
return window.location.protocol === "http:" || window.location.protocol === "https:";
|
||||
}
|
||||
|
||||
@@ -108,6 +108,27 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
|
||||
} : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all indexes that have the specified value
|
||||
* https://stackoverflow.com/a/54954694/1985387
|
||||
*/
|
||||
function indexesOf<T>(array: T[], value: T): number[] {
|
||||
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
|
||||
}
|
||||
|
||||
function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
|
||||
let counter = 0;
|
||||
for (const key in data) {
|
||||
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
|
||||
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
|
||||
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
||||
|
||||
counter++;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function generateUserID(length = 36): string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
@@ -132,5 +153,7 @@ export const GenericUtils = {
|
||||
getFormattedTimeToSeconds,
|
||||
getErrorMessage,
|
||||
getLuminance,
|
||||
generateUserID
|
||||
generateUserID,
|
||||
indexesOf,
|
||||
objectToURI
|
||||
}
|
||||
66
src/utils/warnings.ts
Normal file
66
src/utils/warnings.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import Config from "../config";
|
||||
import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
|
||||
import { ContentContainer } from "../types";
|
||||
import Utils from "../utils";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
const utils = new Utils();
|
||||
|
||||
export interface ChatConfig {
|
||||
displayName: string,
|
||||
composerInitialValue?: string,
|
||||
customDescription?: string
|
||||
}
|
||||
|
||||
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
|
||||
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
userID: Config.config.userID,
|
||||
values: ["warningReason"]
|
||||
});
|
||||
|
||||
if (userInfo.ok) {
|
||||
const warningReason = JSON.parse(userInfo.responseText)?.warningReason;
|
||||
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
|
||||
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
|
||||
const publicUserID = await utils.getHash(Config.config.userID);
|
||||
|
||||
let notice: GenericNotice = null;
|
||||
const options: NoticeOptions = {
|
||||
title: chrome.i18n.getMessage("warningTitle"),
|
||||
textBoxes: [{
|
||||
text: chrome.i18n.getMessage("warningChatInfo"),
|
||||
icon: null
|
||||
}, ...warningReason.split("\n").map((reason) => ({
|
||||
text: reason,
|
||||
icon: null
|
||||
}))],
|
||||
buttons: [{
|
||||
name: chrome.i18n.getMessage("questionButton"),
|
||||
listener: () => openChat({
|
||||
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`
|
||||
})
|
||||
},
|
||||
{
|
||||
name: chrome.i18n.getMessage("warningConfirmButton"),
|
||||
listener: async () => {
|
||||
const result = await utils.asyncRequestToServer("POST", "/api/warnUser", {
|
||||
userID: Config.config.userID,
|
||||
enabled: false
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
notice?.close();
|
||||
} else {
|
||||
alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`);
|
||||
}
|
||||
}
|
||||
}],
|
||||
timed: false
|
||||
};
|
||||
|
||||
notice = new GenericNotice(contentContainer, "warningNotice", options);
|
||||
}
|
||||
}
|
||||
|
||||
export function openChat(config: ChatConfig): void {
|
||||
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
|
||||
}
|
||||
Reference in New Issue
Block a user