diff --git a/integrations/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java b/integrations/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java index 3e31d5ed8..2f46f0087 100644 --- a/integrations/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java +++ b/integrations/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java @@ -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); } } } diff --git a/integrations/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/integrations/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index 05da518b7..61bd92c02 100644 --- a/integrations/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/integrations/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -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) { diff --git a/integrations/java/app/revanced/integrations/shared/PlayerType.kt b/integrations/java/app/revanced/integrations/shared/PlayerType.kt index 371767c59..2cf2c4a94 100644 --- a/integrations/java/app/revanced/integrations/shared/PlayerType.kt +++ b/integrations/java/app/revanced/integrations/shared/PlayerType.kt @@ -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; diff --git a/integrations/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/integrations/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java index d2cea53d8..9b5e27c07 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -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) { diff --git a/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java b/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java index f8f4fffa4..5565ac5b6 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java @@ -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()) diff --git a/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java b/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java index d5b0b6f6d..6a19c0d3c 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java @@ -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 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(" %s
%s to %s", - segment.category.color, segment.category.title, start, end)); + htmlBuilder.append(String.format(" %s
", + 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("
"); if (i + 1 != numberOfSegments) // prevents trailing new line after last segment htmlBuilder.append("
"); - 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); diff --git a/integrations/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java b/integrations/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java index 0e226662e..9b15d086f 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java @@ -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; + } } diff --git a/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java index f5f0da4f6..2a6d30595 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -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 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 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 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; } } diff --git a/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java b/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java index b1ab46832..b2e9e2c9b 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java @@ -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(); } diff --git a/integrations/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java b/integrations/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java index 981569695..dff6e2306 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java @@ -14,6 +14,11 @@ public class SponsorSegment implements Comparable { 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 { } 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 { */ @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 { */ @NonNull public String getSkippedToastText() { - return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()); + return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()).toString(); } @Override diff --git a/integrations/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java b/integrations/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java index f235814a1..cd17eb3e2 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java @@ -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(() -> { diff --git a/integrations/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java b/integrations/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java index 588abd983..65666da80 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java @@ -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; } diff --git a/integrations/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java b/integrations/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java index 834dbc2f6..5925f80c1 100644 --- a/integrations/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java +++ b/integrations/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java @@ -23,9 +23,13 @@ import app.revanced.integrations.utils.ReVancedUtils; public class SponsorBlockViewController { private static WeakReference inlineSponsorOverlayRef = new WeakReference<>(null); private static WeakReference youtubeOverlaysLayoutRef = new WeakReference<>(null); + private static WeakReference skipHighlightButtonRef = new WeakReference<>(null); private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); private static WeakReference 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); } /**