From 6ee94f8532a073d720f5f2cc6862c78c2e06aee6 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:50:52 +0300 Subject: [PATCH] feat(YouTube - SponsorBlock): Add "Undo automatic skip toast" (#5277) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../app/revanced/extension/shared/Utils.java | 10 +- .../shared/settings/EnumSetting.java | 15 +- .../extension/shared/settings/Setting.java | 35 +- .../extension/youtube/settings/Settings.java | 6 + .../SegmentPlaybackController.java | 470 +++++++++++++----- .../sponsorblock/SponsorBlockUtils.java | 64 +-- .../sponsorblock/objects/SponsorSegment.java | 38 +- .../sponsorblock/requests/SBRequester.java | 46 +- .../ui/SponsorBlockPreferenceGroup.java | 91 +++- .../ui/SponsorBlockViewController.java | 2 +- .../resources/addresources/values/arrays.xml | 35 +- .../resources/addresources/values/strings.xml | 22 +- 12 files changed, 591 insertions(+), 243 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 36d7f8d97..609d99b0b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -311,6 +311,10 @@ public class Utils { return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); } + public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array")); + } + public interface MatchFilter { boolean matches(T object); } @@ -579,7 +583,7 @@ public class Utils { Context currentContext = context; if (currentContext == null) { - Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast, null); + Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast); } else { Logger.printDebug(() -> "Showing toast: " + messageToToast); Toast.makeText(currentContext, messageToToast, toastDuration).show(); @@ -809,7 +813,7 @@ public class Utils { // Create content container (message/EditText) inside a ScrollView only if message or editText is provided. ScrollView contentScrollView = null; - LinearLayout contentContainer = null; + LinearLayout contentContainer; if (message != null || editText != null) { contentScrollView = new ScrollView(context); contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar. @@ -833,7 +837,7 @@ public class Utils { contentScrollView.addView(contentContainer); // Message (if not replaced by EditText). - if (editText == null && message != null) { + if (editText == null) { TextView messageView = new TextView(context); messageView.setText(message); // Supports Spanned (HTML). messageView.setTextSize(16); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java index 60972f0f5..88de4029e 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -71,15 +71,20 @@ public class EnumSetting> extends Setting { json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); } - @NonNull - private T getEnumFromString(String enumName) { + /** + * @param enumName Enum name. Casing does not matter. + * @return Enum of this type with the same declared name. + * @throws IllegalArgumentException if the name is not a valid enum of this type. + */ + protected T getEnumFromString(String enumName) { //noinspection ConstantConditions for (Enum value : defaultValue.getClass().getEnumConstants()) { if (value.name().equalsIgnoreCase(enumName)) { - // noinspection unchecked + //noinspection unchecked return (T) value; } } + throw new IllegalArgumentException("Unknown enum value: " + enumName); } @@ -103,7 +108,9 @@ public class EnumSetting> extends Setting { * Availability based on if this setting is currently set to any of the provided types. */ @SafeVarargs - public final Setting.Availability availability(@NonNull T... types) { + public final Setting.Availability availability(T... types) { + Objects.requireNonNull(types); + return () -> { T currentEnumType = get(); for (T enumType : types) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 8294fe423..bbb590558 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -28,16 +28,14 @@ public abstract class Setting { /** * Availability based on a single parent setting being enabled. */ - @NonNull - public static Availability parent(@NonNull BooleanSetting parent) { + public static Availability parent(BooleanSetting parent) { return parent::get; } /** * Availability based on all parents being enabled. */ - @NonNull - public static Availability parentsAll(@NonNull BooleanSetting... parents) { + public static Availability parentsAll(BooleanSetting... parents) { return () -> { for (BooleanSetting parent : parents) { if (!parent.get()) return false; @@ -49,8 +47,7 @@ public abstract class Setting { /** * Availability based on any parent being enabled. */ - @NonNull - public static Availability parentsAny(@NonNull BooleanSetting... parents) { + public static Availability parentsAny(BooleanSetting... parents) { return () -> { for (BooleanSetting parent : parents) { if (parent.get()) return true; @@ -79,7 +76,7 @@ public abstract class Setting { /** * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}. */ - public static void addImportExportCallback(@NonNull ImportExportCallback callback) { + public static void addImportExportCallback(ImportExportCallback callback) { importExportCallbacks.add(Objects.requireNonNull(callback)); } @@ -100,14 +97,13 @@ public abstract class Setting { public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs"); @Nullable - public static Setting getSettingFromPath(@NonNull String str) { + public static Setting getSettingFromPath(String str) { return PATH_TO_SETTINGS.get(str); } /** * @return All settings that have been created. */ - @NonNull public static List> allLoadedSettings() { return Collections.unmodifiableList(SETTINGS); } @@ -115,7 +111,6 @@ public abstract class Setting { /** * @return All settings that have been created, sorted by keys. */ - @NonNull private static List> allLoadedSettingsSorted() { Collections.sort(SETTINGS, (Setting o1, Setting o2) -> o1.key.compareTo(o2.key)); return allLoadedSettings(); @@ -124,13 +119,11 @@ public abstract class Setting { /** * The key used to store the value in the shared preferences. */ - @NonNull public final String key; /** * The default value of the setting. */ - @NonNull public final T defaultValue; /** @@ -161,7 +154,6 @@ public abstract class Setting { /** * The value of the setting. */ - @NonNull protected volatile T value; public Setting(String key, T defaultValue) { @@ -199,8 +191,8 @@ public abstract class Setting { * @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value. * @param availability Condition that must be true, for this setting to be available to configure. */ - public Setting(@NonNull String key, - @NonNull T defaultValue, + public Setting(String key, + T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @@ -227,7 +219,7 @@ public abstract class Setting { /** * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. */ - public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { + public static void migrateOldSettingToNew(Setting oldSetting, Setting newSetting) { if (oldSetting == newSetting) throw new IllegalArgumentException(); if (!oldSetting.isSetToDefault()) { @@ -243,7 +235,7 @@ public abstract class Setting { * This method will be deleted in the future. */ @SuppressWarnings("rawtypes") - public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { + public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) { if (!oldPrefs.preferences.contains(settingKey)) { return; // Nothing to do. } @@ -285,7 +277,7 @@ public abstract class Setting { * This intentionally is a static method to deter * accidental usage when {@link #save(Object)} was intended. */ - public static void privateSetValueFromString(@NonNull Setting setting, @NonNull String newValue) { + public static void privateSetValueFromString(Setting setting, String newValue) { setting.setValueFromString(newValue); // Clear the preference value since default is used, to allow changing @@ -299,7 +291,7 @@ public abstract class Setting { /** * Sets the value of {@link #value}, but do not save to {@link #preferences}. */ - protected abstract void setValueFromString(@NonNull String newValue); + protected abstract void setValueFromString(String newValue); /** * Load and set the value of {@link #value}. @@ -309,7 +301,7 @@ public abstract class Setting { /** * Persistently saves the value. */ - public final void save(@NonNull T newValue) { + public final void save(T newValue) { if (value.equals(newValue)) { return; } @@ -406,7 +398,6 @@ public abstract class Setting { json.put(importExportKey, value); } - @NonNull public static String exportToJson(@Nullable Context alertDialogContext) { try { JSONObject json = new JSONObject(); @@ -445,7 +436,7 @@ public abstract class Setting { /** * @return if any settings that require a reboot were changed. */ - public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) { + public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) { try { if (!settingsJsonString.matches("[\\s\\S]*\\{")) { settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index cdf86fd75..f0eecb5c9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -5,6 +5,7 @@ import static java.lang.Boolean.TRUE; import static app.revanced.extension.shared.settings.Setting.Availability; import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew; import static app.revanced.extension.shared.settings.Setting.parent; +import static app.revanced.extension.shared.settings.Setting.parentsAll; import static app.revanced.extension.shared.settings.Setting.parentsAny; import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor; import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability; @@ -22,6 +23,7 @@ import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPa import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability; import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability; import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle; +import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; @@ -381,7 +383,11 @@ public class Settings extends BaseSettings { public static final BooleanSetting SB_SQUARE_LAYOUT = new BooleanSetting("sb_square_layout", FALSE, parent(SB_ENABLED)); public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED)); public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED)); + public static final EnumSetting SB_AUTO_HIDE_SKIP_BUTTON_DURATION = new EnumSetting<>("sb_auto_hide_skip_button_duration", + SponsorBlockDuration.FOUR_SECONDS, parent(SB_ENABLED)); public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED)); + public static final EnumSetting SB_TOAST_ON_SKIP_DURATION = new EnumSetting<>("sb_toast_on_skip_duration", + SponsorBlockDuration.FOUR_SECONDS, parentsAll(SB_ENABLED, SB_TOAST_ON_SKIP)); public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", TRUE, parent(SB_ENABLED)); public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED)); public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED)); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java index 22259d571..b279de71b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java @@ -1,16 +1,37 @@ package app.revanced.extension.youtube.sponsorblock; import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.dipToPixels; +import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Range; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.widget.LinearLayout; +import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.lang.ref.WeakReference; import java.lang.reflect.Field; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; @@ -30,20 +51,37 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController * 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 skip button for a highlight segment, - * or a regular segment if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled. - * - * Effectively this value is rounded up to the next second. + * Enum for configurable durations (1 to 10 seconds) for skip button and toast display. */ - private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800; + public enum SponsorBlockDuration { + ONE_SECOND(1), + TWO_SECONDS(2), + THREE_SECONDS(3), + FOUR_SECONDS(4), + FIVE_SECONDS(5), + SIX_SECONDS(6), + SEVEN_SECONDS(7), + EIGHT_SECONDS(8), + NINE_SECONDS(9), + TEN_SECONDS(10); + + /** + * Duration, minus 200ms to adjust for exclusive end time checking in scheduled show/hides. + */ + private final long adjustedDuration; + + SponsorBlockDuration(int seconds) { + adjustedDuration = seconds * 1000L - 200; + } + } /* * 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; + private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = dipToPixels(7); @Nullable private static String currentVideoId; @@ -59,7 +97,7 @@ public class SegmentPlaybackController { /** * Because loading can take time, show the skip to highlight for a few seconds after the segments load. * This is the system time (in milliseconds) to no longer show the initial display skip to highlight. - * Value will be zero if no highlight segment exists, or if the system time to show the highlight has passed. + * Value is zero if no highlight segment exists, or if the system time to show the highlight has passed. */ private static long highlightSegmentInitialShowEndTime; @@ -70,7 +108,7 @@ public class SegmentPlaybackController { private static SponsorSegment segmentCurrentlyPlaying; /** * Currently playing manual skip segment that is scheduled to hide. - * This will always be NULL or equal to {@link #segmentCurrentlyPlaying}. + * This is always NULL or equal to {@link #segmentCurrentlyPlaying}. */ @Nullable private static SponsorSegment scheduledHideSegment; @@ -89,31 +127,71 @@ public class SegmentPlaybackController { */ private static final List hiddenSkipSegmentsForCurrentVideoTime = new ArrayList<>(); + /** + * Current segments that have been auto skipped. + * If field is non null then the range will always contain the current video time. + * Range is used to prevent auto-skipping after undo. + * Android Range object has inclusive end time, unlike {@link SponsorSegment}. + */ + @Nullable + private static Range undoAutoSkipRange; + /** + * Range to undo if the toast is tapped. + * Is always null or identical to the last non null value of {@link #undoAutoSkipRange}. + */ + @Nullable + private static Range undoAutoSkipRangeToast; + /** * System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}. * Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null), * or if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is not enabled. */ private static long skipSegmentButtonEndTime; - @Nullable private static String timeWithoutSegments; - private static int sponsorBarAbsoluteLeft; private static int sponsorAbsoluteBarRight; private static int sponsorBarThickness; + @Nullable + private static SponsorSegment lastSegmentSkipped; + private static long lastSegmentSkippedTime; + + @Nullable + private static SponsorSegment toastSegmentSkipped; + private static int toastNumberOfSegmentsSkipped; + + /** + * The last toast dialog showing on screen. + */ + private static WeakReference toastDialogRef = new WeakReference<>(null); + + /** + * @return The adjusted duration to show the skip button, in milliseconds. + */ + private static long getSkipButtonDuration() { + return Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().adjustedDuration; + } + + /** + * @return The adjusted duration to show the skipped toast, in milliseconds. + */ + private static long getToastDuration() { + return Settings.SB_TOAST_ON_SKIP_DURATION.get().adjustedDuration; + } + @Nullable static SponsorSegment[] getSegments() { return segments; } - private static void setSegments(@NonNull SponsorSegment[] videoSegments) { + private static void setSegments(SponsorSegment[] videoSegments) { Arrays.sort(videoSegments); segments = videoSegments; calculateTimeWithoutSegments(); - if (SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY + if (SegmentCategory.HIGHLIGHT.behaviour == SKIP_AUTOMATICALLY || SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.MANUAL_SKIP) { for (SponsorSegment segment : videoSegments) { if (segment.category == SegmentCategory.HIGHLIGHT) { @@ -125,7 +203,7 @@ public class SegmentPlaybackController { highlightSegment = null; } - static void addUnsubmittedSegment(@NonNull SponsorSegment segment) { + static void addUnsubmittedSegment(SponsorSegment segment) { Objects.requireNonNull(segment); if (segments == null) { segments = new SponsorSegment[1]; @@ -140,6 +218,7 @@ public class SegmentPlaybackController { if (segments == null || segments.length == 0) { return; } + List replacement = new ArrayList<>(); for (SponsorSegment segment : segments) { if (segment.category != SegmentCategory.UNSUBMITTED) { @@ -156,7 +235,7 @@ public class SegmentPlaybackController { } /** - * Clears all downloaded data. + * Clear all data. */ private static void clearData() { currentVideoId = null; @@ -170,6 +249,8 @@ public class SegmentPlaybackController { skipSegmentButtonEndTime = 0; toastSegmentSkipped = null; toastNumberOfSegmentsSkipped = 0; + undoAutoSkipRange = null; + undoAutoSkipRangeToast = null; hiddenSkipSegmentsForCurrentVideoTime.clear(); } @@ -186,7 +267,7 @@ public class SegmentPlaybackController { SponsorBlockUtils.clearUnsubmittedSegmentTimes(); Logger.printDebug(() -> "Initialized SponsorBlock"); } catch (Exception ex) { - Logger.printException(() -> "Failed to initialize SponsorBlock", ex); + Logger.printException(() -> "initialize failure", ex); } } @@ -203,7 +284,7 @@ public class SegmentPlaybackController { return; } if (PlayerType.getCurrent().isNoneOrHidden()) { - Logger.printDebug(() -> "ignoring Short"); + Logger.printDebug(() -> "Ignoring Short"); return; } if (!Utils.isNetworkConnected()) { @@ -212,7 +293,7 @@ public class SegmentPlaybackController { } currentVideoId = videoId; - Logger.printDebug(() -> "setCurrentVideoId: " + videoId); + Logger.printDebug(() -> "New video ID: " + videoId); Utils.runOnBackgroundThread(() -> { try { @@ -227,42 +308,39 @@ public class SegmentPlaybackController { } /** - * Must be called off main thread + * Must be called off main thread. */ - static void executeDownloadSegments(@NonNull String videoId) { + static void executeDownloadSegments(String videoId) { Objects.requireNonNull(videoId); - try { - SponsorSegment[] segments = SBRequester.getSegments(videoId); - Utils.runOnMainThread(()-> { - if (!videoId.equals(currentVideoId)) { - // user changed videos before get segments network call could complete - Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId); - return; - } - setSegments(segments); + SponsorSegment[] segments = SBRequester.getSegments(videoId); - final long videoTime = VideoInformation.getVideoTime(); - if (highlightSegment != null) { - // If the current video time is before the highlight. - final long timeUntilHighlight = highlightSegment.start - videoTime; - if (timeUntilHighlight > 0) { - if (highlightSegment.shouldAutoSkip()) { - skipSegment(highlightSegment, false); - return; - } - highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min( - (long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()), - DURATION_TO_SHOW_SKIP_BUTTON); + Utils.runOnMainThread(() -> { + if (!videoId.equals(currentVideoId)) { + // user changed videos before get segments network call could complete + Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId); + return; + } + setSegments(segments); + + final long videoTime = VideoInformation.getVideoTime(); + if (highlightSegment != null) { + // If the current video time is before the highlight. + final long timeUntilHighlight = highlightSegment.start - videoTime; + if (timeUntilHighlight > 0) { + if (highlightSegment.shouldAutoSkip()) { + skipSegment(highlightSegment, false); + return; } + highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min( + (long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()), + getSkipButtonDuration()); } + } - // check for any skips now, instead of waiting for the next update to setVideoTime() - setVideoTime(videoTime); - }); - } catch (Exception ex) { - Logger.printException(() -> "executeDownloadSegments failure", ex); - } + // check for any skips now, instead of waiting for the next update to setVideoTime() + setVideoTime(videoTime); + }); } /** @@ -273,8 +351,8 @@ public class SegmentPlaybackController { public static void setVideoTime(long millis) { try { if (!Settings.SB_ENABLED.get() - || PlayerType.getCurrent().isNoneOrHidden() // Shorts playback. - || segments == null || segments.length == 0) { + || PlayerType.getCurrent().isNoneOrHidden() // Shorts playback. + || segments == null || segments.length == 0) { return; } Logger.printDebug(() -> "setVideoTime: " + millis); @@ -290,7 +368,7 @@ public class SegmentPlaybackController { // // To debug the stale skip logic, set this to a very large value (5000 or more) // then try manually seeking just before playback reaches a segment skip. - final long speedAdjustedTimeThreshold = (long)(playbackSpeed * 1200); + final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1200); final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold; SponsorSegment foundSegmentCurrentlyPlaying = null; @@ -298,22 +376,24 @@ public class SegmentPlaybackController { for (final SponsorSegment segment : segments) { if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR - || segment.category.behaviour == CategoryBehaviour.IGNORE - || segment.category == SegmentCategory.HIGHLIGHT) { + || segment.category.behaviour == CategoryBehaviour.IGNORE + || segment.category == SegmentCategory.HIGHLIGHT) { continue; } if (segment.end <= millis) { - continue; // past this segment + continue; // Past this segment. } + final boolean segmentShouldAutoSkip = shouldAutoSkipAndUndoSkipNotActive(segment, millis); + if (segment.start <= millis) { - // we are in the segment! - if (segment.shouldAutoSkip()) { + // We are in the segment! + if (segmentShouldAutoSkip) { skipSegment(segment, false); - return; // must return, as skipping causes a recursive call back into this method + return; // Must return, as skipping causes a recursive call back into this method. } - // first found segment, or it's an embedded segment and fully inside the outer segment + // First found segment, or it's an embedded segment and fully inside the outer segment. if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) { // If the found segment is not currently displayed, then do not show if the segment is nearly over. // This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time. @@ -327,25 +407,27 @@ public class SegmentPlaybackController { } } // Keep iterating and looking. There may be an upcoming autoskip, - // or there may be another smaller segment nested inside this segment + // or there may be another smaller segment nested inside this segment. continue; } - // segment is upcoming + // Segment is upcoming. if (startTimerLookAheadThreshold < segment.start) { - break; // segment is not close enough to schedule, and no segments after this are of interest + // Segment is not close enough to schedule, and no segments after this are of interest. + break; } - if (segment.shouldAutoSkip()) { // upcoming autoskip + + if (segmentShouldAutoSkip) { foundUpcomingSegment = segment; - break; // must stop here + break; // Must stop here. } - // upcoming manual skip + // Upcoming manual skip. - // do not schedule upcoming segment, if it is not fully contained inside the current segment + // Do not schedule upcoming segment, if it is not fully contained inside the current segment. if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) - // use the most inner upcoming segment - && (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) { + // Use the most inner upcoming segment. + && (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) { // Only schedule, if the segment start time is not near the end time of the current segment. // This check is needed to prevent scheduled hide and show from clashing with each other. @@ -361,8 +443,8 @@ public class SegmentPlaybackController { } if (highlightSegment != null) { - if (millis < DURATION_TO_SHOW_SKIP_BUTTON || (highlightSegmentInitialShowEndTime != 0 - && System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) { + if (millis < getSkipButtonDuration() || (highlightSegmentInitialShowEndTime != 0 + && System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) { SponsorBlockViewController.showSkipHighlightButton(highlightSegment); } else { highlightSegmentInitialShowEndTime = 0; @@ -373,16 +455,17 @@ public class SegmentPlaybackController { if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) { setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying); } else if (foundSegmentCurrentlyPlaying != null - && skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) { + && skipSegmentButtonEndTime != 0 + && skipSegmentButtonEndTime <= System.currentTimeMillis()) { Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying); skipSegmentButtonEndTime = 0; hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying); SponsorBlockViewController.hideSkipSegmentButton(); } - // schedule a hide, only if the segment end is near - final SponsorSegment segmentToHide = - (foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold)) + // Schedule a hide, but only if the segment end is near. + final SponsorSegment segmentToHide = (foundSegmentCurrentlyPlaying != null && + foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold)) ? foundSegmentCurrentlyPlaying : null; @@ -407,7 +490,7 @@ public class SegmentPlaybackController { final long videoTime = VideoInformation.getVideoTime(); if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) { - // current video time is not what's expected. User paused playback + // Current video time is not what's expected. User paused playback. Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide + " videoInformation time: " + videoTime); return; @@ -416,7 +499,7 @@ public class SegmentPlaybackController { // Need more than just hide the skip button, as this may have been an embedded segment // Instead call back into setVideoTime to check everything again. // 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 + // but this scheduled handler was scheduled precisely so we can just use the segment end time. setSegmentCurrentlyPlaying(null); setVideoTime(segmentToHide.end); }, delayUntilHide); @@ -446,12 +529,12 @@ public class SegmentPlaybackController { final long videoTime = VideoInformation.getVideoTime(); if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) { - // current video time is not what's expected. User paused playback + // Current video time is not what's expected. User paused playback. Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip + " videoInformation time: " + videoTime); return; } - if (segmentToSkip.shouldAutoSkip()) { + if (shouldAutoSkipAndUndoSkipNotActive(segmentToSkip, videoTime)) { Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip); skipSegment(segmentToSkip, false); } else { @@ -461,6 +544,12 @@ public class SegmentPlaybackController { }, delayUntilSkip); } } + + // Clear undo range if video time is outside the segment. Must check last. + if (undoAutoSkipRange != null && !undoAutoSkipRange.contains(millis)) { + Logger.printDebug(() -> "Clearing undo range as current time is now outside range: " + undoAutoSkipRange); + undoAutoSkipRange = null; + } } catch (Exception e) { Logger.printException(() -> "setVideoTime failure", e); } @@ -470,14 +559,13 @@ public class SegmentPlaybackController { * Removes all previously hidden segments that are not longer contained in the given video time. */ private static void updateHiddenSegments(long currentVideoTime) { - Iterator i = hiddenSkipSegmentsForCurrentVideoTime.iterator(); - while (i.hasNext()) { - SponsorSegment hiddenSegment = i.next(); + hiddenSkipSegmentsForCurrentVideoTime.removeIf((hiddenSegment) -> { if (!hiddenSegment.containsTime(currentVideoTime)) { Logger.printDebug(() -> "Resetting hide skip button: " + hiddenSegment); - i.remove(); + return true; } - } + return false; + }); } private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) { @@ -488,8 +576,10 @@ public class SegmentPlaybackController { SponsorBlockViewController.hideSkipSegmentButton(); return; } + segmentCurrentlyPlaying = segment; skipSegmentButtonEndTime = 0; + if (Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) { if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) { // Playback exited a nested segment and the outer segment skip button was previously hidden. @@ -497,16 +587,13 @@ public class SegmentPlaybackController { SponsorBlockViewController.hideSkipSegmentButton(); return; } - skipSegmentButtonEndTime = System.currentTimeMillis() + DURATION_TO_SHOW_SKIP_BUTTON; + skipSegmentButtonEndTime = System.currentTimeMillis() + getSkipButtonDuration(); } Logger.printDebug(() -> "Showing segment: " + segment); SponsorBlockViewController.showSkipSegmentButton(segment); } - private static SponsorSegment lastSegmentSkipped; - private static long lastSegmentSkippedTime; - - private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) { + private static void skipSegment(SponsorSegment segmentToSkip, boolean userManuallySkipped) { try { SponsorBlockViewController.hideSkipHighlightButton(); SponsorBlockViewController.hideSkipSegmentButton(); @@ -525,7 +612,7 @@ public class SegmentPlaybackController { } } - Logger.printDebug(() -> "Skipping segment: " + segmentToSkip); + Logger.printDebug(() -> "Skipping segment: " + segmentToSkip + " videoState: " + VideoState.getCurrent()); lastSegmentSkipped = segmentToSkip; lastSegmentSkippedTime = now; setSegmentCurrentlyPlaying(null); @@ -535,29 +622,39 @@ public class SegmentPlaybackController { highlightSegmentInitialShowEndTime = 0; } + // Set or update undo skip range. + Range range = segmentToSkip.getUndoRange(); + if (undoAutoSkipRange == null) { + Logger.printDebug(() -> "Setting new undo range to: " + range); + undoAutoSkipRange = range; + } else { + Range extendedRange = undoAutoSkipRange.extend(range); + Logger.printDebug(() -> "Extending undo range from: " + undoAutoSkipRange + + " to: " + extendedRange); + undoAutoSkipRange = extendedRange; + } + undoAutoSkipRangeToast = undoAutoSkipRange; + // If the seek is successful, then the seek causes a recursive call back into this class. final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end); if (!seekSuccessful) { - // can happen when switching videos and is normal + // Can happen when switching videos and is normal. Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip); return; } - final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED; if (!userManuallySkipped) { - // check for any smaller embedded segments, and count those as autoskipped + // Check for any smaller embedded segments, and count those as auto-skipped. final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get(); - for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) { + for (SponsorSegment otherSegment : Objects.requireNonNull(segments)) { if (segmentToSkip.end < otherSegment.start) { - break; // no other segments can be contained + break; // No other segments can be contained. } + if (otherSegment == segmentToSkip || (otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) { otherSegment.didAutoSkipped = true; - // Do not show a toast if the user is scrubbing thru a paused video. - // Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date. - // So instead, only hide toasts because all other skip logic done while paused causes no harm. - if (showSkipToast && !videoIsPaused) { + if (showSkipToast) { showSkippedSegmentToast(otherSegment); } } @@ -567,7 +664,7 @@ public class SegmentPlaybackController { if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) { removeUnsubmittedSegments(); SponsorBlockUtils.setNewSponsorSegmentPreviewed(); - } else if (!videoIsPaused) { + } else if (VideoState.getCurrent() != VideoState.PAUSED) { SponsorBlockUtils.sendViewRequestAsync(segmentToSkip); } } catch (Exception ex) { @@ -575,29 +672,44 @@ public class SegmentPlaybackController { } } + /** + * Checks if the segment should be auto-skipped _and_ if undo autoskip is not active. + */ + private static boolean shouldAutoSkipAndUndoSkipNotActive(SponsorSegment segment, long currentVideoTime) { + return segment.shouldAutoSkip() && (undoAutoSkipRange == null + || !undoAutoSkipRange.contains(currentVideoTime)); + } - private static int toastNumberOfSegmentsSkipped; - @Nullable - private static SponsorSegment toastSegmentSkipped; - - private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) { + private static void showSkippedSegmentToast(SponsorSegment segment) { Utils.verifyOnMainThread(); - toastNumberOfSegmentsSkipped++; - if (toastNumberOfSegmentsSkipped > 1) { - return; // toast already scheduled - } toastSegmentSkipped = segment; + if (toastNumberOfSegmentsSkipped++ > 0) { + return; // Toast is already scheduled. + } - final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments + // Maximum time between skips to be considered skipping multiple segments. + final long delayToToastMilliseconds = 250; Utils.runOnMainThreadDelayed(() -> { try { - if (toastSegmentSkipped == null) { // video was changed just after skipping segment + // Do not show a toast if the user is scrubbing thru a paused video. + // Cannot do this video state check in setTime or before calling this this method, + // as the video state may not be up to date. So instead, only ignore the toast + // just before it's about to show since the video state is up to date. + if (VideoState.getCurrent() == VideoState.PAUSED) { + Logger.printDebug(() -> "Ignoring scheduled toast as video state is paused"); + return; + } + + if (toastSegmentSkipped == null || undoAutoSkipRangeToast == null) { + // Video was changed immediately after skipping segment. Logger.printDebug(() -> "Ignoring old scheduled show toast"); return; } - Utils.showToastShort(toastNumberOfSegmentsSkipped == 1 + String message = toastNumberOfSegmentsSkipped == 1 ? toastSegmentSkipped.getSkippedToastText() - : str("revanced_sb_skipped_multiple_segments")); + : str("revanced_sb_skipped_multiple_segments"); + + showToastShortWithTapAction(message, undoAutoSkipRangeToast); } catch (Exception ex) { Logger.printException(() -> "showSkippedSegmentToast failure", ex); } finally { @@ -607,13 +719,128 @@ public class SegmentPlaybackController { }, delayToToastMilliseconds); } + private static void showToastShortWithTapAction(String messageToToast, Range rangeToUndo) { + Objects.requireNonNull(messageToToast); + Utils.verifyOnMainThread(); + + Context currentContext = SponsorBlockViewController.getOverLaysViewGroupContext(); + if (currentContext == null) { + Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast); + return; + } + + Logger.printDebug(() -> "Showing toast: " + messageToToast); + + Dialog dialog = new Dialog(currentContext); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + // Do not dismiss dialog if tapped outside the dialog bounds. + dialog.setCanceledOnTouchOutside(false); + + LinearLayout mainLayout = new LinearLayout(currentContext); + mainLayout.setOrientation(LinearLayout.VERTICAL); + final int dip8 = dipToPixels(8); + final int dip16 = dipToPixels(16); + mainLayout.setPadding(dip16, dip8, dip16, dip8); + mainLayout.setGravity(Gravity.CENTER); + mainLayout.setMinimumHeight(dipToPixels(48)); + + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Utils.createCornerRadii(20), null, null)); + background.getPaint().setColor(Utils.getDialogBackgroundColor()); + mainLayout.setBackground(background); + + TextView textView = new TextView(currentContext); + textView.setText(messageToToast); + textView.setTextSize(14); + textView.setTextColor(Utils.getAppForegroundColor()); + textView.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + textParams.gravity = Gravity.CENTER; + textView.setLayoutParams(textParams); + mainLayout.addView(textView); + mainLayout.setAlpha(0.8f); // Opacity for the entire dialog. + + final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast"); + Animation fadeIn = Utils.getResourceAnimation("fade_in"); + Animation fadeOut = Utils.getResourceAnimation("fade_out"); + fadeIn.setDuration(fadeDurationFast); + fadeOut.setDuration(fadeDurationFast); + fadeOut.setAnimationListener(new Animation.AnimationListener() { + public void onAnimationStart(Animation animation) { } + public void onAnimationEnd(Animation animation) { + if (dialog.isShowing()) { + dialog.dismiss(); + } + } + public void onAnimationRepeat(Animation animation) { } + }); + + mainLayout.setOnClickListener(v -> { + try { + Logger.printDebug(() -> "Undoing autoskip using range: " + rangeToUndo); + // Restore undo autoskip range since it's already cleared by now. + undoAutoSkipRange = rangeToUndo; + VideoInformation.seekTo(rangeToUndo.getLower()); + + mainLayout.startAnimation(fadeOut); + } catch (Exception ex) { + Logger.printException(() -> "showToastShortWithTapAction setOnClickListener failure", ex); + dialog.dismiss(); + } + }); + mainLayout.setClickable(true); + dialog.setContentView(mainLayout); + + Window window = dialog.getWindow(); + if (window != null) { + // Remove window animations and use custom fade animation. + window.setWindowAnimations(0); + + WindowManager.LayoutParams params = window.getAttributes(); + params.gravity = Gravity.BOTTOM; + params.y = dipToPixels(72); + DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); + int portraitWidth = (int) (displayMetrics.widthPixels * 0.6); + + if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { + portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.6); + } + params.width = portraitWidth; + params.dimAmount = 0.0f; + window.setAttributes(params); + window.setBackgroundDrawable(null); + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); + window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); + } + + Dialog priorDialog = toastDialogRef.get(); + if (priorDialog != null && priorDialog.isShowing()) { + Logger.printDebug(() -> "Removing previous skip toast that is still on screen: " + priorDialog); + priorDialog.dismiss(); + } + toastDialogRef = new WeakReference<>(dialog); + + mainLayout.startAnimation(fadeIn); + dialog.show(); + + // Fade out and dismiss the dialog if the user does not undo the skip. + Utils.runOnMainThreadDelayed(() -> { + if (dialog.isShowing()) { + mainLayout.startAnimation(fadeOut); + } + }, getToastDuration()); + } + /** * @param segment can be either a highlight or a regular manual skip segment. */ - public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) { + public static void onSkipSegmentClicked(SponsorSegment segment) { try { if (segment != highlightSegment && segment != segmentCurrentlyPlaying) { - Logger.printException(() -> "error: segment not available to skip"); // should never happen + Logger.printException(() -> "error: segment not available to skip"); // Should never happen. SponsorBlockViewController.hideSkipSegmentButton(); SponsorBlockViewController.hideSkipHighlightButton(); return; @@ -628,7 +855,7 @@ public class SegmentPlaybackController { * Injection point */ @SuppressWarnings("unused") - public static void setSponsorBarRect(final Object self) { + public static void setSponsorBarRect(Object self) { try { Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect"); field.setAccessible(true); @@ -651,7 +878,7 @@ public class SegmentPlaybackController { private static void setSponsorBarAbsoluteRight(Rect rect) { final int right = rect.right; if (sponsorAbsoluteBarRight != right) { - Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right); + Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right); sponsorAbsoluteBarRight = right; } } @@ -726,12 +953,6 @@ public class SegmentPlaybackController { } } - /** - * Actual screen pixel width to use for the highlight segment time bar. - */ - private static final int highlightSegmentTimeBarScreenWidth - = Utils.dipToPixels(HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH); - /** * Injection point. */ @@ -752,9 +973,9 @@ public class SegmentPlaybackController { final float left = leftPadding + segment.start * videoMillisecondsToPixels; final float right; if (segment.category == SegmentCategory.HIGHLIGHT) { - right = left + highlightSegmentTimeBarScreenWidth; + right = left + HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH; } else { - right = leftPadding + segment.end * videoMillisecondsToPixels; + right = leftPadding + segment.end * videoMillisecondsToPixels; } canvas.drawRect(left, top, right, bottom, segment.category.paint); } @@ -762,5 +983,4 @@ public class SegmentPlaybackController { Logger.printException(() -> "drawSponsorTimeBars failure", ex); } } - } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java index 5edccbacd..59ee84544 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -223,13 +223,18 @@ public class SponsorBlockUtils { Logger.printException(() -> "invalid parameters"); return; } + clearUnsubmittedSegmentTimes(); Utils.runOnBackgroundThread(() -> { - SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength); - SegmentPlaybackController.executeDownloadSegments(videoId); + try { + SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength); + SegmentPlaybackController.executeDownloadSegments(videoId); + } catch (Exception ex) { + Logger.printException(() -> "submitNewSegment failure", ex); + } }); - } catch (Exception e) { - Logger.printException(() -> "Unable to submit segment", e); + } catch (Exception ex) { + Logger.printException(() -> "submitNewSegment failure", ex); } } @@ -366,7 +371,7 @@ public class SponsorBlockUtils { } - static void sendViewRequestAsync(@NonNull SponsorSegment segment) { + static void sendViewRequestAsync(SponsorSegment segment) { if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) { return; } @@ -409,7 +414,7 @@ public class SponsorBlockUtils { return statsNumberFormatter.format(viewCount); } - private static long parseSegmentTime(@NonNull String time) { + private static long parseSegmentTime(String time) { Matcher matcher = manualEditTimePattern.matcher(time); if (!matcher.matches()) { return -1; @@ -419,9 +424,12 @@ public class SponsorBlockUtils { String secondsStr = matcher.group(4); String millisecondsStr = matcher.group(6); // Milliseconds is optional. + try { final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0; + //noinspection ConstantConditions final int minutes = Integer.parseInt(minutesStr); + //noinspection ConstantConditions final int seconds = Integer.parseInt(secondsStr); final int milliseconds; if (millisecondsStr != null) { @@ -468,32 +476,29 @@ public class SponsorBlockUtils { } public static String getTimeSavedString(long totalSecondsSaved) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - Duration duration = Duration.ofSeconds(totalSecondsSaved); - final long hours = duration.toHours(); - final long minutes = duration.toMinutes() % 60; + Duration duration = Duration.ofSeconds(totalSecondsSaved); + final long hours = duration.toHours(); + final long minutes = duration.toMinutes() % 60; - // Format all numbers so non-western numbers use a consistent appearance. - String minutesFormatted = statsNumberFormatter.format(minutes); - if (hours > 0) { - String hoursFormatted = statsNumberFormatter.format(hours); - return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted); - } - - final long seconds = duration.getSeconds() % 60; - String secondsFormatted = statsNumberFormatter.format(seconds); - if (minutes > 0) { - return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted); - } - - return str("revanced_sb_stats_saved_second_format", secondsFormatted); + // Format all numbers so non-western numbers use a consistent appearance. + String minutesFormatted = statsNumberFormatter.format(minutes); + if (hours > 0) { + String hoursFormatted = statsNumberFormatter.format(hours); + return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted); } - return "error"; // will never be reached. YouTube requires Android O or greater + + final long seconds = duration.getSeconds() % 60; + String secondsFormatted = statsNumberFormatter.format(seconds); + if (minutes > 0) { + return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted); + } + + return str("revanced_sb_stats_saved_second_format", secondsFormatted); } private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener { - boolean settingStart; - WeakReference editTextRef = new WeakReference<>(null); + private boolean settingStart; + private WeakReference editTextRef = new WeakReference<>(null); @Override public void onClick(DialogInterface dialog, int which) { @@ -512,10 +517,11 @@ public class SponsorBlockUtils { } } - if (settingStart) + if (settingStart) { newSponsorSegmentStartMillis = Math.max(time, 0); - else + } else { newSponsorSegmentEndMillis = time; + } if (which == DialogInterface.BUTTON_NEUTRAL) editByHandDialogListener.onClick(dialog, settingStart ? diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java index 375de16b5..4528cfae9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java @@ -9,7 +9,10 @@ import java.util.Objects; import static app.revanced.extension.shared.StringRef.sf; +import android.util.Range; + public class SponsorSegment implements Comparable { + public enum SegmentVote { UPVOTE(sf("revanced_sb_vote_upvote"), 1,false), DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true), @@ -38,7 +41,7 @@ public class SponsorSegment implements Comparable { @NonNull public final SegmentCategory category; /** - * NULL if segment is unsubmitted + * NULL if segment is unsubmitted. */ @Nullable public final String UUID; @@ -64,33 +67,54 @@ public class SponsorSegment implements Comparable { } /** - * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number. */ 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 + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number. */ public boolean endIsNear(long videoTime, long nearThreshold) { return Math.abs(end - videoTime) <= nearThreshold; } /** - * @return if the time parameter is within this segment + * @return if the time parameter is within this segment. */ public boolean containsTime(long videoTime) { return start <= videoTime && videoTime < end; } /** - * @return if the segment is completely contained inside this segment + * @return if the segment is completely contained inside this segment. */ public boolean containsSegment(SponsorSegment other) { return start <= other.start && other.end <= end; } + /** + * @return If the range has any overlap with this segment. + */ + public boolean intersectsRange(Range range) { + return range.getLower() < end && range.getUpper() >= start; + } + + /** + * @return The start/end time in range form. + * Range times are adjusted since it uses inclusive and Segments use exclusive. + * + * {@link SegmentCategory#HIGHLIGHT} is unique and + * returns a range from the start of the video until the highlight. + */ + public Range getUndoRange() { + final long undoStart = category == SegmentCategory.HIGHLIGHT + ? 0 + : start; + return Range.create(undoStart, end - 1); + } + /** * @return the length of this segment, in milliseconds. Always a positive number. */ @@ -99,7 +123,7 @@ public class SponsorSegment implements Comparable { } /** - * @return 'skip segment' UI overlay button text + * @return 'skip segment' UI overlay button text. */ @NonNull public String getSkipButtonText() { @@ -107,7 +131,7 @@ public class SponsorSegment implements Comparable { } /** - * @return 'skipped segment' toast message + * @return 'skipped segment' toast message. */ @NonNull public String getSkippedToastText() { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java index fea7664f1..dc86e1d0d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java @@ -53,7 +53,7 @@ public class SBRequester { private SBRequester() { } - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) { Utils.showToastShort(toastMessage); } @@ -63,7 +63,7 @@ public class SBRequester { } @NonNull - public static SponsorSegment[] getSegments(@NonNull String videoId) { + public static SponsorSegment[] getSegments(String videoId) { Utils.verifyOffMainThread(); List segments = new ArrayList<>(); try { @@ -113,10 +113,10 @@ public class SBRequester { Logger.printException(() -> "getSegments failure", ex); } - // Crude debug tests to verify random features + // Crude debug tests to verify random features. // Could benefit from: - // 1) collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly) - // 2) unit tests (verify everything else) + // 1) Collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly). + // 2) Unit tests (verify everything else). //noinspection ConstantValue if (false) { segments.clear(); @@ -140,10 +140,30 @@ public class SBRequester { segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 200000, 330000, false)); } + // Test undo skip functionality. + // To test enable 'Autoskip always' for intro and self promo. + //noinspection ConstantValue + if (false) { + // Should autoskip to 12 seconds. + // Undoing skip should seek to 2 seconds. + // Skip button should show at 2 seconds, and again at 8 seconds. + // Self promo at 8 second time should not autoskip. + segments.clear(); + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 2000, 12000, false)); + segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 8000, 15000, false)); + + // Test multiple autoskip dialogs rapidly showing. + // Only one toast should be shown at anytime. + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 16000, 17000, false)); + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 18000, 19000, false)); + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 20000, 21000, false)); + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 22000, 23000, false)); + } + return segments.toArray(new SponsorSegment[0]); } - public static void submitSegments(@NonNull String videoId, @NonNull String category, + public static void submitSegments(String videoId, String category, long startTime, long endTime, long videoLength) { Utils.verifyOffMainThread(); @@ -189,7 +209,7 @@ public class SBRequester { } } - public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) { + public static void sendSegmentSkippedViewedRequest(SponsorSegment segment) { Utils.verifyOffMainThread(); try { HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID); @@ -208,13 +228,13 @@ public class SBRequester { } } - public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) { + public static void voteForSegmentOnBackgroundThread(SponsorSegment segment, SegmentVote voteOption) { voteOrRequestCategoryChange(segment, voteOption, null); } - public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) { + public static void voteToChangeCategoryOnBackgroundThread(SponsorSegment segment, SegmentCategory categoryToVoteFor) { voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor); } - private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) { + private static void voteOrRequestCategoryChange(SponsorSegment segment, SegmentVote voteOption, SegmentCategory categoryToVoteFor) { Utils.runOnBackgroundThread(() -> { try { String segmentUuid = segment.UUID; @@ -280,7 +300,7 @@ public class SBRequester { * @return NULL if the call was successful. If unsuccessful, an error message is returned. */ @Nullable - public static String setUsername(@NonNull String username) { + public static String setUsername(String username) { Utils.verifyOffMainThread(); try { HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username); @@ -320,14 +340,14 @@ public class SBRequester { // helpers - private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException { + private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException { HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params); connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS); connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS); return connection; } - private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException { + private static JSONObject getJSONObject(Route route, String... params) throws IOException, JSONException { return Requester.parseJSONObject(getConnectionFromRoute(route, params)); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java index a5d2703dc..e0149f2f6 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java @@ -1,6 +1,7 @@ package app.revanced.extension.youtube.sponsorblock.ui; import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration; import android.annotation.SuppressLint; import android.app.Dialog; @@ -28,6 +29,7 @@ import java.util.List; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.preference.CustomDialogListPreference; import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; @@ -62,6 +64,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { private SwitchPreference trackSkips; private SwitchPreference showTimeWithoutSegments; private SwitchPreference toastOnConnectionError; + private CustomDialogListPreference autoHideSkipSegmentButtonDuration; + private CustomDialogListPreference showSkipToastDuration; private ResettableEditTextPreference newSegmentStep; private ResettableEditTextPreference minSegmentDuration; @@ -69,8 +73,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { private EditTextPreference importExport; private Preference apiUrl; - private final List segmentCategories = new ArrayList<>(); private PreferenceCategory segmentCategory; + private final List segmentCategories = new ArrayList<>(); public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @@ -114,17 +118,23 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get()); votingEnabled.setEnabled(enabled); - autoHideSkipSegmentButton.setEnabled(enabled); autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()); + autoHideSkipSegmentButton.setEnabled(enabled); + + autoHideSkipSegmentButtonDuration.setValue(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().toString()); + autoHideSkipSegmentButtonDuration.setEnabled(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.isAvailable()); compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get()); compactSkipButton.setEnabled(enabled); + showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get()); + showSkipToast.setEnabled(enabled); + squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get()); squareLayout.setEnabled(enabled); - showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get()); - showSkipToast.setEnabled(enabled); + showSkipToastDuration.setValue(Settings.SB_TOAST_ON_SKIP_DURATION.get().toString()); + showSkipToastDuration.setEnabled(Settings.SB_TOAST_ON_SKIP_DURATION.isAvailable()); toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get()); toastOnConnectionError.setEnabled(enabled); @@ -166,7 +176,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { try { super.onAttachedToActivity(); - if (preferencesInitialized) { + if (preferencesInitialized) { if (settingsImported) { settingsImported = false; updateUI(); @@ -205,17 +215,6 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { }); appearanceCategory.addPreference(votingEnabled); - autoHideSkipSegmentButton = new SwitchPreference(context); - autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button")); - autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on")); - autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off")); - autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue); - updateUI(); - return true; - }); - appearanceCategory.addPreference(autoHideSkipSegmentButton); - compactSkipButton = new SwitchPreference(context); compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button")); compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on")); @@ -227,25 +226,38 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { }); appearanceCategory.addPreference(compactSkipButton); - squareLayout = new SwitchPreference(context); - squareLayout.setTitle(str("revanced_sb_square_layout")); - squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on")); - squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off")); - squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue); + autoHideSkipSegmentButton = new SwitchPreference(context); + autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button")); + autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on")); + autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off")); + autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue); updateUI(); return true; }); - appearanceCategory.addPreference(squareLayout); + appearanceCategory.addPreference(autoHideSkipSegmentButton); + + String[] durationEntries = Utils.getResourceStringArray("revanced_sb_duration_entries"); + String[] durationEntryValues = Utils.getResourceStringArray("revanced_sb_duration_entry_values"); + + autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context); + autoHideSkipSegmentButtonDuration.setTitle(str("revanced_sb_auto_hide_skip_button_duration")); + autoHideSkipSegmentButtonDuration.setSummary(str("revanced_sb_auto_hide_skip_button_duration_sum")); + autoHideSkipSegmentButtonDuration.setEntries(durationEntries); + autoHideSkipSegmentButtonDuration.setEntryValues(durationEntryValues); + autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save( + SponsorBlockDuration.valueOf((String) newValue) + ); + updateUI(); + return true; + }); + appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration); showSkipToast = new SwitchPreference(context); showSkipToast.setTitle(str("revanced_sb_general_skiptoast")); showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on")); showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off")); - showSkipToast.setOnPreferenceClickListener(preference1 -> { - Utils.showToastShort(str("revanced_sb_skipped_sponsor")); - return false; - }); showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> { Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue); updateUI(); @@ -253,6 +265,20 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { }); appearanceCategory.addPreference(showSkipToast); + showSkipToastDuration = new CustomDialogListPreference(context); + showSkipToastDuration.setTitle(str("revanced_sb_toast_on_skip_duration")); + showSkipToastDuration.setSummary(str("revanced_sb_toast_on_skip_duration_sum")); + showSkipToastDuration.setEntries(durationEntries); + showSkipToastDuration.setEntryValues(durationEntryValues); + showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TOAST_ON_SKIP_DURATION.save( + SponsorBlockDuration.valueOf((String) newValue) + ); + updateUI(); + return true; + }); + appearanceCategory.addPreference(showSkipToastDuration); + showTimeWithoutSegments = new SwitchPreference(context); showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without")); showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on")); @@ -264,6 +290,17 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { }); appearanceCategory.addPreference(showTimeWithoutSegments); + squareLayout = new SwitchPreference(context); + squareLayout.setTitle(str("revanced_sb_square_layout")); + squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on")); + squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off")); + squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue); + updateUI(); + return true; + }); + appearanceCategory.addPreference(squareLayout); + segmentCategory = new PreferenceCategory(context); segmentCategory.setTitle(str("revanced_sb_diff_segments")); addPreference(segmentCategory); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java index a16ffdd32..4749c6930 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -203,7 +203,7 @@ public class SponsorBlockViewController { setSkipButtonMargins(skipSponsorButton, isWatchFullScreen); setViewVisibility(skipSponsorButton, skipSegment != null); } catch (Exception ex) { - Logger.printException(() -> "Player type changed failure", ex); + Logger.printException(() -> "playerTypeChanged failure", ex); } } diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 6e5c17dea..41a90c129 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -61,6 +61,7 @@ @string/revanced_language_ZH + DEFAULT AM AR @@ -129,7 +130,6 @@ iOS TV - ANDROID_UNPLUGGED ANDROID_VR_NO_AUTH IOS_UNPLUGGED @@ -146,7 +146,6 @@ @string/revanced_swipe_overlay_style_entry_7 - HORIZONTAL HORIZONTAL_MINIMAL_TOP HORIZONTAL_MINIMAL_CENTER @@ -180,7 +179,6 @@ @string/revanced_change_form_factor_entry_4 - DEFAULT SMALL LARGE @@ -210,7 +208,6 @@ @string/revanced_exit_fullscreen_entry_4 - DISABLED PORTRAIT LANDSCAPE @@ -229,7 +226,6 @@ @string/revanced_miniplayer_type_entry_7 - DISABLED DEFAULT MINIMAL @@ -330,13 +326,38 @@ YOUR_CLIPS + + + @string/revanced_sb_duration_1s + @string/revanced_sb_duration_2s + @string/revanced_sb_duration_3s + @string/revanced_sb_duration_4s + @string/revanced_sb_duration_5s + @string/revanced_sb_duration_6s + @string/revanced_sb_duration_7s + @string/revanced_sb_duration_8s + @string/revanced_sb_duration_9s + @string/revanced_sb_duration_10s + + + ONE_SECOND + TWO_SECONDS + THREE_SECONDS + FOUR_SECONDS + FIVE_SECONDS + SIX_SECONDS + SEVEN_SECONDS + EIGHT_SECONDS + NINE_SECONDS + TEN_SECONDS + + @string/revanced_shorts_player_type_shorts @string/revanced_shorts_player_type_regular_player - SHORTS_PLAYER REGULAR_PLAYER @@ -346,7 +367,6 @@ @string/revanced_shorts_player_type_regular_player_fullscreen - SHORTS_PLAYER REGULAR_PLAYER REGULAR_PLAYER_FULLSCREEN @@ -360,7 +380,6 @@ @string/revanced_alt_thumbnail_options_entry_4 - ORIGINAL DEARROW DEARROW_STILL_IMAGES diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index f72851cd4..bec6c8537 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1027,11 +1027,25 @@ This feature works best with a video quality of 720p or lower and when using a v Automatically hide Skip button Skip button hides after a few seconds Skip button is shown for the entire segment - Show a toast when skipping - Toast is shown when a segment is automatically skipped. Tap here to see an example - Toast is not shown. Tap here to see an example + Skip button duration + How long the auto hide skip and skip to highlight buttons are shown + Show undo skip toast + Toast is shown when a segment is automatically skipped. Tap the toast notification to undo the skip + Toast is not shown + Skip toast duration + How long the skip toast notification is shown + 1 second + 2 seconds + 3 seconds + 4 seconds + 5 seconds + 6 seconds + 7 seconds + 8 seconds + 9 seconds + 10 seconds Show video length without segments - Video length minus all segments, shown in parentheses next to the full video length + Video length minus all segments is shown on the seekbar Full video length shown Creating new segments Show Create new segment button