feat(youtube/sponsorblock): skip to video highlight (#352)

This commit is contained in:
LisoUseInAIKyrios 2023-04-16 22:18:32 +04:00 committed by GitHub
parent 76a01d1b7c
commit 03f09cf7bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 439 additions and 296 deletions

View File

@ -28,7 +28,7 @@ public class PlayerTypeHookPatch {
LogHelper.printException(() -> "Unknown PlayerType encountered: " + type);
} else {
PlayerType.setCurrent(newType);
LogHelper.printDebug(() -> "YouTubePlayerOverlaysLayout player type was updated to " + newType);
LogHelper.printDebug(() -> "PlayerType was updated to: " + newType);
}
}
}

View File

@ -64,8 +64,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
try {
final boolean enabled = SettingsEnum.SB_ENABLED.getBoolean();
if (!enabled) {
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideNewSegmentLayout();
SponsorBlockViewController.hideAll();
SegmentPlaybackController.setCurrentVideoId(null);
} else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) {
SponsorBlockViewController.hideNewSegmentLayout();
@ -141,12 +140,13 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
Boolean newValue = (Boolean) o;
if (newValue && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) {
SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true);
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_guidelines_popup_title"))
.setMessage(str("sb_guidelines_popup_content"))
.setNegativeButton(str("sb_guidelines_popup_already_read"), null)
.setPositiveButton(str("sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
.setOnDismissListener(dialog -> SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true))
.setCancelable(false)
.show();
}
SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue);
@ -350,7 +350,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
segmentCategory.removeAll();
Activity activity = getActivity();
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
}
} catch (Exception ex) {

View File

@ -16,7 +16,7 @@ enum class PlayerType {
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
INLINE_MINIMAL,
INLINE_MINIMAL, // home feed video playback
VIRTUAL_REALITY_FULLSCREEN,
WATCH_WHILE_PICTURE_IN_PICTURE;

View File

@ -5,6 +5,7 @@ import static app.revanced.integrations.utils.StringRef.str;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -30,13 +31,39 @@ import app.revanced.integrations.utils.ReVancedUtils;
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
public class SegmentPlaybackController {
/**
* Length of time to show a highlight segment manual skip.
* Because there is no scheduled hide of a skip to highlight,
* effectively this time value is rounded up to the next second.
*/
private static final long HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT = 3800;
/*
* Highlight segments have zero length, as they are a point in time.
* Draw them on screen using a fixed width bar.
* Value is independent of device dpi.
*/
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7;
@Nullable
private static String currentVideoId;
@Nullable
private static SponsorSegment[] segmentsOfCurrentVideo;
/**
* Current segment that user can manually skip
* Highlight segment, if one exists.
*/
@Nullable
private static SponsorSegment highlightSegment;
/**
* Because loading can take time, show the skip to highlight for a few seconds after the segments load.
* This is the end time (in milliseconds) to no longer show the initial display skip to highlight.
*/
private static long highlightSegmentInitialShowEndTime;
/**
* Current (non-highlight) segment that user can manually skip
*/
@Nullable
private static SponsorSegment segmentCurrentlyPlaying;
@ -68,23 +95,28 @@ public class SegmentPlaybackController {
Arrays.sort(segments);
segmentsOfCurrentVideo = segments;
calculateTimeWithoutSegments();
for (SponsorSegment segment : segments) {
if (segment.category == SegmentCategory.HIGHLIGHT) {
highlightSegment = segment;
return;
}
}
highlightSegment = null;
}
public static boolean currentVideoHasSegments() {
return segmentsOfCurrentVideo != null && segmentsOfCurrentVideo.length > 0;
}
@Nullable
static String getCurrentVideoId() {
return currentVideoId;
}
/**
* Clears all downloaded data
*/
private static void clearData() {
currentVideoId = null;
segmentsOfCurrentVideo = null;
highlightSegment = null;
highlightSegmentInitialShowEndTime = 0;
timeWithoutSegments = null;
segmentCurrentlyPlaying = null;
scheduledUpcomingSegment = null; // prevent any existing scheduled skip from running
@ -102,8 +134,7 @@ public class SegmentPlaybackController {
ReVancedUtils.verifyOnMainThread();
SponsorBlockSettings.initialize();
clearData();
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideNewSegmentLayout();
SponsorBlockViewController.hideAll();
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
LogHelper.printDebug(() -> "Initialized SponsorBlock");
} catch (Exception ex) {
@ -164,7 +195,19 @@ public class SegmentPlaybackController {
return;
}
setSegmentsOfCurrentVideo(segments);
setVideoTime(VideoInformation.getVideoTime()); // check for any skips now, instead of waiting for the next update
final long videoTime = VideoInformation.getVideoTime();
// if the current video time is before the highlight
if (highlightSegment != null && videoTime < highlightSegment.end) {
if (highlightSegment.shouldAutoSkip()) {
skipSegment(highlightSegment, false);
return;
}
highlightSegmentInitialShowEndTime = System.currentTimeMillis()
+ HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT;
}
// check for any skips now, instead of waiting for the next update to setVideoTime()
setVideoTime(videoTime);
});
} catch (Exception ex) {
LogHelper.printException(() -> "executeDownloadSegments failure", ex);
@ -196,7 +239,8 @@ public class SegmentPlaybackController {
for (final SponsorSegment segment : segmentsOfCurrentVideo) {
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|| segment.category.behaviour == CategoryBehaviour.IGNORE) {
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|| segment.category == SegmentCategory.HIGHLIGHT) {
continue;
}
if (segment.end <= millis) {
@ -217,7 +261,7 @@ public class SegmentPlaybackController {
// Also prevents showing the skip button if user seeks into the last half second of the segment.
final long minMillisOfSegmentRemainingThreshold = 500;
if (segmentCurrentlyPlaying == segment
|| !segment.timeIsNearEnd(millis, minMillisOfSegmentRemainingThreshold)) {
|| !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) {
foundCurrentSegment = segment;
} else {
LogHelper.printDebug(() -> "Ignoring segment that ends very soon: " + segment);
@ -248,7 +292,7 @@ public class SegmentPlaybackController {
// This check is needed to prevent scheduled hide and show from clashing with each other.
final long minTimeBetweenStartEndOfSegments = 1000;
if (foundCurrentSegment == null
|| !foundCurrentSegment.timeIsNearEnd(segment.start, minTimeBetweenStartEndOfSegments)) {
|| !foundCurrentSegment.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) {
foundUpcomingSegment = segment;
} else {
LogHelper.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment);
@ -256,16 +300,22 @@ public class SegmentPlaybackController {
}
}
if (highlightSegment != null && (millis < HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT
|| System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
SponsorBlockViewController.showSkipHighlightButton(highlightSegment);
} else {
SponsorBlockViewController.hideSkipHighlightButton();
}
if (segmentCurrentlyPlaying != foundCurrentSegment) {
if (foundCurrentSegment == null) {
LogHelper.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying);
segmentCurrentlyPlaying = null;
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideSkipSegmentButton();
} else {
segmentCurrentlyPlaying = foundCurrentSegment;
LogHelper.printDebug(() -> "Showing segment: " + segmentCurrentlyPlaying);
SponsorBlockViewController.showSkipButton(foundCurrentSegment);
SponsorBlockViewController.showSkipSegmentButton(foundCurrentSegment);
}
}
@ -274,7 +324,7 @@ public class SegmentPlaybackController {
// schedule a hide, only if the segment end is near
final SponsorSegment segmentToHide =
(foundCurrentSegment != null && foundCurrentSegment.timeIsNearEnd(millis, lookAheadMilliseconds))
(foundCurrentSegment != null && foundCurrentSegment.endIsNear(millis, lookAheadMilliseconds))
? foundCurrentSegment
: null;
@ -294,7 +344,7 @@ public class SegmentPlaybackController {
scheduledHideSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToHide.timeIsNearEnd(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) {
if (!segmentToHide.endIsNear(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) {
// current video time is not what's expected. User paused playback
LogHelper.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
+ " videoInformation time: " + videoTime);
@ -306,7 +356,7 @@ public class SegmentPlaybackController {
// Should not use VideoInformation time as it is less accurate,
// but this scheduled handler was scheduled precisely so we can just use the segment end time
segmentCurrentlyPlaying = null;
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideSkipSegmentButton();
setVideoTime(segmentToHide.end);
}, delayUntilHide);
}
@ -330,7 +380,7 @@ public class SegmentPlaybackController {
scheduledUpcomingSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToSkip.timeIsNearStart(videoTime,
if (!segmentToSkip.startIsNear(videoTime,
videoInformationTimeUpdateThresholdMilliseconds)) {
// current video time is not what's expected. User paused playback
LogHelper.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
@ -343,7 +393,7 @@ public class SegmentPlaybackController {
} else {
LogHelper.printDebug(() -> "Running scheduled show segment: " + segmentToSkip);
segmentCurrentlyPlaying = segmentToSkip;
SponsorBlockViewController.showSkipButton(segmentToSkip);
SponsorBlockViewController.showSkipSegmentButton(segmentToSkip);
}
}, delayUntilSkip);
}
@ -357,7 +407,7 @@ public class SegmentPlaybackController {
private static SponsorSegment lastSegmentSkipped;
private static long lastSegmentSkippedTime;
private static void skipSegment(@NonNull SponsorSegment segment, boolean userManuallySkipped) {
private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) {
try {
// If trying to seek to end of the video, YouTube can seek just short of the actual end.
// (especially if the video does not end on a whole second boundary).
@ -365,23 +415,27 @@ public class SegmentPlaybackController {
// Check for and ignore repeated skip attempts of the same segment over a short time period.
final long now = System.currentTimeMillis();
final long minimumMillisecondsBetweenSkippingSameSegment = 500;
if ((lastSegmentSkipped == segment) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) {
LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segment);
if ((lastSegmentSkipped == segmentToSkip) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) {
LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip);
return;
}
LogHelper.printDebug(() -> "Skipping segment: " + segment);
lastSegmentSkipped = segment;
LogHelper.printDebug(() -> "Skipping segment: " + segmentToSkip);
lastSegmentSkipped = segmentToSkip;
lastSegmentSkippedTime = now;
segmentCurrentlyPlaying = null;
scheduledHideSegment = null; // if a scheduled has not run yet
scheduledHideSegment = null;
scheduledUpcomingSegment = null;
SponsorBlockViewController.hideSkipButton();
if (segmentToSkip == highlightSegment) {
highlightSegmentInitialShowEndTime = 0;
}
SponsorBlockViewController.hideSkipHighlightButton();
SponsorBlockViewController.hideSkipSegmentButton();
final boolean seekSuccessful = VideoInformation.seekTo(segment.end);
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
if (!seekSuccessful) {
// can happen when switching videos and is normal
LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segment);
LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
return;
}
@ -389,10 +443,10 @@ public class SegmentPlaybackController {
// check for any smaller embedded segments, and count those as autoskipped
final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean();
for (final SponsorSegment otherSegment : segmentsOfCurrentVideo) {
if (segment.end <= otherSegment.start) {
if (segmentToSkip.end < otherSegment.start) {
break; // no other segments can be contained
}
if (segment.containsSegment(otherSegment)) { // includes checking the segment against itself
if (segmentToSkip.containsSegment(otherSegment)) { // includes checking the segment against itself
otherSegment.didAutoSkipped = true; // skipped this segment as well
if (showSkipToast) {
showSkippedSegmentToast(otherSegment);
@ -401,19 +455,19 @@ public class SegmentPlaybackController {
}
}
if (segment.category == SegmentCategory.UNSUBMITTED) {
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
// skipped segment was a preview of unsubmitted segment
// remove the segment from the UI view
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
SponsorSegment[] newSegments = new SponsorSegment[segmentsOfCurrentVideo.length - 1];
int i = 0;
for (SponsorSegment sponsorSegment : segmentsOfCurrentVideo) {
if (sponsorSegment != segment)
newSegments[i++] = sponsorSegment;
for (SponsorSegment segment : segmentsOfCurrentVideo) {
if (segment != segmentToSkip)
newSegments[i++] = segment;
}
setSegmentsOfCurrentVideo(newSegments);
} else {
SponsorBlockUtils.sendViewRequestAsync(segment);
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
}
} catch (Exception ex) {
LogHelper.printException(() -> "skipSegment failure", ex);
@ -433,7 +487,7 @@ public class SegmentPlaybackController {
}
toastSegmentSkipped = segment;
final long delayToToastMilliseconds = 200; // also the maximum time between skips to be considered skipping multiple segments
final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments
ReVancedUtils.runOnMainThreadDelayed(() -> {
try {
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
@ -452,12 +506,20 @@ public class SegmentPlaybackController {
}, delayToToastMilliseconds);
}
public static void onSkipSponsorClicked() {
if (segmentCurrentlyPlaying != null) {
skipSegment(segmentCurrentlyPlaying, true);
} else {
SponsorBlockViewController.hideSkipButton();
LogHelper.printException(() -> "error: segment not available to skip"); // should never happen
/**
* @param segment can be either a highlight or a regular manual skip segment
*/
public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) {
try {
if (segment != highlightSegment && segment != segmentCurrentlyPlaying) {
LogHelper.printException(() -> "error: segment not available to skip"); // should never happen
SponsorBlockViewController.hideSkipSegmentButton();
SponsorBlockViewController.hideSkipHighlightButton();
return;
}
skipSegment(segment, true);
} catch (Exception ex) {
LogHelper.printException(() -> "onSkipSegmentClicked failure", ex);
}
}
@ -512,17 +574,17 @@ public class SegmentPlaybackController {
* Injection point
*/
public static void setSponsorBarThickness(final int thickness) {
try {
setSponsorBarThickness((float) thickness);
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
}
setSponsorBarThickness((float) thickness);
}
public static void setSponsorBarThickness(final float thickness) {
if (sponsorBarThickness != thickness) {
LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness));
sponsorBarThickness = thickness;
try {
if (sponsorBarThickness != thickness) {
LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness));
sponsorBarThickness = thickness;
}
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
}
}
@ -564,6 +626,16 @@ public class SegmentPlaybackController {
}
}
private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use
private static int getHighlightSegmentTimeBarScreenWidth() {
if (highlightSegmentTimeBarScreenWidth == -1) {
highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH,
ReVancedUtils.getContext().getResources().getDisplayMetrics());
}
return highlightSegmentTimeBarScreenWidth;
}
/**
* Injection point
*/
@ -582,8 +654,13 @@ public class SegmentPlaybackController {
final float tmp1 = (1f / currentVideoLength) * (absoluteRight - absoluteLeft);
for (SponsorSegment segment : segmentsOfCurrentVideo) {
float left = segment.start * tmp1 + absoluteLeft;
float right = segment.end * tmp1 + absoluteLeft;
final float left = segment.start * tmp1 + absoluteLeft;
final float right;
if (segment.category == SegmentCategory.HIGHLIGHT) {
right = left + getHighlightSegmentTimeBarScreenWidth();
} else {
right = segment.end * tmp1 + absoluteLeft;
}
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}
} catch (Exception ex) {

View File

@ -29,11 +29,13 @@ public class SponsorBlockSettings {
JSONObject barTypesObject = settingsJson.getJSONObject("barTypes");
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
// clear existing behavior, as browser plugin exports no value for ignored categories
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
// clear existing behavior, as browser plugin exports no behavior for ignored categories
category.behaviour = CategoryBehaviour.IGNORE;
JSONObject categoryObject = barTypesObject.getJSONObject(category.key);
category.setColor(categoryObject.getString("color"));
if (barTypesObject.has(category.key)) {
JSONObject categoryObject = barTypesObject.getJSONObject(category.key);
category.setColor(categoryObject.getString("color"));
}
}
for (int i = 0; i < categorySelectionsArray.length(); i++) {
@ -47,16 +49,19 @@ public class SponsorBlockSettings {
final int desktopKey = categorySelectionObject.getInt("option");
CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKey(desktopKey);
if (behaviour != null) {
category.behaviour = behaviour;
if (behaviour == null) {
ReVancedUtils.showToastLong(categoryKey + " unknown behavior key: " + desktopKey);
} else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) {
ReVancedUtils.showToastLong("Skip-once behavior not allowed for " + category.key);
category.behaviour = CategoryBehaviour.SKIP_AUTOMATICALLY; // use closest match
} else {
LogHelper.printException(() -> "Unknown segment category behavior key: " + desktopKey);
category.behaviour = behaviour;
}
}
SegmentCategory.updateEnabledCategories();
SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit();
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
category.save(editor);
}
editor.apply();
@ -117,17 +122,19 @@ public class SponsorBlockSettings {
JSONObject barTypesObject = new JSONObject(); // categories' colors
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted();
SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted();
for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.key;
categoryObject.put("color", category.colorString());
barTypesObject.put(categoryKey, categoryObject);
JSONObject behaviorObject = new JSONObject();
behaviorObject.put("name", categoryKey);
behaviorObject.put("option", category.behaviour.desktopKey);
categorySelectionsArray.put(behaviorObject);
if (category.behaviour != CategoryBehaviour.IGNORE) {
JSONObject behaviorObject = new JSONObject();
behaviorObject.put("name", categoryKey);
behaviorObject.put("option", category.behaviour.desktopKey);
categorySelectionsArray.put(behaviorObject);
}
}
json.put("userID", SettingsEnum.SB_UUID.getString());
json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean());
@ -182,7 +189,7 @@ public class SponsorBlockSettings {
initialized = true;
String uuid = SettingsEnum.SB_UUID.getString();
if (uuid == null || uuid.isEmpty()) {
if (uuid.isEmpty()) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())

View File

@ -15,10 +15,8 @@ import java.lang.ref.WeakReference;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
@ -74,8 +72,8 @@ public class SponsorBlockUtils {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
SegmentCategory category = SegmentCategory.valuesWithoutUnsubmitted()[which];
boolean enableButton;
SegmentCategory category = SegmentCategory.categoriesWithoutHighlights()[which];
final boolean enableButton;
if (category.behaviour == CategoryBehaviour.IGNORE) {
ReVancedUtils.showToastLong(str("sb_new_segment_disabled_category"));
enableButton = false;
@ -100,7 +98,7 @@ public class SponsorBlockUtils {
Context context = ((AlertDialog) dialog).getContext();
dialog.dismiss();
SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted();
SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights();
CharSequence[] titles = new CharSequence[categories.length];
for (int i = 0, length = categories.length; i < length; i++) {
titles[i] = categories[i].getTitleWithColorDot();
@ -167,7 +165,9 @@ public class SponsorBlockUtils {
}
SponsorSegment segment = currentSegments[which];
SegmentVote[] voteOptions = SegmentVote.values();
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
: SegmentVote.values();
CharSequence[] items = new CharSequence[voteOptions.length];
for (int i = 0; i < voteOptions.length; i++) {
@ -195,7 +195,7 @@ public class SponsorBlockUtils {
})
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onPreviewClicked failure", ex);
LogHelper.printException(() -> "segmentVoteClickListener failure", ex);
}
};
@ -218,11 +218,12 @@ public class SponsorBlockUtils {
final String uuid = SettingsEnum.SB_UUID.getString();
final long start = newSponsorSegmentStartMillis;
final long end = newSponsorSegmentEndMillis;
final String videoId = SegmentPlaybackController.getCurrentVideoId();
final String videoId = VideoInformation.getCurrentVideoId();
final long videoLength = VideoInformation.getCurrentVideoLength();
final SegmentCategory segmentCategory = newUserCreatedSegmentCategory;
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || segmentCategory == null || videoId == null || uuid == null) {
LogHelper.printException(() -> "Unable to submit times, invalid parameters");
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty()
|| segmentCategory == null || uuid.isEmpty()) {
LogHelper.printException(() -> "invalid parameters");
return;
}
clearUnsubmittedSegmentTimes();
@ -258,9 +259,13 @@ public class SponsorBlockUtils {
public static void onPublishClicked() {
try {
ReVancedUtils.verifyOnMainThread();
if (!newSponsorSegmentPreviewed) {
if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) {
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
} else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) {
ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end"));
} else if (!newSponsorSegmentPreviewed && newSponsorSegmentStartMillis != 0) {
ReVancedUtils.showToastLong(str("sb_new_segment_preview_segment_first"));
} else if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
} else {
long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000;
long start = (newSponsorSegmentStartMillis) / 1000;
long end = (newSponsorSegmentEndMillis) / 1000;
@ -273,8 +278,6 @@ public class SponsorBlockUtils {
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener)
.show();
} else {
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
}
} catch (Exception ex) {
LogHelper.printException(() -> "onPublishClicked failure", ex);
@ -303,29 +306,32 @@ public class SponsorBlockUtils {
} else if (currentVideoLength < (10 * 60 * 60 * 1000)) {
formatPattern = "H:mm:ss"; // less than 10 hours
} else {
formatPattern = "HH:mm:ss"; // why is this on YouTube
formatPattern = "HH:mm:ss"; // why is this on YouTube
}
voteSegmentTimeFormatter.applyPattern(formatPattern);
final int numberOfSegments = currentSegments.length;
List<CharSequence> titles = new ArrayList<>(numberOfSegments);
CharSequence[] titles = new CharSequence[numberOfSegments];
for (int i = 0; i < numberOfSegments; i++) {
SponsorSegment segment = currentSegments[i];
if (segment.category == SegmentCategory.UNSUBMITTED) {
continue;
}
String start = voteSegmentTimeFormatter.format(new Date(segment.start));
String end = voteSegmentTimeFormatter.format(new Date(segment.end));
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br> %s to %s",
segment.category.color, segment.category.title, start, end));
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>",
segment.category.color, segment.category.title));
htmlBuilder.append(voteSegmentTimeFormatter.format(new Date(segment.start)));
if (segment.category != SegmentCategory.HIGHLIGHT) {
htmlBuilder.append(" to ").append(voteSegmentTimeFormatter.format(new Date(segment.end)));
}
htmlBuilder.append("</b>");
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
htmlBuilder.append("<br>");
titles.add(Html.fromHtml(htmlBuilder.toString()));
titles[i] = Html.fromHtml(htmlBuilder.toString());
}
new AlertDialog.Builder(context)
.setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener)
.setItems(titles, segmentVoteClickListener)
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onVotingClicked failure", ex);
@ -335,7 +341,7 @@ public class SponsorBlockUtils {
private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) {
try {
ReVancedUtils.verifyOnMainThread();
final SegmentCategory[] values = SegmentCategory.valuesWithoutUnsubmitted();
final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights();
CharSequence[] titles = new CharSequence[values.length];
for (int i = 0; i < values.length; i++) {
titles[i] = values[i].getTitleWithColorDot();
@ -353,7 +359,11 @@ public class SponsorBlockUtils {
public static void onPreviewClicked() {
try {
ReVancedUtils.verifyOnMainThread();
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) {
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
} else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) {
ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end"));
} else {
VideoInformation.seekTo(newSponsorSegmentStartMillis - 2500);
final SponsorSegment[] original = SegmentPlaybackController.getSegmentsOfCurrentVideo();
final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1);
@ -362,8 +372,6 @@ public class SponsorBlockUtils {
newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false);
SegmentPlaybackController.setSegmentsOfCurrentVideo(segments);
} else {
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
}
} catch (Exception ex) {
LogHelper.printException(() -> "onPreviewClicked failure", ex);

View File

@ -11,32 +11,29 @@ import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.StringRef;
public enum CategoryBehaviour {
SKIP_AUTOMATICALLY("skip", 2, sf("sb_skip_automatically"), true),
SKIP_AUTOMATICALLY("skip", 2, true, sf("sb_skip_automatically")),
// desktop does not have skip-once behavior. Key is unique to ReVanced
SKIP_AUTOMATICALLY_ONCE("skip-once", 4, sf("sb_skip_automatically_once"), true),
MANUAL_SKIP("manual-skip", 1, sf("sb_skip_showbutton"), false),
SHOW_IN_SEEKBAR("seekbar-only", 0, sf("sb_skip_seekbaronly"), false),
// Ignore is the default behavior if no desktop behavior key is present
IGNORE("ignore", 3, sf("sb_skip_ignore"), false);
SKIP_AUTOMATICALLY_ONCE("skip-once", 3, true, sf("sb_skip_automatically_once")),
MANUAL_SKIP("manual-skip", 1, false, sf("sb_skip_showbutton")),
SHOW_IN_SEEKBAR("seekbar-only", 0, false, sf("sb_skip_seekbaronly")),
// ignored categories are not exported to json, and ignore is the default behavior when importing
IGNORE("ignore", -1, false, sf("sb_skip_ignore"));
@NonNull
public final String key;
public final int desktopKey;
@NonNull
public final StringRef name;
/**
* If the segment should skip automatically
*/
public final boolean skip;
public final boolean skipAutomatically;
@NonNull
public final StringRef description;
CategoryBehaviour(String key,
int desktopKey,
StringRef name,
boolean skip) {
CategoryBehaviour(String key, int desktopKey, boolean skipAutomatically, StringRef description) {
this.key = Objects.requireNonNull(key);
this.desktopKey = desktopKey;
this.name = Objects.requireNonNull(name);
this.skip = skip;
this.skipAutomatically = skipAutomatically;
this.description = Objects.requireNonNull(description);
}
@Nullable
@ -60,31 +57,60 @@ public enum CategoryBehaviour {
}
private static String[] behaviorKeys;
private static String[] behaviorNames;
private static String[] behaviorDescriptions;
private static String[] behaviorKeysWithoutSkipOnce;
private static String[] behaviorDescriptionsWithoutSkipOnce;
private static void createNameAndKeyArrays() {
ReVancedUtils.verifyOnMainThread();
CategoryBehaviour[] behaviours = values();
behaviorKeys = new String[behaviours.length];
behaviorNames = new String[behaviours.length];
for (int i = 0, length = behaviours.length; i < length; i++) {
CategoryBehaviour behaviour = behaviours[i];
behaviorKeys[i] = behaviour.key;
behaviorNames[i] = behaviour.name.toString();
final int behaviorLength = behaviours.length;
behaviorKeys = new String[behaviorLength];
behaviorDescriptions = new String[behaviorLength];
behaviorKeysWithoutSkipOnce = new String[behaviorLength - 1];
behaviorDescriptionsWithoutSkipOnce = new String[behaviorLength - 1];
int behaviorIndex = 0, behaviorHighlightIndex = 0;
while (behaviorIndex < behaviorLength) {
CategoryBehaviour behaviour = behaviours[behaviorIndex];
String key = behaviour.key;
String description = behaviour.description.toString();
behaviorKeys[behaviorIndex] = key;
behaviorDescriptions[behaviorIndex] = description;
behaviorIndex++;
if (behaviour != SKIP_AUTOMATICALLY_ONCE) {
behaviorKeysWithoutSkipOnce[behaviorHighlightIndex] = key;
behaviorDescriptionsWithoutSkipOnce[behaviorHighlightIndex] = description;
behaviorHighlightIndex++;
}
}
}
public static String[] getBehaviorNames() {
if (behaviorNames == null) {
createNameAndKeyArrays();
}
return behaviorNames;
}
public static String[] getBehaviorKeys() {
static String[] getBehaviorKeys() {
if (behaviorKeys == null) {
createNameAndKeyArrays();
}
return behaviorKeys;
}
static String[] getBehaviorKeysWithoutSkipOnce() {
if (behaviorKeysWithoutSkipOnce == null) {
createNameAndKeyArrays();
}
return behaviorKeysWithoutSkipOnce;
}
static String[] getBehaviorDescriptions() {
if (behaviorDescriptions == null) {
createNameAndKeyArrays();
}
return behaviorDescriptions;
}
static String[] getBehaviorDescriptionsWithoutSkipOnce() {
if (behaviorDescriptionsWithoutSkipOnce == null) {
createNameAndKeyArrays();
}
return behaviorDescriptionsWithoutSkipOnce;
}
}

View File

@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.settings.SharedPrefCategory;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.StringRef;
@ -33,6 +34,11 @@ public enum SegmentCategory {
MANUAL_SKIP, 0xFFFF00),
INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"),
MANUAL_SKIP, 0xCC00FF),
/**
* Unique category that is treated differently than the rest.
*/
HIGHLIGHT("poi_highlight", sf("sb_segments_highlight"), sf("sb_segments_highlight_sum"), sf("sb_skip_button_highlight"), sf("sb_skipped_highlight"),
MANUAL_SKIP, 0xFF1684),
INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"),
sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"),
sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"),
@ -50,7 +56,10 @@ public enum SegmentCategory {
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"),
SKIP_AUTOMATICALLY, 0xFFFFFF);
private static final SegmentCategory[] mValuesWithoutUnsubmitted = new SegmentCategory[]{
private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight");
private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
@ -60,7 +69,19 @@ public enum SegmentCategory {
FILLER,
MUSIC_OFFTOPIC,
};
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * mValuesWithoutUnsubmitted.length);
private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
HIGHLIGHT,
INTRO,
OUTRO,
PREVIEW,
FILLER,
MUSIC_OFFTOPIC,
};
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color";
@ -70,13 +91,18 @@ public enum SegmentCategory {
public static String sponsorBlockAPIFetchCategories = "[]";
static {
for (SegmentCategory value : mValuesWithoutUnsubmitted)
for (SegmentCategory value : categoriesWithoutUnsubmitted)
mValuesMap.put(value.key, value);
}
@NonNull
public static SegmentCategory[] valuesWithoutUnsubmitted() {
return mValuesWithoutUnsubmitted;
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
@NonNull
public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights;
}
@Nullable
@ -87,7 +113,7 @@ public enum SegmentCategory {
public static void loadFromPreferences() {
SharedPreferences preferences = SharedPrefCategory.SPONSOR_BLOCK.preferences;
LogHelper.printDebug(() -> "loadFromPreferences");
for (SegmentCategory category : valuesWithoutUnsubmitted()) {
for (SegmentCategory category : categoriesWithoutUnsubmitted()) {
category.load(preferences);
}
updateEnabledCategories();
@ -97,7 +123,7 @@ public enum SegmentCategory {
* Must be called if behavior of any category is changed
*/
public static void updateEnabledCategories() {
SegmentCategory[] categories = valuesWithoutUnsubmitted();
SegmentCategory[] categories = categoriesWithoutUnsubmitted();
List<String> enabledCategories = new ArrayList<>(categories.length);
for (SegmentCategory category : categories) {
if (category.behaviour != CategoryBehaviour.IGNORE) {
@ -157,6 +183,7 @@ public enum SegmentCategory {
* If value is changed, then also call {@link #save(SharedPreferences.Editor)}
*/
public int color;
/**
* If value is changed, then also call {@link #updateEnabledCategories()}
*/
@ -272,17 +299,23 @@ public enum SegmentCategory {
* @return the skip button text
*/
@NonNull
public String getSkipButtonText(long segmentStartTime, long videoLength) {
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()) {
return (this == SegmentCategory.HIGHLIGHT)
? skipSponsorTextCompactHighlight
: skipSponsorTextCompact;
}
if (videoLength == 0) {
return skipButtonTextBeginning.toString(); // video is still loading. Assume it's the beginning
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skipButtonTextBeginning.toString();
return skipButtonTextBeginning;
} else if (position < 0.75f) {
return skipButtonTextMiddle.toString();
return skipButtonTextMiddle;
}
return skipButtonTextEnd.toString();
return skipButtonTextEnd;
}
/**
@ -291,16 +324,16 @@ public enum SegmentCategory {
* @return 'skipped segment' toast message
*/
@NonNull
public String getSkippedToastText(long segmentStartTime, long videoLength) {
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skippedToastBeginning.toString(); // video is still loading. Assume it's the beginning
return skippedToastBeginning; // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skippedToastBeginning.toString();
return skippedToastBeginning;
} else if (position < 0.75f) {
return skippedToastMiddle.toString();
return skippedToastMiddle;
}
return skippedToastEnd.toString();
return skippedToastEnd;
}
}

View File

@ -29,11 +29,16 @@ public class SegmentCategoryListPreference extends ListPreference {
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
super(context);
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
this.category = Objects.requireNonNull(category);
setKey(category.key);
setDefaultValue(category.behaviour.key);
setEntries(CategoryBehaviour.getBehaviorNames());
setEntryValues(CategoryBehaviour.getBehaviorKeys());
setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeysWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeys());
setSummary(category.description.toString());
updateTitle();
}

View File

@ -14,6 +14,11 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
DOWNVOTE(sf("sb_vote_downvote"), 0, true),
CATEGORY_CHANGE(sf("sb_vote_category"), -1, true); // apiVoteType is not used for category change
public static final SegmentVote[] voteTypesWithoutCategoryChange = {
UPVOTE,
DOWNVOTE,
};
@NonNull
public final StringRef title;
public final int apiVoteType;
@ -51,36 +56,28 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
}
public boolean shouldAutoSkip() {
return category.behaviour.skip && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE);
return category.behaviour.skipAutomatically && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE);
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean timeIsNearStart(long videoTime, long nearThreshold) {
public boolean startIsNear(long videoTime, long nearThreshold) {
return Math.abs(start - videoTime) <= nearThreshold;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean timeIsNearEnd(long videoTime, long nearThreshold) {
public boolean endIsNear(long videoTime, long nearThreshold) {
return Math.abs(end - videoTime) <= nearThreshold;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment
* @return if the time parameter is within or close to this segment
* @return if the time parameter is within this segment
*/
public boolean timeIsInsideOrNear(long videoTime, long nearThreshold) {
return (start - nearThreshold) <= videoTime && videoTime < (end + nearThreshold);
}
/**
* @return if the time parameter is outside this segment
*/
public boolean timeIsOutside(long videoTime) {
return start < videoTime || end <= videoTime;
public boolean containsTime(long videoTime) {
return start <= videoTime && videoTime < end;
}
/**
@ -102,7 +99,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
*/
@NonNull
public String getSkipButtonText() {
return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength());
return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength()).toString();
}
/**
@ -110,7 +107,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
*/
@NonNull
public String getSkippedToastText() {
return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength());
return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()).toString();
}
@Override

View File

@ -65,19 +65,15 @@ public class SBRequester {
JSONArray segment = obj.getJSONArray("segment");
final long start = (long) (segment.getDouble(0) * 1000);
final long end = (long) (segment.getDouble(1) * 1000);
if ((end - start) < minSegmentDuration)
continue;
String categoryKey = obj.getString("category");
String uuid = obj.getString("UUID");
boolean locked = obj.getInt("locked") == 1;
SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(categoryKey);
if (segmentCategory == null) {
final boolean locked = obj.getInt("locked") == 1;
String categoryKey = obj.getString("category");
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
if (category == null) {
LogHelper.printException(() -> "Received unknown category: " + categoryKey); // should never happen
} else if (segmentCategory.behaviour != CategoryBehaviour.IGNORE) {
SponsorSegment sponsorSegment = new SponsorSegment(segmentCategory, uuid, start, end, locked);
segments.add(sponsorSegment);
} else if ((end - start) >= minSegmentDuration || category == SegmentCategory.HIGHLIGHT) {
segments.add(new SponsorSegment(category, uuid, start, end, locked));
}
}
LogHelper.printDebug(() -> {

View File

@ -4,10 +4,8 @@ import static app.revanced.integrations.utils.ReVancedUtils.getResourceColor;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimension;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import static app.revanced.integrations.utils.StringRef.str;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
@ -16,9 +14,10 @@ import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.utils.LogHelper;
@ -27,9 +26,9 @@ public class SkipSponsorButton extends FrameLayout {
private static final boolean highContrast = true;
private final LinearLayout skipSponsorBtnContainer;
private final TextView skipSponsorTextView;
private final CharSequence skipSponsorTextCompact;
private final Paint background;
private final Paint border;
private SponsorSegment segment;
final int defaultBottomMargin;
final int ctaBottomMargin;
@ -61,11 +60,9 @@ public class SkipSponsorButton extends FrameLayout {
skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text;
defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin
ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin
skipSponsorTextCompact = str("sb_skip_button_compact"); // string:skip_ads "Skip ads"
skipSponsorBtnContainer.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Skip button clicked");
SegmentPlaybackController.onSkipSponsorClicked();
SegmentPlaybackController.onSkipSegmentClicked(segment);
});
}
@ -90,10 +87,9 @@ public class SkipSponsorButton extends FrameLayout {
/**
* @return true, if this button state was changed
*/
public boolean updateSkipButtonText(SponsorSegment segment) {
CharSequence newText = SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()
? skipSponsorTextCompact
: segment.getSkipButtonText();
public boolean updateSkipButtonText(@NonNull SponsorSegment segment) {
this.segment = segment;
CharSequence newText = segment.getSkipButtonText();
if (newText.equals(skipSponsorTextView.getText())) {
return false;
}

View File

@ -23,9 +23,13 @@ import app.revanced.integrations.utils.ReVancedUtils;
public class SponsorBlockViewController {
private static WeakReference<RelativeLayout> inlineSponsorOverlayRef = new WeakReference<>(null);
private static WeakReference<ViewGroup> youtubeOverlaysLayoutRef = new WeakReference<>(null);
private static WeakReference<SkipSponsorButton> skipHighlightButtonRef = new WeakReference<>(null);
private static WeakReference<SkipSponsorButton> skipSponsorButtonRef = new WeakReference<>(null);
private static WeakReference<NewSegmentLayout> newSegmentLayoutRef = new WeakReference<>(null);
private static boolean canShowViewElements = true;
private static boolean canShowViewElements;
private static boolean newSegmentLayoutVisible;
@Nullable
private static SponsorSegment skipHighlight;
@Nullable
private static SponsorSegment skipSegment;
@ -51,161 +55,155 @@ public class SponsorBlockViewController {
try {
LogHelper.printDebug(() -> "initializing");
RelativeLayout layout = new RelativeLayout(ReVancedUtils.getContext());
Context context = ReVancedUtils.getContext();
RelativeLayout layout = new RelativeLayout(context);
layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
LayoutInflater.from(ReVancedUtils.getContext()).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout);
LayoutInflater.from(context).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout);
inlineSponsorOverlayRef = new WeakReference<>(layout);
ViewGroup viewGroup = (ViewGroup) obj;
viewGroup.addView(layout, viewGroup.getChildCount() - 2);
viewGroup.addView(layout);
viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@Override
public void onChildViewAdded(View parent, View child) {
// ensure SB buttons and controls are always on top, otherwise the endscreen cards can cover the skip button
RelativeLayout layout = inlineSponsorOverlayRef.get();
if (layout != null) {
layout.bringToFront();
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
}
});
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
skipHighlightButtonRef = new WeakReference<>(
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_highlight_button", "id"))));
skipSponsorButtonRef = new WeakReference<>(
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_sponsor_button", "id"))));
newSegmentLayoutRef = new WeakReference<>(
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_new_segment_view", "id"))));
newSegmentLayoutVisible = false;
skipHighlight = null;
skipSegment = null;
} catch (Exception ex) {
LogHelper.printException(() -> "initialize failure", ex);
}
}
public static void showSkipButton(@NonNull SponsorSegment info) {
skipSegment = Objects.requireNonNull(info);
updateSkipButton();
public static void hideAll() {
hideSkipHighlightButton();
hideSkipSegmentButton();
hideNewSegmentLayout();
}
public static void hideSkipButton() {
skipSegment = null;
updateSkipButton();
}
private static void updateSkipButton() {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
return;
}
if (skipSegment == null) {
setSkipSponsorButtonVisibility(false);
} else {
final boolean layoutNeedsUpdating = skipSponsorButton.updateSkipButtonText(skipSegment);
if (layoutNeedsUpdating) {
bringLayoutToFront();
}
setSkipSponsorButtonVisibility(true);
}
}
public static void showNewSegmentLayout() {
setNewSegmentLayoutVisibility(true);
}
public static void hideNewSegmentLayout() {
public static void showSkipHighlightButton(@NonNull SponsorSegment segment) {
skipHighlight = Objects.requireNonNull(segment);
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
// don't show highlight button if create new segment is visible
final boolean buttonVisibility = newSegmentLayout != null && newSegmentLayout.getVisibility() != View.VISIBLE;
updateSkipButton(skipHighlightButtonRef.get(), segment, buttonVisibility);
}
public static void showSkipSegmentButton(@NonNull SponsorSegment segment) {
skipSegment = Objects.requireNonNull(segment);
updateSkipButton(skipSponsorButtonRef.get(), segment, true);
}
public static void hideSkipHighlightButton() {
skipHighlight = null;
updateSkipButton(skipHighlightButtonRef.get(), null, false);
}
public static void hideSkipSegmentButton() {
skipSegment = null;
updateSkipButton(skipSponsorButtonRef.get(), null, false);
}
private static void updateSkipButton(@Nullable SkipSponsorButton button,
@Nullable SponsorSegment segment, boolean visible) {
if (button == null) {
return;
}
setNewSegmentLayoutVisibility(false);
if (segment != null) {
button.updateSkipButtonText(segment);
}
setViewVisibility(button, visible);
}
public static void toggleNewSegmentLayoutVisibility() {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
if (newSegmentLayout == null) { // should never happen
LogHelper.printException(() -> "toggleNewSegmentLayoutVisibility failure");
return;
}
setNewSegmentLayoutVisibility(newSegmentLayout.getVisibility() == View.VISIBLE ? false : true);
newSegmentLayoutVisible = (newSegmentLayout.getVisibility() != View.VISIBLE);
if (skipHighlight != null) {
setViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible);
}
setViewVisibility(newSegmentLayout, newSegmentLayoutVisible);
}
private static void playerTypeChanged(PlayerType playerType) {
public static void hideNewSegmentLayout() {
newSegmentLayoutVisible = false;
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
return;
}
setViewVisibility(newSegmentLayout, false);
}
private static void setViewVisibility(@Nullable View view, boolean visible) {
if (view == null) {
return;
}
visible &= canShowViewElements;
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
if (view.getVisibility() != desiredVisibility) {
view.setVisibility(desiredVisibility);
}
}
private static void playerTypeChanged(@NonNull PlayerType playerType) {
try {
final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN;
canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED);
setSkipButtonMargins(isWatchFullScreen);
setNewSegmentLayoutMargins(isWatchFullScreen);
updateSkipButton();
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen);
setViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible);
SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get();
setSkipButtonMargins(skipHighlightButton, isWatchFullScreen);
setViewVisibility(skipHighlightButton, skipHighlight != null);
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
setSkipButtonMargins(skipSponsorButton, isWatchFullScreen);
setViewVisibility(skipSponsorButton, skipSegment != null);
} catch (Exception ex) {
LogHelper.printException(() -> "Player type changed error", ex);
LogHelper.printException(() -> "Player type changed failure", ex);
}
}
private static void setSkipButtonMargins(boolean fullScreen) {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "setSkipButtonMargins failure");
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "setSkipButtonMargins failure");
return;
}
params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin;
skipSponsorButton.setLayoutParams(params);
}
private static void setSkipSponsorButtonVisibility(boolean visible) {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "setSkipSponsorButtonVisibility failure");
return;
}
visible &= canShowViewElements;
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
if (skipSponsorButton.getVisibility() != desiredVisibility) {
skipSponsorButton.setVisibility(desiredVisibility);
if (visible) {
bringLayoutToFront();
}
private static void setNewSegmentLayoutMargins(@Nullable NewSegmentLayout layout, boolean fullScreen) {
if (layout != null) {
setLayoutMargins(layout, fullScreen, layout.defaultBottomMargin, layout.ctaBottomMargin);
}
}
private static void setNewSegmentLayoutMargins(boolean fullScreen) {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (button is null)");
return;
private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) {
if (button != null) {
setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin);
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams();
}
private static void setLayoutMargins(@NonNull View view, boolean fullScreen,
int defaultBottomMargin, int ctaBottomMargin) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)");
return;
}
params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin;
newSegmentLayout.setLayoutParams(params);
}
private static void setNewSegmentLayoutVisibility(boolean visible) {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "setNewSegmentLayoutVisibility failure");
return;
}
visible &= canShowViewElements;
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
if (newSegmentLayout.getVisibility() != desiredVisibility) {
newSegmentLayout.setVisibility(desiredVisibility);
if (visible) {
bringLayoutToFront();
}
}
}
private static void bringLayoutToFront() {
RelativeLayout layout = inlineSponsorOverlayRef.get();
if (layout != null) {
// needed to keep skip button overtop end screen cards
layout.bringToFront();
layout.requestLayout();
layout.invalidate();
}
params.bottomMargin = fullScreen ? ctaBottomMargin : defaultBottomMargin;
view.setLayoutParams(params);
}
/**