feat(YouTube - SponsorBlock): Add "Undo automatic skip toast" (#5277)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-06-30 09:50:52 +03:00
committed by GitHub
parent 21688201af
commit 6ee94f8532
12 changed files with 591 additions and 243 deletions

View File

@@ -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<T> {
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);

View File

@@ -71,15 +71,20 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
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<T extends Enum<?>> extends Setting<T> {
* 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) {

View File

@@ -28,16 +28,14 @@ public abstract class Setting<T> {
/**
* 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<T> {
/**
* 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<T> {
/**
* 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<T> {
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<Setting<?>> allLoadedSettings() {
return Collections.unmodifiableList(SETTINGS);
}
@@ -115,7 +111,6 @@ public abstract class Setting<T> {
/**
* @return All settings that have been created, sorted by keys.
*/
@NonNull
private static List<Setting<?>> allLoadedSettingsSorted() {
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
return allLoadedSettings();
@@ -124,13 +119,11 @@ public abstract class Setting<T> {
/**
* 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<T> {
/**
* The value of the setting.
*/
@NonNull
protected volatile T value;
public Setting(String key, T defaultValue) {
@@ -199,8 +191,8 @@ public abstract class Setting<T> {
* @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<T> {
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
if (oldSetting == newSetting) throw new IllegalArgumentException();
if (!oldSetting.isSetToDefault()) {
@@ -243,7 +235,7 @@ public abstract class Setting<T> {
* 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<T> {
* 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<T> {
/**
* 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<T> {
/**
* 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<T> {
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<T> {
/**
* @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

View File

@@ -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<SponsorBlockDuration> 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<SponsorBlockDuration> 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));

View File

@@ -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<SponsorSegment> 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<Long> 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<Long> 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<Dialog> 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<SponsorSegment> 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<SponsorSegment> 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<Long> range = segmentToSkip.getUndoRange();
if (undoAutoSkipRange == null) {
Logger.printDebug(() -> "Setting new undo range to: " + range);
undoAutoSkipRange = range;
} else {
Range<Long> 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<Long> 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);
}
}
}

View File

@@ -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<EditText> editTextRef = new WeakReference<>(null);
private boolean settingStart;
private WeakReference<EditText> 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 ?

View File

@@ -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<SponsorSegment> {
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<SponsorSegment> {
@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<SponsorSegment> {
}
/**
* @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<Long> 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<Long> 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<SponsorSegment> {
}
/**
* @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<SponsorSegment> {
}
/**
* @return 'skipped segment' toast message
* @return 'skipped segment' toast message.
*/
@NonNull
public String getSkippedToastText() {

View File

@@ -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<SponsorSegment> 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));
}
}

View File

@@ -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<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
private PreferenceCategory segmentCategory;
private final List<SegmentCategoryListPreference> 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);

View File

@@ -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);
}
}

View File

@@ -61,6 +61,7 @@
<item>@string/revanced_language_ZH</item>
</string-array>
<string-array name="revanced_language_entry_values">
<!-- Extension enum names. -->
<item>DEFAULT</item>
<item>AM</item>
<item>AR</item>
@@ -129,7 +130,6 @@
<item>iOS TV</item>
</string-array>
<string-array name="revanced_spoof_video_streams_client_type_entry_values">
<!-- Extension enum names. -->
<item>ANDROID_UNPLUGGED</item>
<item>ANDROID_VR_NO_AUTH</item>
<item>IOS_UNPLUGGED</item>
@@ -146,7 +146,6 @@
<item>@string/revanced_swipe_overlay_style_entry_7</item>
</string-array>
<string-array name="revanced_swipe_overlay_style_entry_values">
<!-- Extension enum names. -->
<item>HORIZONTAL</item>
<item>HORIZONTAL_MINIMAL_TOP</item>
<item>HORIZONTAL_MINIMAL_CENTER</item>
@@ -180,7 +179,6 @@
<item>@string/revanced_change_form_factor_entry_4</item>
</string-array>
<string-array name="revanced_change_form_factor_entry_values">
<!-- Extension enum names. -->
<item>DEFAULT</item>
<item>SMALL</item>
<item>LARGE</item>
@@ -210,7 +208,6 @@
<item>@string/revanced_exit_fullscreen_entry_4</item>
</string-array>
<string-array name="revanced_exit_fullscreen_entry_values">
<!-- Enum names from the extension. -->
<item>DISABLED</item>
<item>PORTRAIT</item>
<item>LANDSCAPE</item>
@@ -229,7 +226,6 @@
<item>@string/revanced_miniplayer_type_entry_7</item>
</string-array>
<string-array name="revanced_miniplayer_type_entry_values">
<!-- Extension enum names. -->
<item>DISABLED</item>
<item>DEFAULT</item>
<item>MINIMAL</item>
@@ -330,13 +326,38 @@
<item>YOUR_CLIPS</item>
</string-array>
</patch>
<patch id="layout.sponsorblock.sponsorBlockResourcePatch">
<string-array name="revanced_sb_duration_entries">
<item>@string/revanced_sb_duration_1s</item>
<item>@string/revanced_sb_duration_2s</item>
<item>@string/revanced_sb_duration_3s</item>
<item>@string/revanced_sb_duration_4s</item>
<item>@string/revanced_sb_duration_5s</item>
<item>@string/revanced_sb_duration_6s</item>
<item>@string/revanced_sb_duration_7s</item>
<item>@string/revanced_sb_duration_8s</item>
<item>@string/revanced_sb_duration_9s</item>
<item>@string/revanced_sb_duration_10s</item>
</string-array>
<string-array name="revanced_sb_duration_entry_values">
<item>ONE_SECOND</item>
<item>TWO_SECONDS</item>
<item>THREE_SECONDS</item>
<item>FOUR_SECONDS</item>
<item>FIVE_SECONDS</item>
<item>SIX_SECONDS</item>
<item>SEVEN_SECONDS</item>
<item>EIGHT_SECONDS</item>
<item>NINE_SECONDS</item>
<item>TEN_SECONDS</item>
</string-array>
</patch>
<patch id="layout.shortsplayer.shortsPlayerTypePatch">
<string-array name="revanced_shorts_player_type_legacy_entries">
<item>@string/revanced_shorts_player_type_shorts</item>
<item>@string/revanced_shorts_player_type_regular_player</item>
</string-array>
<string-array name="revanced_shorts_player_type_legacy_entry_values">
<!-- Extension enum names. -->
<item>SHORTS_PLAYER</item>
<item>REGULAR_PLAYER</item>
</string-array>
@@ -346,7 +367,6 @@
<item>@string/revanced_shorts_player_type_regular_player_fullscreen</item>
</string-array>
<string-array name="revanced_shorts_player_type_entry_values">
<!-- Enum names from extension -->
<item>SHORTS_PLAYER</item>
<item>REGULAR_PLAYER</item>
<item>REGULAR_PLAYER_FULLSCREEN</item>
@@ -360,7 +380,6 @@
<item>@string/revanced_alt_thumbnail_options_entry_4</item>
</string-array>
<string-array name="revanced_alt_thumbnail_options_entry_values">
<!-- Extension enum names. -->
<item>ORIGINAL</item>
<item>DEARROW</item>
<item>DEARROW_STILL_IMAGES</item>

View File

@@ -1027,11 +1027,25 @@ This feature works best with a video quality of 720p or lower and when using a v
<string name="revanced_sb_enable_auto_hide_skip_segment_button">Automatically hide Skip button</string>
<string name="revanced_sb_enable_auto_hide_skip_segment_button_sum_on">Skip button hides after a few seconds</string>
<string name="revanced_sb_enable_auto_hide_skip_segment_button_sum_off">Skip button is shown for the entire segment</string>
<string name="revanced_sb_general_skiptoast">Show a toast when skipping</string>
<string name="revanced_sb_general_skiptoast_sum_on">Toast is shown when a segment is automatically skipped. Tap here to see an example</string>
<string name="revanced_sb_general_skiptoast_sum_off">Toast is not shown. Tap here to see an example</string>
<string name="revanced_sb_auto_hide_skip_button_duration">Skip button duration</string>
<string name="revanced_sb_auto_hide_skip_button_duration_sum">How long the auto hide skip and skip to highlight buttons are shown</string>
<string name="revanced_sb_general_skiptoast">Show undo skip toast</string>
<string name="revanced_sb_general_skiptoast_sum_on">Toast is shown when a segment is automatically skipped. Tap the toast notification to undo the skip</string>
<string name="revanced_sb_general_skiptoast_sum_off">Toast is not shown</string>
<string name="revanced_sb_toast_on_skip_duration">Skip toast duration</string>
<string name="revanced_sb_toast_on_skip_duration_sum">How long the skip toast notification is shown</string>
<string name="revanced_sb_duration_1s">1 second</string>
<string name="revanced_sb_duration_2s">2 seconds</string>
<string name="revanced_sb_duration_3s">3 seconds</string>
<string name="revanced_sb_duration_4s">4 seconds</string>
<string name="revanced_sb_duration_5s">5 seconds</string>
<string name="revanced_sb_duration_6s">6 seconds</string>
<string name="revanced_sb_duration_7s">7 seconds</string>
<string name="revanced_sb_duration_8s">8 seconds</string>
<string name="revanced_sb_duration_9s">9 seconds</string>
<string name="revanced_sb_duration_10s">10 seconds</string>
<string name="revanced_sb_general_time_without">Show video length without segments</string>
<string name="revanced_sb_general_time_without_sum_on">Video length minus all segments, shown in parentheses next to the full video length</string>
<string name="revanced_sb_general_time_without_sum_on">Video length minus all segments is shown on the seekbar</string>
<string name="revanced_sb_general_time_without_sum_off">Full video length shown</string>
<string name="revanced_sb_create_segment_category">Creating new segments</string>
<string name="revanced_sb_enable_create_segment">Show Create new segment button</string>