feat(YouTube - Settings): Add ability to search in settings (#4881)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD 2025-05-17 10:40:16 +03:00 committed by GitHub
parent a082914e76
commit aca8b207c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 2346 additions and 1185 deletions

View File

@ -371,7 +371,7 @@ public class Utils {
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 class Utils {
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 @@ public class Utils {
/**
* 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 class Utils {
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 class Utils {
}
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));
}
}

View File

@ -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 enum AppLanguage {
return Locale.getDefault();
}
return Locale.forLanguageTag(language);
return locale;
}
}

View File

@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
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);
}

View File

@ -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<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
SortedMap<String, Pair<CharSequence, CharSequence>> lastEntries = new TreeMap<>();
for (int i = 0; i < entrySize; i++) {
Pair<CharSequence, CharSequence> 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<CharSequence, CharSequence> pair : firstEntries) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
for (Pair<CharSequence, CharSequence> 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());
}
}

View File

@ -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 class ThemeHelper {
window.setNavigationBarContrastEnforced(true);
}
}
/**
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
* <p>
* 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);
}
}

View File

@ -18,7 +18,6 @@ import app.revanced.extension.shared.Logger;
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 @@ public class ReturnYouTubeDislikePatch {
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);

View File

@ -36,7 +36,7 @@ public final class WideSearchbarPatch {
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);

View File

@ -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 @@ import app.revanced.extension.youtube.settings.Settings;
@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.
* <p>
@ -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 @@ public class CustomPlaybackSpeedPatch {
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.
*/

View File

@ -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 @@ public final class SeekbarColorPatch {
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));
}
}

View File

@ -235,7 +235,7 @@ public class ReturnYouTubeDislike {
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 @@ public class ReturnYouTubeDislike {
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.
*/

View File

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

View File

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

View File

@ -11,8 +11,6 @@ import android.view.ViewGroup;
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.ThemeHelper;
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 class LicenseActivityHook {
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 class LicenseActivityHook {
}
@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 @@ public class LicenseActivityHook {
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 @@ public class LicenseActivityHook {
}
setToolbarLayoutParams(toolbar);
// Add Search Icon and EditText for ReVancedPreferenceFragment only.
if (fragment instanceof ReVancedPreferenceFragment) {
SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
}
toolBarParent.addView(toolbar, 0);
}
}

View File

@ -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<String> 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<String> {
public SearchHistoryAdapter(Context context, List<String> 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;
}
}
}

View File

@ -25,6 +25,8 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehavi
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;
@ -218,6 +220,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<FormFactor> 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);
@ -341,19 +345,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));
@ -417,12 +419,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
@ -465,10 +465,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

View File

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

View File

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

View File

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

View File

@ -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.graphics.drawable.Drawable;
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<AbstractPreferenceSearchData<?>> allPreferences = new ArrayList<>();
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
@ -47,85 +80,140 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
}
/**
* 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;
if (entrySize != entryValues.length) {
// Xml array declaration has a missing/extra entry.
throw new IllegalStateException();
}
List<Pair<String, String>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
List<Pair<String, String>> pairsToSort = new ArrayList<>(entrySize);
for (int i = 0; i < entrySize; i++) {
Pair<String, String> pair = new Pair<>(entries[i].toString(), entryValues[i].toString());
if (i < firstEntriesToPreserve) {
firstPairs.add(pair);
} else {
pairsToSort.add(pair);
}
}
pairsToSort.sort((pair1, pair2)
-> pair1.first.compareToIgnoreCase(pair2.first));
CharSequence[] sortedEntries = new CharSequence[entrySize];
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
int i = 0;
for (Pair<String, String> pair : firstPairs) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
for (Pair<String, String> pair : pairsToSort) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
listPreference.setEntries(sortedEntries);
listPreference.setEntryValues(sortedEntryValues);
}
@Override
protected void initialize() {
super.initialize();
try {
setPreferenceScreenToolbar(getPreferenceScreen());
preferenceScreen = getPreferenceScreen();
Utils.sortPreferenceGroups(preferenceScreen);
// 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);
// 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));
}
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
sortPreferenceListMenu(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE);
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
setPreferenceScreenToolbar(preferenceScreen);
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
private void sortPreferenceListMenu(EnumSetting<?> setting) {
Preference preference = findPreference(setting.key);
if (preference instanceof ListPreference languagePreference) {
sortListPreferenceByValues(languagePreference, 1);
/**
* 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");
// 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);
}
}
/**
* 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)) {
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);
}
allPreferences.add(data);
}
if (preference instanceof PreferenceGroup subGroup) {
collectPreferences(subGroup, includeDepth, currentDepth + 1);
}
}
}
/**
* Filters the preferences using the given query string and applies highlighting.
*/
public void filterPreferences(String query) {
preferenceScreen.removeAll();
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));
}
for (AbstractPreferenceSearchData<?> data : allPreferences) {
data.clearHighlighting();
}
return;
}
// Navigation path -> Category
Map<String, PreferenceCategory> 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 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
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 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
}
}
}
@SuppressWarnings("deprecation")
class AbstractPreferenceSearchData<T extends Preference> {
/**
* @return The navigation path for the given preference, such as "Player > Action buttons".
*/
private static String getPreferenceNavigationString(Preference preference) {
Deque<CharSequence> 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<Preference> {
@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<SwitchPreference> {
@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<ListPreference> {
@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);
}
}

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import app.revanced.extension.shared.Logger;
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) {

View File

@ -55,7 +55,7 @@ public class SegmentCategoryListPreference extends ListPreference {
: CategoryBehaviour.getBehaviorKeyValues());
setSummary(category.description.toString());
updateTitleFromCategory();
updateUI();
}
@Override
@ -202,7 +202,7 @@ public class SegmentCategoryListPreference extends ListPreference {
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 @@ public class SegmentCategoryListPreference extends ListPreference {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
updateTitleFromCategory();
updateUI();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
@ -251,7 +251,7 @@ public class SegmentCategoryListPreference extends ListPreference {
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
}
private void updateTitleFromCategory() {
public void updateUI() {
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
applyOpacityToCategoryColor();

View File

@ -5,13 +5,19 @@ import androidx.annotation.NonNull;
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 class UserStats {
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 + '\''

View File

@ -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 class SBRequester {
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 @@ public class SBRequester {
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) {

View File

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

View File

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

View File

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

View File

@ -1472,8 +1472,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;
}

View File

@ -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.

View File

@ -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(

View File

@ -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")
)

View File

@ -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,

View File

@ -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"),

View File

@ -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")
)

View File

@ -65,35 +65,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) {
@ -114,6 +85,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.

View File

@ -42,6 +42,10 @@ Second \"item\" text"</string>
<string name="revanced_settings_import_reset">ReVanced settings reset to default</string>
<string name="revanced_settings_import_success">Imported %d settings</string>
<string name="revanced_settings_import_failure_parse">Import failed: %s</string>
<string name="revanced_settings_search_hint">Search settings</string>
<string name="revanced_settings_search_no_results_title">No results found for \'%s\'</string>
<string name="revanced_settings_search_no_results_summary">Try another keyword</string>
<string name="revanced_settings_search_remove_message">Remove from search history?</string>
<string name="revanced_show_menu_icons_title">Show ReVanced setting icons</string>
<string name="revanced_show_menu_icons_summary_on">Setting icons are shown</string>
<string name="revanced_show_menu_icons_summary_off">Setting icons are not shown</string>
@ -145,11 +149,15 @@ Tap the continue button and allow optimization changes."</string>
<string name="revanced_settings_screen_06_shorts_title">Shorts</string>
<string name="revanced_settings_screen_07_seekbar_title">Seekbar</string>
<string name="revanced_settings_screen_08_swipe_controls_title">Swipe controls</string>
<string name="revanced_settings_screen_09_return_youtube_dislike_title" translatable="false">Return YouTube Dislike</string>
<string name="revanced_settings_screen_11_misc_title">Miscellaneous</string>
<string name="revanced_settings_screen_12_video_title">Video</string>
<string name="revanced_restore_old_settings_menus_title">Restore old settings menus</string>
<string name="revanced_restore_old_settings_menus_summary_on">Old settings menus are shown</string>
<string name="revanced_restore_old_settings_menus_summary_off">Old settings menus are not shown</string>
<string name="revanced_settings_search_history_title">Show settings search history</string>
<string name="revanced_settings_search_history_summary_on">Settings search history is shown</string>
<string name="revanced_settings_search_history_summary_off">Settings search history is not shown</string>
</patch>
<patch id="misc.backgroundplayback.backgroundPlaybackPatch">
<string name="revanced_shorts_disable_background_playback_title">Disable Shorts background play</string>
@ -888,7 +896,6 @@ Settings → Playback → Autoplay next video"</string>
<string name="revanced_player_overlay_opacity_invalid_toast">Player overlay opacity must be between 0-100</string>
</patch>
<patch id="layout.returnyoutubedislike.returnYouTubeDislikePatch">
<string name="revanced_ryd_settings_title" translatable="false">Return YouTube Dislike</string>
<!-- Toast shown if network connection times out. Translations of this should not be longer than the original English or the text can be clipped and not entirely shown. -->
<string name="revanced_ryd_failure_connection_timeout">Dislikes are temporarily not available (API timed out)</string>
<string name="revanced_ryd_failure_connection_status_code">Dislikes are not available (status %d)</string>
@ -898,11 +905,11 @@ Settings → Playback → Autoplay next video"</string>
<string name="revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted">Reload video to vote using Return YouTube Dislike</string>
<!-- Video likes have been set to hidden by the video uploader. -->
<string name="revanced_ryd_video_likes_hidden_by_video_owner">Hidden by owner</string>
<string name="revanced_ryd_enable_title" translatable="false">Return YouTube Dislike</string>
<string name="revanced_ryd_enable_summary_on">Dislikes are shown</string>
<string name="revanced_ryd_enable_summary_off">Dislikes are not shown</string>
<string name="revanced_ryd_enabled_title" translatable="false">Return YouTube Dislike</string>
<string name="revanced_ryd_enabled_summary_on">Dislikes are shown</string>
<string name="revanced_ryd_enabled_summary_off">Dislikes are not shown</string>
<string name="revanced_ryd_shorts_title">Show dislikes on Shorts</string>
<string name="revanced_ryd_shorts_summary_on_disclaimer">"Dislikes on Shorts are shown
<string name="revanced_ryd_shorts_summary_on">"Dislikes on Shorts are shown
Limitation: Dislikes may not appear in incognito mode"</string>
<string name="revanced_ryd_shorts_summary_off">Dislikes on Shorts are not shown</string>
@ -919,7 +926,6 @@ Limitation: Dislikes may not appear in incognito mode"</string>
<string name="revanced_ryd_toast_on_connection_error_title">Show a toast if API is not available</string>
<string name="revanced_ryd_toast_on_connection_error_summary_on">Toast is shown if Return YouTube Dislike is not available</string>
<string name="revanced_ryd_toast_on_connection_error_summary_off">Toast is not shown if Return YouTube Dislike is not available</string>
<string name="revanced_ryd_about">About</string>
<string name="revanced_ryd_attribution_title" translatable="false">ReturnYouTubeDislike.com</string>
<string name="revanced_ryd_attribution_summary">Data is provided by the Return YouTube Dislike API. Tap here to learn more</string>
<!-- Statistic strings are shown in the settings only when ReVanced debug mode is enabled. Typical users will never see these. -->
@ -961,7 +967,7 @@ This feature works best with a video quality of 720p or lower and when using a v
<string name="revanced_restore_old_seekbar_thumbnails_summary_off">Seekbar thumbnails will appear in fullscreen</string>
</patch>
<patch id="layout.sponsorblock.sponsorBlockResourcePatch">
<string name="revanced_sb_settings_title" translatable="false">SponsorBlock</string>
<string name="revanced_settings_screen_10_sponsorblock_title" translatable="false">SponsorBlock</string>
<string name="revanced_sb_enable_sb">Enable SponsorBlock</string>
<string name="revanced_sb_enable_sb_sum">SponsorBlock is a crowdsourced system for skipping annoying parts of YouTube videos</string>
<string name="revanced_sb_appearance_category">Appearance</string>
@ -1128,7 +1134,7 @@ Ready to submit?"</string>
<string name="revanced_sb_new_segment_edit_by_hand_title">Edit timing of segment manually</string>
<string name="revanced_sb_new_segment_edit_by_hand_content">Do you want to edit the timing for the start or end of the segment?</string>
<string name="revanced_sb_new_segment_edit_by_hand_parse_error">Invalid time given</string>
<string name="revanced_sb_stats">Stats</string>
<string name="revanced_sb_stats_title">Stats</string>
<!-- Shown in the settings preferences, and translations can be any text length. -->
<string name="revanced_sb_stats_connection_failure">Stats are temporarily not available (API is down)</string>
<string name="revanced_sb_stats_loading">Loading...</string>
@ -1157,9 +1163,9 @@ Ready to submit?"</string>
<string name="revanced_sb_color_invalid">Invalid color code</string>
<string name="revanced_sb_reset_color">Reset color</string>
<string name="revanced_sb_reset">Reset</string>
<string name="revanced_sb_about">About</string>
<string name="revanced_sb_about_api" translatable="false">sponsor.ajay.app</string>
<string name="revanced_sb_about_api_sum">Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms</string>
<string name="revanced_sb_about_title">About</string>
<string name="revanced_sb_about_api_title" translatable="false">sponsor.ajay.app</string>
<string name="revanced_sb_about_api_summary">Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms</string>
</patch>
<patch id="layout.formfactor.changeFormFactorPatch">
<string name="revanced_change_form_factor_title">Layout form factor</string>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:id="@+id/revanced_settings_cursor">
<solid android:color="?ytTextPrimary" />
<size android:width="1dp" />
</shape>

View File

@ -22,4 +22,8 @@
<item name="android:windowIsTranslucent">true</item>
<item name="android:background">@android:color/transparent</item>
</style>
<style name="revanced_searchbar_cursor">
<item name="android:textCursorDrawable">@drawable/revanced_settings_cursor</item>
<item name="android:textSize">16sp</item>
</style>
</resources>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="54dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="18dp"
android:contentDescription="@null" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:paddingVertical="8dp">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/medium_font_size"
android:textColor="?ytTextPrimary"
android:textAlignment="viewStart" />
<TextView
android:id="@android:id/summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/small_font_size"
android:textColor="?ytTextSecondary"
android:textAlignment="viewStart"
android:maxLines="2"
android:ellipsize="end" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:gravity="center_vertical">
<!-- History icon -->
<ImageView
android:id="@+id/history_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:contentDescription="History icon"
android:src="@drawable/yt_outline_arrow_time_vd_theme_24" />
<!-- Suggestion text -->
<TextView
android:id="@+id/suggestion_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>

View File

@ -1,34 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
android:transitionGroup="true">
<FrameLayout
android:id="@+id/revanced_toolbar_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/yt_white1"
android:elevation="0dp">
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
android:transitionGroup="true">
<android.support.v7.widget.Toolbar
android:id="@+id/revanced_toolbar"
<!-- Parent container for Toolbar and SearchView -->
<FrameLayout
android:id="@+id/revanced_toolbar_parent"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/ytBrandBackgroundSolid"
app:navigationIcon="@drawable/yt_outline_arrow_left_black_24"
app:title="@string/revanced_settings_title" />
android:background="@color/yt_white1"
android:elevation="0dp">
<!-- Toolbar -->
<android.support.v7.widget.Toolbar
android:id="@+id/revanced_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/ytBrandBackgroundSolid"
app:navigationIcon="@drawable/yt_outline_arrow_left_black_24"
app:title="@string/revanced_settings_title" />
<!-- Container for SearchView -->
<FrameLayout
android:id="@+id/revanced_search_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:visibility="gone">
<android.widget.SearchView
android:id="@+id/revanced_search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_gravity="center_vertical"
android:iconifiedByDefault="false"
android:searchIcon="@null"
android:queryBackground="@null"
android:theme="@style/revanced_searchbar_cursor"
android:padding="2dp" />
</FrameLayout>
</FrameLayout>
<!-- Container for fragments -->
<FrameLayout
android:id="@+id/revanced_settings_fragments"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:id="@+id/revanced_settings_fragments"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</merge>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_search"
android:icon="@drawable/yt_outline_search_black_24"
android:title=""
android:showAsAction="always"/>
</menu>