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 8343d595c8..3cb809a2de 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 @@ -371,7 +371,7 @@ public static void setContext(Context appContext) { if (language != AppLanguage.DEFAULT) { // Create a new context with the desired language. Logger.printDebug(() -> "Using app language: " + language); - Configuration config = appContext.getResources().getConfiguration(); + Configuration config = new Configuration(appContext.getResources().getConfiguration()); config.setLocale(language.getLocale()); context = appContext.createConfigurationContext(config); } @@ -391,16 +391,47 @@ public static boolean isTablet() { private static Boolean isRightToLeftTextLayout; /** - * If the device language uses right to left text layout (hebrew, arabic, etc) + * @return If the device language uses right to left text layout (Hebrew, Arabic, etc). + * If this should match any ReVanced language override then instead use + * {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}. + * This is the default locale of the device, which may differ if + * {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language. */ - public static boolean isRightToLeftTextLayout() { + public static boolean isRightToLeftLocale() { if (isRightToLeftTextLayout == null) { - String displayLanguage = Locale.getDefault().getDisplayLanguage(); - isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft(); + isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault()); } return isRightToLeftTextLayout; } + /** + * @return If the locale uses right to left text layout (Hebrew, Arabic, etc). + */ + public static boolean isRightToLeftLocale(Locale locale) { + String displayLanguage = locale.getDisplayLanguage(); + return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft(); + } + + /** + * @return A UTF8 string containing a left-to-right or right-to-left + * character of the device locale. If this should match any ReVanced language + * override then instead use {@link #getTextDirectionString(Locale)} with + * {@link BaseSettings#REVANCED_LANGUAGE}. + */ + public static String getTextDirectionString() { + return getTextDirectionString(isRightToLeftLocale()); + } + + public static String getTextDirectionString(Locale locale) { + return getTextDirectionString(isRightToLeftLocale(locale)); + } + + private static String getTextDirectionString(boolean isRightToLeft) { + return isRightToLeft + ? "\u200F" // u200F = right to left character. + : "\u200E"; // u200E = left to right character. + } + /** * @return if the text contains at least 1 number character, * including any unicode numbers such as Arabic. @@ -692,9 +723,10 @@ static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) { /** * Strips all punctuation and converts to lower case. A null parameter returns an empty string. */ - public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { + public static String removePunctuationToLowercase(@Nullable CharSequence original) { if (original == null) return ""; - return punctuationPattern.matcher(original).replaceAll("").toLowerCase(); + return punctuationPattern.matcher(original).replaceAll("") + .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale()); } /** @@ -726,7 +758,7 @@ public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { final String sortValue; switch (preferenceSort) { case BY_TITLE: - sortValue = removePunctuationConvertToLowercase(preference.getTitle()); + sortValue = removePunctuationToLowercase(preference.getTitle()); break; case BY_KEY: sortValue = preference.getKey(); @@ -810,4 +842,12 @@ public static int getColorFromString(String colorString) throws IllegalArgumentE } return getResourceColor(colorString); } + + public static int clamp(int value, int lower, int upper) { + return Math.max(lower, Math.min(value, upper)); + } + + public static float clamp(float value, float lower, float upper) { + return Math.max(lower, Math.min(value, upper)); + } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java index 877f4ba227..944529f4ed 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java @@ -89,9 +89,11 @@ public enum AppLanguage { ZU; private final String language; + private final Locale locale; AppLanguage() { language = name().toLowerCase(Locale.US); + locale = Locale.forLanguageTag(language); } /** @@ -112,6 +114,6 @@ public Locale getLocale() { return Locale.getDefault(); } - return Locale.forLanguageTag(language); + return locale; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java index 73255b028e..d6b895f22a 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java @@ -21,6 +21,10 @@ public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defSty super(context, attrs, defStyleAttr); } + public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public NoTitlePreferenceCategory(Context context) { super(context); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java new file mode 100644 index 0000000000..c5d166a7f0 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java @@ -0,0 +1,107 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import app.revanced.extension.shared.Utils; + +/** + * PreferenceList that sorts itself. + * By default the first entry is preserved in its original position, + * and all other entries are sorted alphabetically. + * + * Ideally the 'keep first entries to preserve' is an xml parameter, + * but currently that's not so simple since Extensions code cannot use + * generated code from the Patches repo (which is required for custom xml parameters). + * + * If any class wants to use a different getFirstEntriesToPreserve value, + * it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class SortedListPreference extends ListPreference { + + /** + * Sorts the current list entries. + * + * @param firstEntriesToPreserve The number of entries to preserve in their original position. + */ + public void sortEntryAndValues(int firstEntriesToPreserve) { + CharSequence[] entries = getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (entries == null || entryValues == null) { + return; + } + + final int entrySize = entries.length; + if (entrySize != entryValues.length) { + // Xml array declaration has a missing/extra entry. + throw new IllegalStateException(); + } + + List> firstEntries = new ArrayList<>(firstEntriesToPreserve); + SortedMap> lastEntries = new TreeMap<>(); + + for (int i = 0; i < entrySize; i++) { + Pair pair = new Pair<>(entries[i], entryValues[i]); + if (i < firstEntriesToPreserve) { + firstEntries.add(pair); + } else { + lastEntries.put(Utils.removePunctuationToLowercase(pair.first), pair); + } + } + + CharSequence[] sortedEntries = new CharSequence[entrySize]; + CharSequence[] sortedEntryValues = new CharSequence[entrySize]; + + int i = 0; + for (Pair pair : firstEntries) { + sortedEntries[i] = pair.first; + sortedEntryValues[i] = pair.second; + i++; + } + + for (Pair pair : lastEntries.values()) { + sortedEntries[i] = pair.first; + sortedEntryValues[i] = pair.second; + i++; + } + + super.setEntries(sortedEntries); + super.setEntryValues(sortedEntryValues); + } + + protected int getFirstEntriesToPreserve() { + return 1; + } + + public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context) { + super(context); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java index f541b23476..0177c9cbcf 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java @@ -1,10 +1,18 @@ package app.revanced.extension.youtube; +import static app.revanced.extension.shared.Utils.clamp; + import android.app.Activity; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; import android.os.Build; +import android.text.style.ReplacementSpan; +import android.text.TextPaint; import android.view.Window; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import app.revanced.extension.shared.Logger; @@ -121,4 +129,43 @@ public static void setNavigationBarColor(@Nullable Window window) { window.setNavigationBarContrastEnforced(true); } } + + /** + * Adjusts the brightness of a color by lightening or darkening it based on the given factor. + *

+ * If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF). + * If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000). + * The alpha channel remains unchanged. + * + * @param color The input color to adjust, in ARGB format. + * @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening) + * or values <= 1.0f to darken (e.g., 0.95f for slight darkening). + * @return The adjusted color in ARGB format. + */ + public static int adjustColorBrightness(int color, float factor) { + final int alpha = Color.alpha(color); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + + if (factor > 1.0f) { + // Lighten: Interpolate toward white (255) + final float t = 1.0f - (1.0f / factor); // Interpolation parameter + red = Math.round(red + (255 - red) * t); + green = Math.round(green + (255 - green) * t); + blue = Math.round(blue + (255 - blue) * t); + } else { + // Darken or no change: Scale toward black + red = (int) (red * factor); + green = (int) (green * factor); + blue = (int) (blue * factor); + } + + // Ensure values are within [0, 255] + red = clamp(red, 0, 255); + green = clamp(green, 0, 255); + blue = clamp(blue, 0, 255); + + return Color.argb(alpha, red, green, blue); + } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java index c5ba1c0333..2db744ae6f 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -18,7 +18,6 @@ import app.revanced.extension.shared.Utils; import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; -import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.PlayerType; @@ -69,13 +68,6 @@ public class ReturnYouTubeDislikePatch { @Nullable private static volatile String lastPrefetchedVideoId; - public static void onRYDStatusChange(boolean rydEnabled) { - ReturnYouTubeDislikeApi.resetRateLimits(); - // Must remove all values to protect against using stale data - // if the user enables RYD while a video is on screen. - clearData(); - } - private static void clearData() { currentVideoData = null; lastLithoShortsVideoData = null; @@ -274,7 +266,7 @@ private static void addRollingNumberPatchChanges(TextView view) { Logger.printDebug(() -> "Adding rolling number TextView changes"); view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels); ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable(); - if (Utils.isRightToLeftTextLayout()) { + if (Utils.isRightToLeftLocale()) { view.setCompoundDrawables(null, null, separator, null); } else { view.setCompoundDrawables(separator, null, null, null); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java index aeff4ac26e..90f18a6c28 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java @@ -36,7 +36,7 @@ public static void setActionBar(View view) { final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, Resources.getSystem().getDisplayMetrics()); - if (Utils.isRightToLeftTextLayout()) { + if (Utils.isRightToLeftLocale()) { searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom); } else { searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index 8cde513bdf..9b62241064 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -1,16 +1,12 @@ package app.revanced.extension.youtube.patches.playback.speed; -import static app.revanced.extension.shared.StringRef.sf; import static app.revanced.extension.shared.StringRef.str; -import android.preference.ListPreference; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; -import androidx.annotation.NonNull; - import java.util.Arrays; import app.revanced.extension.shared.Logger; @@ -21,8 +17,6 @@ @SuppressWarnings("unused") public class CustomPlaybackSpeedPatch { - private static final float PLAYBACK_SPEED_AUTO = Settings.PLAYBACK_SPEED_DEFAULT.defaultValue; - /** * Maximum playback speed, exclusive value. Custom speeds must be less than this value. *

@@ -47,11 +41,6 @@ public class CustomPlaybackSpeedPatch { */ private static long lastTimeOldPlaybackMenuInvoked; - /** - * PreferenceList entries and values, of all available playback speeds. - */ - private static String[] preferenceListEntries, preferenceListEntryValues; - static { final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); @@ -117,33 +106,6 @@ private static boolean arrayContains(float[] array, float value) { return false; } - /** - * Initialize a settings preference list with the available playback speeds. - */ - @SuppressWarnings("deprecation") - public static void initializeListPreference(ListPreference preference) { - if (preferenceListEntries == null) { - final int numberOfEntries = customPlaybackSpeeds.length + 1; - preferenceListEntries = new String[numberOfEntries]; - preferenceListEntryValues = new String[numberOfEntries]; - - // Auto speed (same behavior as unpatched). - preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString(); - preferenceListEntryValues[0] = String.valueOf(PLAYBACK_SPEED_AUTO); - - int i = 1; - for (float speed : customPlaybackSpeeds) { - String speedString = String.valueOf(speed); - preferenceListEntries[i] = speedString + "x"; - preferenceListEntryValues[i] = speedString; - i++; - } - } - - preference.setEntries(preferenceListEntries); - preference.setEntryValues(preferenceListEntryValues); - } - /** * Injection point. */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java index 2283106bf9..4d036509ef 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -1,6 +1,7 @@ package app.revanced.extension.youtube.patches.theme; import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.clamp; import android.content.res.Resources; import android.graphics.Color; @@ -378,14 +379,4 @@ private static int getSeekbarColorValue(int originalColor) { return originalColor; } } - - /** @noinspection SameParameterValue */ - private static int clamp(int value, int lower, int upper) { - return Math.max(lower, Math.min(value, upper)); - } - - /** @noinspection SameParameterValue */ - private static float clamp(float value, float lower, float upper) { - return Math.max(lower, Math.min(value, upper)); - } } \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index a258dffd25..a0730c055e 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -235,7 +235,7 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get(); if (!compactLayout) { - String leftSeparatorString = getTextDirectionString(); + String leftSeparatorString = Utils.getTextDirectionString(); final Spannable leftSeparatorSpan; if (isRollingNumber) { leftSeparatorSpan = new SpannableString(leftSeparatorString); @@ -279,12 +279,6 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, return new SpannableString(builder); } - private static @NonNull String getTextDirectionString() { - return Utils.isRightToLeftTextLayout() - ? "\u200F" // u200F = right to left character - : "\u200E"; // u200E = left to right character - } - /** * @return If the text is likely for a previously created likes/dislikes segmented span. */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java new file mode 100644 index 0000000000..c37b472d38 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java @@ -0,0 +1,29 @@ +package app.revanced.extension.youtube.returnyoutubedislike.ui; + +import android.content.Context; +import android.util.AttributeSet; + +import app.revanced.extension.youtube.settings.preference.UrlLinkPreference; + +/** + * Allows tapping the RYD about preference to open the website. + */ +@SuppressWarnings("unused") +public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference { + { + externalUrl = "https://returnyoutubedislike.com"; + } + + public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReturnYouTubeDislikeAboutPreference(Context context) { + super(context); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java new file mode 100644 index 0000000000..dac275e4fc --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeDebugStatsPreferenceCategory.java @@ -0,0 +1,126 @@ +package app.revanced.extension.youtube.returnyoutubedislike.ui; + +import static app.revanced.extension.shared.StringRef.str; + +import android.content.Context; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; + +@SuppressWarnings({"unused", "deprecation"}) +public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory { + + private static final boolean SHOW_RYD_DEBUG_STATS = BaseSettings.DEBUG.get(); + + private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) { + if (value == 0) { + return str(summaryStringZeroKey); + } + return str(summaryStringOneOrMoreKey, value); + } + + private static String createMillisecondStringFromNumber(long number) { + return String.format(str("revanced_ryd_statistics_millisecond_text"), number); + } + + public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected View onCreateView(ViewGroup parent) { + if (!SHOW_RYD_DEBUG_STATS) { + // Use an empty view to hide without removing. + return new View(getContext()); + } + + return super.onCreateView(parent); + } + + protected void onAttachedToActivity() { + try { + super.onAttachedToActivity(); + if (!SHOW_RYD_DEBUG_STATS) { + return; + } + + Logger.printDebug(() -> "Updating stats preferences"); + removeAll(); + + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallResponseTimeAverage_title", + createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()) + ); + + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallResponseTimeMin_title", + createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()) + ); + + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallResponseTimeMax_title", + createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()) + ); + + String fetchCallTimeWaitingLastSummary; + final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast(); + if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { + fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary"); + } else { + fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast); + } + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallResponseTimeLast_title", + fetchCallTimeWaitingLastSummary + ); + + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallCount_title", + createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(), + "revanced_ryd_statistics_getFetchCallCount_zero_summary", + "revanced_ryd_statistics_getFetchCallCount_non_zero_summary" + ) + ); + + addStatisticPreference( + "revanced_ryd_statistics_getFetchCallNumberOfFailures_title", + createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(), + "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary", + "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary" + ) + ); + + addStatisticPreference( + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title", + createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(), + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary", + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary" + ) + ); + } catch (Exception ex) { + Logger.printException(() -> "onAttachedToActivity failure", ex); + } + } + + private void addStatisticPreference(String titleKey, String SummaryText) { + Preference statisticPreference = new Preference(getContext()); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str(titleKey)); + statisticPreference.setSummary(SummaryText); + addPreference(statisticPreference); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java index b9c193d1ae..5a14ca39c9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java @@ -11,8 +11,6 @@ import android.widget.TextView; import android.widget.Toolbar; -import java.util.Objects; - import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.AppLanguage; @@ -21,8 +19,6 @@ import app.revanced.extension.youtube.patches.VersionCheckPatch; import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; -import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment; -import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment; /** * Hooks LicenseActivity. @@ -88,28 +84,15 @@ public static void initialize(Activity licenseActivity) { licenseActivity.setContentView(getResourceIdentifier( "revanced_settings_with_toolbar", "layout")); - PreferenceFragment fragment; - String toolbarTitleResourceName; - String dataString = Objects.requireNonNull(licenseActivity.getIntent().getDataString()); - switch (dataString) { - case "revanced_sb_settings_intent": - toolbarTitleResourceName = "revanced_sb_settings_title"; - fragment = new SponsorBlockPreferenceFragment(); - break; - case "revanced_ryd_settings_intent": - toolbarTitleResourceName = "revanced_ryd_settings_title"; - fragment = new ReturnYouTubeDislikePreferenceFragment(); - break; - case "revanced_settings_intent": - toolbarTitleResourceName = "revanced_settings_title"; - fragment = new ReVancedPreferenceFragment(); - break; - default: - Logger.printException(() -> "Unknown setting: " + dataString); - return; + // Sanity check. + String dataString = licenseActivity.getIntent().getDataString(); + if (!"revanced_settings_intent".equals(dataString)) { + Logger.printException(() -> "Unknown intent: " + dataString); + return; } - createToolbar(licenseActivity, toolbarTitleResourceName); + PreferenceFragment fragment = new ReVancedPreferenceFragment(); + createToolbar(licenseActivity, fragment); //noinspection deprecation licenseActivity.getFragmentManager() @@ -122,7 +105,7 @@ public static void initialize(Activity licenseActivity) { } @SuppressLint("UseCompatLoadingForDrawables") - private static void createToolbar(Activity activity, String toolbarTitleResourceName) { + private static void createToolbar(Activity activity, PreferenceFragment fragment) { // Replace dummy placeholder toolbar. // This is required to fix submenu title alignment issue with Android ASOP 15+ ViewGroup toolBarParent = activity.findViewById( @@ -134,8 +117,7 @@ private static void createToolbar(Activity activity, String toolbarTitleResource Toolbar toolbar = new Toolbar(toolBarParent.getContext()); toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor()); toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable()); - toolbar.setNavigationOnClickListener(view -> activity.onBackPressed()); - toolbar.setTitle(getResourceIdentifier(toolbarTitleResourceName, "string")); + toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string")); final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, Utils.getContext().getResources().getDisplayMetrics()); @@ -148,6 +130,11 @@ private static void createToolbar(Activity activity, String toolbarTitleResource } setToolbarLayoutParams(toolbar); + // Add Search Icon and EditText for ReVancedPreferenceFragment only. + if (fragment instanceof ReVancedPreferenceFragment) { + SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment); + } + toolBarParent.addView(toolbar, 0); } -} \ No newline at end of file +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java new file mode 100644 index 0000000000..cb2b42d7af --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java @@ -0,0 +1,381 @@ +package app.revanced.extension.youtube.settings; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.Toolbar; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.AppLanguage; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.youtube.ThemeHelper; +import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; + +/** + * Controller for managing the search view in ReVanced settings. + */ +@SuppressWarnings({"deprecated", "DiscouragedApi"}) +public class SearchViewController { + private static final int MAX_HISTORY_SIZE = 5; + + private final SearchView searchView; + private final FrameLayout searchContainer; + private final Toolbar toolbar; + private final Activity activity; + private boolean isSearchActive; + private final CharSequence originalTitle; + private final Deque searchHistory; + private final AutoCompleteTextView autoCompleteTextView; + private final boolean showSettingsSearchHistory; + + /** + * Creates a background drawable for the SearchView with rounded corners. + */ + private static GradientDrawable createBackgroundDrawable(Context context) { + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius. + int baseColor = ThemeHelper.getBackgroundColor(); + int adjustedColor = ThemeHelper.isDarkTheme() + ? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme. + : ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme. + background.setColor(adjustedColor); + return background; + } + + /** + * Creates a background drawable for suggestion items with rounded corners. + */ + private static GradientDrawable createSuggestionBackgroundDrawable(Context context) { + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius. + return background; + } + + /** + * Adds search view components to the activity. + */ + public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) { + new SearchViewController(activity, toolbar, fragment); + } + + private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) { + this.activity = activity; + this.toolbar = toolbar; + this.originalTitle = toolbar.getTitle(); + this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get(); + this.searchHistory = new LinkedList<>(); + StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES; + if (showSettingsSearchHistory) { + String entries = searchEntries.get(); + if (!entries.isBlank()) { + searchHistory.addAll(Arrays.asList(entries.split("\n"))); + } + } else { + // Clear old saved history if the user turns off the feature. + searchEntries.resetToDefault(); + } + + // Retrieve SearchView and container from XML. + searchView = activity.findViewById(getResourceIdentifier( + "revanced_search_view", "id")); + searchContainer = activity.findViewById(getResourceIdentifier( + "revanced_search_view_container", "id")); + + // Initialize AutoCompleteTextView. + autoCompleteTextView = searchView.findViewById( + searchView.getContext().getResources().getIdentifier( + "android:id/search_src_text", null, null)); + + // Set background and query hint. + searchView.setBackground(createBackgroundDrawable(toolbar.getContext())); + searchView.setQueryHint(str("revanced_settings_search_hint")); + + // Configure RTL support based on app language. + AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get(); + if (Utils.isRightToLeftLocale(appLanguage.getLocale())) { + searchView.setTextDirection(View.TEXT_DIRECTION_RTL); + searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + } + + // Set up search history suggestions. + if (showSettingsSearchHistory) { + setupSearchHistory(); + } + + // Set up query text listener. + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + try { + String queryTrimmed = query.trim(); + if (!queryTrimmed.isEmpty()) { + saveSearchQuery(queryTrimmed); + } + // Hide suggestions on submit. + if (showSettingsSearchHistory && autoCompleteTextView != null) { + autoCompleteTextView.dismissDropDown(); + } + } catch (Exception ex) { + Logger.printException(() -> "onQueryTextSubmit failure", ex); + } + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + try { + Logger.printDebug(() -> "Search query: " + newText); + fragment.filterPreferences(newText); + // Prevent suggestions from showing during text input. + if (showSettingsSearchHistory && autoCompleteTextView != null) { + if (!newText.isEmpty()) { + autoCompleteTextView.dismissDropDown(); + autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions. + } else { + autoCompleteTextView.setThreshold(1); // Re-enable for empty input. + } + } + } catch (Exception ex) { + Logger.printException(() -> "onQueryTextChange failure", ex); + } + return true; + } + }); + + // Set menu and search icon. + final int actionSearchId = getResourceIdentifier("action_search", "id"); + toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu")); + MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId); + searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme() + ? "yt_outline_search_white_24" + : "yt_outline_search_black_24", + "drawable")).setTooltipText(null); + + // Set menu item click listener. + toolbar.setOnMenuItemClickListener(item -> { + try { + if (item.getItemId() == actionSearchId) { + if (!isSearchActive) { + openSearch(); + } + return true; + } + } catch (Exception ex) { + Logger.printException(() -> "menu click failure", ex); + } + return false; + }); + + // Set navigation click listener. + toolbar.setNavigationOnClickListener(view -> { + try { + if (isSearchActive) { + closeSearch(); + } else { + activity.onBackPressed(); + } + } catch (Exception ex) { + Logger.printException(() -> "navigation click failure", ex); + } + }); + } + + /** + * Sets up the search history suggestions for the SearchView with custom adapter. + */ + private void setupSearchHistory() { + if (autoCompleteTextView != null) { + SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory)); + autoCompleteTextView.setAdapter(adapter); + autoCompleteTextView.setThreshold(1); // Initial threshold for empty input. + autoCompleteTextView.setLongClickable(true); + + // Show suggestions only when search bar is active and query is empty. + autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) { + autoCompleteTextView.showDropDown(); + } + }); + } + } + + /** + * Saves a search query to the search history. + * @param query The search query to save. + */ + private void saveSearchQuery(String query) { + if (!showSettingsSearchHistory) { + return; + } + searchHistory.remove(query); // Remove if already exists to update position. + searchHistory.addFirst(query); // Add to the most recent. + + // Remove extra old entries. + while (searchHistory.size() > MAX_HISTORY_SIZE) { + String last = searchHistory.removeLast(); + Logger.printDebug(() -> "Removing search history query: " + last); + } + + saveSearchHistory(); + + updateSearchHistoryAdapter(); + } + + /** + * Removes a search query from the search history. + * @param query The search query to remove. + */ + private void removeSearchQuery(String query) { + searchHistory.remove(query); + + saveSearchHistory(); + + updateSearchHistoryAdapter(); + } + + /** + * Save the search history to the shared preferences. + */ + private void saveSearchHistory() { + Logger.printDebug(() -> "Saving search history: " + searchHistory); + + Settings.SETTINGS_SEARCH_ENTRIES.save( + String.join("\n", searchHistory) + ); + } + + /** + * Updates the search history adapter with the latest history. + */ + private void updateSearchHistoryAdapter() { + if (autoCompleteTextView == null) { + return; + } + + SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter(); + if (adapter != null) { + adapter.clear(); + adapter.addAll(searchHistory); + adapter.notifyDataSetChanged(); + } + } + + /** + * Opens the search view and shows the keyboard. + */ + private void openSearch() { + isSearchActive = true; + toolbar.getMenu().findItem(getResourceIdentifier( + "action_search", "id")).setVisible(false); + toolbar.setTitle(""); + searchContainer.setVisibility(View.VISIBLE); + searchView.requestFocus(); + + // Show keyboard. + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT); + + // Show suggestions with a slight delay. + if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) { + searchView.postDelayed(() -> { + if (isSearchActive && autoCompleteTextView.getText().length() == 0) { + autoCompleteTextView.showDropDown(); + } + }, 100); // 100ms delay to ensure focus is stable. + } + } + + /** + * Closes the search view and hides the keyboard. + */ + private void closeSearch() { + isSearchActive = false; + toolbar.getMenu().findItem(getResourceIdentifier( + "action_search", "id")) + .setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme() + ? "yt_outline_search_white_24" + : "yt_outline_search_black_24", + "drawable") + ).setVisible(true); + toolbar.setTitle(originalTitle); + searchContainer.setVisibility(View.GONE); + searchView.setQuery("", false); + + // Hide keyboard. + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0); + } + + /** + * Custom ArrayAdapter for search history. + */ + private class SearchHistoryAdapter extends ArrayAdapter { + public SearchHistoryAdapter(Context context, List history) { + super(context, 0, history); + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { + if (convertView == null) { + convertView = LinearLayout.inflate(getContext(), getResourceIdentifier( + "revanced_search_suggestion_item", "layout"), null); + } + + // Apply rounded corners programmatically. + convertView.setBackground(createSuggestionBackgroundDrawable(getContext())); + String query = getItem(position); + + // Set query text. + TextView textView = convertView.findViewById(getResourceIdentifier( + "suggestion_text", "id")); + if (textView != null) { + textView.setText(query); + } + + // Set click listener for inserting query into SearchView. + convertView.setOnClickListener(v -> { + searchView.setQuery(query, true); // Insert selected query and submit. + }); + + // Set long click listener for deletion confirmation. + convertView.setOnLongClickListener(v -> { + new AlertDialog.Builder(activity) + .setTitle(query) + .setMessage(str("revanced_settings_search_remove_message")) + .setPositiveButton(android.R.string.ok, + (dialog, which) -> removeSearchQuery(query)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + return true; + }); + + return convertView; + } + } +} 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 29debf67e8..91134bcd9b 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 @@ -25,6 +25,8 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE; + +import app.revanced.extension.shared.settings.preference.SharedPrefCategory; import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle; import android.graphics.Color; @@ -217,6 +219,8 @@ public class Settings extends BaseSettings { // General layout public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true); + public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true); + public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true); public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message"); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true); @@ -340,19 +344,17 @@ public class Settings extends BaseSettings { public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS)); // ReturnYoutubeDislike - public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE); - public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false); - public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED)); - public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED)); - public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED)); - public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", TRUE, parent(RYD_ENABLED)); - public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE); + public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false); + public static final BooleanSetting RYD_SHORTS = new BooleanSetting("revanced_ryd_shorts", TRUE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE, true, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE, true, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", TRUE, true, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED)); // SponsorBlock public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE); - /** - * Do not use directly, instead use {@link SponsorBlockSettings} - */ + /** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */ public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", ""); public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED)); public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED)); @@ -416,12 +418,10 @@ public class Settings extends BaseSettings { // region Migration migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS); - migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER); - migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO); - migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU); + migrateOldSettingToNew(DEPRECATED_AUTO_CAPTIONS, DISABLE_AUTO_CAPTIONS); // Migrate renamed enum. //noinspection deprecation @@ -464,10 +464,15 @@ public class Settings extends BaseSettings { SPOOF_APP_VERSION_TARGET.resetToDefault(); } - if (!DEPRECATED_AUTO_CAPTIONS.isSetToDefault()) { - DISABLE_AUTO_CAPTIONS.save(true); - DEPRECATED_AUTO_CAPTIONS.resetToDefault(); - } + // RYD requires manually migrating old settings since the lack of + // a "revanced_" on the old setting causes duplicate key exceptions during export. + SharedPrefCategory revancedPrefs = Setting.preferences; + Setting.migrateFromOldPreferences(revancedPrefs, RYD_USER_ID, "ryd_user_id"); + Setting.migrateFromOldPreferences(revancedPrefs, RYD_ENABLED, "ryd_enabled"); + Setting.migrateFromOldPreferences(revancedPrefs, RYD_DISLIKE_PERCENTAGE, "ryd_dislike_percentage"); + Setting.migrateFromOldPreferences(revancedPrefs, RYD_COMPACT_LAYOUT, "ryd_compact_layout"); + Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like"); + Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error"); // endregion diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java index 5ca2e65dcb..e5a16947fe 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -1,23 +1,15 @@ package app.revanced.extension.youtube.settings.preference; import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.preference.Preference; import android.util.AttributeSet; /** * Allows tapping the DeArrow about preference to open the DeArrow website. */ -@SuppressWarnings({"unused", "deprecation"}) -public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { +@SuppressWarnings("unused") +public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference { { - setOnPreferenceClickListener(pref -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://dearrow.ajay.app")); - pref.getContext().startActivity(i); - return false; - }); + externalUrl = "https://dearrow.ajay.app"; } public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java new file mode 100644 index 0000000000..eee8d2bca0 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java @@ -0,0 +1,62 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.StringRef.sf; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; + +import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Custom video speeds used by {@link CustomPlaybackSpeedPatch}. + */ +@SuppressWarnings({"unused", "deprecation"}) +public final class CustomVideoSpeedListPreference extends ListPreference { + + /** + * Initialize a settings preference list with the available playback speeds. + */ + private void initializeEntryValues() { + float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds; + final int numberOfEntries = customPlaybackSpeeds.length + 1; + String[] preferenceListEntries = new String[numberOfEntries]; + String[] preferenceListEntryValues = new String[numberOfEntries]; + + // Auto speed (same behavior as unpatched). + preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString(); + preferenceListEntryValues[0] = String.valueOf(Settings.PLAYBACK_SPEED_DEFAULT.defaultValue); + + int i = 1; + for (float speed : customPlaybackSpeeds) { + String speedString = String.valueOf(speed); + preferenceListEntries[i] = speedString + "x"; + preferenceListEntryValues[i] = speedString; + i++; + } + + setEntries(preferenceListEntries); + setEntryValues(preferenceListEntryValues); + } + + { + initializeEntryValues(); + } + + public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomVideoSpeedListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomVideoSpeedListPreference(Context context) { + super(context); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java index ecdcf03cf8..1994bf3681 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java @@ -19,12 +19,15 @@ public class HtmlPreference extends Preference { public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } + public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } + public HtmlPreference(Context context, AttributeSet attrs) { super(context, attrs); } + public HtmlPreference(Context context) { super(context); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java index a2b6dbbfbd..d8dfce95e2 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java @@ -1,5 +1,6 @@ package app.revanced.extension.youtube.settings.preference; +import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.Utils.getResourceIdentifier; import android.annotation.SuppressLint; @@ -9,34 +10,66 @@ import android.os.Build; import android.preference.ListPreference; import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; -import android.util.Pair; +import android.preference.SwitchPreference; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; import android.util.TypedValue; import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.TextView; import android.widget.Toolbar; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; + +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.BaseSettings; -import app.revanced.extension.shared.settings.EnumSetting; import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; +import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory; import app.revanced.extension.youtube.ThemeHelper; -import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; import app.revanced.extension.youtube.settings.LicenseActivityHook; -import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup; /** * Preference fragment for ReVanced settings. - * - * @noinspection deprecation */ +@SuppressWarnings("deprecation") public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { + /** + * The main PreferenceScreen used to display the current set of preferences. + * This screen is manipulated during initialization and filtering to show or hide preferences. + */ + private PreferenceScreen preferenceScreen; + + /** + * A copy of the original PreferenceScreen created during initialization. + * Used to restore the preference structure to its initial state after filtering or other modifications. + */ + private PreferenceScreen originalPreferenceScreen; + + /** + * Used for searching preferences. A Collection of all preferences including nested preferences. + * Root preferences are excluded (no need to search what's on the root screen), + * but their sub preferences are included. + */ + private final List> allPreferences = new ArrayList<>(); + @SuppressLint("UseCompatLoadingForDrawables") public static Drawable getBackButtonDrawable() { final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme() @@ -47,85 +80,140 @@ public static Drawable getBackButtonDrawable() { } /** - * Sorts a preference list by menu entries, but preserves the first value as the first entry. - * - * @noinspection SameParameterValue + * Initializes the preference fragment, copying the original screen to allow full restoration. */ - private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) { - CharSequence[] entries = listPreference.getEntries(); - CharSequence[] entryValues = listPreference.getEntryValues(); - final int entrySize = entries.length; + @Override + protected void initialize() { + super.initialize(); + + try { + preferenceScreen = getPreferenceScreen(); + Utils.sortPreferenceGroups(preferenceScreen); - if (entrySize != entryValues.length) { - // Xml array declaration has a missing/extra entry. - throw new IllegalStateException(); + // Store the original structure for restoration after filtering. + originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext()); + for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) { + originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i)); + } + + setPreferenceScreenToolbar(preferenceScreen); + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); } + } - List> firstPairs = new ArrayList<>(firstEntriesToPreserve); - List> pairsToSort = new ArrayList<>(entrySize); + /** + * Called when the fragment starts, ensuring all preferences are collected after initialization. + */ + @Override + public void onStart() { + super.onStart(); + try { + if (allPreferences.isEmpty()) { + // Must collect preferences on start and not in initialize since + // legacy SB settings are not loaded yet. + Logger.printDebug(() -> "Collecting preferences to search"); - for (int i = 0; i < entrySize; i++) { - Pair pair = new Pair<>(entries[i].toString(), entryValues[i].toString()); - if (i < firstEntriesToPreserve) { - firstPairs.add(pair); - } else { - pairsToSort.add(pair); + // Do not show root menu preferences in search results. + // Instead search for everything that's not shown when search is not active. + collectPreferences(preferenceScreen, 1, 0); } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure", ex); } + } - pairsToSort.sort((pair1, pair2) - -> pair1.first.compareToIgnoreCase(pair2.first)); + /** + * Recursively collects all preferences from the screen or group. + * @param includeDepth Menu depth to start including preferences. + * A value of 0 adds all preferences. + */ + private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) { + for (int i = 0, count = group.getPreferenceCount(); i < count; i++) { + Preference preference = group.getPreference(i); + if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory) + && !(preference instanceof SponsorBlockPreferenceGroup)) { - CharSequence[] sortedEntries = new CharSequence[entrySize]; - CharSequence[] sortedEntryValues = new CharSequence[entrySize]; + AbstractPreferenceSearchData data; + if (preference instanceof SwitchPreference switchPref) { + data = new SwitchPreferenceSearchData(switchPref); + } else if (preference instanceof ListPreference listPref) { + data = new ListPreferenceSearchData(listPref); + } else { + data = new PreferenceSearchData(preference); + } - int i = 0; - for (Pair pair : firstPairs) { - sortedEntries[i] = pair.first; - sortedEntryValues[i] = pair.second; - i++; - } + allPreferences.add(data); + } - for (Pair pair : pairsToSort) { - sortedEntries[i] = pair.first; - sortedEntryValues[i] = pair.second; - i++; + if (preference instanceof PreferenceGroup subGroup) { + collectPreferences(subGroup, includeDepth, currentDepth + 1); + } } - - listPreference.setEntries(sortedEntries); - listPreference.setEntryValues(sortedEntryValues); } - @Override - protected void initialize() { - super.initialize(); + /** + * Filters the preferences using the given query string and applies highlighting. + */ + public void filterPreferences(String query) { + preferenceScreen.removeAll(); - try { - setPreferenceScreenToolbar(getPreferenceScreen()); + if (TextUtils.isEmpty(query)) { + // Restore original preferences and their titles/summaries/entries. + for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) { + preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i)); + } - // If the preference was included, then initialize it based on the available playback speed. - Preference preference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key); - if (preference instanceof ListPreference playbackPreference) { - CustomPlaybackSpeedPatch.initializeListPreference(playbackPreference); + for (AbstractPreferenceSearchData data : allPreferences) { + data.clearHighlighting(); } - sortPreferenceListMenu(Settings.CHANGE_START_PAGE); - sortPreferenceListMenu(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE); - sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE); - } catch (Exception ex) { - Logger.printException(() -> "initialize failure", ex); + return; } - } - private void sortPreferenceListMenu(EnumSetting setting) { - Preference preference = findPreference(setting.key); - if (preference instanceof ListPreference languagePreference) { - sortListPreferenceByValues(languagePreference, 1); + // Navigation path -> Category + Map categoryMap = new HashMap<>(); + String queryLower = Utils.removePunctuationToLowercase(query); + + Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)), + Pattern.CASE_INSENSITIVE); + + for (AbstractPreferenceSearchData data : allPreferences) { + if (data.matchesSearchQuery(queryLower)) { + data.applyHighlighting(queryLower, queryPattern); + + String navigationPath = data.navigationPath; + PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> { + PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext()); + newGroup.setTitle(navigationPath); + preferenceScreen.addPreference(newGroup); + return newGroup; + }); + group.addPreference(data.preference); + } + } + + // Show 'No results found' if search results are empty. + if (categoryMap.isEmpty()) { + Preference noResultsPreference = new Preference(preferenceScreen.getContext()); + noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query)); + noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary")); + noResultsPreference.setSelectable(false); + // Set icon for the placeholder preference. + noResultsPreference.setLayoutResource(getResourceIdentifier( + "revanced_preference_with_icon_no_search_result", "layout")); + noResultsPreference.setIcon(getResourceIdentifier( + ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24", + "drawable")); + preferenceScreen.addPreference(noResultsPreference); } } + /** + * Sets toolbar for all nested preference screens. + */ private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { - for (int i = 0, preferenceCount = parentScreen.getPreferenceCount(); i < preferenceCount; i++) { + for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) { Preference childPreference = parentScreen.getPreference(i); if (childPreference instanceof PreferenceScreen) { // Recursively set sub preferences. @@ -156,6 +244,7 @@ private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { toolbar.setTitle(childScreen.getTitle()); toolbar.setNavigationIcon(getBackButtonDrawable()); toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); + final int margin = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics() ); @@ -177,3 +266,271 @@ private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { } } } + +@SuppressWarnings("deprecation") +class AbstractPreferenceSearchData { + /** + * @return The navigation path for the given preference, such as "Player > Action buttons". + */ + private static String getPreferenceNavigationString(Preference preference) { + Deque pathElements = new ArrayDeque<>(); + + while (true) { + preference = preference.getParent(); + + if (preference == null) { + if (pathElements.isEmpty()) { + return ""; + } + Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale(); + return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements); + } + + if (!(preference instanceof NoTitlePreferenceCategory) + && !(preference instanceof SponsorBlockPreferenceGroup)) { + CharSequence title = preference.getTitle(); + if (title != null && title.length() > 0) { + pathElements.addFirst(title); + } + } + } + } + + /** + * Highlights the search query in the given text by applying color span. + * @param text The original text to process. + * @param queryPattern The search query to highlight. + * @return The text with highlighted query matches as a SpannableStringBuilder. + */ + static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) { + if (TextUtils.isEmpty(text)) { + return text; + } + + final int baseColor = ThemeHelper.getBackgroundColor(); + final int adjustedColor = ThemeHelper.isDarkTheme() + ? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme. + : ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme. + BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor); + + SpannableStringBuilder spannable = new SpannableStringBuilder(text); + Matcher matcher = queryPattern.matcher(text); + + while (matcher.find()) { + spannable.setSpan( + highlightSpan, + matcher.start(), + matcher.end(), + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + return spannable; + } + + final T preference; + final String key; + final String navigationPath; + boolean highlightingApplied; + + @Nullable + CharSequence originalTitle; + @Nullable + String searchTitle; + + AbstractPreferenceSearchData(T pref) { + preference = pref; + key = Utils.removePunctuationToLowercase(pref.getKey()); + navigationPath = getPreferenceNavigationString(pref); + } + + @CallSuper + void updateSearchDataIfNeeded() { + if (highlightingApplied) { + // Must clear, otherwise old highlighting is still applied. + clearHighlighting(); + } + + CharSequence title = preference.getTitle(); + if (originalTitle != title) { // Check using reference equality. + originalTitle = title; + searchTitle = Utils.removePunctuationToLowercase(title); + } + } + + @CallSuper + boolean matchesSearchQuery(String query) { + updateSearchDataIfNeeded(); + + return key.contains(query) + || searchTitle != null && searchTitle.contains(query); + } + + @CallSuper + void applyHighlighting(String query, Pattern queryPattern) { + preference.setTitle(highlightSearchQuery(originalTitle, queryPattern)); + highlightingApplied = true; + } + + @CallSuper + void clearHighlighting() { + if (highlightingApplied) { + preference.setTitle(originalTitle); + highlightingApplied = false; + } + } +} + +/** + * Regular preference type that only uses the base preference summary. + * Should only be used if a more specific data class does not exist. + */ +@SuppressWarnings("deprecation") +class PreferenceSearchData extends AbstractPreferenceSearchData { + @Nullable + CharSequence originalSummary; + @Nullable + String searchSummary; + + PreferenceSearchData(Preference pref) { + super(pref); + } + + void updateSearchDataIfNeeded() { + super.updateSearchDataIfNeeded(); + + CharSequence summary = preference.getSummary(); + if (originalSummary != summary) { + originalSummary = summary; + searchSummary = Utils.removePunctuationToLowercase(summary); + } + } + + boolean matchesSearchQuery(String query) { + return super.matchesSearchQuery(query) + || searchSummary != null && searchSummary.contains(query); + } + + @Override + void applyHighlighting(String query, Pattern queryPattern) { + super.applyHighlighting(query, queryPattern); + + preference.setSummary(highlightSearchQuery(originalSummary, queryPattern)); + } + + @CallSuper + void clearHighlighting() { + super.clearHighlighting(); + + preference.setSummary(originalSummary); + } +} + +/** + * Switch preference type that uses summaryOn and summaryOff. + */ +@SuppressWarnings("deprecation") +class SwitchPreferenceSearchData extends AbstractPreferenceSearchData { + @Nullable + CharSequence originalSummaryOn, originalSummaryOff; + @Nullable + String searchSummaryOn, searchSummaryOff; + + SwitchPreferenceSearchData(SwitchPreference pref) { + super(pref); + } + + void updateSearchDataIfNeeded() { + super.updateSearchDataIfNeeded(); + + CharSequence summaryOn = preference.getSummaryOn(); + if (originalSummaryOn != summaryOn) { + originalSummaryOn = summaryOn; + searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn); + } + + CharSequence summaryOff = preference.getSummaryOff(); + if (originalSummaryOff != summaryOff) { + originalSummaryOff = summaryOff; + searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff); + } + } + + boolean matchesSearchQuery(String query) { + return super.matchesSearchQuery(query) + || searchSummaryOn != null && searchSummaryOn.contains(query) + || searchSummaryOff != null && searchSummaryOff.contains(query); + } + + @Override + void applyHighlighting(String query, Pattern queryPattern) { + super.applyHighlighting(query, queryPattern); + + preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern)); + preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern)); + } + + @CallSuper + void clearHighlighting() { + super.clearHighlighting(); + + preference.setSummaryOn(originalSummaryOn); + preference.setSummaryOff(originalSummaryOff); + } +} + +/** + * List preference type that uses entries. + */ +@SuppressWarnings("deprecation") +class ListPreferenceSearchData extends AbstractPreferenceSearchData { + @Nullable + CharSequence[] originalEntries; + @Nullable + String searchEntries; + + ListPreferenceSearchData(ListPreference pref) { + super(pref); + } + + void updateSearchDataIfNeeded() { + super.updateSearchDataIfNeeded(); + + CharSequence[] entries = preference.getEntries(); + if (originalEntries != entries) { + originalEntries = entries; + searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries)); + } + } + + boolean matchesSearchQuery(String query) { + return super.matchesSearchQuery(query) + || searchEntries != null && searchEntries.contains(query); + } + + @Override + void applyHighlighting(String query, Pattern queryPattern) { + super.applyHighlighting(query, queryPattern); + + if (originalEntries != null) { + final int length = originalEntries.length; + CharSequence[] highlightedEntries = new CharSequence[length]; + + for (int i = 0; i < length; i++) { + highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern); + + // Cannot highlight the summary text, because ListPreference uses + // the toString() of the summary CharSequence which strips away all formatting. + } + + preference.setEntries(highlightedEntries); + } + } + + @CallSuper + void clearHighlighting() { + super.clearHighlighting(); + + preference.setEntries(originalEntries); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java deleted file mode 100644 index bb62386ac3..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java +++ /dev/null @@ -1,257 +0,0 @@ -package app.revanced.extension.youtube.settings.preference; - -import static app.revanced.extension.shared.StringRef.str; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.preference.SwitchPreference; - -import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.settings.Setting; -import app.revanced.extension.shared.settings.BaseSettings; -import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch; -import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; -import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; -import app.revanced.extension.youtube.settings.Settings; - -/** @noinspection deprecation*/ -public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment { - - /** - * If dislikes are shown on Shorts. - */ - private SwitchPreference shortsPreference; - - /** - * If dislikes are shown as percentage. - */ - private SwitchPreference percentagePreference; - - /** - * If segmented like/dislike button uses smaller compact layout. - */ - private SwitchPreference compactLayoutPreference; - - /** - * If hidden likes are replaced with an estimated value. - */ - private SwitchPreference estimatedLikesPreference; - - /** - * If segmented like/dislike button uses smaller compact layout. - */ - private SwitchPreference toastOnRYDNotAvailable; - - private void updateUIState() { - shortsPreference.setEnabled(Settings.RYD_SHORTS.isAvailable()); - percentagePreference.setEnabled(Settings.RYD_DISLIKE_PERCENTAGE.isAvailable()); - compactLayoutPreference.setEnabled(Settings.RYD_COMPACT_LAYOUT.isAvailable()); - estimatedLikesPreference.setEnabled(Settings.RYD_ESTIMATED_LIKE.isAvailable()); - toastOnRYDNotAvailable.setEnabled(Settings.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable()); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - try { - Activity context = getActivity(); - PreferenceManager manager = getPreferenceManager(); - manager.setSharedPreferencesName(Setting.preferences.name); - PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context); - setPreferenceScreen(preferenceScreen); - - SwitchPreference enabledPreference = new SwitchPreference(context); - enabledPreference.setChecked(Settings.RYD_ENABLED.get()); - enabledPreference.setTitle(str("revanced_ryd_enable_title")); - enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on")); - enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off")); - enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> { - final Boolean rydIsEnabled = (Boolean) newValue; - Settings.RYD_ENABLED.save(rydIsEnabled); - ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled); - - updateUIState(); - return true; - }); - preferenceScreen.addPreference(enabledPreference); - - shortsPreference = new SwitchPreference(context); - shortsPreference.setChecked(Settings.RYD_SHORTS.get()); - shortsPreference.setTitle(str("revanced_ryd_shorts_title")); - String shortsSummary = str("revanced_ryd_shorts_summary_on_disclaimer"); - shortsPreference.setSummaryOn(shortsSummary); - shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off")); - shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> { - Settings.RYD_SHORTS.save((Boolean) newValue); - updateUIState(); - return true; - }); - preferenceScreen.addPreference(shortsPreference); - - percentagePreference = new SwitchPreference(context); - percentagePreference.setChecked(Settings.RYD_DISLIKE_PERCENTAGE.get()); - percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title")); - percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on")); - percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off")); - percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { - Settings.RYD_DISLIKE_PERCENTAGE.save((Boolean) newValue); - ReturnYouTubeDislike.clearAllUICaches(); - updateUIState(); - return true; - }); - preferenceScreen.addPreference(percentagePreference); - - compactLayoutPreference = new SwitchPreference(context); - compactLayoutPreference.setChecked(Settings.RYD_COMPACT_LAYOUT.get()); - compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title")); - compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on")); - compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off")); - compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { - Settings.RYD_COMPACT_LAYOUT.save((Boolean) newValue); - ReturnYouTubeDislike.clearAllUICaches(); - updateUIState(); - return true; - }); - preferenceScreen.addPreference(compactLayoutPreference); - - estimatedLikesPreference = new SwitchPreference(context); - estimatedLikesPreference.setChecked(Settings.RYD_ESTIMATED_LIKE.get()); - estimatedLikesPreference.setTitle(str("revanced_ryd_estimated_like_title")); - estimatedLikesPreference.setSummaryOn(str("revanced_ryd_estimated_like_summary_on")); - estimatedLikesPreference.setSummaryOff(str("revanced_ryd_estimated_like_summary_off")); - estimatedLikesPreference.setOnPreferenceChangeListener((pref, newValue) -> { - Settings.RYD_ESTIMATED_LIKE.save((Boolean) newValue); - ReturnYouTubeDislike.clearAllUICaches(); - updateUIState(); - return true; - }); - preferenceScreen.addPreference(estimatedLikesPreference); - - toastOnRYDNotAvailable = new SwitchPreference(context); - toastOnRYDNotAvailable.setChecked(Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()); - toastOnRYDNotAvailable.setTitle(str("revanced_ryd_toast_on_connection_error_title")); - toastOnRYDNotAvailable.setSummaryOn(str("revanced_ryd_toast_on_connection_error_summary_on")); - toastOnRYDNotAvailable.setSummaryOff(str("revanced_ryd_toast_on_connection_error_summary_off")); - toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> { - Settings.RYD_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue); - updateUIState(); - return true; - }); - preferenceScreen.addPreference(toastOnRYDNotAvailable); - - updateUIState(); - - - // About category - - PreferenceCategory aboutCategory = new PreferenceCategory(context); - aboutCategory.setTitle(str("revanced_ryd_about")); - preferenceScreen.addPreference(aboutCategory); - - // ReturnYouTubeDislike Website - - Preference aboutWebsitePreference = new Preference(context); - aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title")); - aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary")); - aboutWebsitePreference.setOnPreferenceClickListener(pref -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://returnyoutubedislike.com")); - pref.getContext().startActivity(i); - return false; - }); - aboutCategory.addPreference(aboutWebsitePreference); - - // RYD API connection statistics - - if (BaseSettings.DEBUG.get()) { - PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding - preferenceScreen.addPreference(emptyCategory); - - PreferenceCategory statisticsCategory = new PreferenceCategory(context); - statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title")); - preferenceScreen.addPreference(statisticsCategory); - - Preference statisticPreference; - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title")); - statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())); - preferenceScreen.addPreference(statisticPreference); - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title")); - statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())); - preferenceScreen.addPreference(statisticPreference); - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title")); - statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())); - preferenceScreen.addPreference(statisticPreference); - - String fetchCallTimeWaitingLastSummary; - final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast(); - if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { - fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary"); - } else { - fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast); - } - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title")); - statisticPreference.setSummary(fetchCallTimeWaitingLastSummary); - preferenceScreen.addPreference(statisticPreference); - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title")); - statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(), - "revanced_ryd_statistics_getFetchCallCount_zero_summary", - "revanced_ryd_statistics_getFetchCallCount_non_zero_summary")); - preferenceScreen.addPreference(statisticPreference); - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title")); - statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(), - "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary", - "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary")); - preferenceScreen.addPreference(statisticPreference); - - statisticPreference = new Preference(context); - statisticPreference.setSelectable(false); - statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title")); - statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(), - "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary", - "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary")); - preferenceScreen.addPreference(statisticPreference); - } - - Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen); - } catch (Exception ex) { - Logger.printException(() -> "onCreate failure", ex); - } - } - - private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) { - if (value == 0) { - return str(summaryStringZeroKey); - } - return String.format(str(summaryStringOneOrMoreKey), value); - } - - private static String createMillisecondStringFromNumber(long number) { - return String.format(str("revanced_ryd_statistics_millisecond_text"), number); - } - -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java deleted file mode 100644 index 7511252749..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ /dev/null @@ -1,629 +0,0 @@ -package app.revanced.extension.youtube.settings.preference; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.preference.*; -import android.text.Html; -import android.text.InputType; -import android.util.TypedValue; -import android.widget.EditText; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.settings.Setting; -import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference; -import app.revanced.extension.youtube.settings.Settings; -import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; -import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; -import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; -import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; -import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference; -import app.revanced.extension.youtube.sponsorblock.objects.UserStats; -import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; -import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController; - -import static android.text.Html.fromHtml; -import static app.revanced.extension.shared.StringRef.str; - -@SuppressWarnings("deprecation") -public class SponsorBlockPreferenceFragment extends PreferenceFragment { - - private SwitchPreference sbEnabled; - private SwitchPreference addNewSegment; - private SwitchPreference votingEnabled; - private SwitchPreference autoHideSkipSegmentButton; - private SwitchPreference compactSkipButton; - private SwitchPreference squareLayout; - private SwitchPreference showSkipToast; - private SwitchPreference trackSkips; - private SwitchPreference showTimeWithoutSegments; - private SwitchPreference toastOnConnectionError; - - private ResettableEditTextPreference newSegmentStep; - private ResettableEditTextPreference minSegmentDuration; - private EditTextPreference privateUserId; - private EditTextPreference importExport; - private Preference apiUrl; - - private PreferenceCategory statsCategory; - private PreferenceCategory segmentCategory; - - private void updateUI() { - try { - final boolean enabled = Settings.SB_ENABLED.get(); - if (!enabled) { - SponsorBlockViewController.hideAll(); - SegmentPlaybackController.setCurrentVideoId(null); - } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) { - SponsorBlockViewController.hideNewSegmentLayout(); - } - // Voting and add new segment buttons automatically show/hide themselves. - - SponsorBlockViewController.updateLayout(); - - sbEnabled.setChecked(enabled); - - addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get()); - addNewSegment.setEnabled(enabled); - - votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get()); - votingEnabled.setEnabled(enabled); - - autoHideSkipSegmentButton.setEnabled(enabled); - autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()); - - compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get()); - compactSkipButton.setEnabled(enabled); - - squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get()); - squareLayout.setEnabled(enabled); - - showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get()); - showSkipToast.setEnabled(enabled); - - toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get()); - toastOnConnectionError.setEnabled(enabled); - - trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get()); - trackSkips.setEnabled(enabled); - - showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get()); - showTimeWithoutSegments.setEnabled(enabled); - - newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString()); - newSegmentStep.setEnabled(enabled); - - minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString()); - minSegmentDuration.setEnabled(enabled); - - privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get()); - privateUserId.setEnabled(enabled); - - // If the user has a private user id, then include a subtext that mentions not to share it. - String importExportSummary = SponsorBlockSettings.userHasSBPrivateId() - ? str("revanced_sb_settings_ie_sum_warning") - : str("revanced_sb_settings_ie_sum"); - importExport.setSummary(importExportSummary); - - apiUrl.setEnabled(enabled); - importExport.setEnabled(enabled); - segmentCategory.setEnabled(enabled); - statsCategory.setEnabled(enabled); - } catch (Exception ex) { - Logger.printException(() -> "update settings UI failure", ex); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - try { - Activity context = getActivity(); - PreferenceManager manager = getPreferenceManager(); - manager.setSharedPreferencesName(Setting.preferences.name); - PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context); - setPreferenceScreen(preferenceScreen); - - SponsorBlockSettings.initialize(); - - sbEnabled = new SwitchPreference(context); - sbEnabled.setTitle(str("revanced_sb_enable_sb")); - sbEnabled.setSummary(str("revanced_sb_enable_sb_sum")); - preferenceScreen.addPreference(sbEnabled); - sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_ENABLED.save((Boolean) newValue); - updateUI(); - return true; - }); - - addAppearanceCategory(context, preferenceScreen); - - segmentCategory = new PreferenceCategory(context); - segmentCategory.setTitle(str("revanced_sb_diff_segments")); - preferenceScreen.addPreference(segmentCategory); - updateSegmentCategories(); - - addCreateSegmentCategory(context, preferenceScreen); - - addGeneralCategory(context, preferenceScreen); - - statsCategory = new PreferenceCategory(context); - statsCategory.setTitle(str("revanced_sb_stats")); - preferenceScreen.addPreference(statsCategory); - fetchAndDisplayStats(); - - addAboutCategory(context, preferenceScreen); - - Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen); - - updateUI(); - } catch (Exception ex) { - Logger.printException(() -> "onCreate failure", ex); - } - } - - private void addAppearanceCategory(Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); - screen.addPreference(category); - category.setTitle(str("revanced_sb_appearance_category")); - - votingEnabled = new SwitchPreference(context); - votingEnabled.setTitle(str("revanced_sb_enable_voting")); - votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on")); - votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off")); - category.addPreference(votingEnabled); - votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_VOTING_BUTTON.save((Boolean) newValue); - updateUI(); - return true; - }); - - 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")); - category.addPreference(autoHideSkipSegmentButton); - autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue); - updateUI(); - return true; - }); - - compactSkipButton = new SwitchPreference(context); - compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button")); - compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on")); - compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off")); - category.addPreference(compactSkipButton); - compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue); - updateUI(); - return true; - }); - - 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")); - category.addPreference(squareLayout); - squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue); - updateUI(); - return true; - }); - - 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(); - return true; - }); - category.addPreference(showSkipToast); - - showTimeWithoutSegments = new SwitchPreference(context); - showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without")); - showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on")); - showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off")); - showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue); - updateUI(); - return true; - }); - category.addPreference(showTimeWithoutSegments); - } - - private void addCreateSegmentCategory(Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); - screen.addPreference(category); - category.setTitle(str("revanced_sb_create_segment_category")); - - addNewSegment = new SwitchPreference(context); - addNewSegment.setTitle(str("revanced_sb_enable_create_segment")); - addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on")); - addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off")); - category.addPreference(addNewSegment); - addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { - Boolean newValue = (Boolean) o; - if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) { - new AlertDialog.Builder(preference1.getContext()) - .setTitle(str("revanced_sb_guidelines_popup_title")) - .setMessage(str("revanced_sb_guidelines_popup_content")) - .setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null) - .setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) - .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true)) - .setCancelable(false) - .show(); - } - Settings.SB_CREATE_NEW_SEGMENT.save(newValue); - updateUI(); - return true; - }); - - newSegmentStep = new ResettableEditTextPreference(context); - newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP); - newSegmentStep.setTitle(str("revanced_sb_general_adjusting")); - newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum")); - newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> { - try { - final int newAdjustmentValue = Integer.parseInt(newValue.toString()); - if (newAdjustmentValue != 0) { - Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue); - return true; - } - } catch (NumberFormatException ex) { - Logger.printInfo(() -> "Invalid new segment step", ex); - } - - Utils.showToastLong(str("revanced_sb_general_adjusting_invalid")); - updateUI(); - return false; - }); - category.addPreference(newSegmentStep); - - Preference guidelinePreferences = new Preference(context); - guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title")); - guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum")); - guidelinePreferences.setOnPreferenceClickListener(preference1 -> { - openGuidelines(); - return true; - }); - category.addPreference(guidelinePreferences); - } - - private void addGeneralCategory(final Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); - screen.addPreference(category); - category.setTitle(str("revanced_sb_general")); - - toastOnConnectionError = new SwitchPreference(context); - toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title")); - toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on")); - toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off")); - toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue); - updateUI(); - return true; - }); - category.addPreference(toastOnConnectionError); - - trackSkips = new SwitchPreference(context); - trackSkips.setTitle(str("revanced_sb_general_skipcount")); - trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on")); - trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off")); - trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> { - Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue); - updateUI(); - return true; - }); - category.addPreference(trackSkips); - - minSegmentDuration = new ResettableEditTextPreference(context); - minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION); - minSegmentDuration.setTitle(str("revanced_sb_general_min_duration")); - minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum")); - minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { - try { - Float minTimeDuration = Float.valueOf(newValue.toString()); - Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration); - return true; - } catch (NumberFormatException ex) { - Logger.printInfo(() -> "Invalid minimum segment duration", ex); - } - - Utils.showToastLong(str("revanced_sb_general_min_duration_invalid")); - updateUI(); - return false; - }); - category.addPreference(minSegmentDuration); - - privateUserId = new EditTextPreference(context) { - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - Utils.setEditTextDialogTheme(builder); - - builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); - }); - } - }; - privateUserId.setTitle(str("revanced_sb_general_uuid")); - privateUserId.setSummary(str("revanced_sb_general_uuid_sum")); - privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> { - String newUUID = newValue.toString(); - if (!SponsorBlockSettings.isValidSBUserId(newUUID)) { - Utils.showToastLong(str("revanced_sb_general_uuid_invalid")); - return false; - } - - Settings.SB_PRIVATE_USER_ID.save(newUUID); - updateUI(); - fetchAndDisplayStats(); - return true; - }); - category.addPreference(privateUserId); - - apiUrl = new Preference(context); - apiUrl.setTitle(str("revanced_sb_general_api_url")); - apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum"))); - apiUrl.setOnPreferenceClickListener(preference1 -> { - EditText editText = new EditText(context); - editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - editText.setText(Settings.SB_API_URL.get()); - - DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { - if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { - Settings.SB_API_URL.resetToDefault(); - Utils.showToastLong(str("revanced_sb_api_url_reset")); - } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { - String serverAddress = editText.getText().toString(); - if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) { - Utils.showToastLong(str("revanced_sb_api_url_invalid")); - } else if (!serverAddress.equals(Settings.SB_API_URL.get())) { - Settings.SB_API_URL.save(serverAddress); - Utils.showToastLong(str("revanced_sb_api_url_changed")); - } - } - }; - new AlertDialog.Builder(context) - .setTitle(apiUrl.getTitle()) - .setView(editText) - .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(str("revanced_sb_reset"), urlChangeListener) - .setPositiveButton(android.R.string.ok, urlChangeListener) - .show(); - return true; - }); - category.addPreference(apiUrl); - - importExport = new EditTextPreference(context) { - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - Utils.setEditTextDialogTheme(builder); - - builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); - }); - } - }; - importExport.setTitle(str("revanced_sb_settings_ie")); - // Summary is set in updateUI() - importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_FLAG_MULTI_LINE - | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - importExport.getEditText().setAutofillHints((String) null); - importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); - importExport.setOnPreferenceClickListener(preference1 -> { - importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings()); - return true; - }); - importExport.setOnPreferenceChangeListener((preference1, newValue) -> { - SponsorBlockSettings.importDesktopSettings((String) newValue); - updateSegmentCategories(); - fetchAndDisplayStats(); - updateUI(); - return true; - }); - category.addPreference(importExport); - } - - private void updateSegmentCategories() { - try { - segmentCategory.removeAll(); - - Activity activity = getActivity(); - for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { - segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category)); - } - } catch (Exception ex) { - Logger.printException(() -> "updateSegmentCategories failure", ex); - } - } - - private void addAboutCategory(Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); - screen.addPreference(category); - category.setTitle(str("revanced_sb_about")); - - { - Preference preference = new Preference(context); - category.addPreference(preference); - preference.setTitle(str("revanced_sb_about_api")); - preference.setSummary(str("revanced_sb_about_api_sum")); - preference.setOnPreferenceClickListener(preference1 -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://sponsor.ajay.app")); - preference1.getContext().startActivity(i); - return false; - }); - } - } - - private void openGuidelines() { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); - getActivity().startActivity(intent); - } - - private void fetchAndDisplayStats() { - try { - statsCategory.removeAll(); - if (!SponsorBlockSettings.userHasSBPrivateId()) { - // User has never voted or created any segments. No stats to show. - addLocalUserStats(); - return; - } - - Preference loadingPlaceholderPreference = new Preference(this.getActivity()); - loadingPlaceholderPreference.setEnabled(false); - statsCategory.addPreference(loadingPlaceholderPreference); - if (Settings.SB_ENABLED.get()) { - loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading")); - Utils.runOnBackgroundThread(() -> { - UserStats stats = SBRequester.retrieveUserStats(); - Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements - addUserStats(loadingPlaceholderPreference, stats); - addLocalUserStats(); - }); - }); - } else { - loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled")); - } - } catch (Exception ex) { - Logger.printException(() -> "fetchAndDisplayStats failure", ex); - } - } - - private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) { - Utils.verifyOnMainThread(); - try { - if (stats == null) { - loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure")); - return; - } - statsCategory.removeAll(); - Context context = statsCategory.getContext(); - - if (stats.totalSegmentCountIncludingIgnored > 0) { - // If user has not created any segments, there's no reason to set a username. - EditTextPreference preference = new ResettableEditTextPreference(context); - statsCategory.addPreference(preference); - String userName = stats.userName; - preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName))); - preference.setSummary(str("revanced_sb_stats_username_change")); - preference.setText(userName); - preference.setOnPreferenceChangeListener((preference1, value) -> { - Utils.runOnBackgroundThread(() -> { - String newUserName = (String) value; - String errorMessage = SBRequester.setUsername(newUserName); - Utils.runOnMainThread(() -> { - if (errorMessage == null) { - preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName))); - preference.setText(newUserName); - Utils.showToastLong(str("revanced_sb_stats_username_changed")); - } else { - preference.setText(userName); // revert to previous - SponsorBlockUtils.showErrorDialog(errorMessage); - } - }); - }); - return true; - }); - } - - { - // number of segment submissions (does not include ignored segments) - Preference preference = new Preference(context); - statsCategory.addPreference(preference); - String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount); - preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted))); - preference.setSummary(str("revanced_sb_stats_submissions_sum")); - if (stats.totalSegmentCountIncludingIgnored == 0) { - preference.setSelectable(false); - } else { - preference.setOnPreferenceClickListener(preference1 -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId)); - preference1.getContext().startActivity(i); - return true; - }); - } - } - - { - // "user reputation". Usually not useful, since it appears most users have zero reputation. - // But if there is a reputation, then show it here - Preference preference = new Preference(context); - preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation))); - preference.setSelectable(false); - if (stats.reputation != 0) { - statsCategory.addPreference(preference); - } - } - - { - // time saved for other users - Preference preference = new Preference(context); - statsCategory.addPreference(preference); - - String stats_saved; - String stats_saved_sum; - if (stats.totalSegmentCountIncludingIgnored == 0) { - stats_saved = str("revanced_sb_stats_saved_zero"); - stats_saved_sum = str("revanced_sb_stats_saved_sum_zero"); - } else { - stats_saved = str("revanced_sb_stats_saved", - SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount)); - stats_saved_sum = str("revanced_sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved))); - } - preference.setTitle(fromHtml(stats_saved)); - preference.setSummary(fromHtml(stats_saved_sum)); - preference.setOnPreferenceClickListener(preference1 -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://sponsor.ajay.app/stats/")); - preference1.getContext().startActivity(i); - return false; - }); - } - } catch (Exception ex) { - Logger.printException(() -> "addUserStats failure", ex); - } - } - - private void addLocalUserStats() { - // time the user saved by using SB - Preference preference = new Preference(statsCategory.getContext()); - statsCategory.addPreference(preference); - - Runnable updateStatsSelfSaved = () -> { - String formatted = SponsorBlockUtils.getNumberOfSkipsString(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get()); - preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted))); - String formattedSaved = SponsorBlockUtils.getTimeSavedString(Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000); - preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved))); - }; - updateStatsSelfSaved.run(); - preference.setOnPreferenceClickListener(preference1 -> { - new AlertDialog.Builder(preference1.getContext()) - .setTitle(str("revanced_sb_stats_self_saved_reset_title")) - .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault(); - Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault(); - updateStatsSelfSaved.run(); - }) - .setNegativeButton(android.R.string.no, null).show(); - return true; - }); - } - -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java new file mode 100644 index 0000000000..9570883cb9 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.util.AttributeSet; + +import app.revanced.extension.shared.Logger; + +/** + * Simple preference that opens a url when clicked. + */ +@SuppressWarnings("deprecation") +public class UrlLinkPreference extends Preference { + + protected String externalUrl; + + { + setOnPreferenceClickListener(pref -> { + if (externalUrl == null) { + Logger.printException(() -> "URL not set " + getClass().getSimpleName()); + return false; + } + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(externalUrl)); + pref.getContext().startActivity(i); + return true; + }); + } + + public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public UrlLinkPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public UrlLinkPreference(Context context) { + super(context); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java index e8d64fb50d..d03bde3bb6 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -18,6 +18,7 @@ import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup; import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; @@ -31,6 +32,7 @@ public class SponsorBlockSettings { @Override public void settingsImported(@Nullable Context context) { SegmentCategory.loadAllCategoriesFromSettings(); + SponsorBlockPreferenceGroup.settingsImported = true; } @Override public void settingsExported(@Nullable Context context) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java index ae92caaff0..aa24c8dbfb 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java @@ -55,7 +55,7 @@ public SegmentCategoryListPreference(Context context, SegmentCategory category) : CategoryBehaviour.getBehaviorKeyValues()); setSummary(category.description.toString()); - updateTitleFromCategory(); + updateUI(); } @Override @@ -202,7 +202,7 @@ public void afterTextChanged(Editable edit) { builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { try { category.resetColorAndOpacity(); - updateTitleFromCategory(); + updateUI(); Utils.showToastShort(str("revanced_sb_color_reset")); } catch (Exception ex) { Logger.printException(() -> "setNeutralButton failure", ex); @@ -240,7 +240,7 @@ protected void onDialogClosed(boolean positiveResult) { Utils.showToastShort(str("revanced_sb_color_invalid")); } - updateTitleFromCategory(); + updateUI(); } } catch (Exception ex) { Logger.printException(() -> "onDialogClosed failure", ex); @@ -251,7 +251,7 @@ private void applyOpacityToCategoryColor() { categoryColor = applyOpacityToColor(categoryColor, categoryOpacity); } - private void updateTitleFromCategory() { + public void updateUI() { categoryColor = category.getColorNoOpacity(); categoryOpacity = category.getOpacity(); applyOpacityToCategoryColor(); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java index 4889c7671d..ff0eaffcf1 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java @@ -5,13 +5,19 @@ import org.json.JSONException; import org.json.JSONObject; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; + /** * SponsorBlock user stats */ public class UserStats { - @NonNull + /** + * How long to cache user stats objects. + */ + private static final long STATS_EXPIRATION_MILLISECONDS = 60 * 60 * 1000; // 60 minutes. + + private final String privateUserId; public final String publicUserId; - @NonNull public final String userName; /** * "User reputation". Unclear how SB determines this value. @@ -26,7 +32,13 @@ public class UserStats { public final int viewCount; public final double minutesSaved; - public UserStats(@NonNull JSONObject json) throws JSONException { + /** + * When this stat was fetched. + */ + public final long fetchTime; + + public UserStats(String privateSbId, @NonNull JSONObject json) throws JSONException { + privateUserId = privateSbId; publicUserId = json.getString("userID"); userName = json.getString("userName"); reputation = (float)json.getDouble("reputation"); @@ -35,11 +47,23 @@ public UserStats(@NonNull JSONObject json) throws JSONException { totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount; viewCount = json.getInt("viewCount"); minutesSaved = json.getDouble("minutesSaved"); + fetchTime = System.currentTimeMillis(); + } + + public boolean isExpired() { + if (STATS_EXPIRATION_MILLISECONDS < System.currentTimeMillis() - fetchTime) { + return true; + } + + // User changed their SB private user id. + return !SponsorBlockSettings.userHasSBPrivateId() + || !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserId); } @NonNull @Override public String toString() { + // Do not include private user id in toString(). return "UserStats{" + "publicUserId='" + publicUserId + '\'' + ", userName='" + userName + '\'' 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 445b711dcf..fea7664f14 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 @@ -47,6 +47,9 @@ public class SBRequester { */ private static final int HTTP_STATUS_CODE_SUCCESS = 200; + @Nullable + private static volatile UserStats lastFetchedStats; + private SBRequester() { } @@ -181,6 +184,8 @@ public static void submitSegments(@NonNull String videoId, @NonNull String categ Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage())); } catch (Exception ex) { Logger.printException(() -> "failed to submit segments", ex); // Should never happen. + } finally { + lastFetchedStats = null; // Fetch updated stats if needed. } } @@ -252,9 +257,17 @@ private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, public static UserStats retrieveUserStats() { Utils.verifyOffMainThread(); try { - UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID())); - Logger.printDebug(() -> "user stats: " + stats); - return stats; + UserStats stats = lastFetchedStats; + if (stats != null && !stats.isExpired()) { + return stats; + } + + String privateUserID = SponsorBlockSettings.getSBPrivateUserID(); + UserStats fetchedStats = new UserStats(privateUserID, + getJSONObject(SBRoutes.GET_USER_STATS, privateUserID)); + Logger.printDebug(() -> "user stats: " + fetchedStats); + lastFetchedStats = fetchedStats; + return fetchedStats; } catch (IOException ex) { Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast } catch (Exception ex) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java new file mode 100644 index 0000000000..098f8d5994 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java @@ -0,0 +1,26 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import android.content.Context; +import android.util.AttributeSet; + +import app.revanced.extension.youtube.settings.preference.UrlLinkPreference; + +@SuppressWarnings("unused") +public class SponsorBlockAboutPreference extends UrlLinkPreference { + { + externalUrl = "https://sponsor.ajay.app"; + } + + public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public SponsorBlockAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public SponsorBlockAboutPreference(Context context) { + super(context); + } +} 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 new file mode 100644 index 0000000000..4ed3bf2389 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java @@ -0,0 +1,471 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.preference.*; +import android.text.Html; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference; + +/** + * Lots of old code that could be converted to a half dozen custom preferences, + * but instead it's wrapped in this group container and all logic is handled here. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class SponsorBlockPreferenceGroup extends PreferenceGroup { + + /** + * ReVanced settings were recently imported and the UI needs to be updated. + */ + public static boolean settingsImported; + + /** + * If the preferences have been created and added to this group. + */ + private boolean preferencesInitialized; + + private SwitchPreference sbEnabled; + private SwitchPreference addNewSegment; + private SwitchPreference votingEnabled; + private SwitchPreference autoHideSkipSegmentButton; + private SwitchPreference compactSkipButton; + private SwitchPreference squareLayout; + private SwitchPreference showSkipToast; + private SwitchPreference trackSkips; + private SwitchPreference showTimeWithoutSegments; + private SwitchPreference toastOnConnectionError; + + private ResettableEditTextPreference newSegmentStep; + private ResettableEditTextPreference minSegmentDuration; + private EditTextPreference privateUserId; + private EditTextPreference importExport; + private Preference apiUrl; + + private final List segmentCategories = new ArrayList<>(); + private PreferenceCategory segmentCategory; + + public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + @SuppressLint("MissingSuperCall") + protected View onCreateView(ViewGroup parent) { + // Title is not shown. + return new View(getContext()); + } + + private void updateUI() { + try { + Logger.printDebug(() -> "updateUI"); + + final boolean enabled = Settings.SB_ENABLED.get(); + if (!enabled) { + SponsorBlockViewController.hideAll(); + SegmentPlaybackController.setCurrentVideoId(null); + } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) { + SponsorBlockViewController.hideNewSegmentLayout(); + } + // Voting and add new segment buttons automatically show/hide themselves. + + SponsorBlockViewController.updateLayout(); + + sbEnabled.setChecked(enabled); + + addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get()); + addNewSegment.setEnabled(enabled); + + votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get()); + votingEnabled.setEnabled(enabled); + + autoHideSkipSegmentButton.setEnabled(enabled); + autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()); + + compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get()); + compactSkipButton.setEnabled(enabled); + + squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get()); + squareLayout.setEnabled(enabled); + + showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get()); + showSkipToast.setEnabled(enabled); + + toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get()); + toastOnConnectionError.setEnabled(enabled); + + trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get()); + trackSkips.setEnabled(enabled); + + showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get()); + showTimeWithoutSegments.setEnabled(enabled); + + newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString()); + newSegmentStep.setEnabled(enabled); + + minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString()); + minSegmentDuration.setEnabled(enabled); + + privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get()); + privateUserId.setEnabled(enabled); + + // If the user has a private user id, then include a subtext that mentions not to share it. + String importExportSummary = SponsorBlockSettings.userHasSBPrivateId() + ? str("revanced_sb_settings_ie_sum_warning") + : str("revanced_sb_settings_ie_sum"); + importExport.setSummary(importExportSummary); + + apiUrl.setEnabled(enabled); + importExport.setEnabled(enabled); + segmentCategory.setEnabled(enabled); + + for (SegmentCategoryListPreference category : segmentCategories) { + category.updateUI(); + } + } catch (Exception ex) { + Logger.printException(() -> "updateUI failure", ex); + } + } + + protected void onAttachedToActivity() { + try { + super.onAttachedToActivity(); + + if (preferencesInitialized) { + if (settingsImported) { + settingsImported = false; + updateUI(); + } + return; + } + + preferencesInitialized = true; + + Logger.printDebug(() -> "Creating settings preferences"); + Context context = getContext(); + SponsorBlockSettings.initialize(); + + sbEnabled = new SwitchPreference(context); + sbEnabled.setTitle(str("revanced_sb_enable_sb")); + sbEnabled.setSummary(str("revanced_sb_enable_sb_sum")); + addPreference(sbEnabled); + sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_ENABLED.save((Boolean) newValue); + updateUI(); + return true; + }); + + PreferenceCategory appearanceCategory = new PreferenceCategory(context); + appearanceCategory.setTitle(str("revanced_sb_appearance_category")); + addPreference(appearanceCategory); + + votingEnabled = new SwitchPreference(context); + votingEnabled.setTitle(str("revanced_sb_enable_voting")); + votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on")); + votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off")); + votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_VOTING_BUTTON.save((Boolean) newValue); + updateUI(); + return true; + }); + 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")); + compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off")); + compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue); + updateUI(); + return true; + }); + 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); + updateUI(); + return true; + }); + appearanceCategory.addPreference(squareLayout); + + 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(); + return true; + }); + appearanceCategory.addPreference(showSkipToast); + + showTimeWithoutSegments = new SwitchPreference(context); + showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without")); + showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on")); + showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off")); + showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue); + updateUI(); + return true; + }); + appearanceCategory.addPreference(showTimeWithoutSegments); + + segmentCategory = new PreferenceCategory(context); + segmentCategory.setTitle(str("revanced_sb_diff_segments")); + addPreference(segmentCategory); + + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category); + segmentCategories.add(categoryPreference); + segmentCategory.addPreference(categoryPreference); + } + + PreferenceCategory createSegmentCategory = new PreferenceCategory(context); + createSegmentCategory.setTitle(str("revanced_sb_create_segment_category")); + addPreference(createSegmentCategory); + + addNewSegment = new SwitchPreference(context); + addNewSegment.setTitle(str("revanced_sb_enable_create_segment")); + addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on")); + addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off")); + addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { + Boolean newValue = (Boolean) o; + if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) { + new AlertDialog.Builder(preference1.getContext()) + .setTitle(str("revanced_sb_guidelines_popup_title")) + .setMessage(str("revanced_sb_guidelines_popup_content")) + .setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null) + .setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) + .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true)) + .setCancelable(false) + .show(); + } + Settings.SB_CREATE_NEW_SEGMENT.save(newValue); + updateUI(); + return true; + }); + createSegmentCategory.addPreference(addNewSegment); + + newSegmentStep = new ResettableEditTextPreference(context); + newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP); + newSegmentStep.setTitle(str("revanced_sb_general_adjusting")); + newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum")); + newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); + newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> { + try { + final int newAdjustmentValue = Integer.parseInt(newValue.toString()); + if (newAdjustmentValue != 0) { + Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue); + return true; + } + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid new segment step", ex); + } + + Utils.showToastLong(str("revanced_sb_general_adjusting_invalid")); + updateUI(); + return false; + }); + createSegmentCategory.addPreference(newSegmentStep); + + Preference guidelinePreferences = new Preference(context); + guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title")); + guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum")); + guidelinePreferences.setOnPreferenceClickListener(preference1 -> { + openGuidelines(); + return true; + }); + createSegmentCategory.addPreference(guidelinePreferences); + + PreferenceCategory generalCategory = new PreferenceCategory(context); + generalCategory.setTitle(str("revanced_sb_general")); + addPreference(generalCategory); + + toastOnConnectionError = new SwitchPreference(context); + toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title")); + toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on")); + toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off")); + toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue); + updateUI(); + return true; + }); + generalCategory.addPreference(toastOnConnectionError); + + trackSkips = new SwitchPreference(context); + trackSkips.setTitle(str("revanced_sb_general_skipcount")); + trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on")); + trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off")); + trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue); + updateUI(); + return true; + }); + generalCategory.addPreference(trackSkips); + + minSegmentDuration = new ResettableEditTextPreference(context); + minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION); + minSegmentDuration.setTitle(str("revanced_sb_general_min_duration")); + minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum")); + minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { + try { + Float minTimeDuration = Float.valueOf(newValue.toString()); + Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration); + return true; + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid minimum segment duration", ex); + } + + Utils.showToastLong(str("revanced_sb_general_min_duration_invalid")); + updateUI(); + return false; + }); + generalCategory.addPreference(minSegmentDuration); + + privateUserId = new EditTextPreference(context) { + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { + Utils.setClipboard(getEditText().getText().toString()); + }); + } + }; + privateUserId.setTitle(str("revanced_sb_general_uuid")); + privateUserId.setSummary(str("revanced_sb_general_uuid_sum")); + privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> { + String newUUID = newValue.toString(); + if (!SponsorBlockSettings.isValidSBUserId(newUUID)) { + Utils.showToastLong(str("revanced_sb_general_uuid_invalid")); + return false; + } + + Settings.SB_PRIVATE_USER_ID.save(newUUID); + updateUI(); + return true; + }); + generalCategory.addPreference(privateUserId); + + apiUrl = new Preference(context); + apiUrl.setTitle(str("revanced_sb_general_api_url")); + apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum"))); + apiUrl.setOnPreferenceClickListener(preference1 -> { + EditText editText = new EditText(context); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + editText.setText(Settings.SB_API_URL.get()); + + DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { + if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { + Settings.SB_API_URL.resetToDefault(); + Utils.showToastLong(str("revanced_sb_api_url_reset")); + } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { + String serverAddress = editText.getText().toString(); + if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) { + Utils.showToastLong(str("revanced_sb_api_url_invalid")); + } else if (!serverAddress.equals(Settings.SB_API_URL.get())) { + Settings.SB_API_URL.save(serverAddress); + Utils.showToastLong(str("revanced_sb_api_url_changed")); + } + } + }; + new AlertDialog.Builder(context) + .setTitle(apiUrl.getTitle()) + .setView(editText) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_sb_reset"), urlChangeListener) + .setPositiveButton(android.R.string.ok, urlChangeListener) + .show(); + return true; + }); + generalCategory.addPreference(apiUrl); + + importExport = new EditTextPreference(context) { + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { + Utils.setClipboard(getEditText().getText().toString()); + }); + } + }; + importExport.setTitle(str("revanced_sb_settings_ie")); + // Summary is set in updateUI() + importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + importExport.getEditText().setAutofillHints((String) null); + importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); + importExport.setOnPreferenceClickListener(preference1 -> { + importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings()); + return true; + }); + importExport.setOnPreferenceChangeListener((preference1, newValue) -> { + SponsorBlockSettings.importDesktopSettings((String) newValue); + updateUI(); + return true; + }); + generalCategory.addPreference(importExport); + + Utils.setPreferenceTitlesToMultiLineIfNeeded(this); + + updateUI(); + } catch (Exception ex) { + Logger.printException(() -> "onAttachedToActivity failure", ex); + } + } + + private void openGuidelines() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); + getContext().startActivity(intent); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java new file mode 100644 index 0000000000..d26a6f6be9 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java @@ -0,0 +1,210 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static android.text.Html.fromHtml; +import static app.revanced.extension.shared.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; +import app.revanced.extension.youtube.sponsorblock.objects.UserStats; +import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; + +/** + * User skip stats. + * + * None of the preferences here show up in search results because + * a category cannot be added to another category for the search results. + * Additionally the stats must load remotely on a background thread which means the + * preferences are not available to collect for search when the settings first load. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory { + + public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void onAttachedToActivity() { + try { + super.onAttachedToActivity(); + + Logger.printDebug(() -> "Updating SB stats UI"); + final boolean enabled = Settings.SB_ENABLED.get(); + setEnabled(enabled); + removeAll(); + + if (!SponsorBlockSettings.userHasSBPrivateId()) { + // User has never voted or created any segments. Only local stats exist. + addLocalUserStats(); + return; + } + + Preference loadingPlaceholderPreference = new Preference(getContext()); + loadingPlaceholderPreference.setEnabled(false); + addPreference(loadingPlaceholderPreference); + + if (enabled) { + loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading")); + Utils.runOnBackgroundThread(() -> { + UserStats stats = SBRequester.retrieveUserStats(); + Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements + addUserStats(loadingPlaceholderPreference, stats); + addLocalUserStats(); + }); + }); + } else { + loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled")); + } + } catch (Exception ex) { + Logger.printException(() -> "onAttachedToActivity failure", ex); + } + } + + private void addUserStats(Preference loadingPlaceholder, @Nullable UserStats stats) { + Utils.verifyOnMainThread(); + try { + if (stats == null) { + loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure")); + return; + } + removeAll(); + Context context = getContext(); + + if (stats.totalSegmentCountIncludingIgnored > 0) { + // If user has not created any segments, there's no reason to set a username. + String userName = stats.userName; + EditTextPreference preference = new ResettableEditTextPreference(context); + preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName))); + preference.setSummary(str("revanced_sb_stats_username_change")); + preference.setText(userName); + preference.setOnPreferenceChangeListener((preference1, value) -> { + Utils.runOnBackgroundThread(() -> { + String newUserName = (String) value; + String errorMessage = SBRequester.setUsername(newUserName); + Utils.runOnMainThread(() -> { + if (errorMessage == null) { + preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName))); + preference.setText(newUserName); + Utils.showToastLong(str("revanced_sb_stats_username_changed")); + } else { + preference.setText(userName); // revert to previous + SponsorBlockUtils.showErrorDialog(errorMessage); + } + }); + }); + return true; + }); + addPreference(preference); + } + + { + // Number of segment submissions (does not include ignored segments). + Preference preference = new Preference(context); + String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount); + preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted))); + preference.setSummary(str("revanced_sb_stats_submissions_sum")); + if (stats.totalSegmentCountIncludingIgnored == 0) { + preference.setSelectable(false); + } else { + preference.setOnPreferenceClickListener(preference1 -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId)); + preference1.getContext().startActivity(i); + return true; + }); + } + addPreference(preference); + } + + { + // "user reputation". Usually not useful since it appears most users have zero reputation. + // But if there is a reputation then show it here. + Preference preference = new Preference(context); + preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation))); + preference.setSelectable(false); + if (stats.reputation != 0) { + addPreference(preference); + } + } + + { + // Time saved for other users. + Preference preference = new Preference(context); + + String stats_saved; + String stats_saved_sum; + if (stats.totalSegmentCountIncludingIgnored == 0) { + stats_saved = str("revanced_sb_stats_saved_zero"); + stats_saved_sum = str("revanced_sb_stats_saved_sum_zero"); + } else { + stats_saved = str("revanced_sb_stats_saved", + SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount)); + stats_saved_sum = str("revanced_sb_stats_saved_sum", + SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved))); + } + preference.setTitle(fromHtml(stats_saved)); + preference.setSummary(fromHtml(stats_saved_sum)); + preference.setOnPreferenceClickListener(preference1 -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sponsor.ajay.app/stats/")); + preference1.getContext().startActivity(i); + return false; + }); + addPreference(preference); + } + } catch (Exception ex) { + Logger.printException(() -> "addUserStats failure", ex); + } + } + + private void addLocalUserStats() { + // Time the user saved by using SB. + Preference preference = new Preference(getContext()); + Runnable updateStatsSelfSaved = () -> { + String formatted = SponsorBlockUtils.getNumberOfSkipsString( + Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get()); + preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted))); + + String formattedSaved = SponsorBlockUtils.getTimeSavedString( + Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000); + preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved))); + }; + updateStatsSelfSaved.run(); + + preference.setOnPreferenceClickListener(preference1 -> { + new AlertDialog.Builder(preference1.getContext()) + .setTitle(str("revanced_sb_stats_self_saved_reset_title")) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault(); + Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault(); + updateStatsSelfSaved.run(); + }) + .setNegativeButton(android.R.string.no, null).show(); + return true; + }); + + addPreference(preference); + } +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 1efb1246b5..96327ce506 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -1440,8 +1440,10 @@ public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen : public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getRETURN_YOUTUBE_DISLIKE ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getSPONSORBLOCK ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt index 1f33163966..bfedf0d1c1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -6,7 +6,10 @@ import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.IntentPreference +import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch @@ -15,15 +18,18 @@ import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_10_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch -import app.revanced.patches.youtube.misc.settings.addSettingPreference -import app.revanced.patches.youtube.misc.settings.newIntent +import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.shared.conversionContextFingerprintToString import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.revanced.patches.youtube.video.videoid.hookVideoId import app.revanced.patches.youtube.video.videoid.videoIdPatch -import app.revanced.util.* +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findFreeRegister +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.returnLate import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @@ -68,15 +74,24 @@ val returnYouTubeDislikePatch = bytecodePatch( execute { addResources("youtube", "layout.returnyoutubedislike.returnYouTubeDislikePatch") - addSettingPreference( - IntentPreference( - key = "revanced_settings_screen_09", - titleKey = "revanced_ryd_settings_title", - summaryKey = null, - icon = "@drawable/revanced_settings_screen_09_ryd", - layout = "@layout/preference_with_icon", - intent = newIntent("revanced_ryd_settings_intent"), + PreferenceScreen.RETURN_YOUTUBE_DISLIKE.addPreferences( + SwitchPreference("revanced_ryd_enabled"), + SwitchPreference("revanced_ryd_shorts"), + SwitchPreference("revanced_ryd_dislike_percentage"), + SwitchPreference("revanced_ryd_compact_layout"), + SwitchPreference("revanced_ryd_estimated_like"), + SwitchPreference("revanced_ryd_toast_on_connection_error"), + NonInteractivePreference( + key = "revanced_ryd_attribution", + tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeAboutPreference", + selectable = true, ), + PreferenceCategory( + key = "revanced_ryd_statistics_category", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = emptySet(), // Preferences are added by custom class at runtime. + tag = "app.revanced.extension.youtube.returnyoutubedislike.ui.ReturnYouTubeDislikeDebugStatsPreferenceCategory" + ) ) // region Inject newVideoLoaded event handler to update dislikes when a new video is loaded. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt index 832b70dcf4..a4ae22d35c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt @@ -12,12 +12,13 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.patches.shared.misc.settings.preference.IntentPreference +import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playercontrols.* import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch -import app.revanced.patches.youtube.misc.settings.addSettingPreference -import app.revanced.patches.youtube.misc.settings.newIntent +import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.shared.* import app.revanced.patches.youtube.video.information.onCreateHook @@ -43,15 +44,32 @@ private val sponsorBlockResourcePatch = resourcePatch { execute { addResources("youtube", "layout.sponsorblock.sponsorBlockResourcePatch") - addSettingPreference( - IntentPreference( - key = "revanced_settings_screen_10", - titleKey = "revanced_sb_settings_title", - summaryKey = null, - icon = "@drawable/revanced_settings_screen_10_sb", - layout = "@layout/preference_with_icon", - intent = newIntent("revanced_sb_settings_intent"), + PreferenceScreen.SPONSORBLOCK.addPreferences( + // SB setting is old code with lots of custom preferences and updating behavior. + // Added as a preference group and not a fragment so the preferences are searchable. + PreferenceCategory( + key = "revanced_settings_screen_10_sponsorblock", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = emptySet(), // Preferences are added by custom class at runtime. + tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup" ), + PreferenceCategory( + key = "revanced_sb_stats", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = emptySet(), // Preferences are added by custom class at runtime. + tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockStatsPreferenceCategory" + ), + PreferenceCategory( + key = "revanced_sb_about", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + NonInteractivePreference( + key = "revanced_sb_about_api", + tag = "app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockAboutPreference", + selectable = true, + ) + ) + ) ) arrayOf( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt index 7dda5c3085..a6cf8283ab 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt @@ -54,6 +54,7 @@ val changeStartPagePatch = bytecodePatch( ListPreference( key = "revanced_change_start_page", summaryKey = null, + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" ), SwitchPreference("revanced_change_start_page_always") ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt index ec7d89363e..06a28e7f34 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt @@ -74,6 +74,7 @@ private val settingsResourcePatch = resourcePatch { arrayOf( ResourceGroup("drawable", + "revanced_settings_cursor.xml", "revanced_settings_icon.xml", "revanced_settings_screen_00_about.xml", "revanced_settings_screen_01_ads.xml", @@ -84,12 +85,16 @@ private val settingsResourcePatch = resourcePatch { "revanced_settings_screen_06_shorts.xml", "revanced_settings_screen_07_seekbar.xml", "revanced_settings_screen_08_swipe_controls.xml", - "revanced_settings_screen_09_ryd.xml", - "revanced_settings_screen_10_sb.xml", + "revanced_settings_screen_09_return_youtube_dislike.xml", + "revanced_settings_screen_10_sponsorblock.xml", "revanced_settings_screen_11_misc.xml", "revanced_settings_screen_12_video.xml", ), - ResourceGroup("layout", "revanced_settings_with_toolbar.xml"), + ResourceGroup("layout", + "revanced_preference_with_icon_no_search_result.xml", + "revanced_search_suggestion_item.xml", + "revanced_settings_with_toolbar.xml"), + ResourceGroup("menu", "revanced_search_menu.xml") ).forEach { resourceGroup -> copyResources("settings", resourceGroup) } @@ -188,6 +193,7 @@ val settingsPatch = bytecodePatch( } PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_settings_search_history"), SwitchPreference("revanced_show_menu_icons") ) @@ -201,7 +207,8 @@ val settingsPatch = bytecodePatch( ), ListPreference( key = "revanced_language", - summaryKey = null + summaryKey = null, + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" ) ) @@ -347,10 +354,20 @@ object PreferenceScreen : BasePreferenceScreen() { layout = "@layout/preference_with_icon", sorting = Sorting.UNSORTED, ) - - // RYD and SB are items 9 and 10. - // Menus are added in their own patch because they use an Intent and not a Screen. - + val RETURN_YOUTUBE_DISLIKE = Screen( + key = "revanced_settings_screen_09_return_youtube_dislike", + summaryKey = null, + icon = "@drawable/revanced_settings_screen_09_return_youtube_dislike", + layout = "@layout/preference_with_icon", + sorting = Sorting.UNSORTED, + ) + val SPONSORBLOCK = Screen( + key = "revanced_settings_screen_10_sponsorblock", + summaryKey = null, + icon = "@drawable/revanced_settings_screen_10_sponsorblock", + layout = "@layout/preference_with_icon", + sorting = Sorting.UNSORTED, + ) val MISC = Screen( key = "revanced_settings_screen_11_misc", summaryKey = null, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index f75eef3280..9fbf5ddca6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -62,7 +62,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch({ summaryKey = null, // Language strings are declared in Setting patch. entriesKey = "revanced_language_entries", - entryValuesKey = "revanced_language_entry_values" + entryValuesKey = "revanced_language_entry_values", + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" ), SwitchPreference("revanced_spoof_video_streams_ios_force_avc"), SwitchPreference("revanced_spoof_streaming_data_stats_for_nerds"), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt index ad6ee4a6cf..9f601c74ab 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt @@ -38,6 +38,7 @@ internal val rememberPlaybackSpeedPatch = bytecodePatch { // Entries and values are set by the extension code based on the actual speeds available. entriesKey = null, entryValuesKey = null, + tag = "app.revanced.extension.youtube.settings.preference.CustomVideoSpeedListPreference" ), SwitchPreference("revanced_remember_playback_speed_last_selected") ) diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 7005ed47ac..bddb105ed5 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -64,35 +64,6 @@ fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): In val instruction = getInstruction(i) val instructionRegisters = instruction.registersUsed - if (instruction.isReturnInstruction) { - usedRegisters.addAll(instructionRegisters) - - // Use lowest register that hasn't been encountered. - val freeRegister = (0 until implementation!!.registerCount).find { - it !in usedRegisters - } - if (freeRegister != null) { - return freeRegister - } - if (bestFreeRegisterFound != null) { - return bestFreeRegisterFound - } - - // Somehow every method register was read from before any register was wrote to. - // In practice this never occurs. - throw IllegalArgumentException("Could not find a free register from startIndex: " + - "$startIndex excluding: $registersToExclude") - } - - if (instruction.isBranchInstruction) { - if (bestFreeRegisterFound != null) { - return bestFreeRegisterFound - } - // This method is simple and does not follow branching. - throw IllegalArgumentException("Encountered a branch statement before a free register could be found") - } - - val writeRegister = instruction.writeRegister if (writeRegister != null) { if (writeRegister !in usedRegisters) { @@ -113,6 +84,32 @@ fun Method.findFreeRegister(startIndex: Int, vararg registersToExclude: Int): In } usedRegisters.addAll(instructionRegisters) + + if (instruction.isBranchInstruction) { + if (bestFreeRegisterFound != null) { + return bestFreeRegisterFound + } + // This method is simple and does not follow branching. + throw IllegalArgumentException("Encountered a branch statement before a free register could be found") + } + + if (instruction.isReturnInstruction) { + // Use lowest register that hasn't been encountered. + val freeRegister = (0 until implementation!!.registerCount).find { + it !in usedRegisters + } + if (freeRegister != null) { + return freeRegister + } + if (bestFreeRegisterFound != null) { + return bestFreeRegisterFound + } + + // Somehow every method register was read from before any register was wrote to. + // In practice this never occurs. + throw IllegalArgumentException("Could not find a free register from startIndex: " + + "$startIndex excluding: $registersToExclude") + } } // Some methods can have array payloads at the end of the method after a return statement. diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 98ce9cb162..90e9e1ddab 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -42,6 +42,10 @@ Second \"item\" text" ReVanced settings reset to default Imported %d settings Import failed: %s + Search settings + No results found for \'%s\' + Try another keyword + Remove from search history? Show ReVanced setting icons Setting icons are shown Setting icons are not shown @@ -145,11 +149,15 @@ Tap the continue button and allow optimization changes." Shorts Seekbar Swipe controls + Return YouTube Dislike Miscellaneous Video Restore old settings menus Old settings menus are shown Old settings menus are not shown + Show settings search history + Settings search history is shown + Settings search history is not shown Disable Shorts background play @@ -883,7 +891,6 @@ Settings → Playback → Autoplay next video" Player overlay opacity must be between 0-100 - Return YouTube Dislike Dislikes are temporarily not available (API timed out) Dislikes are not available (status %d) @@ -893,11 +900,11 @@ Settings → Playback → Autoplay next video" Reload video to vote using Return YouTube Dislike Hidden by owner - Return YouTube Dislike - Dislikes are shown - Dislikes are not shown + Return YouTube Dislike + Dislikes are shown + Dislikes are not shown Show dislikes on Shorts - "Dislikes on Shorts are shown + "Dislikes on Shorts are shown Limitation: Dislikes may not appear in incognito mode" Dislikes on Shorts are not shown @@ -914,7 +921,6 @@ Limitation: Dislikes may not appear in incognito mode" Show a toast if API is not available Toast is shown if Return YouTube Dislike is not available Toast is not shown if Return YouTube Dislike is not available - About ReturnYouTubeDislike.com Data is provided by the Return YouTube Dislike API. Tap here to learn more @@ -956,7 +962,7 @@ This feature works best with a video quality of 720p or lower and when using a v Seekbar thumbnails will appear in fullscreen - SponsorBlock + SponsorBlock Enable SponsorBlock SponsorBlock is a crowdsourced system for skipping annoying parts of YouTube videos Appearance @@ -1123,7 +1129,7 @@ Ready to submit?" Edit timing of segment manually Do you want to edit the timing for the start or end of the segment? Invalid time given - Stats + Stats Stats are temporarily not available (API is down) Loading... @@ -1152,9 +1158,9 @@ Ready to submit?" Invalid color code Reset color Reset - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms Layout form factor diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_cursor.xml b/patches/src/main/resources/settings/drawable/revanced_settings_cursor.xml new file mode 100644 index 0000000000..8443ab1e6b --- /dev/null +++ b/patches/src/main/resources/settings/drawable/revanced_settings_cursor.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_screen_09_ryd.xml b/patches/src/main/resources/settings/drawable/revanced_settings_screen_09_return_youtube_dislike.xml similarity index 100% rename from patches/src/main/resources/settings/drawable/revanced_settings_screen_09_ryd.xml rename to patches/src/main/resources/settings/drawable/revanced_settings_screen_09_return_youtube_dislike.xml diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_screen_10_sb.xml b/patches/src/main/resources/settings/drawable/revanced_settings_screen_10_sponsorblock.xml similarity index 100% rename from patches/src/main/resources/settings/drawable/revanced_settings_screen_10_sb.xml rename to patches/src/main/resources/settings/drawable/revanced_settings_screen_10_sponsorblock.xml diff --git a/patches/src/main/resources/settings/host/values/styles.xml b/patches/src/main/resources/settings/host/values/styles.xml index 7c8412f9f6..bf13d53ac0 100644 --- a/patches/src/main/resources/settings/host/values/styles.xml +++ b/patches/src/main/resources/settings/host/values/styles.xml @@ -22,4 +22,8 @@ true @android:color/transparent + diff --git a/patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml b/patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml new file mode 100644 index 0000000000..c81406ca69 --- /dev/null +++ b/patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml b/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml new file mode 100644 index 0000000000..3d65900d2c --- /dev/null +++ b/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml b/patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml index 238dc5f798..14c2d0e130 100644 --- a/patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml +++ b/patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml @@ -1,34 +1,63 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - - + android:layout_height="match_parent" + android:fitsSystemWindows="true" + android:orientation="vertical" + android:transitionGroup="true"> - + + android:background="@color/yt_white1" + android:elevation="0dp"> + + + + + + + + + + + + + android:id="@+id/revanced_settings_fragments" + android:layout_width="match_parent" + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/patches/src/main/resources/settings/menu/revanced_search_menu.xml b/patches/src/main/resources/settings/menu/revanced_search_menu.xml new file mode 100644 index 0000000000..de562b2dd5 --- /dev/null +++ b/patches/src/main/resources/settings/menu/revanced_search_menu.xml @@ -0,0 +1,8 @@ + +

+ + \ No newline at end of file