mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-09 13:07:05 +03:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d3487b07 | ||
|
|
3ef2673bfc | ||
|
|
ac6cd2cec1 | ||
|
|
995ed929ca | ||
|
|
592af4e20f | ||
|
|
ecfcb0b846 | ||
|
|
18d10ada5e | ||
|
|
3a7b6b27c2 | ||
|
|
fea8f93b5a | ||
|
|
daa7a653c9 | ||
|
|
59f63f1b4b | ||
|
|
e432abe79d | ||
|
|
08a063b612 | ||
|
|
2d14176542 | ||
|
|
5fad4509f0 | ||
|
|
bd44c4721b | ||
|
|
606b2fbee3 | ||
|
|
f18aa19172 | ||
|
|
8337b54a44 | ||
|
|
3879cc6de3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ web-ext-artifacts
|
|||||||
.vscode/
|
.vscode/
|
||||||
dist/
|
dist/
|
||||||
tmp/
|
tmp/
|
||||||
|
.DS_Store
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "__MSG_fullName__",
|
"name": "__MSG_fullName__",
|
||||||
"short_name": "SponsorBlock",
|
"short_name": "SponsorBlock",
|
||||||
"version": "2.0.14.2",
|
"version": "2.0.15",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"description": "__MSG_Description__",
|
"description": "__MSG_Description__",
|
||||||
"content_scripts": [{
|
"content_scripts": [{
|
||||||
|
|||||||
@@ -79,6 +79,9 @@
|
|||||||
"sponsorEnd": {
|
"sponsorEnd": {
|
||||||
"message": "Segment Ends Now"
|
"message": "Segment Ends Now"
|
||||||
},
|
},
|
||||||
|
"sponsorCancel": {
|
||||||
|
"message": "Cancel Creating Segment"
|
||||||
|
},
|
||||||
"noVideoID": {
|
"noVideoID": {
|
||||||
"message": "No YouTube video found.\nIf this is incorrect, refresh the tab."
|
"message": "No YouTube video found.\nIf this is incorrect, refresh the tab."
|
||||||
},
|
},
|
||||||
@@ -407,15 +410,6 @@
|
|||||||
"areYouSureReset": {
|
"areYouSureReset": {
|
||||||
"message": "Are you sure you would like to reset this?"
|
"message": "Are you sure you would like to reset this?"
|
||||||
},
|
},
|
||||||
"confirmPrivacy": {
|
|
||||||
"message": "The video has been detected as unlisted. Click cancel if you do not want to check for skip segments."
|
|
||||||
},
|
|
||||||
"unlistedCheck": {
|
|
||||||
"message": "Ignore Unlisted/Private Videos"
|
|
||||||
},
|
|
||||||
"whatUnlistedCheck": {
|
|
||||||
"message": "This setting will slightly slow down SponsorBlock. Skip segment lookups require sending the video ID to the server. If you are concerned about unlisted video IDs being sent over the internet, enable this option."
|
|
||||||
},
|
|
||||||
"mobileUpdateInfo": {
|
"mobileUpdateInfo": {
|
||||||
"message": "m.youtube.com is now supported"
|
"message": "m.youtube.com is now supported"
|
||||||
},
|
},
|
||||||
@@ -606,9 +600,6 @@
|
|||||||
"permissionRequestFailed": {
|
"permissionRequestFailed": {
|
||||||
"message": "Permission request failed, did you click deny?"
|
"message": "Permission request failed, did you click deny?"
|
||||||
},
|
},
|
||||||
"adblockerIssueUnlistedVideosInfo": {
|
|
||||||
"message": "If you are unable to resolve this, then disable the setting 'Ignore unlisted/private videos', as SponsorBlock is unable to retrieve the visibility information for this video"
|
|
||||||
},
|
|
||||||
"adblockerIssueWhitelist": {
|
"adblockerIssueWhitelist": {
|
||||||
"message": "If you are unable to resolve this, then disable the setting 'Force Channel Check Before Skipping', as SponsorBlock is unable to retrieve the channel information for this video"
|
"message": "If you are unable to resolve this, then disable the setting 'Force Channel Check Before Skipping', as SponsorBlock is unable to retrieve the channel information for this video"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,6 +79,9 @@
|
|||||||
"sponsorEnd": {
|
"sponsorEnd": {
|
||||||
"message": "Koniec segmentu"
|
"message": "Koniec segmentu"
|
||||||
},
|
},
|
||||||
|
"sponsorCancel": {
|
||||||
|
"message": "Anuluj tworzenie segmentu"
|
||||||
|
},
|
||||||
"noVideoID": {
|
"noVideoID": {
|
||||||
"message": "Nie znaleziono filmu YouTube.\nJeżeli to błąd, odśwież stronę."
|
"message": "Nie znaleziono filmu YouTube.\nJeżeli to błąd, odśwież stronę."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,12 +42,12 @@
|
|||||||
<img src="https://i.imgur.com/caf5Bju.png">
|
<img src="https://i.imgur.com/caf5Bju.png">
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
Videos will automatically be skipped if they are found in the database. You can open the popup by clicking the extension icon to get a preview of what they are.
|
Video segments will automatically be skipped if they are found in the database. You can open the popup by clicking the extension icon to get a preview of what they are.
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
Whenever you skip a video, you will get a notice report that submission. If the timing seems wrong, report it! You can also vote in the popup. The extension auto upvotes it if you don't report it, so make sure to report when necessary (this can be disabled in the options).
|
Whenever you skip a segment, you will get notice. If the timing seems wrong vote down by clicking downvote! You can also vote in the popup.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="center"><img height="120px" src="https://user-images.githubusercontent.com/12688112/63067735-5a638700-bede-11e9-8147-f321b57527ec.gif"></div>
|
<div class="center"><img height="120px" src="https://user-images.githubusercontent.com/12688112/63067735-5a638700-bede-11e9-8147-f321b57527ec.gif"></div>
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
<h1>This is too slow</h1>
|
<h1>This is too slow</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
There are hotkeys if you want to use them. You must be focused on the YouTube player to use them. Press the semicolon key to indicate the start/end of a sponsor segment and click the appostrophe to submit.
|
There are hotkeys if you want to use them. You must be focused on the YouTube player to use them. Press the semicolon key to indicate the start/end of a sponsor segment and click the apostrophe to submit.
|
||||||
These can be changed in the options. If you don't use QWERTY, you should probably change the keybinds.
|
These can be changed in the options. If you don't use QWERTY, you should probably change the keybinding.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1>I hate these buttons, they are so ugly</h1>
|
<h1>I hate these buttons, they are so ugly</h1>
|
||||||
|
|||||||
@@ -309,23 +309,6 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div option-type="toggle" sync-option="hashPrefix">
|
|
||||||
<label class="switch-container" label-name="__MSG_enableQueryByHashPrefix__">
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" checked>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div class="small-description">__MSG_whatQueryByHashPrefix__</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div option-type="toggle" sync-option="refetchWhenNotFound">
|
<div option-type="toggle" sync-option="refetchWhenNotFound">
|
||||||
<label class="switch-container" label-name="__MSG_enableRefetchWhenNotFound__">
|
<label class="switch-container" label-name="__MSG_enableRefetchWhenNotFound__">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
@@ -343,23 +326,6 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div option-type="toggle" sync-option="checkForUnlistedVideos">
|
|
||||||
<label class="switch-container" label-name="__MSG_unlistedCheck__">
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox">
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div class="small-description">__MSG_whatUnlistedCheck__</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div option-type="private-text-change" sync-option="userID" confirm-message="userIDChangeWarning">
|
<div option-type="private-text-change" sync-option="userID" confirm-message="userIDChangeWarning">
|
||||||
<div class="option-button trigger-button">
|
<div class="option-button trigger-button">
|
||||||
__MSG_changeUserID__
|
__MSG_changeUserID__
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="submissionSection" style="display: none">
|
<div id="submissionSection" style="display: none">
|
||||||
<b style="display: block; margin-top: 12px;">__MSG_submissionEditHint__</b>
|
<b style="display: block; margin-top: 12px;">__MSG_submissionEditHint__</b>
|
||||||
<div id="submitTimesContainer" style="display: none; margin-top: 12px;">
|
<div id="submitTimesContainer" style="margin-top: 12px;">
|
||||||
<button id="submitTimes" class="mediumButton">__MSG_submitTimesButton__</button>
|
<button id="submitTimes" class="mediumButton">__MSG_submitTimesButton__</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
//if it is not a complete sponsor time
|
//if it is not a complete sponsor time
|
||||||
if (sponsorTimes[index].segment.length < 2) {
|
if (sponsorTimes[index].segment.length < 2) {
|
||||||
//update video player
|
//update video player
|
||||||
this.props.contentContainer().changeStartSponsorButton(true, false);
|
this.props.contentContainer().updateEditButtonsOnPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
sponsorTimes.splice(index, 1);
|
sponsorTimes.splice(index, 1);
|
||||||
@@ -359,7 +359,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
this.props.submissionNotice.cancel();
|
this.props.submissionNotice.cancel();
|
||||||
|
|
||||||
//update video player
|
//update video player
|
||||||
this.props.contentContainer().changeStartSponsorButton(true, false);
|
this.props.contentContainer().updateEditButtonsOnPlayer();
|
||||||
} else {
|
} else {
|
||||||
//update display
|
//update display
|
||||||
this.props.submissionNotice.forceUpdate();
|
this.props.submissionNotice.forceUpdate();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const utils = new Utils();
|
|||||||
|
|
||||||
interface SBConfig {
|
interface SBConfig {
|
||||||
userID: string,
|
userID: string,
|
||||||
|
/** Contains unsubmitted segments that the user has created. */
|
||||||
segmentTimes: SBMap<string, SponsorTime[]>,
|
segmentTimes: SBMap<string, SponsorTime[]>,
|
||||||
defaultCategory: string,
|
defaultCategory: string,
|
||||||
whitelistedChannels: string[],
|
whitelistedChannels: string[],
|
||||||
@@ -34,7 +35,6 @@ interface SBConfig {
|
|||||||
audioNotificationOnSkip,
|
audioNotificationOnSkip,
|
||||||
checkForUnlistedVideos: boolean,
|
checkForUnlistedVideos: boolean,
|
||||||
testingServer: boolean,
|
testingServer: boolean,
|
||||||
hashPrefix: boolean,
|
|
||||||
refetchWhenNotFound: boolean,
|
refetchWhenNotFound: boolean,
|
||||||
ytInfoPermissionGranted: boolean,
|
ytInfoPermissionGranted: boolean,
|
||||||
|
|
||||||
@@ -168,7 +168,6 @@ const Config: SBObject = {
|
|||||||
audioNotificationOnSkip: false,
|
audioNotificationOnSkip: false,
|
||||||
checkForUnlistedVideos: false,
|
checkForUnlistedVideos: false,
|
||||||
testingServer: false,
|
testingServer: false,
|
||||||
hashPrefix: true,
|
|
||||||
refetchWhenNotFound: true,
|
refetchWhenNotFound: true,
|
||||||
ytInfoPermissionGranted: false,
|
ytInfoPermissionGranted: false,
|
||||||
|
|
||||||
|
|||||||
466
src/content.ts
466
src/content.ts
@@ -1,6 +1,6 @@
|
|||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
|
|
||||||
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, FetchResponse, VideoInfo, StorageChangesObject } from "./types";
|
import { SponsorTime, IncompleteSponsorTime, CategorySkipOption, VideoID, SponsorHideType, FetchResponse, VideoInfo, StorageChangesObject } from "./types";
|
||||||
|
|
||||||
import { ContentContainer } from "./types";
|
import { ContentContainer } from "./types";
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
@@ -71,8 +71,11 @@ let channelWhitelisted = false;
|
|||||||
// create preview bar
|
// create preview bar
|
||||||
let previewBar: PreviewBar = null;
|
let previewBar: PreviewBar = null;
|
||||||
|
|
||||||
//the player controls on the YouTube player
|
/** Element containing the player controls on the YouTube player. */
|
||||||
let controls = null;
|
let controls: HTMLElement | null = null;
|
||||||
|
|
||||||
|
/** Contains buttons created by `createButton()`. */
|
||||||
|
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement}> = {};
|
||||||
|
|
||||||
// Direct Links after the config is loaded
|
// Direct Links after the config is loaded
|
||||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
|
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
|
||||||
@@ -81,10 +84,10 @@ utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYo
|
|||||||
//this only happens if there is an error
|
//this only happens if there is an error
|
||||||
let sponsorLookupRetries = 0;
|
let sponsorLookupRetries = 0;
|
||||||
|
|
||||||
//if showing the start sponsor button or the end sponsor button on the player
|
/** Currently timed segment, which will be added to the unsubmitted segments when ready. */
|
||||||
let showingStartSponsor = true;
|
let currentlyTimedSegment: IncompleteSponsorTime | null = null;
|
||||||
|
|
||||||
//the sponsor times being prepared to be submitted
|
/** Segments created by the user which have not yet been submitted. */
|
||||||
let sponsorTimesSubmitting: SponsorTime[] = [];
|
let sponsorTimesSubmitting: SponsorTime[] = [];
|
||||||
|
|
||||||
//becomes true when isInfoFound is called
|
//becomes true when isInfoFound is called
|
||||||
@@ -111,12 +114,15 @@ const skipNoticeContentContainer: ContentContainer = () => ({
|
|||||||
onMobileYouTube,
|
onMobileYouTube,
|
||||||
sponsorSubmissionNotice: submissionNotice,
|
sponsorSubmissionNotice: submissionNotice,
|
||||||
resetSponsorSubmissionNotice,
|
resetSponsorSubmissionNotice,
|
||||||
changeStartSponsorButton,
|
updateEditButtonsOnPlayer,
|
||||||
previewTime,
|
previewTime,
|
||||||
videoInfo,
|
videoInfo,
|
||||||
getRealCurrentTime: getRealCurrentTime
|
getRealCurrentTime: getRealCurrentTime
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
||||||
|
const manualSkipPercentCount = 0.5;
|
||||||
|
|
||||||
//get messages from the background script and the popup
|
//get messages from the background script and the popup
|
||||||
chrome.runtime.onMessage.addListener(messageListener);
|
chrome.runtime.onMessage.addListener(messageListener);
|
||||||
|
|
||||||
@@ -127,11 +133,11 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
videoIDChange(getYouTubeVideoID(document.URL));
|
videoIDChange(getYouTubeVideoID(document.URL));
|
||||||
break;
|
break;
|
||||||
case "sponsorStart":
|
case "sponsorStart":
|
||||||
sponsorMessageStarted(sendResponse);
|
startOrEndTimingNewSegment()
|
||||||
|
|
||||||
break;
|
sendResponse({
|
||||||
case "sponsorDataChanged":
|
creatingSegment: currentlyTimedSegment !== null,
|
||||||
updateSponsorTimesSubmitting();
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "isInfoFound":
|
case "isInfoFound":
|
||||||
@@ -150,7 +156,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
break;
|
break;
|
||||||
case "getVideoID":
|
case "getVideoID":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
videoID: sponsorVideoID
|
videoID: sponsorVideoID,
|
||||||
|
creatingSegment: currentlyTimedSegment !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -170,10 +177,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
channelWhitelisted = request.value;
|
channelWhitelisted = request.value;
|
||||||
sponsorsLookup(sponsorVideoID);
|
sponsorsLookup(sponsorVideoID);
|
||||||
|
|
||||||
break;
|
|
||||||
case "changeStartSponsorButton":
|
|
||||||
changeStartSponsorButton(request.showStartSponsor, request.uploadButtonVisible);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "submitTimes":
|
case "submitTimes":
|
||||||
submitSponsorTimes();
|
submitSponsorTimes();
|
||||||
@@ -250,29 +253,25 @@ async function videoIDChange(id) {
|
|||||||
// Wait for options to be ready
|
// Wait for options to be ready
|
||||||
await utils.wait(() => Config.config !== null, 5000, 1);
|
await utils.wait(() => Config.config !== null, 5000, 1);
|
||||||
|
|
||||||
|
// If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up
|
||||||
|
if (Config.config.checkForUnlistedVideos) {
|
||||||
|
const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled."
|
||||||
|
+ " Due to a change in how segment fetching works, this setting is not needed anymore as it cannot leak your video ID to the server."
|
||||||
|
+ " It instead sends just the first 4 characters of a longer hash of the videoID to the server, and filters through a subset of the database."
|
||||||
|
+ " More info about this implementation can be found here: https://github.com/ajayyy/SponsorBlockServer/issues/25"
|
||||||
|
+ "\n\nPlease click okay to confirm that you acknowledge this and continue using SponsorBlock.");
|
||||||
|
if (shouldContinue) {
|
||||||
|
Config.config.checkForUnlistedVideos = false;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get new video info
|
// Get new video info
|
||||||
getVideoInfo();
|
getVideoInfo();
|
||||||
|
|
||||||
// If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up
|
|
||||||
if (Config.config.checkForUnlistedVideos) {
|
|
||||||
try {
|
|
||||||
await utils.wait(() => !!videoInfo, 5000, 1);
|
|
||||||
} catch (err) {
|
|
||||||
await videoInfoFetchFailed("adblockerIssueUnlistedVideosInfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUnlisted()) {
|
|
||||||
const shouldContinue = confirm(chrome.i18n.getMessage("confirmPrivacy"));
|
|
||||||
if(!shouldContinue) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update whitelist data when the video data is loaded
|
// Update whitelist data when the video data is loaded
|
||||||
utils.wait(() => !!videoInfo, 5000, 10).then(whitelistCheck).catch(() => {
|
whitelistCheck();
|
||||||
if (Config.config.forceChannelCheck) {
|
|
||||||
videoInfoFetchFailed("adblockerIssueWhitelist");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//setup the preview bar
|
//setup the preview bar
|
||||||
if (previewBar === null) {
|
if (previewBar === null) {
|
||||||
@@ -301,30 +300,18 @@ async function videoIDChange(id) {
|
|||||||
|
|
||||||
sponsorsLookup(id);
|
sponsorsLookup(id);
|
||||||
|
|
||||||
//make sure everything is properly added
|
// Make sure all player buttons are properly added
|
||||||
updateVisibilityOfPlayerControlsButton().then(() => {
|
|
||||||
//see if the onvideo control image needs to be changed
|
|
||||||
const segments = Config.config.segmentTimes.get(sponsorVideoID);
|
|
||||||
if (segments != null && segments.length > 0 && segments[segments.length - 1].segment.length >= 2) {
|
|
||||||
changeStartSponsorButton(true, true);
|
|
||||||
} else if (segments != null && segments.length > 0 && segments[segments.length - 1].segment.length < 2) {
|
|
||||||
changeStartSponsorButton(false, true);
|
|
||||||
} else {
|
|
||||||
changeStartSponsorButton(true, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//reset sponsor times submitting
|
|
||||||
sponsorTimesSubmitting = [];
|
|
||||||
updateSponsorTimesSubmitting();
|
|
||||||
|
|
||||||
//see if video controls buttons should be added
|
|
||||||
if (!onInvidious) {
|
|
||||||
updateVisibilityOfPlayerControlsButton();
|
updateVisibilityOfPlayerControlsButton();
|
||||||
}
|
|
||||||
|
// Clear unsubmitted segments from the previous video
|
||||||
|
sponsorTimesSubmitting = [];
|
||||||
|
currentlyTimedSegment = null;
|
||||||
|
updateSponsorTimesSubmitting();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMobileControlsMutations(): void {
|
function handleMobileControlsMutations(): void {
|
||||||
|
updateVisibilityOfPlayerControlsButton();
|
||||||
|
|
||||||
if (previewBar !== null) {
|
if (previewBar !== null) {
|
||||||
if (document.body.contains(previewBar.container)) {
|
if (document.body.contains(previewBar.container)) {
|
||||||
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
|
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
|
||||||
@@ -577,22 +564,12 @@ async function sponsorsLookup(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for hashPrefix setting
|
// Check for hashPrefix setting
|
||||||
let getRequest;
|
|
||||||
if (Config.config.hashPrefix) {
|
|
||||||
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
|
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
|
||||||
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
|
utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
|
||||||
categories
|
categories
|
||||||
});
|
}).then(async (response: FetchResponse) => {
|
||||||
} else {
|
|
||||||
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments", {
|
|
||||||
videoID: id,
|
|
||||||
categories
|
|
||||||
});
|
|
||||||
}
|
|
||||||
getRequest.then(async (response: FetchResponse) => {
|
|
||||||
if (response?.ok) {
|
if (response?.ok) {
|
||||||
let result = JSON.parse(response.responseText);
|
let result = JSON.parse(response.responseText);
|
||||||
if (Config.config.hashPrefix) {
|
|
||||||
result = result.filter((video) => video.videoID === id);
|
result = result.filter((video) => video.videoID === id);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
result = result[0].segments;
|
result = result[0].segments;
|
||||||
@@ -604,7 +581,6 @@ async function sponsorsLookup(id: string) {
|
|||||||
retryFetch(id);
|
retryFetch(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const recievedSegments: SponsorTime[] = result;
|
const recievedSegments: SponsorTime[] = result;
|
||||||
if (!recievedSegments.length) {
|
if (!recievedSegments.length) {
|
||||||
@@ -842,8 +818,31 @@ function updatePreviewBar(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//checks if this channel is whitelisted, should be done only after the channelID has been loaded
|
//checks if this channel is whitelisted, should be done only after the channelID has been loaded
|
||||||
function whitelistCheck() {
|
async function whitelistCheck() {
|
||||||
channelID = videoInfo?.videoDetails?.channelId;
|
const whitelistedChannels = Config.config.whitelistedChannels;
|
||||||
|
|
||||||
|
const getChannelID = () => videoInfo?.videoDetails?.channelId
|
||||||
|
?? document.querySelector(".ytd-channel-name a")?.getAttribute("href")?.replace(/\/.+\//, "") // YouTube
|
||||||
|
?? document.querySelector(".ytp-title-channel-logo")?.getAttribute("href")?.replace(/https:\/.+\//, "") // YouTube Embed
|
||||||
|
?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, ""); // Invidious
|
||||||
|
|
||||||
|
try {
|
||||||
|
await utils.wait(() => !!getChannelID(), 6000, 20);
|
||||||
|
} catch {
|
||||||
|
if (Config.config.forceChannelCheck) {
|
||||||
|
// treat as not whitelisted
|
||||||
|
channelID = "";
|
||||||
|
|
||||||
|
// Don't warn for Invidious embeds
|
||||||
|
if (!(onInvidious && document.URL.includes("/embed/"))) {
|
||||||
|
videoInfoFetchFailed("adblockerIssueWhitelist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelID = getChannelID();
|
||||||
if (!channelID) {
|
if (!channelID) {
|
||||||
channelID = null;
|
channelID = null;
|
||||||
|
|
||||||
@@ -851,8 +850,6 @@ function whitelistCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//see if this is a whitelisted channel
|
//see if this is a whitelisted channel
|
||||||
const whitelistedChannels = Config.config.whitelistedChannels;
|
|
||||||
|
|
||||||
if (whitelistedChannels != undefined && whitelistedChannels.includes(channelID)) {
|
if (whitelistedChannels != undefined && whitelistedChannels.includes(channelID)) {
|
||||||
channelWhitelisted = true;
|
channelWhitelisted = true;
|
||||||
}
|
}
|
||||||
@@ -982,6 +979,26 @@ function previewTime(time: number, unpause = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//send telemetry and count skip
|
||||||
|
function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: number, fullSkip: boolean) {
|
||||||
|
if (!Config.config.trackViewCount) return;
|
||||||
|
|
||||||
|
let counted = false;
|
||||||
|
for (const segment of skippingSegments) {
|
||||||
|
const index = sponsorTimes.indexOf(segment);
|
||||||
|
if (index !== -1 && !sponsorSkipped[index]) {
|
||||||
|
sponsorSkipped[index] = true;
|
||||||
|
if (!counted) {
|
||||||
|
Config.config.minutesSaved = Config.config.minutesSaved + secondsSkipped / 60;
|
||||||
|
Config.config.skipCount = Config.config.skipCount + 1;
|
||||||
|
counted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullSkip) utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//skip from the start time to the end time for a certain index sponsor time
|
//skip from the start time to the end time for a certain index sponsor time
|
||||||
function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: SponsorTime[], openNotice: boolean) {
|
function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: SponsorTime[], openNotice: boolean) {
|
||||||
// There will only be one submission if it is manual skip
|
// There will only be one submission if it is manual skip
|
||||||
@@ -1005,29 +1022,7 @@ function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
//send telemetry that a this sponsor was skipped
|
//send telemetry that a this sponsor was skipped
|
||||||
if (Config.config.trackViewCount && autoSkip) {
|
if (autoSkip) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
|
||||||
let alreadySkipped = false;
|
|
||||||
let isPreviewSegment = false;
|
|
||||||
|
|
||||||
for (const segment of skippingSegments) {
|
|
||||||
const index = sponsorTimes.indexOf(segment);
|
|
||||||
if (index !== -1 && !sponsorSkipped[index]) {
|
|
||||||
utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID);
|
|
||||||
|
|
||||||
sponsorSkipped[index] = true;
|
|
||||||
} else if (sponsorSkipped[index]) {
|
|
||||||
alreadySkipped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === -1) isPreviewSegment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count this as a skip
|
|
||||||
if (!alreadySkipped && !isPreviewSegment) {
|
|
||||||
Config.config.minutesSaved = Config.config.minutesSaved + (skipTime[1] - skipTime[0]) / 60;
|
|
||||||
Config.config.skipCount = Config.config.skipCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unskipSponsorTime(segment: SponsorTime) {
|
function unskipSponsorTime(segment: SponsorTime) {
|
||||||
@@ -1038,13 +1033,18 @@ function unskipSponsorTime(segment: SponsorTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reskipSponsorTime(segment: SponsorTime) {
|
function reskipSponsorTime(segment: SponsorTime) {
|
||||||
video.currentTime = segment.segment[1];
|
const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0);
|
||||||
|
const segmentDuration = segment.segment[1] - segment.segment[0];
|
||||||
|
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
|
||||||
|
|
||||||
|
video.currentTime = segment.segment[1];
|
||||||
|
sendTelemetryAndCount([segment], skippedTime, fullSkip);
|
||||||
startSponsorSchedule(true, segment.segment[1], false);
|
startSponsorSchedule(true, segment.segment[1], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createButton(baseID, title, callback, imageName, isDraggable=false): boolean {
|
function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement {
|
||||||
if (document.getElementById(baseID + "Button") != null) return false;
|
const existingElement = document.getElementById(baseID + "Button");
|
||||||
|
if (existingElement !== null) return existingElement;
|
||||||
|
|
||||||
// Button HTML
|
// Button HTML
|
||||||
const newButton = document.createElement("button");
|
const newButton = document.createElement("button");
|
||||||
@@ -1068,9 +1068,15 @@ function createButton(baseID, title, callback, imageName, isDraggable=false): bo
|
|||||||
newButton.appendChild(newButtonImage);
|
newButton.appendChild(newButtonImage);
|
||||||
|
|
||||||
// Add the button to player
|
// Add the button to player
|
||||||
controls.prepend(newButton);
|
if (controls) controls.prepend(newButton);
|
||||||
|
|
||||||
return true;
|
// Store the elements to prevent unnecessary querying
|
||||||
|
playerButtons[baseID] = {
|
||||||
|
button: newButton,
|
||||||
|
image: newButtonImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getControls(): HTMLElement | false {
|
function getControls(): HTMLElement | false {
|
||||||
@@ -1080,8 +1086,8 @@ function getControls(): HTMLElement | false {
|
|||||||
// Mobile YouTube
|
// Mobile YouTube
|
||||||
".player-controls-top",
|
".player-controls-top",
|
||||||
// Invidious/videojs video element's controls element
|
// Invidious/videojs video element's controls element
|
||||||
".vjs-control-bar"
|
".vjs-control-bar",
|
||||||
]
|
];
|
||||||
|
|
||||||
for (const controlsSelector of controlsSelectors) {
|
for (const controlsSelector of controlsSelectors) {
|
||||||
const controls = document.querySelectorAll(controlsSelector);
|
const controls = document.querySelectorAll(controlsSelector);
|
||||||
@@ -1094,53 +1100,75 @@ function getControls(): HTMLElement | false {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//adds all the player controls buttons
|
/** Creates any missing buttons on the YouTube player if possible. */
|
||||||
async function createButtons(): Promise<boolean> {
|
async function createButtons(): Promise<void> {
|
||||||
if (onMobileYouTube) return;
|
if (onMobileYouTube) return;
|
||||||
|
|
||||||
const result = await utils.wait(getControls).catch();
|
controls = await utils.wait(getControls).catch();
|
||||||
|
|
||||||
//set global controls variable
|
|
||||||
controls = result;
|
|
||||||
|
|
||||||
let createdButton = false;
|
|
||||||
|
|
||||||
// Add button if does not already exist in html
|
// Add button if does not already exist in html
|
||||||
createdButton = createButton("startSponsor", "sponsorStart", startSponsorClicked, "PlayerStartIconSponsorBlocker256px.png") || createdButton;
|
createButton("startSponsor", "sponsorStart", () => closeInfoMenuAnd(() => startOrEndTimingNewSegment()), "PlayerStartIconSponsorBlocker256px.png");
|
||||||
createdButton = createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker256px.png") || createdButton;
|
createButton("cancelSponsor", "sponsorCancel", () => closeInfoMenuAnd(() => cancelCreatingSegment()), "PlayerUploadFailedIconSponsorBlocker256px.png");
|
||||||
createdButton = createButton("delete", "clearTimes", clearSponsorTimes, "PlayerDeleteIconSponsorBlocker256px.png") || createdButton;
|
createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker256px.png");
|
||||||
createdButton = createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker256px.png") || createdButton;
|
createButton("delete", "clearTimes", () => closeInfoMenuAnd(() => clearSponsorTimes()), "PlayerDeleteIconSponsorBlocker256px.png");
|
||||||
|
createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker256px.png");
|
||||||
return createdButton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//adds or removes the player controls button to what it should be
|
/** Creates any missing buttons on the player and updates their visiblity. */
|
||||||
async function updateVisibilityOfPlayerControlsButton(): Promise<boolean> {
|
async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
|
||||||
//not on a proper video yet
|
// Not on a proper video yet
|
||||||
if (!sponsorVideoID) return false;
|
if (!sponsorVideoID) return;
|
||||||
|
|
||||||
const createdButtons = await createButtons();
|
await createButtons();
|
||||||
if (!createdButtons) return;
|
|
||||||
|
|
||||||
if (Config.config.hideVideoPlayerControls || onInvidious) {
|
updateEditButtonsOnPlayer();
|
||||||
document.getElementById("startSponsorButton").style.display = "none";
|
|
||||||
document.getElementById("submitButton").style.display = "none";
|
|
||||||
} else {
|
|
||||||
document.getElementById("startSponsorButton").style.removeProperty("display");
|
|
||||||
}
|
|
||||||
|
|
||||||
//don't show the info button on embeds
|
// Don't show the info button on embeds
|
||||||
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) {
|
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) {
|
||||||
document.getElementById("infoButton").style.display = "none";
|
playerButtons.info.button.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("infoButton").style.removeProperty("display");
|
playerButtons.info.button.style.removeProperty("display");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.config.hideDeleteButtonPlayerControls || onInvidious) {
|
/** Updates the visibility of buttons on the player related to creating segments. */
|
||||||
document.getElementById("deleteButton").style.display = "none";
|
function updateEditButtonsOnPlayer(): void {
|
||||||
|
// Don't try to update the buttons if we aren't on a YouTube video page
|
||||||
|
if (!sponsorVideoID) return;
|
||||||
|
|
||||||
|
const buttonsEnabled = !Config.config.hideVideoPlayerControls && !onInvidious;
|
||||||
|
|
||||||
|
let creatingSegment = false;
|
||||||
|
let submitButtonVisible = false;
|
||||||
|
let deleteButtonVisible = false;
|
||||||
|
|
||||||
|
// Only check if buttons should be visible if they're enabled
|
||||||
|
if (buttonsEnabled) {
|
||||||
|
creatingSegment = currentlyTimedSegment !== null;
|
||||||
|
|
||||||
|
// Show only if there are any segments to submit
|
||||||
|
submitButtonVisible = sponsorTimesSubmitting.length > 0;
|
||||||
|
|
||||||
|
// Show only if there are any segments to delete
|
||||||
|
deleteButtonVisible = sponsorTimesSubmitting.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdButtons;
|
// Update the elements
|
||||||
|
playerButtons.startSponsor.button.style.display = buttonsEnabled ? "unset" : "none";
|
||||||
|
playerButtons.cancelSponsor.button.style.display = buttonsEnabled && creatingSegment ? "unset" : "none";
|
||||||
|
|
||||||
|
if (buttonsEnabled) {
|
||||||
|
if (creatingSegment) {
|
||||||
|
playerButtons.startSponsor.image.src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker256px.png");
|
||||||
|
playerButtons.startSponsor.button.setAttribute("title", chrome.i18n.getMessage("sponsorEnd"));
|
||||||
|
} else {
|
||||||
|
playerButtons.startSponsor.image.src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png");
|
||||||
|
playerButtons.startSponsor.button.setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerButtons.submit.button.style.display = submitButtonVisible && !Config.config.hideUploadButtonPlayerControls ? "unset" : "none";
|
||||||
|
playerButtons.delete.button.style.display = deleteButtonVisible && !Config.config.hideDeleteButtonPlayerControls ? "unset" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1161,30 +1189,40 @@ function getRealCurrentTime(): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSponsorClicked() {
|
function startOrEndTimingNewSegment() {
|
||||||
//it can't update to this info yet
|
if (!currentlyTimedSegment) {
|
||||||
closeInfoMenu();
|
// Start a new segment
|
||||||
|
currentlyTimedSegment = {
|
||||||
toggleStartSponsorButton();
|
|
||||||
|
|
||||||
//add to sponsorTimes
|
|
||||||
if (sponsorTimesSubmitting.length > 0 && sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.length < 2) {
|
|
||||||
//it is an end time
|
|
||||||
sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment[1] = getRealCurrentTime();
|
|
||||||
sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0));
|
|
||||||
} else {
|
|
||||||
//it is a start time
|
|
||||||
sponsorTimesSubmitting.push({
|
|
||||||
segment: [getRealCurrentTime()],
|
segment: [getRealCurrentTime()],
|
||||||
UUID: null,
|
UUID: null,
|
||||||
category: Config.config.defaultCategory
|
category: Config.config.defaultCategory,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Finish creating the new segment
|
||||||
|
const existingTime = currentlyTimedSegment.segment[0];
|
||||||
|
const currentTime = getRealCurrentTime();
|
||||||
|
|
||||||
|
sponsorTimesSubmitting.push({
|
||||||
|
...currentlyTimedSegment,
|
||||||
|
// Swap timestamps if the user put the segment end before the start
|
||||||
|
segment: [Math.min(existingTime, currentTime), Math.max(existingTime, currentTime)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentlyTimedSegment = null;
|
||||||
|
|
||||||
|
// Save the newly created segment
|
||||||
|
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
|
||||||
}
|
}
|
||||||
|
|
||||||
//save this info
|
updateEditButtonsOnPlayer();
|
||||||
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
|
|
||||||
|
|
||||||
updateSponsorTimesSubmitting(false)
|
updateSponsorTimesSubmitting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCreatingSegment() {
|
||||||
|
currentlyTimedSegment = null;
|
||||||
|
|
||||||
|
updateEditButtonsOnPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSponsorTimesSubmitting(getFromConfig = true) {
|
function updateSponsorTimesSubmitting(getFromConfig = true) {
|
||||||
@@ -1213,38 +1251,6 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeStartSponsorButton(showStartSponsor: boolean, uploadButtonVisible: boolean): Promise<boolean> {
|
|
||||||
if(!sponsorVideoID || onMobileYouTube) return false;
|
|
||||||
|
|
||||||
//if it isn't visible, there is no data
|
|
||||||
const shouldHide = (uploadButtonVisible && !(Config.config.hideDeleteButtonPlayerControls || onInvidious)) ? "unset" : "none"
|
|
||||||
document.getElementById("deleteButton").style.display = shouldHide;
|
|
||||||
|
|
||||||
if (showStartSponsor) {
|
|
||||||
showingStartSponsor = true;
|
|
||||||
(<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png");
|
|
||||||
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
|
|
||||||
|
|
||||||
if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !Config.config.hideUploadButtonPlayerControls && !onInvidious) {
|
|
||||||
document.getElementById("submitButton").style.display = "unset";
|
|
||||||
} else if (!uploadButtonVisible || onInvidious) {
|
|
||||||
//disable submit button
|
|
||||||
document.getElementById("submitButton").style.display = "none";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showingStartSponsor = false;
|
|
||||||
(<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker256px.png");
|
|
||||||
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorEND"));
|
|
||||||
|
|
||||||
//disable submit button
|
|
||||||
document.getElementById("submitButton").style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStartSponsorButton() {
|
|
||||||
changeStartSponsorButton(!showingStartSponsor, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openInfoMenu() {
|
function openInfoMenu() {
|
||||||
if (document.getElementById("sponsorBlockPopupContainer") != null) {
|
if (document.getElementById("sponsorBlockPopupContainer") != null) {
|
||||||
//it's already added
|
//it's already added
|
||||||
@@ -1254,7 +1260,7 @@ function openInfoMenu() {
|
|||||||
popupInitialised = false;
|
popupInitialised = false;
|
||||||
|
|
||||||
//hide info button
|
//hide info button
|
||||||
document.getElementById("infoButton").style.display = "none";
|
if (playerButtons.info) playerButtons.info.button.style.display = "none";
|
||||||
|
|
||||||
sendRequestToCustomServer('GET', chrome.extension.getURL("popup.html"), function(xmlhttp) {
|
sendRequestToCustomServer('GET', chrome.extension.getURL("popup.html"), function(xmlhttp) {
|
||||||
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
|
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
|
||||||
@@ -1316,20 +1322,28 @@ function openInfoMenu() {
|
|||||||
|
|
||||||
function closeInfoMenu() {
|
function closeInfoMenu() {
|
||||||
const popup = document.getElementById("sponsorBlockPopupContainer");
|
const popup = document.getElementById("sponsorBlockPopupContainer");
|
||||||
if (popup != null) {
|
if (popup === null) return;
|
||||||
|
|
||||||
popup.remove();
|
popup.remove();
|
||||||
|
|
||||||
//show info button if it's not an embed
|
// Show info button if it's not an embed
|
||||||
if (!document.URL.includes("/embed/")) {
|
if (!document.URL.includes("/embed/") && playerButtons.info) {
|
||||||
document.getElementById("infoButton").style.display = "unset";
|
playerButtons.info.button.style.display = "unset";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content script currently has no way to notify the info menu of changes. As a workaround we close it, thus making it query the new information when reopened.
|
||||||
|
*
|
||||||
|
* This function and all its uses should be removed when this issue is fixed.
|
||||||
|
* */
|
||||||
|
function closeInfoMenuAnd<T>(func: () => T): T {
|
||||||
|
closeInfoMenu();
|
||||||
|
|
||||||
|
return func();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSponsorTimes() {
|
function clearSponsorTimes() {
|
||||||
//it can't update to this info yet
|
|
||||||
closeInfoMenu();
|
|
||||||
|
|
||||||
const currentVideoID = sponsorVideoID;
|
const currentVideoID = sponsorVideoID;
|
||||||
|
|
||||||
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
|
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
|
||||||
@@ -1347,8 +1361,7 @@ function clearSponsorTimes() {
|
|||||||
|
|
||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
|
|
||||||
//set buttons to be correct
|
updateEditButtonsOnPlayer();
|
||||||
changeStartSponsorButton(true, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1414,18 +1427,6 @@ function dontShowNoticeAgain() {
|
|||||||
closeAllSkipNotices();
|
closeAllSkipNotices();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sponsorMessageStarted(callback: (response: MessageResponse) => void) {
|
|
||||||
video = document.querySelector('video');
|
|
||||||
|
|
||||||
//send back current time
|
|
||||||
callback({
|
|
||||||
time: video.currentTime
|
|
||||||
})
|
|
||||||
|
|
||||||
//update button
|
|
||||||
toggleStartSponsorButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method for the submission notice to clear itself when it closes
|
* Helper method for the submission notice to clear itself when it closes
|
||||||
*/
|
*/
|
||||||
@@ -1436,9 +1437,6 @@ function resetSponsorSubmissionNotice() {
|
|||||||
function submitSponsorTimes() {
|
function submitSponsorTimes() {
|
||||||
if (submissionNotice !== null) return;
|
if (submissionNotice !== null) return;
|
||||||
|
|
||||||
//it can't update to this info yet
|
|
||||||
closeInfoMenu();
|
|
||||||
|
|
||||||
if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) {
|
if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) {
|
||||||
submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage);
|
submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage);
|
||||||
}
|
}
|
||||||
@@ -1447,10 +1445,10 @@ function submitSponsorTimes() {
|
|||||||
|
|
||||||
//send the message to the background js
|
//send the message to the background js
|
||||||
//called after all the checks have been made that it's okay to do so
|
//called after all the checks have been made that it's okay to do so
|
||||||
async function sendSubmitMessage(): Promise<void> {
|
async function sendSubmitMessage() {
|
||||||
//add loading animation
|
// Add loading animation
|
||||||
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
|
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
|
||||||
document.getElementById("submitButton").style.animation = "rotate 1s 0s infinite";
|
playerButtons.submit.button.style.animation = "rotate 1s 0s infinite";
|
||||||
|
|
||||||
//check if a sponsor exceeds the duration of the video
|
//check if a sponsor exceeds the duration of the video
|
||||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
||||||
@@ -1477,17 +1475,19 @@ async function sendSubmitMessage(): Promise<void> {
|
|||||||
const response = await utils.asyncRequestToServer("POST", "/api/skipSegments", {
|
const response = await utils.asyncRequestToServer("POST", "/api/skipSegments", {
|
||||||
videoID: sponsorVideoID,
|
videoID: sponsorVideoID,
|
||||||
userID: Config.config.userID,
|
userID: Config.config.userID,
|
||||||
segments: sponsorTimesSubmitting
|
segments: sponsorTimesSubmitting,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
//hide loading message
|
// Handle submission success
|
||||||
const submitButton = document.getElementById("submitButton");
|
const submitButton = playerButtons.submit.button;
|
||||||
|
|
||||||
|
// Make the animation finite
|
||||||
submitButton.style.animation = "rotate 1s";
|
submitButton.style.animation = "rotate 1s";
|
||||||
//finish this animation
|
|
||||||
//when the animation is over, hide the button
|
// When the animation is over, hide the button
|
||||||
const animationEndListener = function() {
|
const animationEndListener = () => {
|
||||||
changeStartSponsorButton(true, false);
|
updateEditButtonsOnPlayer();
|
||||||
|
|
||||||
submitButton.style.animation = "none";
|
submitButton.style.animation = "none";
|
||||||
|
|
||||||
@@ -1496,13 +1496,12 @@ async function sendSubmitMessage(): Promise<void> {
|
|||||||
|
|
||||||
submitButton.addEventListener("animationend", animationEndListener);
|
submitButton.addEventListener("animationend", animationEndListener);
|
||||||
|
|
||||||
//clear the sponsor times
|
// Remove segments from storage since they've already been submitted
|
||||||
Config.config.segmentTimes.delete(sponsorVideoID);
|
Config.config.segmentTimes.delete(sponsorVideoID);
|
||||||
|
|
||||||
//add submissions to current sponsors list
|
// Add submissions to current sponsors list
|
||||||
if (sponsorTimes === null) sponsorTimes = [];
|
// FIXME: segments from sponsorTimesSubmitting do not contain UUIDs .-.
|
||||||
|
sponsorTimes = (sponsorTimes || []).concat(sponsorTimesSubmitting);
|
||||||
sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting);
|
|
||||||
|
|
||||||
// Increase contribution count
|
// Increase contribution count
|
||||||
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
|
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
|
||||||
@@ -1512,13 +1511,14 @@ async function sendSubmitMessage(): Promise<void> {
|
|||||||
Config.config.submissionCountSinceCategories = Config.config.submissionCountSinceCategories + 1;
|
Config.config.submissionCountSinceCategories = Config.config.submissionCountSinceCategories + 1;
|
||||||
|
|
||||||
// Empty the submitting times
|
// Empty the submitting times
|
||||||
|
currentlyTimedSegment = null;
|
||||||
sponsorTimesSubmitting = [];
|
sponsorTimesSubmitting = [];
|
||||||
|
|
||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
} else {
|
} else {
|
||||||
//show that the upload failed
|
// Show that the upload failed
|
||||||
document.getElementById("submitButton").style.animation = "unset";
|
playerButtons.submit.button.style.animation = "unset";
|
||||||
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
|
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
|
||||||
|
|
||||||
alert(utils.getErrorMessage(response.status, response.responseText));
|
alert(utils.getErrorMessage(response.status, response.responseText));
|
||||||
}
|
}
|
||||||
@@ -1575,7 +1575,7 @@ function hotkeyListener(e: KeyboardEvent): void {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case startSponsorKey:
|
case startSponsorKey:
|
||||||
startSponsorClicked();
|
startOrEndTimingNewSegment();
|
||||||
break;
|
break;
|
||||||
case submitKey:
|
case submitKey:
|
||||||
submitSponsorTimes();
|
submitSponsorTimes();
|
||||||
@@ -1583,14 +1583,6 @@ function hotkeyListener(e: KeyboardEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this an unlisted YouTube video.
|
|
||||||
* Assumes that the the privacy info is available.
|
|
||||||
*/
|
|
||||||
function isUnlisted(): boolean {
|
|
||||||
return videoInfo?.microformat?.playerMicroformatRenderer?.isUnlisted || videoInfo?.videoDetails?.isPrivate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the CSS to the page if needed. Required on optional sites with Chrome.
|
* Adds the CSS to the page if needed. Required on optional sites with Chrome.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ interface DefaultMessage {
|
|||||||
message:
|
message:
|
||||||
"update"
|
"update"
|
||||||
| "sponsorStart"
|
| "sponsorStart"
|
||||||
| "sponsorDataChanged"
|
|
||||||
| "isInfoFound"
|
| "isInfoFound"
|
||||||
| "getVideoID"
|
| "getVideoID"
|
||||||
| "getChannelID"
|
| "getChannelID"
|
||||||
@@ -25,13 +24,7 @@ interface BoolValueMessage {
|
|||||||
value: boolean;
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChangeStartSponsorButtonMessage {
|
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage);
|
||||||
message: "changeStartSponsorButton";
|
|
||||||
showStartSponsor: boolean;
|
|
||||||
uploadButtonVisible: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | ChangeStartSponsorButtonMessage);
|
|
||||||
|
|
||||||
interface IsInfoFoundMessageResponse {
|
interface IsInfoFoundMessageResponse {
|
||||||
found: boolean;
|
found: boolean;
|
||||||
@@ -47,7 +40,7 @@ interface GetChannelIDResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SponsorStartResponse {
|
interface SponsorStartResponse {
|
||||||
time: number;
|
creatingSegment: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IsChannelWhitelistedResponse {
|
interface IsChannelWhitelistedResponse {
|
||||||
|
|||||||
113
src/popup.ts
113
src/popup.ts
@@ -126,8 +126,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
PageElements.optionsButton.addEventListener("click", openOptions);
|
PageElements.optionsButton.addEventListener("click", openOptions);
|
||||||
PageElements.helpButton.addEventListener("click", openHelp);
|
PageElements.helpButton.addEventListener("click", openHelp);
|
||||||
|
|
||||||
//if true, the button now selects the end time
|
/** If true, the content script is in the process of creating a new segment. */
|
||||||
let startTimeChosen = false;
|
let creatingSegment = false;
|
||||||
|
|
||||||
//the start and end time pairs (2d)
|
//the start and end time pairs (2d)
|
||||||
let sponsorTimes: SponsorTime[] = [];
|
let sponsorTimes: SponsorTime[] = [];
|
||||||
@@ -233,10 +233,12 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
|
|
||||||
function onTabs(tabs) {
|
function onTabs(tabs) {
|
||||||
messageHandler.sendMessage(tabs[0].id, {message: 'getVideoID'}, function(result) {
|
messageHandler.sendMessage(tabs[0].id, {message: 'getVideoID'}, function(result) {
|
||||||
if (result != undefined && result.videoID) {
|
if (result !== undefined && result.videoID) {
|
||||||
currentVideoID = result.videoID;
|
currentVideoID = result.videoID;
|
||||||
|
creatingSegment = result.creatingSegment;
|
||||||
|
|
||||||
loadTabData(tabs);
|
loadTabData(tabs);
|
||||||
} else if (result == undefined && chrome.runtime.lastError) {
|
} else if (result === undefined && chrome.runtime.lastError) {
|
||||||
//this isn't a YouTube video then, or at least the content script is not loaded
|
//this isn't a YouTube video then, or at least the content script is not loaded
|
||||||
displayNoVideo();
|
displayNoVideo();
|
||||||
}
|
}
|
||||||
@@ -253,19 +255,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
//load video times for this video
|
//load video times for this video
|
||||||
const sponsorTimesStorage = Config.config.segmentTimes.get(currentVideoID);
|
const sponsorTimesStorage = Config.config.segmentTimes.get(currentVideoID);
|
||||||
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
|
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
|
||||||
if (sponsorTimesStorage[sponsorTimesStorage.length - 1] != undefined && sponsorTimesStorage[sponsorTimesStorage.length - 1].segment.length < 2) {
|
|
||||||
startTimeChosen = true;
|
|
||||||
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorEnd");
|
|
||||||
}
|
|
||||||
|
|
||||||
sponsorTimes = sponsorTimesStorage;
|
sponsorTimes = sponsorTimesStorage;
|
||||||
|
|
||||||
//show submission section
|
|
||||||
PageElements.submissionSection.style.display = "unset";
|
|
||||||
|
|
||||||
showSubmitTimesIfNecessary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSegmentEditingUI();
|
||||||
|
|
||||||
//check if this video's sponsors are known
|
//check if this video's sponsors are known
|
||||||
messageHandler.sendMessage(
|
messageHandler.sendMessage(
|
||||||
tabs[0].id,
|
tabs[0].id,
|
||||||
@@ -321,51 +315,44 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
//the content script will get the message if a YouTube page is open
|
//the content script will get the message if a YouTube page is open
|
||||||
messageHandler.query({
|
messageHandler.query({
|
||||||
active: true,
|
active: true,
|
||||||
currentWindow: true
|
currentWindow: true,
|
||||||
}, tabs => {
|
}, (tabs) => {
|
||||||
messageHandler.sendMessage(
|
messageHandler.sendMessage(
|
||||||
tabs[0].id,
|
tabs[0].id,
|
||||||
{from: 'popup', message: 'sponsorStart'},
|
{from: 'popup', message: 'sponsorStart'},
|
||||||
startSponsorCallback
|
async (response) => {
|
||||||
);
|
startSponsorCallback(response);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSponsorCallback(response) {
|
// Perform a second update after the config changes take effect as a workaround for a race condition
|
||||||
const sponsorTimesIndex = sponsorTimes.length - (startTimeChosen ? 1 : 0);
|
const removeListener = (listener: typeof lateUpdate) => {
|
||||||
|
const index = Config.configListeners.indexOf(listener);
|
||||||
if (sponsorTimes[sponsorTimesIndex] == undefined) {
|
if (index !== -1) Config.configListeners.splice(index, 1);
|
||||||
sponsorTimes[sponsorTimesIndex] = {
|
|
||||||
segment: [],
|
|
||||||
category: Config.config.defaultCategory,
|
|
||||||
UUID: null
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
sponsorTimes[sponsorTimesIndex].segment[startTimeChosen ? 1 : 0] = response.time;
|
const lateUpdate = () => {
|
||||||
|
startSponsorCallback(response);
|
||||||
|
removeListener(lateUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
const localStartTimeChosen = startTimeChosen;
|
Config.configListeners.push(lateUpdate);
|
||||||
Config.config.segmentTimes.set(currentVideoID, sponsorTimes);
|
|
||||||
|
|
||||||
//send a message to the client script
|
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
|
||||||
if (localStartTimeChosen) {
|
setTimeout(() => removeListener(lateUpdate), 200);
|
||||||
messageHandler.query({
|
},
|
||||||
active: true,
|
|
||||||
currentWindow: true
|
|
||||||
}, tabs => {
|
|
||||||
messageHandler.sendMessage(
|
|
||||||
tabs[0].id,
|
|
||||||
{message: "sponsorDataChanged"}
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStartTimeChosen();
|
function startSponsorCallback(response: {creatingSegment: boolean}) {
|
||||||
|
creatingSegment = response.creatingSegment;
|
||||||
|
|
||||||
//show submission section
|
// Only update the segments after a segment was created
|
||||||
PageElements.submissionSection.style.display = "unset";
|
if (!creatingSegment) {
|
||||||
|
sponsorTimes = Config.config.segmentTimes.get(currentVideoID) || [];
|
||||||
|
}
|
||||||
|
|
||||||
showSubmitTimesIfNecessary();
|
// Update the UI
|
||||||
|
updateSegmentEditingUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
//display the video times from the array at the top, in a different section
|
//display the video times from the array at the top, in a different section
|
||||||
@@ -484,32 +471,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
PageElements.showNoticeAgain.style.display = "none";
|
PageElements.showNoticeAgain.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStartTimeChosen() {
|
/** Updates any UI related to segment editing and submission according to the current state. */
|
||||||
//update startTimeChosen letiable
|
function updateSegmentEditingUI() {
|
||||||
if (!startTimeChosen) {
|
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(creatingSegment ? "sponsorEnd" : "sponsorStart");
|
||||||
startTimeChosen = true;
|
|
||||||
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorEnd");
|
|
||||||
} else {
|
|
||||||
resetStartTimeChosen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//set it to false
|
PageElements.submissionSection.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
|
||||||
function resetStartTimeChosen() {
|
|
||||||
startTimeChosen = false;
|
|
||||||
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorStart");
|
|
||||||
}
|
|
||||||
|
|
||||||
//hides and shows the submit times button when needed
|
|
||||||
function showSubmitTimesIfNecessary() {
|
|
||||||
//check if an end time has been specified for the latest sponsor time
|
|
||||||
if (sponsorTimes.length > 0 && sponsorTimes[sponsorTimes.length - 1].segment.length > 1) {
|
|
||||||
//show submit times button
|
|
||||||
document.getElementById("submitTimesContainer").style.display = "flex";
|
|
||||||
} else {
|
|
||||||
//hide submit times button
|
|
||||||
document.getElementById("submitTimesContainer").style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//make the options div visible
|
//make the options div visible
|
||||||
@@ -726,9 +692,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
* @param {float} seconds
|
* @param {float} seconds
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getFormattedHours(minues) {
|
function getFormattedHours(minutes) {
|
||||||
const hours = Math.floor(minues / 60);
|
minutes = Math.round(minutes * 10) / 10
|
||||||
return (hours > 0 ? hours + "h " : "") + (minues % 60).toFixed(1);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
return (hours > 0 ? hours + "h " : "") + (minutes % 60).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
//end of function
|
//end of function
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface ContentContainer {
|
|||||||
onMobileYouTube: boolean,
|
onMobileYouTube: boolean,
|
||||||
sponsorSubmissionNotice: SubmissionNotice,
|
sponsorSubmissionNotice: SubmissionNotice,
|
||||||
resetSponsorSubmissionNotice: () => void,
|
resetSponsorSubmissionNotice: () => void,
|
||||||
changeStartSponsorButton: (showStartSponsor: boolean, uploadButtonVisible: boolean) => Promise<boolean>,
|
updateEditButtonsOnPlayer: () => void,
|
||||||
previewTime: (time: number, unpause?: boolean) => void,
|
previewTime: (time: number, unpause?: boolean) => void,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
getRealCurrentTime: () => number
|
getRealCurrentTime: () => number
|
||||||
@@ -60,6 +60,10 @@ export interface SponsorTime {
|
|||||||
hidden?: SponsorHideType;
|
hidden?: SponsorHideType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IncompleteSponsorTime = Omit<SponsorTime, 'segment'> & {
|
||||||
|
segment: [number];
|
||||||
|
};
|
||||||
|
|
||||||
export interface PreviewBarOption {
|
export interface PreviewBarOption {
|
||||||
color: string,
|
color: string,
|
||||||
opacity: string
|
opacity: string
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export default class Utils {
|
|||||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function that can be used to wait for a condition before returning
|
/** Function that can be used to wait for a condition before returning. */
|
||||||
async wait(condition: () => HTMLElement | boolean, timeout = 5000, check = 100): Promise<HTMLElement | boolean> {
|
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
setTimeout(() => reject("TIMEOUT"), timeout);
|
setTimeout(() => reject("TIMEOUT"), timeout);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user