Compare commits

..

20 Commits

Author SHA1 Message Date
Ajay Ramachandran
32d3487b07 Increase version number 2021-05-19 16:25:32 -04:00
Ajay Ramachandran
3ef2673bfc Remove hash prefix option (always uses hash prefix) 2021-05-19 16:11:07 -04:00
Ajay Ramachandran
ac6cd2cec1 Add new channel ID detection logic 2021-05-19 16:08:09 -04:00
Ajay Ramachandran
995ed929ca Merge pull request #744 from wilkmaciej/master
Skip count sending and counting on manual skip
2021-05-19 11:18:25 -04:00
Ajay Ramachandran
592af4e20f Merge pull request #569 from opl-/cleanup/segment-creation
Clean up segment creation code
2021-05-16 20:48:22 -04:00
Ajay Ramachandran
ecfcb0b846 Make buttons not appear on invidious 2021-05-16 20:45:16 -04:00
Ajay Ramachandran
18d10ada5e Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/opl-/569 2021-05-16 20:29:31 -04:00
Ajay Ramachandran
3a7b6b27c2 Change when close info menu is called 2021-05-16 20:27:46 -04:00
Ajay Ramachandran
fea8f93b5a Semicolon 2021-05-16 18:45:06 -04:00
Ajay Ramachandran
daa7a653c9 Merge pull request #747 from wilkmaciej/fix_saved_time_rounding
fix wrong saved time rounding
2021-05-16 18:43:54 -04:00
Ajay Ramachandran
59f63f1b4b Add semi colon 2021-05-14 21:15:36 -04:00
Ajay Ramachandran
e432abe79d Formatting changes + prevent negative value 2021-05-14 21:15:03 -04:00
Maciej Wilk
08a063b612 if(config.trackViewCount) changed 2021-05-15 01:05:45 +02:00
Maciej Wilk
2d14176542 added .DS_Store to not commit by mistake 2021-05-15 01:02:58 +02:00
Maciej Wilk
5fad4509f0 applied proposed fixes 2021-05-15 00:24:27 +02:00
Maciej Wilk
bd44c4721b Update public/help/index_en.html
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2021-05-15 00:10:30 +02:00
Maciej Wilk
606b2fbee3 fix wrong saved time rounding 2021-05-14 20:18:02 +02:00
Maciej Wilk
f18aa19172 updated help
changed few confusing names
deleted auto upvote information
fixed minor typos
2021-05-14 19:23:58 +02:00
Maciej Wilk
8337b54a44 telemetry sending and counting on manual skip 2021-05-14 14:32:32 +02:00
opl-
3879cc6de3 Clean up segment creation code
Noteworthy changes:
- Adds ability to cancel creating a segment
- Makes segment creation fully the responsibility of the content script, with the popup script buttons simply doing RPC
- Adds types to the Utils.wait function
- Fixes segment timestamps backwards if user marks segment end earlier than the start
- Makes the info menu (in-page popup) workaround clearer and easier to remove in the future.
2021-02-20 15:52:13 +01:00
14 changed files with 312 additions and 396 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ web-ext-artifacts
.vscode/
dist/
tmp/
.DS_Store

View File

@@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "2.0.14.2",
"version": "2.0.15",
"default_locale": "en",
"description": "__MSG_Description__",
"content_scripts": [{

View File

@@ -79,6 +79,9 @@
"sponsorEnd": {
"message": "Segment Ends Now"
},
"sponsorCancel": {
"message": "Cancel Creating Segment"
},
"noVideoID": {
"message": "No YouTube video found.\nIf this is incorrect, refresh the tab."
},
@@ -407,15 +410,6 @@
"areYouSureReset": {
"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": {
"message": "m.youtube.com is now supported"
},
@@ -606,9 +600,6 @@
"permissionRequestFailed": {
"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": {
"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"
},

View File

@@ -79,6 +79,9 @@
"sponsorEnd": {
"message": "Koniec segmentu"
},
"sponsorCancel": {
"message": "Anuluj tworzenie segmentu"
},
"noVideoID": {
"message": "Nie znaleziono filmu YouTube.\nJeżeli to błąd, odśwież stronę."
},

View File

@@ -42,12 +42,12 @@
<img src="https://i.imgur.com/caf5Bju.png">
</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/>
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>
<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>
<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.
These can be changed in the options. If you don't use QWERTY, you should probably change the keybinds.
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 keybinding.
</p>
<h1>I hate these buttons, they are so ugly</h1>

View File

@@ -309,23 +309,6 @@
<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">
<label class="switch-container" label-name="__MSG_enableRefetchWhenNotFound__">
<label class="switch">
@@ -343,23 +326,6 @@
<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 class="option-button trigger-button">
__MSG_changeUserID__

View File

@@ -71,7 +71,7 @@
</div>
<div id="submissionSection" style="display: none">
<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>
</div>
</div>

View File

@@ -344,7 +344,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
//if it is not a complete sponsor time
if (sponsorTimes[index].segment.length < 2) {
//update video player
this.props.contentContainer().changeStartSponsorButton(true, false);
this.props.contentContainer().updateEditButtonsOnPlayer();
}
sponsorTimes.splice(index, 1);
@@ -359,7 +359,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.props.submissionNotice.cancel();
//update video player
this.props.contentContainer().changeStartSponsorButton(true, false);
this.props.contentContainer().updateEditButtonsOnPlayer();
} else {
//update display
this.props.submissionNotice.forceUpdate();

View File

@@ -6,6 +6,7 @@ const utils = new Utils();
interface SBConfig {
userID: string,
/** Contains unsubmitted segments that the user has created. */
segmentTimes: SBMap<string, SponsorTime[]>,
defaultCategory: string,
whitelistedChannels: string[],
@@ -34,7 +35,6 @@ interface SBConfig {
audioNotificationOnSkip,
checkForUnlistedVideos: boolean,
testingServer: boolean,
hashPrefix: boolean,
refetchWhenNotFound: boolean,
ytInfoPermissionGranted: boolean,
@@ -168,7 +168,6 @@ const Config: SBObject = {
audioNotificationOnSkip: false,
checkForUnlistedVideos: false,
testingServer: false,
hashPrefix: true,
refetchWhenNotFound: true,
ytInfoPermissionGranted: false,

View File

@@ -1,6 +1,6 @@
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 Utils from "./utils";
@@ -71,8 +71,11 @@ let channelWhitelisted = false;
// create preview bar
let previewBar: PreviewBar = null;
//the player controls on the YouTube player
let controls = null;
/** Element containing the player controls on the YouTube player. */
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
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
let sponsorLookupRetries = 0;
//if showing the start sponsor button or the end sponsor button on the player
let showingStartSponsor = true;
/** Currently timed segment, which will be added to the unsubmitted segments when ready. */
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[] = [];
//becomes true when isInfoFound is called
@@ -111,12 +114,15 @@ const skipNoticeContentContainer: ContentContainer = () => ({
onMobileYouTube,
sponsorSubmissionNotice: submissionNotice,
resetSponsorSubmissionNotice,
changeStartSponsorButton,
updateEditButtonsOnPlayer,
previewTime,
videoInfo,
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
chrome.runtime.onMessage.addListener(messageListener);
@@ -127,11 +133,11 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
videoIDChange(getYouTubeVideoID(document.URL));
break;
case "sponsorStart":
sponsorMessageStarted(sendResponse);
startOrEndTimingNewSegment()
break;
case "sponsorDataChanged":
updateSponsorTimesSubmitting();
sendResponse({
creatingSegment: currentlyTimedSegment !== null,
});
break;
case "isInfoFound":
@@ -150,7 +156,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
break;
case "getVideoID":
sendResponse({
videoID: sponsorVideoID
videoID: sponsorVideoID,
creatingSegment: currentlyTimedSegment !== null,
});
break;
@@ -170,10 +177,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
channelWhitelisted = request.value;
sponsorsLookup(sponsorVideoID);
break;
case "changeStartSponsorButton":
changeStartSponsorButton(request.showStartSponsor, request.uploadButtonVisible);
break;
case "submitTimes":
submitSponsorTimes();
@@ -250,29 +253,25 @@ async function videoIDChange(id) {
// Wait for options to be ready
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
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
utils.wait(() => !!videoInfo, 5000, 10).then(whitelistCheck).catch(() => {
if (Config.config.forceChannelCheck) {
videoInfoFetchFailed("adblockerIssueWhitelist");
}
});
whitelistCheck();
//setup the preview bar
if (previewBar === null) {
@@ -301,30 +300,18 @@ async function videoIDChange(id) {
sponsorsLookup(id);
//make sure everything is 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) {
// Make sure all player buttons are properly added
updateVisibilityOfPlayerControlsButton();
}
// Clear unsubmitted segments from the previous video
sponsorTimesSubmitting = [];
currentlyTimedSegment = null;
updateSponsorTimesSubmitting();
}
function handleMobileControlsMutations(): void {
updateVisibilityOfPlayerControlsButton();
if (previewBar !== null) {
if (document.body.contains(previewBar.container)) {
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
@@ -577,22 +564,12 @@ async function sponsorsLookup(id: string) {
}
// Check for hashPrefix setting
let getRequest;
if (Config.config.hashPrefix) {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories
});
} else {
getRequest = utils.asyncRequestToServer('GET', "/api/skipSegments", {
videoID: id,
categories
});
}
getRequest.then(async (response: FetchResponse) => {
}).then(async (response: FetchResponse) => {
if (response?.ok) {
let result = JSON.parse(response.responseText);
if (Config.config.hashPrefix) {
result = result.filter((video) => video.videoID === id);
if (result.length > 0) {
result = result[0].segments;
@@ -604,7 +581,6 @@ async function sponsorsLookup(id: string) {
retryFetch(id);
return;
}
}
const recievedSegments: SponsorTime[] = result;
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
function whitelistCheck() {
channelID = videoInfo?.videoDetails?.channelId;
async function whitelistCheck() {
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) {
channelID = null;
@@ -851,8 +850,6 @@ function whitelistCheck() {
}
//see if this is a whitelisted channel
const whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels != undefined && whitelistedChannels.includes(channelID)) {
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
function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: SponsorTime[], openNotice: boolean) {
// 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
if (Config.config.trackViewCount && autoSkip) {
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;
}
}
if (autoSkip) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
}
function unskipSponsorTime(segment: SponsorTime) {
@@ -1038,13 +1033,18 @@ function unskipSponsorTime(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);
}
function createButton(baseID, title, callback, imageName, isDraggable=false): boolean {
if (document.getElementById(baseID + "Button") != null) return false;
function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement {
const existingElement = document.getElementById(baseID + "Button");
if (existingElement !== null) return existingElement;
// Button HTML
const newButton = document.createElement("button");
@@ -1068,9 +1068,15 @@ function createButton(baseID, title, callback, imageName, isDraggable=false): bo
newButton.appendChild(newButtonImage);
// 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 {
@@ -1080,8 +1086,8 @@ function getControls(): HTMLElement | false {
// Mobile YouTube
".player-controls-top",
// Invidious/videojs video element's controls element
".vjs-control-bar"
]
".vjs-control-bar",
];
for (const controlsSelector of controlsSelectors) {
const controls = document.querySelectorAll(controlsSelector);
@@ -1094,53 +1100,75 @@ function getControls(): HTMLElement | false {
return false;
}
//adds all the player controls buttons
async function createButtons(): Promise<boolean> {
/** Creates any missing buttons on the YouTube player if possible. */
async function createButtons(): Promise<void> {
if (onMobileYouTube) return;
const result = await utils.wait(getControls).catch();
//set global controls variable
controls = result;
let createdButton = false;
controls = await utils.wait(getControls).catch();
// Add button if does not already exist in html
createdButton = createButton("startSponsor", "sponsorStart", startSponsorClicked, "PlayerStartIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("delete", "clearTimes", clearSponsorTimes, "PlayerDeleteIconSponsorBlocker256px.png") || createdButton;
createdButton = createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker256px.png") || createdButton;
return createdButton;
createButton("startSponsor", "sponsorStart", () => closeInfoMenuAnd(() => startOrEndTimingNewSegment()), "PlayerStartIconSponsorBlocker256px.png");
createButton("cancelSponsor", "sponsorCancel", () => closeInfoMenuAnd(() => cancelCreatingSegment()), "PlayerUploadFailedIconSponsorBlocker256px.png");
createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker256px.png");
createButton("delete", "clearTimes", () => closeInfoMenuAnd(() => clearSponsorTimes()), "PlayerDeleteIconSponsorBlocker256px.png");
createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker256px.png");
}
//adds or removes the player controls button to what it should be
async function updateVisibilityOfPlayerControlsButton(): Promise<boolean> {
//not on a proper video yet
if (!sponsorVideoID) return false;
/** Creates any missing buttons on the player and updates their visiblity. */
async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
// Not on a proper video yet
if (!sponsorVideoID) return;
const createdButtons = await createButtons();
if (!createdButtons) return;
await createButtons();
if (Config.config.hideVideoPlayerControls || onInvidious) {
document.getElementById("startSponsorButton").style.display = "none";
document.getElementById("submitButton").style.display = "none";
} else {
document.getElementById("startSponsorButton").style.removeProperty("display");
}
updateEditButtonsOnPlayer();
//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) {
document.getElementById("infoButton").style.display = "none";
playerButtons.info.button.style.display = "none";
} else {
document.getElementById("infoButton").style.removeProperty("display");
playerButtons.info.button.style.removeProperty("display");
}
}
/** Updates the visibility of buttons on the player related to creating segments. */
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;
}
if (Config.config.hideDeleteButtonPlayerControls || onInvidious) {
document.getElementById("deleteButton").style.display = "none";
// 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"));
}
}
return createdButtons;
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() {
//it can't update to this info yet
closeInfoMenu();
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({
function startOrEndTimingNewSegment() {
if (!currentlyTimedSegment) {
// Start a new segment
currentlyTimedSegment = {
segment: [getRealCurrentTime()],
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
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false)
updateSponsorTimesSubmitting(false);
}
function cancelCreatingSegment() {
currentlyTimedSegment = null;
updateEditButtonsOnPlayer();
}
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() {
if (document.getElementById("sponsorBlockPopupContainer") != null) {
//it's already added
@@ -1254,7 +1260,7 @@ function openInfoMenu() {
popupInitialised = false;
//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) {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
@@ -1316,20 +1322,28 @@ function openInfoMenu() {
function closeInfoMenu() {
const popup = document.getElementById("sponsorBlockPopupContainer");
if (popup != null) {
if (popup === null) return;
popup.remove();
//show info button if it's not an embed
if (!document.URL.includes("/embed/")) {
document.getElementById("infoButton").style.display = "unset";
}
// Show info button if it's not an embed
if (!document.URL.includes("/embed/") && playerButtons.info) {
playerButtons.info.button.style.display = "unset";
}
}
function clearSponsorTimes() {
//it can't update to this info yet
/**
* 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() {
const currentVideoID = sponsorVideoID;
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
@@ -1347,8 +1361,7 @@ function clearSponsorTimes() {
updatePreviewBar();
//set buttons to be correct
changeStartSponsorButton(true, false);
updateEditButtonsOnPlayer();
}
}
@@ -1414,18 +1427,6 @@ function dontShowNoticeAgain() {
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
*/
@@ -1436,9 +1437,6 @@ function resetSponsorSubmissionNotice() {
function submitSponsorTimes() {
if (submissionNotice !== null) return;
//it can't update to this info yet
closeInfoMenu();
if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) {
submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage);
}
@@ -1447,10 +1445,10 @@ function submitSponsorTimes() {
//send the message to the background js
//called after all the checks have been made that it's okay to do so
async function sendSubmitMessage(): Promise<void> {
//add loading animation
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
document.getElementById("submitButton").style.animation = "rotate 1s 0s infinite";
async function sendSubmitMessage() {
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
playerButtons.submit.button.style.animation = "rotate 1s 0s infinite";
//check if a sponsor exceeds the duration of the video
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", {
videoID: sponsorVideoID,
userID: Config.config.userID,
segments: sponsorTimesSubmitting
segments: sponsorTimesSubmitting,
});
if (response.status === 200) {
//hide loading message
const submitButton = document.getElementById("submitButton");
// Handle submission success
const submitButton = playerButtons.submit.button;
// Make the animation finite
submitButton.style.animation = "rotate 1s";
//finish this animation
//when the animation is over, hide the button
const animationEndListener = function() {
changeStartSponsorButton(true, false);
// When the animation is over, hide the button
const animationEndListener = () => {
updateEditButtonsOnPlayer();
submitButton.style.animation = "none";
@@ -1496,13 +1496,12 @@ async function sendSubmitMessage(): Promise<void> {
submitButton.addEventListener("animationend", animationEndListener);
//clear the sponsor times
// Remove segments from storage since they've already been submitted
Config.config.segmentTimes.delete(sponsorVideoID);
//add submissions to current sponsors list
if (sponsorTimes === null) sponsorTimes = [];
sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting);
// Add submissions to current sponsors list
// FIXME: segments from sponsorTimesSubmitting do not contain UUIDs .-.
sponsorTimes = (sponsorTimes || []).concat(sponsorTimesSubmitting);
// Increase contribution count
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
@@ -1512,13 +1511,14 @@ async function sendSubmitMessage(): Promise<void> {
Config.config.submissionCountSinceCategories = Config.config.submissionCountSinceCategories + 1;
// Empty the submitting times
currentlyTimedSegment = null;
sponsorTimesSubmitting = [];
updatePreviewBar();
} else {
//show that the upload failed
document.getElementById("submitButton").style.animation = "unset";
(<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
// Show that the upload failed
playerButtons.submit.button.style.animation = "unset";
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
alert(utils.getErrorMessage(response.status, response.responseText));
}
@@ -1575,7 +1575,7 @@ function hotkeyListener(e: KeyboardEvent): void {
}
break;
case startSponsorKey:
startSponsorClicked();
startOrEndTimingNewSegment();
break;
case submitKey:
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.
*/

View File

@@ -12,7 +12,6 @@ interface DefaultMessage {
message:
"update"
| "sponsorStart"
| "sponsorDataChanged"
| "isInfoFound"
| "getVideoID"
| "getChannelID"
@@ -25,13 +24,7 @@ interface BoolValueMessage {
value: boolean;
}
interface ChangeStartSponsorButtonMessage {
message: "changeStartSponsorButton";
showStartSponsor: boolean;
uploadButtonVisible: boolean;
}
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | ChangeStartSponsorButtonMessage);
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage);
interface IsInfoFoundMessageResponse {
found: boolean;
@@ -47,7 +40,7 @@ interface GetChannelIDResponse {
}
interface SponsorStartResponse {
time: number;
creatingSegment: boolean;
}
interface IsChannelWhitelistedResponse {

View File

@@ -126,8 +126,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.optionsButton.addEventListener("click", openOptions);
PageElements.helpButton.addEventListener("click", openHelp);
//if true, the button now selects the end time
let startTimeChosen = false;
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
@@ -233,11 +233,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
function onTabs(tabs) {
messageHandler.sendMessage(tabs[0].id, {message: 'getVideoID'}, function(result) {
if (result != undefined && result.videoID) {
if (result !== undefined && result.videoID) {
currentVideoID = result.videoID;
creatingSegment = result.creatingSegment;
loadTabData(tabs);
} else if (result == undefined && chrome.runtime.lastError) {
// this isn't a YouTube video then, or at least the content script is not loaded
} else if (result === undefined && chrome.runtime.lastError) {
//this isn't a YouTube video then, or at least the content script is not loaded
displayNoVideo();
}
});
@@ -253,19 +255,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//load video times for this video
const sponsorTimesStorage = Config.config.segmentTimes.get(currentVideoID);
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;
//show submission section
PageElements.submissionSection.style.display = "unset";
showSubmitTimesIfNecessary();
}
updateSegmentEditingUI();
//check if this video's sponsors are known
messageHandler.sendMessage(
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
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
currentWindow: true,
}, (tabs) => {
messageHandler.sendMessage(
tabs[0].id,
{from: 'popup', message: 'sponsorStart'},
startSponsorCallback
);
});
}
async (response) => {
startSponsorCallback(response);
function startSponsorCallback(response) {
const sponsorTimesIndex = sponsorTimes.length - (startTimeChosen ? 1 : 0);
if (sponsorTimes[sponsorTimesIndex] == undefined) {
sponsorTimes[sponsorTimesIndex] = {
segment: [],
category: Config.config.defaultCategory,
UUID: null
// Perform a second update after the config changes take effect as a workaround for a race condition
const removeListener = (listener: typeof lateUpdate) => {
const index = Config.configListeners.indexOf(listener);
if (index !== -1) Config.configListeners.splice(index, 1);
};
}
sponsorTimes[sponsorTimesIndex].segment[startTimeChosen ? 1 : 0] = response.time;
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
const localStartTimeChosen = startTimeChosen;
Config.config.segmentTimes.set(currentVideoID, sponsorTimes);
Config.configListeners.push(lateUpdate);
//send a message to the client script
if (localStartTimeChosen) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{message: "sponsorDataChanged"}
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
},
);
});
}
updateStartTimeChosen();
function startSponsorCallback(response: {creatingSegment: boolean}) {
creatingSegment = response.creatingSegment;
//show submission section
PageElements.submissionSection.style.display = "unset";
// Only update the segments after a segment was created
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
@@ -484,32 +471,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "none";
}
function updateStartTimeChosen() {
//update startTimeChosen letiable
if (!startTimeChosen) {
startTimeChosen = true;
PageElements.sponsorStart.innerHTML = chrome.i18n.getMessage("sponsorEnd");
} else {
resetStartTimeChosen();
}
}
/** Updates any UI related to segment editing and submission according to the current state. */
function updateSegmentEditingUI() {
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(creatingSegment ? "sponsorEnd" : "sponsorStart");
//set it to false
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";
}
PageElements.submissionSection.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
}
//make the options div visible
@@ -726,9 +692,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
* @param {float} seconds
* @returns {string}
*/
function getFormattedHours(minues) {
const hours = Math.floor(minues / 60);
return (hours > 0 ? hours + "h " : "") + (minues % 60).toFixed(1);
function getFormattedHours(minutes) {
minutes = Math.round(minutes * 10) / 10
const hours = Math.floor(minutes / 60);
return (hours > 0 ? hours + "h " : "") + (minutes % 60).toFixed(1);
}
//end of function

View File

@@ -17,7 +17,7 @@ export interface ContentContainer {
onMobileYouTube: boolean,
sponsorSubmissionNotice: SubmissionNotice,
resetSponsorSubmissionNotice: () => void,
changeStartSponsorButton: (showStartSponsor: boolean, uploadButtonVisible: boolean) => Promise<boolean>,
updateEditButtonsOnPlayer: () => void,
previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo,
getRealCurrentTime: () => number
@@ -60,6 +60,10 @@ export interface SponsorTime {
hidden?: SponsorHideType;
}
export type IncompleteSponsorTime = Omit<SponsorTime, 'segment'> & {
segment: [number];
};
export interface PreviewBarOption {
color: string,
opacity: string

View File

@@ -23,8 +23,8 @@ export default class Utils {
this.backgroundScriptContainer = backgroundScriptContainer;
}
// Function that can be used to wait for a condition before returning
async wait(condition: () => HTMLElement | boolean, timeout = 5000, check = 100): Promise<HTMLElement | boolean> {
/** Function that can be used to wait for a condition before returning. */
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => reject("TIMEOUT"), timeout);