chore: Merge branch dev to main (#5160)

This commit is contained in:
LisoUseInAIKyrios
2025-06-20 12:14:29 +04:00
committed by GitHub
218 changed files with 3130 additions and 3415 deletions

View File

@ -1,3 +1,59 @@
# [5.28.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.7...v5.28.0-dev.8) (2025-06-19)
### Bug Fixes
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440))
# [5.28.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.6...v5.28.0-dev.7) (2025-06-18)
### Bug Fixes
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772))
# [5.28.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.5...v5.28.0-dev.6) (2025-06-17)
### Bug Fixes
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0))
# [5.28.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.4...v5.28.0-dev.5) (2025-06-17)
### Bug Fixes
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0))
# [5.28.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.3...v5.28.0-dev.4) (2025-06-13)
### Features
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01))
# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11)
### Bug Fixes
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2))
# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11)
### Bug Fixes
* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59))
# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11)
### Features
* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd))
# [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09) # [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09)

View File

@ -1,16 +1,24 @@
package app.revanced.extension.messenger.metaai; package app.revanced.extension.messenger.metaai;
import java.util.*;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class RemoveMetaAIPatch { public class RemoveMetaAIPatch {
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
public static boolean overrideBooleanFlag(long id, boolean value) { public static boolean overrideBooleanFlag(long id, boolean value) {
// This catches all flag IDs related to Meta AI. try {
// The IDs change slightly with every update, if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
// so to work around this, IDs from different versions were compared if (loggedIDs.add(id))
// to find what they have in common, which turned out to be those first bits. Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
// TODO: Find the specific flags that we care about and patch the code they control instead.
if ((id & 0x7FFFFFC000000000L) == 0x810A8000000000L) {
return false; return false;
} }
} catch (Exception ex) {
Logger.printException(() -> "overrideBooleanFlag failure", ex);
}
return value; return value;
} }

View File

@ -5,7 +5,7 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.Dialog;
import android.app.SearchManager; import android.app.SearchManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
@ -15,6 +15,8 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.Settings; import android.provider.Settings;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
@ -26,6 +28,7 @@ import java.util.Locale;
import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route; import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class GmsCoreSupport { public class GmsCoreSupport {
@ -78,13 +81,27 @@ public class GmsCoreSupport {
// Use a delay to allow the activity to finish initializing. // Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme. // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> { Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("gms_core_dialog_title"), // Title.
str(dialogMessageRef), // Message.
null, // No EditText.
str(positiveButtonTextRef), // OK button text.
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
null, // No Cancel button action.
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
// Do not set cancelable to false, to allow using back button to skip the action, // Do not set cancelable to false, to allow using back button to skip the action,
// just in case the battery change can never be satisfied. // just in case the battery change can never be satisfied.
var dialog = new AlertDialog.Builder(context) dialog.setCancelable(true);
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef)) // Show the dialog
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
.create();
Utils.showDialog(context, dialog); Utils.showDialog(context, dialog);
}, 100); }, 100);
} }

View File

@ -19,7 +19,8 @@ import app.revanced.extension.shared.settings.preference.LogBufferManager;
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB), * ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible thru {@link LogBufferManager}. * and additionally accessible thru {@link LogBufferManager}.
* *
* All methods are thread safe. * All methods are thread safe, and are safe to call even
* if {@link Utils#getContext()} is not available.
*/ */
public class Logger { public class Logger {
@ -138,6 +139,20 @@ public class Logger {
} }
} }
private static boolean shouldLogDebug() {
// If the app is still starting up and the context is not yet set,
// then allow debug logging regardless what the debug setting actually is.
return Utils.context == null || DEBUG.get();
}
private static boolean shouldShowErrorToast() {
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
}
private static boolean includeStackTrace() {
return Utils.context != null && DEBUG_STACKTRACE.get();
}
/** /**
* Logs debug messages under the outer class name of the code calling this method. * Logs debug messages under the outer class name of the code calling this method.
* <p> * <p>
@ -157,8 +172,8 @@ public class Logger {
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled. * building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/ */
public static void printDebug(LogMessage message, @Nullable Exception ex) { public static void printDebug(LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) { if (shouldLogDebug()) {
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false); logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
} }
} }
@ -173,7 +188,7 @@ public class Logger {
* Logs information messages using the outer class name of the code calling this method. * Logs information messages using the outer class name of the code calling this method.
*/ */
public static void printInfo(LogMessage message, @Nullable Exception ex) { public static void printInfo(LogMessage message, @Nullable Exception ex) {
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false); logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
} }
/** /**
@ -194,22 +209,6 @@ public class Logger {
* @param ex exception (optional) * @param ex exception (optional)
*/ */
public static void printException(LogMessage message, @Nullable Throwable ex) { public static void printException(LogMessage message, @Nullable Throwable ex) {
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get()); logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationInfo(LogMessage message) {
logInternal(LogLevel.INFO, message, null, false, false);
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationException(LogMessage message, @Nullable Exception ex) {
logInternal(LogLevel.ERROR, message, ex, false, false);
} }
} }

View File

@ -3,15 +3,21 @@ package app.revanced.extension.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT; import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.text.Html; import android.text.Html;
import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -86,14 +92,15 @@ abstract class Check {
); );
Utils.runOnMainThreadDelayed(() -> { Utils.runOnMainThreadDelayed(() -> {
AlertDialog alert = new AlertDialog.Builder(activity) // Create the custom dialog.
.setCancelable(false) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setIconAttribute(android.R.attr.alertDialogIcon) activity,
.setTitle(str("revanced_check_environment_failed_title")) str("revanced_check_environment_failed_title"), // Title.
.setMessage(message) message, // Message.
.setPositiveButton( null, // No EditText.
" ", str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
(dialog, which) -> { () -> {
// Action for the OK (website) button.
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent); activity.startActivity(intent);
@ -102,22 +109,42 @@ abstract class Check {
// which is no longer showing a warning dialog. // which is no longer showing a warning dialog.
activity.finishAffinity(); activity.finishAffinity();
System.exit(0); System.exit(0);
} },
).setNegativeButton( null, // No cancel button.
" ", str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
(dialog, which) -> { () -> {
// Neutral button action.
// Cleanup data if the user incorrectly imported a huge negative number. // Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
},
true // Dismiss dialog when onNeutralClick.
);
dialog.dismiss(); // Get the dialog and main layout.
} Dialog dialog = dialogPair.first;
).create(); LinearLayout mainLayout = dialogPair.second;
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() { // Add icon to the dialog.
ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
final int dip8 = dipToPixels(8);
iconView.setPadding(0, dip8, 0, dip8);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
iconParams.gravity = Gravity.CENTER;
mainLayout.addView(iconView, 0); // Add icon at the top.
dialog.setCancelable(false);
// Show the dialog.
Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
boolean hasRun; boolean hasRun;
@Override @Override
public void onStart(AlertDialog dialog) { public void onStart(Dialog dialog) {
// Only run this once, otherwise if the user changes to a different app // Only run this once, otherwise if the user changes to a different app
// then changes back, this handler will run again and disable the buttons. // then changes back, this handler will run again and disable the buttons.
if (hasRun) { if (hasRun) {
@ -125,19 +152,43 @@ abstract class Check {
} }
hasRun = true; hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); // Get the button container to access buttons.
LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
Button openWebsiteButton;
Button ignoreButton;
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
// Neutral button is the first child (index 0).
ignoreButton = (Button) rowContainer.getChildAt(0);
// OK button is the last child.
openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
} else {
// Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
LinearLayout okContainer =
(LinearLayout) buttonContainer.getChildAt(0); // OK is first.
openWebsiteButton = (Button) okContainer.getChildAt(0);
LinearLayout neutralContainer =
(LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
ignoreButton = (Button) neutralContainer.getChildAt(0);
}
// Initially set buttons to INVISIBLE and disabled.
openWebsiteButton.setVisibility(View.INVISIBLE);
openWebsiteButton.setEnabled(false); openWebsiteButton.setEnabled(false);
ignoreButton.setVisibility(View.INVISIBLE);
ignoreButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); // Start the countdown for showing and enabling buttons.
dismissButton.setEnabled(false); getCountdownRunnable(ignoreButton, openWebsiteButton).run();
getCountdownRunnable(dismissButton, openWebsiteButton).run();
} }
}); });
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs. }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
} }
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) { private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) {
return new Runnable() { return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@ -146,17 +197,15 @@ abstract class Check {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
if (secondsRemaining > 0) { if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) { if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button")); openWebsiteButton.setVisibility(View.VISIBLE);
openWebsiteButton.setEnabled(true); openWebsiteButton.setEnabled(true);
} }
secondsRemaining--; secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000); Utils.runOnMainThreadDelayed(this, 1000);
} else { } else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button")); ignoreButton.setVisibility(View.VISIBLE);
dismissButton.setEnabled(true); ignoreButton.setEnabled(true);
} }
} }
}; };

View File

@ -52,7 +52,7 @@ public class Route {
private int countMatches(CharSequence seq, char c) { private int countMatches(CharSequence seq, char c) {
int count = 0; int count = 0;
for (int i = 0; i < seq.length(); i++) { for (int i = 0, length = seq.length(); i < length; i++) {
if (seq.charAt(i) == c) if (seq.charAt(i) == c)
count++; count++;
} }

View File

@ -3,12 +3,20 @@ package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.*; import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -44,7 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Set by subclasses if Strings cannot be added as a resource. * Set by subclasses if Strings cannot be added as a resource.
*/ */
@Nullable @Nullable
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle; protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { try {
@ -76,7 +84,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
updatingPreference = true; updatingPreference = true;
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
// Updating here can can cause a recursive call back into this same method. // Updating here can cause a recursive call back into this same method.
updatePreference(pref, setting, true, settingImportInProgress); updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different. // Update any other preference availability that may now be different.
updateUIAvailability(); updateUIAvailability();
@ -116,11 +124,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
showingUserDialogMessage = true; showingUserDialogMessage = true;
new AlertDialog.Builder(context) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setTitle(confirmDialogTitle) context,
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString()) confirmDialogTitle, // Title.
.setPositiveButton(android.R.string.ok, (dialog, id) -> { Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
// User confirmed, save to the Setting. null, // No EditText.
null, // OK button text.
() -> {
// OK button action. User confirmed, save to the Setting.
updatePreference(pref, setting, true, false); updatePreference(pref, setting, true, false);
// Update availability of other preferences that may be changed. // Update availability of other preferences that may be changed.
@ -129,23 +140,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (setting.rebootApp) { if (setting.rebootApp) {
showRestartDialog(context); showRestartDialog(context);
} }
}) },
.setNegativeButton(android.R.string.cancel, (dialog, id) -> { () -> {
// Restore whatever the setting was before the change. // Cancel button action. Restore whatever the setting was before the change.
updatePreference(pref, setting, true, true); updatePreference(pref, setting, true, true);
}) },
.setOnDismissListener(dialog -> { null, // No Neutral button.
showingUserDialogMessage = false; null, // No Neutral button action.
}) true // Dismiss dialog when onNeutralClick.
.setCancelable(false) );
.show();
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
// Show the dialog.
dialogPair.first.show();
} }
/** /**
* Updates all Preferences values and their availability using the current values in {@link Setting}. * Updates all Preferences values and their availability using the current values in {@link Setting}.
*/ */
protected void updateUIToSettingValues() { protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true); updatePreferenceScreen(getPreferenceScreen(), true, true);
} }
/** /**
@ -280,17 +295,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (restartDialogTitle == null) { if (restartDialogTitle == null) {
restartDialogTitle = str("revanced_settings_restart_title"); restartDialogTitle = str("revanced_settings_restart_title");
} }
if (restartDialogMessage == null) {
restartDialogMessage = str("revanced_settings_restart_dialog_message");
}
if (restartDialogButtonText == null) { if (restartDialogButtonText == null) {
restartDialogButtonText = str("revanced_settings_restart"); restartDialogButtonText = str("revanced_settings_restart");
} }
new AlertDialog.Builder(context) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
.setMessage(restartDialogTitle) restartDialogTitle, // Title.
.setPositiveButton(restartDialogButtonText, (dialog, id) restartDialogMessage, // Message.
-> Utils.restartApp(context)) null, // No EditText.
.setNegativeButton(android.R.string.cancel, null) restartDialogButtonText, // OK button text.
.setCancelable(false) () -> Utils.restartApp(context), // OK button action.
.show(); () -> {}, // Cancel button action (dismiss only).
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} }
@SuppressLint("ResourceType") @SuppressLint("ResourceType")

View File

@ -2,8 +2,9 @@ package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier; import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
@ -18,6 +19,7 @@ import android.text.TextWatcher;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -216,86 +218,6 @@ public class ColorPickerPreference extends EditTextPreference {
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA); widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
} }
/**
* Creates a layout with a color preview and EditText for hex color input.
*
* @param context The context for creating the layout.
* @return A LinearLayout containing the color preview and EditText.
*/
private LinearLayout createDialogLayout(Context context) {
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(70, 0, 70, 0);
// Inflate color picker.
View colorPicker = LayoutInflater.from(context).inflate(
getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPicker.findViewById(
getResourceIdentifier("color_picker_view", "id"));
dialogColorPickerView.setColor(currentColor);
layout.addView(colorPicker);
// Horizontal layout for preview and EditText.
LinearLayout inputLayout = new LinearLayout(context);
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
inputLayout.setPadding(0, 20, 0, 0);
dialogColorPreview = new TextView(context);
inputLayout.addView(dialogColorPreview);
updateColorPreview();
EditText editText = getEditText();
ViewParent parent = editText.getParent();
if (parent instanceof ViewGroup parentViewGroup) {
parentViewGroup.removeView(editText);
}
editText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
String currentColorString = getColorString(currentColor);
editText.setText(currentColorString);
editText.setSelection(currentColorString.length());
editText.setTypeface(Typeface.MONOSPACE);
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
editText.addTextChangedListener(colorTextWatcher);
inputLayout.addView(editText);
// Add a dummy view to take up remaining horizontal space,
// otherwise it will show an oversize underlined text view.
View paddingView = new View(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
);
paddingView.setLayoutParams(params);
inputLayout.addView(paddingView);
layout.addView(inputLayout);
// Set up color picker listener with debouncing.
// Add listener last to prevent callbacks from set calls above.
dialogColorPickerView.setOnColorChangedListener(color -> {
// Check if it actually changed, since this callback
// can be caused by updates in afterTextChanged().
if (currentColor == color) {
return;
}
String updatedColorString = getColorString(color);
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
currentColor = color;
editText.setText(updatedColorString);
editText.setSelection(updatedColorString.length());
updateColorPreview();
updateWidgetColorDot();
});
return layout;
}
/** /**
* Updates the color preview TextView with a colored dot. * Updates the color preview TextView with a colored dot.
*/ */
@ -360,65 +282,142 @@ public class ColorPickerPreference extends EditTextPreference {
} }
/** /**
* Prepares the dialog builder with a custom view and reset button. * Creates a Dialog with a color preview and EditText for hex color input.
*
* @param builder The AlertDialog.Builder to configure.
*/ */
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void showDialog(Bundle state) {
Utils.setEditTextDialogTheme(builder); Context context = getContext();
LinearLayout dialogLayout = createDialogLayout(builder.getContext());
builder.setView(dialogLayout);
final int originalColor = currentColor;
builder.setNeutralButton(str("revanced_settings_reset_color"), null); // Inflate color picker view.
View colorPicker = LayoutInflater.from(context).inflate(
getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPicker.findViewById(
getResourceIdentifier("revanced_color_picker_view", "id"));
dialogColorPickerView.setColor(currentColor);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { // Horizontal layout for preview and EditText.
LinearLayout inputLayout = new LinearLayout(context);
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
inputLayout.setPadding(0, 0, 0, dipToPixels(10));
dialogColorPreview = new TextView(context);
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
dialogColorPreview.setLayoutParams(previewParams);
inputLayout.addView(dialogColorPreview);
updateColorPreview();
EditText editText = getEditText();
ViewParent parent = editText.getParent();
if (parent instanceof ViewGroup parentViewGroup) {
parentViewGroup.removeView(editText);
}
editText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
String currentColorString = getColorString(currentColor);
editText.setText(currentColorString);
editText.setSelection(currentColorString.length());
editText.setTypeface(Typeface.MONOSPACE);
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
editText.addTextChangedListener(colorTextWatcher);
inputLayout.addView(editText);
// Add a dummy view to take up remaining horizontal space,
// otherwise it will show an oversize underlined text view.
View paddingView = new View(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
);
paddingView.setLayoutParams(params);
inputLayout.addView(paddingView);
// Create main container for color picker and input layout.
LinearLayout container = new LinearLayout(context);
container.setOrientation(LinearLayout.VERTICAL);
container.addView(colorPicker);
container.addView(inputLayout);
// Create custom dialog.
final int originalColor = currentColor & 0x00FFFFFF;
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
null, // No message.
null, // No EditText.
null, // OK button text.
() -> {
// OK button action.
try { try {
String colorString = getEditText().getText().toString(); String colorString = editText.getText().toString();
if (colorString.length() != COLOR_STRING_LENGTH) { if (colorString.length() != COLOR_STRING_LENGTH) {
Utils.showToastShort(str("revanced_settings_color_invalid")); Utils.showToastShort(str("revanced_settings_color_invalid"));
setText(getColorString(originalColor)); setText(getColorString(originalColor));
return; return;
} }
setText(colorString); setText(colorString);
} catch (Exception ex) { } catch (Exception ex) {
// Should never happen due to a bad color string, // Should never happen due to a bad color string,
// since the text is validated and fixed while the user types. // since the text is validated and fixed while the user types.
Logger.printException(() -> "setPositiveButton failure", ex); Logger.printException(() -> "OK button failure", ex);
} }
}); },
() -> {
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { // Cancel button action.
try { try {
// Restore the original color. // Restore the original color.
setText(getColorString(originalColor)); setText(getColorString(originalColor));
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "setNegativeButton failure", ex); Logger.printException(() -> "Cancel button failure", ex);
} }
}); },
} str("revanced_settings_reset_color"), // Neutral button text.
() -> {
@Override // Neutral button action.
protected void showDialog(Bundle state) {
super.showDialog(state);
AlertDialog dialog = (AlertDialog) getDialog();
dialog.setCanceledOnTouchOutside(false);
// Do not close dialog when reset is pressed.
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
button.setOnClickListener(view -> {
try { try {
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF; final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
// Setting view color causes listener callback into this class. // Setting view color causes listener callback into this class.
dialogColorPickerView.setColor(defaultColor); dialogColorPickerView.setColor(defaultColor);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "setOnClickListener failure", ex); Logger.printException(() -> "Reset button failure", ex);
} }
},
false // Do not dismiss dialog when onNeutralClick.
);
// Add the custom container to the dialog's main layout.
LinearLayout dialogMainLayout = dialogPair.second;
dialogMainLayout.addView(container, 1);
// Set up color picker listener with debouncing.
// Add listener last to prevent callbacks from set calls above.
dialogColorPickerView.setOnColorChangedListener(color -> {
// Check if it actually changed, since this callback
// can be caused by updates in afterTextChanged().
if (currentColor == color) {
return;
}
String updatedColorString = getColorString(color);
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
currentColor = color;
editText.setText(updatedColorString);
editText.setSelection(updatedColorString.length());
updateColorPreview();
updateWidgetColorDot();
}); });
// Configure and show the dialog.
Dialog dialog = dialogPair.first;
dialog.setCanceledOnTouchOutside(false);
dialog.show();
} }
@Override @Override

View File

@ -29,8 +29,8 @@ import app.revanced.extension.shared.Utils;
* <p> * <p>
* This view displays two main components for color selection: * This view displays two main components for color selection:
* <ul> * <ul>
* <li><b>Hue Bar:</b> A vertical bar on the right that allows the user to select the hue component of the color. * <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
* <li><b>Saturation-Value Selector:</b> A rectangular area that allows the user to select the saturation and value (brightness) * <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
* components of the color based on the selected hue. * components of the color based on the selected hue.
* </ul> * </ul>
* *
@ -63,7 +63,7 @@ public class ColorPickerView extends View {
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24); private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
private static final float VIEW_PADDING = dipToPixels(16); private static final float VIEW_PADDING = dipToPixels(16);
private static final float HUE_BAR_WIDTH = dipToPixels(12); private static final float HUE_BAR_HEIGHT = dipToPixels(12);
private static final float HUE_CORNER_RADIUS = dipToPixels(6); private static final float HUE_CORNER_RADIUS = dipToPixels(6);
private static final float SELECTOR_RADIUS = dipToPixels(12); private static final float SELECTOR_RADIUS = dipToPixels(12);
private static final float SELECTOR_STROKE_WIDTH = 8; private static final float SELECTOR_STROKE_WIDTH = 8;
@ -144,17 +144,17 @@ public class ColorPickerView extends View {
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8 final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
final int minWidth = Utils.dipToPixels(250); final int minWidth = Utils.dipToPixels(250);
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO); final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
int width = resolveSize(minWidth, widthMeasureSpec); int width = resolveSize(minWidth, widthMeasureSpec);
int height = resolveSize(minHeight, heightMeasureSpec); int height = resolveSize(minHeight, heightMeasureSpec);
// Ensure minimum dimensions for usability // Ensure minimum dimensions for usability.
width = Math.max(width, minWidth); width = Math.max(width, minWidth);
height = Math.max(height, minHeight); height = Math.max(height, minHeight);
// Adjust height to maintain desired aspect ratio if possible // Adjust height to maintain desired aspect ratio if possible.
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO); final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) { if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
height = desiredHeight; height = desiredHeight;
} }
@ -171,22 +171,22 @@ public class ColorPickerView extends View {
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight); super.onSizeChanged(width, height, oldWidth, oldHeight);
// Calculate bounds with hue bar on the right // Calculate bounds with hue bar at the bottom.
final float effectiveWidth = width - (2 * VIEW_PADDING); final float effectiveWidth = width - (2 * VIEW_PADDING);
final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS; final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
// Adjust rectangles to account for padding and density-independent dimensions // Adjust rectangles to account for padding and density-independent dimensions.
saturationValueRect.set( saturationValueRect.set(
VIEW_PADDING, VIEW_PADDING,
VIEW_PADDING, VIEW_PADDING,
VIEW_PADDING + selectorWidth, VIEW_PADDING + effectiveWidth,
height - VIEW_PADDING VIEW_PADDING + effectiveHeight
); );
hueRect.set( hueRect.set(
width - VIEW_PADDING - HUE_BAR_WIDTH,
VIEW_PADDING, VIEW_PADDING,
width - VIEW_PADDING, height - VIEW_PADDING - HUE_BAR_HEIGHT,
VIEW_PADDING + effectiveWidth,
height - VIEW_PADDING height - VIEW_PADDING
); );
@ -201,7 +201,7 @@ public class ColorPickerView extends View {
private void updateHueShader() { private void updateHueShader() {
LinearGradient hueShader = new LinearGradient( LinearGradient hueShader = new LinearGradient(
hueRect.left, hueRect.top, hueRect.left, hueRect.top,
hueRect.left, hueRect.bottom, hueRect.right, hueRect.top,
HUE_COLORS, HUE_COLORS,
null, null,
Shader.TileMode.CLAMP Shader.TileMode.CLAMP
@ -263,8 +263,8 @@ public class ColorPickerView extends View {
// Draw the hue bar. // Draw the hue bar.
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint); canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
final float hueSelectorX = hueRect.centerX(); final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height(); final float hueSelectorY = hueRect.centerY();
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
@ -316,17 +316,17 @@ public class ColorPickerView extends View {
// Define touch expansion for the hue bar. // Define touch expansion for the hue bar.
RectF expandedHueRect = new RectF( RectF expandedHueRect = new RectF(
hueRect.left - TOUCH_EXPANSION, hueRect.left,
hueRect.top, hueRect.top - TOUCH_EXPANSION,
hueRect.right + TOUCH_EXPANSION, hueRect.right,
hueRect.bottom hueRect.bottom + TOUCH_EXPANSION
); );
switch (action) { switch (action) {
case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_DOWN:
// Calculate current handle positions. // Calculate current handle positions.
final float hueSelectorX = hueRect.centerX(); final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height(); final float hueSelectorY = hueRect.centerY();
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
@ -348,14 +348,14 @@ public class ColorPickerView extends View {
// Check if the touch started on a handle or within the expanded hue bar area. // Check if the touch started on a handle or within the expanded hue bar area.
if (hueHitRect.contains(x, y)) { if (hueHitRect.contains(x, y)) {
isDraggingHue = true; isDraggingHue = true;
updateHueFromTouch(y); updateHueFromTouch(x);
} else if (satValHitRect.contains(x, y)) { } else if (satValHitRect.contains(x, y)) {
isDraggingSaturation = true; isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y); updateSaturationValueFromTouch(x, y);
} else if (expandedHueRect.contains(x, y)) { } else if (expandedHueRect.contains(x, y)) {
// Handle touch within the expanded hue bar area. // Handle touch within the expanded hue bar area.
isDraggingHue = true; isDraggingHue = true;
updateHueFromTouch(y); updateHueFromTouch(x);
} else if (saturationValueRect.contains(x, y)) { } else if (saturationValueRect.contains(x, y)) {
isDraggingSaturation = true; isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y); updateSaturationValueFromTouch(x, y);
@ -365,7 +365,7 @@ public class ColorPickerView extends View {
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
// Continue updating values even if touch moves outside the view. // Continue updating values even if touch moves outside the view.
if (isDraggingHue) { if (isDraggingHue) {
updateHueFromTouch(y); updateHueFromTouch(x);
} else if (isDraggingSaturation) { } else if (isDraggingSaturation) {
updateSaturationValueFromTouch(x, y); updateSaturationValueFromTouch(x, y);
} }
@ -387,12 +387,12 @@ public class ColorPickerView extends View {
/** /**
* Updates the hue value based on touch position, clamping to valid range. * Updates the hue value based on touch position, clamping to valid range.
* *
* @param y The y-coordinate of the touch position. * @param x The x-coordinate of the touch position.
*/ */
private void updateHueFromTouch(float y) { private void updateHueFromTouch(float x) {
// Clamp y to the hue rectangle bounds. // Clamp x to the hue rectangle bounds.
final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom); final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f; final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
if (hue == updatedHue) { if (hue == updatedHue) {
return; return;
} }

View File

@ -0,0 +1,197 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Utils;
/**
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
*/
@SuppressWarnings({"unused", "deprecation"})
public class CustomDialogListPreference extends ListPreference {
/**
* Custom ArrayAdapter to handle checkmark visibility.
*/
private static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
private static class SubViewDataContainer {
ImageView checkIcon;
View placeholder;
TextView itemText;
}
final int layoutResourceId;
final CharSequence[] entryValues;
String selectedValue;
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
CharSequence[] entryValues, String selectedValue) {
super(context, resource, entries);
this.layoutResourceId = resource;
this.entryValues = entryValues;
this.selectedValue = selectedValue;
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View view = convertView;
SubViewDataContainer holder;
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(layoutResourceId, parent, false);
holder = new SubViewDataContainer();
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
"revanced_check_icon", "id"));
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
"revanced_check_icon_placeholder", "id"));
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
"revanced_item_text", "id"));
view.setTag(holder);
} else {
holder = (SubViewDataContainer) view.getTag();
}
// Set text.
holder.itemText.setText(getItem(position));
holder.itemText.setTextColor(Utils.getAppForegroundColor());
// Show or hide checkmark and placeholder.
String currentValue = entryValues[position].toString();
boolean isSelected = currentValue.equals(selectedValue);
holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
holder.checkIcon.setColorFilter(Utils.getAppForegroundColor());
holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE);
return view;
}
public void setSelectedValue(String value) {
this.selectedValue = value;
}
}
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomDialogListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDialogListPreference(Context context) {
super(context);
}
@Override
protected void showDialog(Bundle state) {
// Create ListView.
ListView listView = new ListView(getContext());
listView.setId(android.R.id.list);
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
// Create custom adapter for the ListView.
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
getContext(),
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
getEntries(),
getEntryValues(),
getValue()
);
listView.setAdapter(adapter);
// Set checked item.
String currentValue = getValue();
if (currentValue != null) {
CharSequence[] entryValues = getEntryValues();
for (int i = 0, length = entryValues.length; i < length; i++) {
if (currentValue.equals(entryValues[i].toString())) {
listView.setItemChecked(i, true);
listView.setSelection(i);
break;
}
}
}
// Create the custom dialog without OK button.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
getContext(),
getTitle() != null ? getTitle().toString() : "",
null,
null,
null, // No OK button text.
null, // No OK button action.
() -> {}, // Cancel button action (just dismiss).
null,
null,
true
);
Dialog dialog = dialogPair.first;
LinearLayout mainLayout = dialogPair.second;
// Measure content height before adding ListView to layout.
// Otherwise, the ListView will push the buttons off the screen.
int totalHeight = 0;
int widthSpec = View.MeasureSpec.makeMeasureSpec(
getContext().getResources().getDisplayMetrics().widthPixels,
View.MeasureSpec.AT_MOST
);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
for (int i = 0; i < adapter.getCount(); i++) {
View listItem = adapter.getView(i, null, listView);
listItem.measure(widthSpec, heightSpec);
totalHeight += listItem.getMeasuredHeight();
}
// Cap the height at maxHeight.
int maxHeight = (int) (getContext().getResources().getDisplayMetrics().heightPixels * 0.6);
int finalHeight = Math.min(totalHeight, maxHeight);
// Add ListView to the main layout with calculated height.
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
finalHeight // Use calculated height directly.
);
final int marginHorizontal = dipToPixels(8);
listViewParams.setMargins(0, marginHorizontal, 0, marginHorizontal);
mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams);
// Handle item click to select value and dismiss dialog.
listView.setOnItemClickListener((parent, view, position, id) -> {
String selectedValue = getEntryValues()[position].toString();
if (callChangeListener(selectedValue)) {
setValue(selectedValue);
adapter.setSelectedValue(selectedValue);
adapter.notifyDataSetChanged();
}
dialog.dismiss();
});
// Show the dialog.
dialog.show();
}
}

View File

@ -1,19 +1,30 @@
package app.revanced.extension.shared.settings.preference; package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import app.revanced.extension.shared.settings.Setting; import android.widget.LinearLayout;
import android.widget.TextView;
import android.graphics.Color;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
@ -54,7 +65,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
try { try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. // Must set text before showing dialog,
// otherwise text is non-selectable if this preference is later reopened.
existingSettings = Setting.exportToJson(getContext()); existingSettings = Setting.exportToJson(getContext());
getEditText().setText(existingSettings); getEditText().setText(existingSettings);
} catch (Exception ex) { } catch (Exception ex) {
@ -64,18 +76,32 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
} }
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void showDialog(Bundle state) {
try { try {
Utils.setEditTextDialogTheme(builder); Context context = getContext();
EditText editText = getEditText();
// Show the user the settings in JSON format. // Create a custom dialog with the EditText.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Utils.setClipboard(getEditText().getText()); context,
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { str("revanced_pref_import_export_title"), // Title.
importSettings(builder.getContext(), getEditText().getText().toString()); null, // No message (EditText replaces it).
}); editText, // Pass the EditText.
str("revanced_settings_import"), // OK button text.
() -> importSettings(context, editText.getText().toString()), // OK button action.
() -> {}, // Cancel button action (dismiss only).
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
() -> {
// Neutral button (Copy) action. Show the user the settings in JSON format.
Utils.setClipboard(editText.getText());
},
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex); Logger.printException(() -> "showDialog failure", ex);
} }
} }
@ -88,7 +114,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
if (rebootNeeded) { if (rebootNeeded) {
AbstractPreferenceFragment.showRestartDialog(getContext()); AbstractPreferenceFragment.showRestartDialog(context);
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex); Logger.printException(() -> "importSettings failure", ex);
@ -96,5 +122,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
AbstractPreferenceFragment.settingImportInProgress = false; AbstractPreferenceFragment.settingImportInProgress = false;
} }
} }
} }

View File

@ -1,6 +1,7 @@
package app.revanced.extension.shared.settings.preference; package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.requests.Route.Method.GET; import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -8,7 +9,8 @@ import android.app.Dialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -18,6 +20,7 @@ import android.util.AttributeSet;
import android.view.Window; import android.view.Window;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -48,28 +51,6 @@ public class ReVancedAboutPreference extends Preference {
return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen. return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen.
} }
private static String getColorHexString(int color) {
return String.format("#%06X", (0x00FFFFFF & color));
}
protected boolean isDarkModeEnabled() {
return Utils.isDarkModeEnabled();
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getLightColor() {
return Color.WHITE;
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getDarkColor() {
return Color.BLACK;
}
/** /**
* Apps that do not support bundling resources must override this. * Apps that do not support bundling resources must override this.
* *
@ -86,9 +67,8 @@ public class ReVancedAboutPreference extends Preference {
builder.append("<html>"); builder.append("<html>");
builder.append("<body style=\"text-align: center; padding: 10px;\">"); builder.append("<body style=\"text-align: center; padding: 10px;\">");
final boolean isDarkMode = isDarkModeEnabled(); String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor()); String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
// Apply light/dark mode colors. // Apply light/dark mode colors.
builder.append(String.format( builder.append(String.format(
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>", "<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
@ -220,14 +200,36 @@ class WebViewDialog extends Dialog {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
// Create main layout.
LinearLayout mainLayout = new LinearLayout(getContext());
mainLayout.setOrientation(LinearLayout.VERTICAL);
final int padding = dipToPixels(10);
mainLayout.setPadding(padding, padding, padding, padding);
// Set rounded rectangle background.
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(28), null, null));
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
mainLayout.setBackground(mainBackground);
// Create WebView.
WebView webView = new WebView(getContext()); WebView webView = new WebView(getContext());
webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new OpenLinksExternallyWebClient()); webView.setWebViewClient(new OpenLinksExternallyWebClient());
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
setContentView(webView); // Add WebView to layout.
mainLayout.addView(webView);
setContentView(mainLayout);
// Set dialog window attributes
Window window = getWindow();
if (window != null) {
Utils.setDialogWindowParameters(getContext(), window);
}
} }
private class OpenLinksExternallyWebClient extends WebViewClient { private class OpenLinksExternallyWebClient extends WebViewClient {

View File

@ -1,14 +1,28 @@
package app.revanced.extension.shared.settings.preference; package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.Paint.Style;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -45,10 +59,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
} }
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void showDialog(Bundle state) {
super.onPrepareDialogBuilder(builder); try {
Utils.setEditTextDialogTheme(builder); Context context = getContext();
EditText editText = getEditText();
// Resolve setting if not already set.
if (setting == null) { if (setting == null) {
String key = getKey(); String key = getKey();
if (key != null) { if (key != null) {
@ -56,29 +72,47 @@ public class ResettableEditTextPreference extends EditTextPreference {
} }
} }
// Set initial EditText value to the current persisted value or empty string.
String initialValue = getText() != null ? getText() : "";
editText.setText(initialValue);
editText.setSelection(initialValue.length()); // Move cursor to end.
// Create custom dialog.
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action. Persist the EditText value when OK is clicked.
String newValue = editText.getText().toString();
if (callChangeListener(newValue)) {
setText(newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
neutralButtonText, // Neutral button text (Reset).
() -> {
// Neutral button action.
if (setting != null) { if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// Override the button click listener to prevent dismissing the dialog.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
if (button == null) {
return;
}
button.setOnClickListener(v -> {
try { try {
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString(); String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue); editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "reset failure", ex); Logger.printException(() -> "reset failure", ex);
} }
}); }
},
false // Do not dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
} }
} }

View File

@ -1,7 +1,6 @@
package app.revanced.extension.shared.settings.preference; package app.revanced.extension.shared.settings.preference;
import android.content.Context; import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair; import android.util.Pair;
@ -24,12 +23,14 @@ import app.revanced.extension.shared.Utils;
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}. * it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
*/ */
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class SortedListPreference extends ListPreference { public class SortedListPreference extends CustomDialogListPreference {
/** /**
* Sorts the current list entries. * Sorts the current list entries.
* *
* @param firstEntriesToPreserve The number of entries to preserve in their original position. * @param firstEntriesToPreserve The number of entries to preserve in their original position,
* or a negative value to not sort and leave entries
* as they current are.
*/ */
public void sortEntryAndValues(int firstEntriesToPreserve) { public void sortEntryAndValues(int firstEntriesToPreserve) {
CharSequence[] entries = getEntries(); CharSequence[] entries = getEntries();
@ -44,6 +45,10 @@ public class SortedListPreference extends ListPreference {
throw new IllegalStateException(); throw new IllegalStateException();
} }
if (firstEntriesToPreserve < 0) {
return; // Nothing to do.
}
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve); List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
// Android does not have a triple class like Kotlin, So instead use a nested pair. // Android does not have a triple class like Kotlin, So instead use a nested pair.
@ -85,10 +90,6 @@ public class SortedListPreference extends ListPreference {
super.setEntryValues(sortedEntryValues); super.setEntryValues(sortedEntryValues);
} }
protected int getFirstEntriesToPreserve() {
return 1;
}
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
@ -112,4 +113,12 @@ public class SortedListPreference extends ListPreference {
sortEntryAndValues(getFirstEntriesToPreserve()); sortEntryAndValues(getFirstEntriesToPreserve());
} }
/**
* @return The number of first entries to leave exactly where they are, and do not sort them.
* A negative value indicates do not sort any entries.
*/
protected int getFirstEntriesToPreserve() {
return 1;
}
} }

View File

@ -37,6 +37,7 @@ public final class HideCreateButtonPatch {
return null; return null;
} }
try {
String stringifiedNavigationBarItem = navigationBarItem.toString(); String stringifiedNavigationBarItem = navigationBarItem.toString();
for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) { for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) {
@ -47,11 +48,14 @@ public final class HideCreateButtonPatch {
} }
if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) { if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) {
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " + navigationBarItem + Logger.printInfo(() -> "Hiding Create button because the navigation bar item " +
" matched the filter " + componentFilter.getFilterRepresentation()); navigationBarItem + " matched the filter " + componentFilter.getFilterRepresentation());
return null; return null;
} }
} }
} catch (Exception ex) {
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
}
return navigationBarItem; return navigationBarItem;
} }

View File

@ -143,7 +143,7 @@ public final class UnlockPremiumPatch {
originalValue = ((com.spotify.remoteconfig.internal.AccountAttribute) attribute).value_; originalValue = ((com.spotify.remoteconfig.internal.AccountAttribute) attribute).value_;
} }
if (overrideValue == originalValue) { if (overrideValue.equals(originalValue)) {
continue; continue;
} }
@ -202,6 +202,7 @@ public final class UnlockPremiumPatch {
return false; return false;
} }
try {
String stringifiedContextMenuItem = contextMenuItem.toString(); String stringifiedContextMenuItem = contextMenuItem.toString();
for (List<ComponentFilter> componentFilters : CONTEXT_MENU_ITEMS_COMPONENT_FILTERS) { for (List<ComponentFilter> componentFilters : CONTEXT_MENU_ITEMS_COMPONENT_FILTERS) {
@ -234,6 +235,9 @@ public final class UnlockPremiumPatch {
return true; return true;
} }
} }
} catch (Exception ex) {
Logger.printException(() -> "isFilteredContextMenuItem failure", ex);
}
return false; return false;
} }

View File

@ -1,11 +1,14 @@
package app.revanced.extension.spotify.shared; package app.revanced.extension.spotify.shared;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
public final class ComponentFilters { public final class ComponentFilters {
public interface ComponentFilter { public interface ComponentFilter {
@NonNull
String getFilterValue(); String getFilterValue();
String getFilterRepresentation(); String getFilterRepresentation();
default boolean filterUnavailable() { default boolean filterUnavailable() {
@ -20,7 +23,8 @@ public final class ComponentFilters {
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded. // Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
// 0 is returned when a resource has not been found. // 0 is returned when a resource has not been found.
private int resourceId = -1; private int resourceId = -1;
private String stringfiedResourceId = null; @Nullable
private String stringfiedResourceId;
public ResourceIdComponentFilter(String resourceName, String resourceType) { public ResourceIdComponentFilter(String resourceName, String resourceType) {
this.resourceName = resourceName; this.resourceName = resourceName;
@ -34,6 +38,7 @@ public final class ComponentFilters {
return resourceId; return resourceId;
} }
@NonNull
@Override @Override
public String getFilterValue() { public String getFilterValue() {
if (stringfiedResourceId == null) { if (stringfiedResourceId == null) {
@ -66,6 +71,7 @@ public final class ComponentFilters {
this.string = string; this.string = string;
} }
@NonNull
@Override @Override
public String getFilterValue() { public String getFilterValue() {
return string; return string;

View File

@ -16,7 +16,7 @@ public class SpoofSimPatch {
return false; return false;
} }
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null); Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
return true; return true;
} }

View File

@ -1,179 +0,0 @@
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;
import app.revanced.extension.shared.Utils;
public class ThemeHelper {
@Nullable
private static Integer darkThemeColor, lightThemeColor;
private static int themeValue;
/**
* Injection point.
*/
@SuppressWarnings("unused")
public static void setTheme(Enum<?> value) {
final int newOrdinalValue = value.ordinal();
if (themeValue != newOrdinalValue) {
themeValue = newOrdinalValue;
Logger.printDebug(() -> "Theme value: " + newOrdinalValue);
}
}
public static boolean isDarkTheme() {
return themeValue == 1;
}
public static void setActivityTheme(Activity activity) {
final var theme = isDarkTheme()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
}
/**
* Injection point.
*/
@SuppressWarnings("SameReturnValue")
private static String darkThemeResourceName() {
// Value is changed by Theme patch, if included.
return "@color/yt_black3";
}
private static int getThemeColor(String resourceName, int defaultColor) {
try {
return Utils.getColorFromString(resourceName);
} catch (Exception ex) {
// User entered an invalid custom theme color.
// Normally this should never be reached, and no localized strings are needed.
Utils.showToastLong("Invalid custom theme color: " + resourceName);
return defaultColor;
}
}
/**
* @return The dark theme color as specified by the Theme patch (if included),
* or the dark mode background color unpatched YT uses.
*/
public static int getDarkThemeColor() {
if (darkThemeColor == null) {
darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK);
}
return darkThemeColor;
}
/**
* Injection point.
*/
@SuppressWarnings("SameReturnValue")
private static String lightThemeResourceName() {
// Value is changed by Theme patch, if included.
return "@color/yt_white1";
}
/**
* @return The light theme color as specified by the Theme patch (if included),
* or the non dark mode background color unpatched YT uses.
*/
public static int getLightThemeColor() {
if (lightThemeColor == null) {
lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE);
}
return lightThemeColor;
}
public static int getBackgroundColor() {
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
}
public static int getForegroundColor() {
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
}
public static int getDialogBackgroundColor() {
final String colorName = isDarkTheme()
? "yt_black1"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
public static int getToolbarBackgroundColor() {
final String colorName = isDarkTheme()
? "yt_black3"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
/**
* Sets the system navigation bar color for the activity.
* Applies the background color obtained from {@link #getBackgroundColor()} to the navigation bar.
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
*/
public static void setNavigationBarColor(@Nullable Window window) {
if (window == null) {
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
return;
}
window.setNavigationBarColor(getBackgroundColor());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
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

@ -3,7 +3,10 @@ package app.revanced.extension.youtube.patches;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog;
import android.text.Html; import android.text.Html;
import android.util.Pair;
import android.widget.LinearLayout;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@ -63,18 +66,28 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
} }
Utils.runOnMainThread(() -> { Utils.runOnMainThread(() -> {
var alert = new android.app.AlertDialog.Builder(context) try {
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title")) // Create the custom dialog.
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setIconAttribute(android.R.attr.alertDialogIcon) context,
.setPositiveButton(android.R.string.ok, (dialog, which) -> { str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
dialog.dismiss(); Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { null, // No EditText.
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); null, // OK button text.
dialog.dismiss(); () -> {}, // OK button action (just dismiss).
}).create(); () -> {}, // Cancel button action (just dismiss).
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
() -> Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
true // Dismiss dialog on Neutral button click.
);
Utils.showDialog(context, alert, false, null); // Show the dialog.
Dialog dialog = dialogPair.first;
Utils.showDialog(context, dialog, false, null);
} catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver dialog creation failure", ex);
}
}); });
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver failure", ex); Logger.printException(() -> "checkDnsResolver failure", ex);

View File

@ -7,11 +7,17 @@ public class VersionCheckPatch {
return Utils.getAppVersionName().compareTo(version) >= 0; return Utils.getAppVersionName().compareTo(version) >= 0;
} }
@Deprecated
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00"); public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
@Deprecated
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00"); public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
@Deprecated
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00"); public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
@Deprecated
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00"); public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
@Deprecated
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00"); public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
@Deprecated
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00"); public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00"); public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
} }

View File

@ -2,13 +2,16 @@ package app.revanced.extension.youtube.patches.announcements;
import static android.text.Html.FROM_HTML_MODE_COMPACT; import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS; import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS; import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.Dialog;
import android.text.Html; import android.text.Html;
import android.text.method.LinkMovementMethod; import android.util.Pair;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.json.JSONArray; import org.json.JSONArray;
@ -120,25 +123,38 @@ public final class AnnouncementsPatch {
final Level finalLevel = level; final Level finalLevel = level;
Utils.runOnMainThread(() -> { Utils.runOnMainThread(() -> {
// Show the announcement. // Create the custom dialog and show the announcement.
var alert = new AlertDialog.Builder(context) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setTitle(finalTitle) context,
.setMessage(finalMessage) finalTitle, // Title.
.setIcon(finalLevel.icon) finalMessage, // Message.
.setPositiveButton(android.R.string.ok, (dialog, which) -> { null, // No EditText.
Settings.ANNOUNCEMENT_LAST_ID.save(finalId); null, // OK button text.
dialog.dismiss(); () -> Settings.ANNOUNCEMENT_LAST_ID.save(finalId), // OK button action.
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> { () -> {}, // Cancel button action (dismiss only).
dialog.dismiss(); str("revanced_announcements_dialog_dismiss"), // Neutral button text.
}) () -> {}, // Neutral button action (dismiss only).
.setCancelable(false) true // Dismiss dialog when onNeutralClick.
.create(); );
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> { Dialog dialog = dialogPair.first;
// Make links clickable. LinearLayout mainLayout = dialogPair.second;
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance()); // Set the icon for the title TextView
}); for (int i = 0, childCould = mainLayout.getChildCount(); i < childCould; i++) {
View child = mainLayout.getChildAt(i);
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
childTextView.setCompoundDrawablesWithIntrinsicBounds(
finalLevel.icon, 0, 0, 0);
childTextView.setCompoundDrawablePadding(dipToPixels(8));
}
}
// Set dialog as non-cancelable.
dialog.setCancelable(false);
// Show the dialog.
Utils.showDialog(context, dialog);
}); });
} catch (Exception e) { } catch (Exception e) {
final var message = "Failed to get announcement"; final var message = "Failed to get announcement";

View File

@ -6,7 +6,7 @@ import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQual
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
/** /**
* Abuse LithoFilter for {@link AdvancedVideoQualityMenuPatch}. * LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
*/ */
public final class AdvancedVideoQualityMenuFilter extends Filter { public final class AdvancedVideoQualityMenuFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread // Must be volatile or synchronized, as litho filtering runs off main thread

View File

@ -13,8 +13,6 @@ import app.revanced.extension.youtube.settings.Settings;
/** /**
* This patch contains the logic to always open the advanced video quality menu. * This patch contains the logic to always open the advanced video quality menu.
* Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
* and a ListView in the old one.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class AdvancedVideoQualityMenuPatch { public final class AdvancedVideoQualityMenuPatch {
@ -76,7 +74,7 @@ public final class AdvancedVideoQualityMenuPatch {
/** /**
* Injection point. * Injection point.
* *
* Used if spoofing to an old app version, and also used for the Shorts video quality flyout. * Shorts video quality flyout.
*/ */
public static void showAdvancedVideoQualityMenu(ListView listView) { public static void showAdvancedVideoQualityMenu(ListView listView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return; if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
@ -90,14 +88,12 @@ public final class AdvancedVideoQualityMenuPatch {
final var indexOfAdvancedQualityMenuItem = 4; final var indexOfAdvancedQualityMenuItem = 4;
if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return; if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return;
Logger.printDebug(() -> "Found advanced menu item in old type of quality menu");
listView.setSoundEffectsEnabled(false); listView.setSoundEffectsEnabled(false);
final var qualityItemMenuPosition = 4; final var qualityItemMenuPosition = 4;
listView.performItemClick(null, qualityItemMenuPosition, 0); listView.performItemClick(null, qualityItemMenuPosition, 0);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "showOldVideoQualityMenu failure", ex); Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
} }
} }

View File

@ -19,13 +19,15 @@ import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.RoundRectShape;
import android.icu.text.NumberFormat; import android.icu.text.NumberFormat;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.animation.Animation;
import android.view.Gravity; import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.ViewParent;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.GridLayout; import android.widget.GridLayout;
@ -39,7 +41,6 @@ import java.util.function.Function;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch; import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@ -240,6 +241,9 @@ public class CustomPlaybackSpeedPatch {
// Store the dialog reference. // Store the dialog reference.
currentDialog = new WeakReference<>(dialog); currentDialog = new WeakReference<>(dialog);
// Enable dismissing the dialog when tapping outside.
dialog.setCanceledOnTouchOutside(true);
// Create main vertical LinearLayout for dialog content. // Create main vertical LinearLayout for dialog content.
LinearLayout mainLayout = new LinearLayout(context); LinearLayout mainLayout = new LinearLayout(context);
mainLayout.setOrientation(LinearLayout.VERTICAL); mainLayout.setOrientation(LinearLayout.VERTICAL);
@ -259,15 +263,15 @@ public class CustomPlaybackSpeedPatch {
// Set rounded rectangle background for the main layout. // Set rounded rectangle background for the main layout.
RoundRectShape roundRectShape = new RoundRectShape( RoundRectShape roundRectShape = new RoundRectShape(
createCornerRadii(12), null, null); Utils.createCornerRadii(12), null, null);
ShapeDrawable background = new ShapeDrawable(roundRectShape); ShapeDrawable background = new ShapeDrawable(roundRectShape);
background.getPaint().setColor(ThemeHelper.getDialogBackgroundColor()); background.getPaint().setColor(Utils.getDialogBackgroundColor());
mainLayout.setBackground(background); mainLayout.setBackground(background);
// Add handle bar at the top. // Add handle bar at the top.
View handleBar = new View(context); View handleBar = new View(context);
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape( ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(4), null, null)); Utils.createCornerRadii(4), null, null));
handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true)); handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
handleBar.setBackground(handleBackground); handleBar.setBackground(handleBackground);
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
@ -285,7 +289,7 @@ public class CustomPlaybackSpeedPatch {
float currentSpeed = VideoInformation.getPlaybackSpeed(); float currentSpeed = VideoInformation.getPlaybackSpeed();
// Initially show with only 0 minimum digits, so 1.0 shows as 1x // Initially show with only 0 minimum digits, so 1.0 shows as 1x
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0)); currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
currentSpeedText.setTextColor(ThemeHelper.getForegroundColor()); currentSpeedText.setTextColor(Utils.getAppForegroundColor());
currentSpeedText.setTextSize(16); currentSpeedText.setTextSize(16);
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD); currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
currentSpeedText.setGravity(Gravity.CENTER); currentSpeedText.setGravity(Gravity.CENTER);
@ -305,7 +309,8 @@ public class CustomPlaybackSpeedPatch {
// Create minus button. // Create minus button.
Button minusButton = new Button(context, null, 0); // Disable default theme style. Button minusButton = new Button(context, null, 0); // Disable default theme style.
minusButton.setText(""); // No text on button. minusButton.setText(""); // No text on button.
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null)); ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(20), null, null));
minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
minusButton.setBackground(minusBackground); minusButton.setBackground(minusBackground);
OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol. OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
@ -319,9 +324,9 @@ public class CustomPlaybackSpeedPatch {
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax)); speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
speedSlider.setProgress(speedToProgressValue(currentSpeed)); speedSlider.setProgress(speedToProgressValue(currentSpeed));
speedSlider.getProgressDrawable().setColorFilter( speedSlider.getProgressDrawable().setColorFilter(
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar. Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
speedSlider.getThumb().setColorFilter( speedSlider.getThumb().setColorFilter(
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb. Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons. sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
@ -331,7 +336,7 @@ public class CustomPlaybackSpeedPatch {
Button plusButton = new Button(context, null, 0); // Disable default theme style. Button plusButton = new Button(context, null, 0); // Disable default theme style.
plusButton.setText(""); // No text on button. plusButton.setText(""); // No text on button.
ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape( ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(20), null, null)); Utils.createCornerRadii(20), null, null));
plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
plusButton.setBackground(plusBackground); plusButton.setBackground(plusBackground);
OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol. OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
@ -418,13 +423,13 @@ public class CustomPlaybackSpeedPatch {
// Create speed button. // Create speed button.
Button speedButton = new Button(context, null, 0); Button speedButton = new Button(context, null, 0);
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format. speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
speedButton.setTextColor(ThemeHelper.getForegroundColor()); speedButton.setTextColor(Utils.getAppForegroundColor());
speedButton.setTextSize(12); speedButton.setTextSize(12);
speedButton.setAllCaps(false); speedButton.setAllCaps(false);
speedButton.setGravity(Gravity.CENTER); speedButton.setGravity(Gravity.CENTER);
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape( ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(20), null, null)); Utils.createCornerRadii(20), null, null));
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false)); buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
speedButton.setBackground(buttonBackground); speedButton.setBackground(buttonBackground);
speedButton.setPadding(dip5, dip5, dip5, dip5); speedButton.setPadding(dip5, dip5, dip5, dip5);
@ -442,7 +447,7 @@ public class CustomPlaybackSpeedPatch {
TextView normalLabel = new TextView(context); TextView normalLabel = new TextView(context);
// Use same 'Normal' string as stock YouTube. // Use same 'Normal' string as stock YouTube.
normalLabel.setText(str("normal_playback_rate_label")); normalLabel.setText(str("normal_playback_rate_label"));
normalLabel.setTextColor(ThemeHelper.getForegroundColor()); normalLabel.setTextColor(Utils.getAppForegroundColor());
normalLabel.setTextSize(10); normalLabel.setTextSize(10);
normalLabel.setGravity(Gravity.CENTER); normalLabel.setGravity(Gravity.CENTER);
@ -489,6 +494,77 @@ public class CustomPlaybackSpeedPatch {
window.setBackgroundDrawable(null); // Remove default dialog background. window.setBackgroundDrawable(null); // Remove default dialog background.
} }
// Apply slide-in animation when showing the dialog.
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
slideInABottomAnimation.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideInABottomAnimation);
// Set touch listener on mainLayout to enable drag-to-dismiss.
//noinspection ClickableViewAccessibility
mainLayout.setOnTouchListener(new View.OnTouchListener() {
/** Threshold for dismissing the dialog. */
final float dismissThreshold = dipToPixels(100); // Distance to drag to dismiss.
/** Store initial Y position of touch. */
float touchY;
/** Track current translation. */
float translationY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Capture initial Y position of touch.
touchY = event.getRawY();
translationY = mainLayout.getTranslationY();
return true;
case MotionEvent.ACTION_MOVE:
// Calculate drag distance and apply translation downwards only.
final float deltaY = event.getRawY() - touchY;
// Only allow downward drag (positive deltaY).
if (deltaY >= 0) {
mainLayout.setTranslationY(translationY + deltaY);
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Check if dialog should be dismissed based on drag distance.
if (mainLayout.getTranslationY() > dismissThreshold) {
// Animate dialog off-screen and dismiss.
//noinspection ExtractMethodRecommender
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
- mainLayout.getTop();
TranslateAnimation slideOut = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), remainingDistance);
slideOut.setDuration(fadeDurationFast);
slideOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
dialog.dismiss();
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mainLayout.startAnimation(slideOut);
} else {
// Animate back to original position if not dragged far enough.
TranslateAnimation slideBack = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), 0);
slideBack.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideBack);
mainLayout.setTranslationY(0);
}
return true;
default:
return false;
}
}
});
// Create observer for PlayerType changes. // Create observer for PlayerType changes.
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() { Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
@Override @Override
@ -515,27 +591,9 @@ public class CustomPlaybackSpeedPatch {
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss"); Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
}); });
// Apply slide-in animation when showing the dialog.
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
slideInABottomAnimation.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideInABottomAnimation);
dialog.show(); // Display the dialog. dialog.show(); // Display the dialog.
} }
/**
* Creates an array of corner radii for a rounded rectangle shape.
*
* @param dp The radius in density-independent pixels (dp) to apply to all corners.
* @return An array of eight float values representing the corner radii
* (top-left, top-right, bottom-right, bottom-left).
*/
private static float[] createCornerRadii(float dp) {
final float radius = dipToPixels(dp);
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
}
/** /**
* @param speed The playback speed value to format. * @param speed The playback speed value to format.
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x"). * @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
@ -573,12 +631,12 @@ public class CustomPlaybackSpeedPatch {
* for light themes to ensure visual contrast. * for light themes to ensure visual contrast.
*/ */
public static int getAdjustedBackgroundColor(boolean isHandleBar) { public static int getAdjustedBackgroundColor(boolean isHandleBar) {
final int baseColor = ThemeHelper.getDialogBackgroundColor(); final int baseColor = Utils.getDialogBackgroundColor();
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme. float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme. float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
return ThemeHelper.isDarkTheme() return Utils.isDarkModeEnabled()
? ThemeHelper.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme. ? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme. : Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
} }
} }
@ -592,7 +650,7 @@ class OutlineSymbolDrawable extends Drawable {
OutlineSymbolDrawable(boolean isPlus) { OutlineSymbolDrawable(boolean isPlus) {
this.isPlus = isPlus; this.isPlus = isPlus;
paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering. paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering.
paint.setColor(ThemeHelper.getForegroundColor()); paint.setColor(Utils.getAppForegroundColor());
paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline. paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline.
paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width. paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width.
} }

View File

@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -75,7 +74,7 @@ public class ThemePatch {
* @return The new or original color value * @return The new or original color value
*/ */
public static int getValue(int originalValue) { public static int getValue(int originalValue) {
if (ThemeHelper.isDarkTheme()) { if (Utils.isDarkModeEnabled()) {
if (anyEquals(originalValue, DARK_VALUES)) return BLACK_COLOR; if (anyEquals(originalValue, DARK_VALUES)) return BLACK_COLOR;
} else { } else {
if (anyEquals(originalValue, WHITE_VALUES)) return WHITE_COLOR; if (anyEquals(originalValue, WHITE_VALUES)) return WHITE_COLOR;

View File

@ -30,11 +30,15 @@ import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData; import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@ -177,7 +181,7 @@ public class ReturnYouTubeDislike {
* Ideally, this would be the actual color YT uses at runtime. * Ideally, this would be the actual color YT uses at runtime.
*/ */
private static int getSeparatorColor() { private static int getSeparatorColor() {
return ThemeHelper.isDarkTheme() return Utils.isDarkModeEnabled()
? 0x33FFFFFF ? 0x33FFFFFF
: 0xFFD9D9D9; : 0xFFD9D9D9;
} }

View File

@ -14,7 +14,6 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.patches.VersionCheckPatch; import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
@ -27,6 +26,8 @@ import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFrag
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class LicenseActivityHook { public class LicenseActivityHook {
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
private static ViewGroup.LayoutParams toolbarLayoutParams; private static ViewGroup.LayoutParams toolbarLayoutParams;
public static void setToolbarLayoutParams(Toolbar toolbar) { public static void setToolbarLayoutParams(Toolbar toolbar) {
@ -78,8 +79,8 @@ public class LicenseActivityHook {
*/ */
public static void initialize(Activity licenseActivity) { public static void initialize(Activity licenseActivity) {
try { try {
ThemeHelper.setActivityTheme(licenseActivity); setActivityTheme(licenseActivity);
ThemeHelper.setNavigationBarColor(licenseActivity.getWindow()); ReVancedPreferenceFragment.setNavigationBarColor(licenseActivity.getWindow());
licenseActivity.setContentView(getResourceIdentifier( licenseActivity.setContentView(getResourceIdentifier(
"revanced_settings_with_toolbar", "layout")); "revanced_settings_with_toolbar", "layout"));
@ -114,7 +115,7 @@ public class LicenseActivityHook {
toolBarParent.removeView(dummyToolbar); toolBarParent.removeView(dummyToolbar);
Toolbar toolbar = new Toolbar(toolBarParent.getContext()); Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor()); toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable()); toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string")); toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
@ -124,7 +125,7 @@ public class LicenseActivityHook {
TextView toolbarTextView = Utils.getChildView(toolbar, false, TextView toolbarTextView = Utils.getChildView(toolbar, false,
view -> view instanceof TextView); view -> view instanceof TextView);
if (toolbarTextView != null) { if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor()); toolbarTextView.setTextColor(Utils.getAppForegroundColor());
} }
setToolbarLayoutParams(toolbar); setToolbarLayoutParams(toolbar);
@ -135,4 +136,34 @@ public class LicenseActivityHook {
toolBarParent.addView(toolbar, 0); toolBarParent.addView(toolbar, 0);
} }
public static void setActivityTheme(Activity activity) {
final var theme = Utils.isDarkModeEnabled()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(getResourceIdentifier(theme, "style"));
}
public static int getToolbarBackgroundColor() {
final String colorName = Utils.isDarkModeEnabled()
? "yt_black3"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
/**
* Injection point.
*
* Updates dark/light mode since YT settings can force light/dark mode
* which can differ from the global device settings.
*/
@SuppressWarnings("unused")
public static void updateLightDarkModeStatus(Enum<?> value) {
final int themeOrdinal = value.ordinal();
if (currentThemeValueOrdinal != themeOrdinal) {
currentThemeValueOrdinal = themeOrdinal;
Utils.setIsDarkModeEnabled(themeOrdinal == 1);
}
}
} }

View File

@ -4,9 +4,10 @@ import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier; import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable;
import android.util.Pair;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@ -31,7 +32,6 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
/** /**
@ -58,10 +58,10 @@ public class SearchViewController {
GradientDrawable background = new GradientDrawable(); GradientDrawable background = new GradientDrawable();
background.setShape(GradientDrawable.RECTANGLE); background.setShape(GradientDrawable.RECTANGLE);
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius. background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
int baseColor = ThemeHelper.getBackgroundColor(); int baseColor = Utils.getAppBackgroundColor();
int adjustedColor = ThemeHelper.isDarkTheme() int adjustedColor = Utils.isDarkModeEnabled()
? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme. ? Utils.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme. : Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
background.setColor(adjustedColor); background.setColor(adjustedColor);
return background; return background;
} }
@ -171,10 +171,6 @@ public class SearchViewController {
final int actionSearchId = getResourceIdentifier("action_search", "id"); final int actionSearchId = getResourceIdentifier("action_search", "id");
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu")); toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId); 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. // Set menu item click listener.
toolbar.setOnMenuItemClickListener(item -> { toolbar.setOnMenuItemClickListener(item -> {
@ -316,12 +312,7 @@ public class SearchViewController {
private void closeSearch() { private void closeSearch() {
isSearchActive = false; isSearchActive = false;
toolbar.getMenu().findItem(getResourceIdentifier( toolbar.getMenu().findItem(getResourceIdentifier(
"action_search", "id")) "action_search", "id")).setVisible(true);
.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_search_white_24"
: "yt_outline_search_black_24",
"drawable")
).setVisible(true);
toolbar.setTitle(originalTitle); toolbar.setTitle(originalTitle);
searchContainer.setVisibility(View.GONE); searchContainer.setVisibility(View.GONE);
searchView.setQuery("", false); searchView.setQuery("", false);
@ -365,13 +356,22 @@ public class SearchViewController {
// Set long click listener for deletion confirmation. // Set long click listener for deletion confirmation.
convertView.setOnLongClickListener(v -> { convertView.setOnLongClickListener(v -> {
new AlertDialog.Builder(activity) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setTitle(query) activity,
.setMessage(str("revanced_settings_search_remove_message")) query, // Title.
.setPositiveButton(android.R.string.ok, str("revanced_settings_search_remove_message"), // Message.
(dialog, which) -> removeSearchQuery(query)) null, // No EditText.
.setNegativeButton(android.R.string.cancel, null) null, // OK button text.
.show(); () -> removeSearchQuery(query), // OK button action.
() -> {}, // Cancel button action (dismiss only).
null, // No Neutral button text.
() -> {}, // Neutral button action (dismiss only).
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
dialog.setCancelable(true); // Allow dismissal via back button.
dialog.show(); // Show the dialog.
return true; return true;
}); });

View File

@ -3,17 +3,18 @@ package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.StringRef.sf; import static app.revanced.extension.shared.StringRef.sf;
import android.content.Context; import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
/** /**
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
* Custom video speeds used by {@link CustomPlaybackSpeedPatch}. * Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
*/ */
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public final class CustomVideoSpeedListPreference extends ListPreference { public final class CustomVideoSpeedListPreference extends CustomDialogListPreference {
/** /**
* Initialize a settings preference list with the available playback speeds. * Initialize a settings preference list with the available playback speeds.
@ -59,4 +60,5 @@ public final class CustomVideoSpeedListPreference extends ListPreference {
public CustomVideoSpeedListPreference(Context context) { public CustomVideoSpeedListPreference(Context context) {
super(context); super(context);
} }
} }

View File

@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
@ -40,7 +41,6 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory; import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.LicenseActivityHook; import app.revanced.extension.youtube.settings.LicenseActivityHook;
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup; import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
@ -71,11 +71,27 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@SuppressLint("UseCompatLoadingForDrawables") @SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() { public static Drawable getBackButtonDrawable() {
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme() final int backButtonResource = getResourceIdentifier("revanced_settings_toolbar_arrow_left", "drawable");
? "yt_outline_arrow_left_white_24" Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
: "yt_outline_arrow_left_black_24", drawable.setTint(Utils.getAppForegroundColor());
"drawable"); return drawable;
return Utils.getContext().getResources().getDrawable(backButtonResource); }
/**
* Sets the system navigation bar color for the activity.
* Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
*/
public static void setNavigationBarColor(@Nullable Window window) {
if (window == null) {
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
return;
}
window.setNavigationBarColor(Utils.getAppBackgroundColor());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(true);
}
} }
/** /**
@ -201,9 +217,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
// Set icon for the placeholder preference. // Set icon for the placeholder preference.
noResultsPreference.setLayoutResource(getResourceIdentifier( noResultsPreference.setLayoutResource(getResourceIdentifier(
"revanced_preference_with_icon_no_search_result", "layout")); "revanced_preference_with_icon_no_search_result", "layout"));
noResultsPreference.setIcon(getResourceIdentifier( noResultsPreference.setIcon(getResourceIdentifier("revanced_settings_search_icon", "drawable"));
ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
"drawable"));
preferenceScreen.addPreference(noResultsPreference); preferenceScreen.addPreference(noResultsPreference);
} }
} }
@ -226,7 +240,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
.getParent(); .getParent();
// Fix the system navigation bar color for submenus. // Fix the system navigation bar color for submenus.
ThemeHelper.setNavigationBarColor(preferenceScreenDialog.getWindow()); setNavigationBarColor(preferenceScreenDialog.getWindow());
// Fix edge-to-edge screen with Android 15 and YT 19.45+ // Fix edge-to-edge screen with Android 15 and YT 19.45+
// https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
@ -250,7 +264,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
TextView toolbarTextView = Utils.getChildView(toolbar, TextView toolbarTextView = Utils.getChildView(toolbar,
true, TextView.class::isInstance); true, TextView.class::isInstance);
if (toolbarTextView != null) { if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor()); toolbarTextView.setTextColor(Utils.getAppForegroundColor());
} }
LicenseActivityHook.setToolbarLayoutParams(toolbar); LicenseActivityHook.setToolbarLayoutParams(toolbar);
@ -304,10 +318,10 @@ class AbstractPreferenceSearchData<T extends Preference> {
return text; return text;
} }
final int baseColor = ThemeHelper.getBackgroundColor(); final int baseColor = Utils.getAppBackgroundColor();
final int adjustedColor = ThemeHelper.isDarkTheme() final int adjustedColor = Utils.isDarkModeEnabled()
? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme. ? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme. : Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor); BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
SpannableStringBuilder spannable = new SpannableStringBuilder(text); SpannableStringBuilder spannable = new SpannableStringBuilder(text);

View File

@ -1,32 +0,0 @@
package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
import app.revanced.extension.youtube.ThemeHelper;
@SuppressWarnings("unused")
public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference {
public int getLightColor() {
return ThemeHelper.getLightThemeColor();
}
public int getDarkColor() {
return ThemeHelper.getDarkThemeColor();
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedYouTubeAboutPreference(Context context) {
super(context);
}
}

View File

@ -2,9 +2,11 @@ package app.revanced.extension.youtube.sponsorblock;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.util.Pair;
import android.util.Patterns; import android.util.Patterns;
import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -56,7 +58,7 @@ public class SponsorBlockSettings {
} }
} }
for (int i = 0; i < categorySelectionsArray.length(); i++) { for (int i = 0, length = categorySelectionsArray.length(); i < length; i++) {
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i); JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
String categoryKey = categorySelectionObject.getString("name"); String categoryKey = categorySelectionObject.getString("name");
@ -181,13 +183,25 @@ public class SponsorBlockSettings {
// If user has a SponsorBlock user id then show a warning. // If user has a SponsorBlock user id then show a warning.
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId() if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) { && !Settings.SB_HIDE_EXPORT_WARNING.get()) {
new AlertDialog.Builder(dialogContext) // Create the custom dialog.
.setMessage(str("revanced_sb_settings_revanced_export_user_id_warning")) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setNeutralButton(str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"), dialogContext,
(dialog, which) -> Settings.SB_HIDE_EXPORT_WARNING.save(true)) null, // No title.
.setPositiveButton(android.R.string.ok, null) str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
.setCancelable(false) null, // No EditText.
.show(); null, // OK button text.
() -> {}, // OK button action (dismiss only).
null, // No cancel button action.
str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"), // Neutral button text.
() -> Settings.SB_HIDE_EXPORT_WARNING.save(true), // Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as non-cancelable.
dialogPair.first.setCancelable(false);
// Show the dialog.
dialogPair.first.show();
} }
} }

View File

@ -2,12 +2,12 @@ package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier; import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString; import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor; import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Bundle; import android.os.Bundle;
@ -15,12 +15,14 @@ import android.preference.ListPreference;
import android.text.Editable; import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.GridLayout; import android.widget.GridLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
@ -52,6 +54,7 @@ public class SegmentCategoryListPreference extends ListPreference {
private EditText dialogColorEditText; private EditText dialogColorEditText;
private EditText dialogOpacityEditText; private EditText dialogOpacityEditText;
private ColorPickerView dialogColorPickerView; private ColorPickerView dialogColorPickerView;
private Dialog dialog;
public SegmentCategoryListPreference(Context context, SegmentCategory category) { public SegmentCategoryListPreference(Context context, SegmentCategory category) {
super(context); super(context);
@ -75,30 +78,47 @@ public class SegmentCategoryListPreference extends ListPreference {
} }
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void showDialog(Bundle state) {
try { try {
Utils.setEditTextDialogTheme(builder); Context context = getContext();
categoryColor = category.getColorNoOpacity(); categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity(); categoryOpacity = category.getOpacity();
selectedDialogEntryIndex = findIndexOfValue(getValue());
Context context = builder.getContext(); // Create the main layout for the dialog content.
LinearLayout mainLayout = new LinearLayout(context); LinearLayout contentLayout = new LinearLayout(context);
mainLayout.setOrientation(LinearLayout.VERTICAL); contentLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setPadding(70, 0, 70, 0); final int dip10 = dipToPixels(10);
contentLayout.setPadding(0, 0, 0, dip10);
// Add behavior selection radio buttons.
RadioGroup radioGroup = new RadioGroup(context);
radioGroup.setOrientation(RadioGroup.VERTICAL);
CharSequence[] entries = getEntries();
for (int i = 0; i < entries.length; i++) {
RadioButton radioButton = new RadioButton(context);
radioButton.setText(entries[i]);
radioButton.setId(i);
radioButton.setChecked(i == selectedDialogEntryIndex);
radioGroup.addView(radioButton);
}
radioGroup.setOnCheckedChangeListener((group, checkedId) -> selectedDialogEntryIndex = checkedId);
radioGroup.setPadding(dip10, 0, 0, 0);
contentLayout.addView(radioGroup);
// Inflate the color picker view. // Inflate the color picker view.
View colorPickerContainer = LayoutInflater.from(context) View colorPickerContainer = LayoutInflater.from(context)
.inflate(getResourceIdentifier("revanced_color_picker", "layout"), null); .inflate(getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPickerContainer.findViewById( dialogColorPickerView = colorPickerContainer.findViewById(
getResourceIdentifier("color_picker_view", "id")); getResourceIdentifier("revanced_color_picker_view", "id"));
dialogColorPickerView.setColor(categoryColor); dialogColorPickerView.setColor(categoryColor);
mainLayout.addView(colorPickerContainer); contentLayout.addView(colorPickerContainer);
// Grid layout for color and opacity inputs. // Grid layout for color and opacity inputs.
GridLayout gridLayout = new GridLayout(context); GridLayout gridLayout = new GridLayout(context);
gridLayout.setColumnCount(3); gridLayout.setColumnCount(3);
gridLayout.setRowCount(2); gridLayout.setRowCount(2);
gridLayout.setPadding(dipToPixels(16), 0, 0, 0);
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams(); GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row. gridParams.rowSpec = GridLayout.spec(0); // First row.
@ -111,7 +131,7 @@ public class SegmentCategoryListPreference extends ListPreference {
gridParams = new GridLayout.LayoutParams(); gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row. gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(1); // Second column. gridParams.columnSpec = GridLayout.spec(1); // Second column.
gridParams.setMargins(0, 0, 10, 0); gridParams.setMargins(0, 0, dip10, 0);
dialogColorDotView = new TextView(context); dialogColorDotView = new TextView(context);
dialogColorDotView.setLayoutParams(gridParams); dialogColorDotView.setLayoutParams(gridParams);
gridLayout.addView(dialogColorDotView); gridLayout.addView(dialogColorDotView);
@ -162,8 +182,7 @@ public class SegmentCategoryListPreference extends ListPreference {
} }
} }
}); });
dialogColorEditText.setLayoutParams(gridParams); gridLayout.addView(dialogColorEditText, gridParams);
gridLayout.addView(dialogColorEditText);
gridParams = new GridLayout.LayoutParams(); gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(1); // Second row. gridParams.rowSpec = GridLayout.spec(1); // Second row.
@ -226,11 +245,10 @@ public class SegmentCategoryListPreference extends ListPreference {
} }
} }
}); });
dialogOpacityEditText.setLayoutParams(gridParams); gridLayout.addView(dialogOpacityEditText, gridParams);
gridLayout.addView(dialogOpacityEditText);
updateOpacityText(); updateOpacityText();
mainLayout.addView(gridLayout); contentLayout.addView(gridLayout);
// Set up color picker listener. // Set up color picker listener.
// Do last to prevent listener callbacks while setting up view. // Do last to prevent listener callbacks while setting up view.
@ -247,46 +265,16 @@ public class SegmentCategoryListPreference extends ListPreference {
dialogColorEditText.setSelection(hexColor.length()); dialogColorEditText.setSelection(hexColor.length());
}); });
builder.setView(mainLayout); // Create the custom dialog.
builder.setTitle(category.title.toString()); Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { category.title.toString(), // Title.
onClick(dialog, DialogInterface.BUTTON_POSITIVE); null, // No message (replaced by contentLayout).
}); null, // No EditText.
builder.setNeutralButton(str("revanced_settings_reset_color"), null); null, // OK button text.
builder.setNegativeButton(android.R.string.cancel, null); () -> {
// OK button action.
selectedDialogEntryIndex = findIndexOfValue(getValue()); if (selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
(dialog, which) -> selectedDialogEntryIndex = which);
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// Do not close dialog when reset is pressed.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
button.setOnClickListener(view -> {
try {
// Setting view color causes callback to update the UI.
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
categoryOpacity = category.getOpacityDefault();
updateOpacityText();
} catch (Exception ex) {
Logger.printException(() -> "setOnClickListener failure", ex);
}
});
}
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[selectedDialogEntryIndex].toString(); String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) { if (callChangeListener(value)) {
setValue(value); setValue(value);
@ -303,13 +291,48 @@ public class SegmentCategoryListPreference extends ListPreference {
updateUI(); updateUI();
} }
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_settings_reset_color"), // Neutral button text.
() -> {
// Neutral button action (Reset).
try {
// Setting view color causes callback to update the UI.
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
categoryOpacity = category.getOpacityDefault();
updateOpacityText();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex); Logger.printException(() -> "resetButton onClick failure", ex);
} finally { }
},
false // Do not dismiss dialog on Neutral button click.
);
dialog = dialogPair.first;
LinearLayout dialogMainLayout = dialogPair.second;
// Add the custom content to the dialog's main layout.
dialogMainLayout.addView(contentLayout, 1); // Add after title, before buttons.
// Show the dialog.
dialog.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
@Override
protected void onDialogClosed(boolean positiveResult) {
// Nullify dialog references.
dialogColorDotView = null; dialogColorDotView = null;
dialogColorEditText = null; dialogColorEditText = null;
dialogOpacityEditText = null; dialogOpacityEditText = null;
dialogColorPickerView = null; dialogColorPickerView = null;
if (dialog != null) {
dialog.dismiss();
dialog = null;
} }
} }

View File

@ -3,19 +3,25 @@ package app.revanced.extension.youtube.sponsorblock.ui;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.preference.*; import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceGroup;
import android.preference.SwitchPreference;
import android.text.Html; import android.text.Html;
import android.text.InputType; import android.text.InputType;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -279,14 +285,26 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
Boolean newValue = (Boolean) o; Boolean newValue = (Boolean) o;
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) { if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
new AlertDialog.Builder(preference1.getContext()) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setTitle(str("revanced_sb_guidelines_popup_title")) preference1.getContext(),
.setMessage(str("revanced_sb_guidelines_popup_content")) str("revanced_sb_guidelines_popup_title"), // Title.
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null) str("revanced_sb_guidelines_popup_content"), // Message.
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) null, // No EditText.
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true)) str("revanced_sb_guidelines_popup_open"), // OK button text.
.setCancelable(false) () -> openGuidelines(), // OK button action.
.show(); null, // Cancel button action.
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
() -> {}, // Neutral button action (dismiss only).
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as non-cancelable.
dialogPair.first.setCancelable(false);
dialogPair.first.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true));
// Show the dialog.
dialogPair.first.show();
} }
Settings.SB_CREATE_NEW_SEGMENT.save(newValue); Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
updateUI(); updateUI();
@ -372,16 +390,52 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
generalCategory.addPreference(minSegmentDuration); generalCategory.addPreference(minSegmentDuration);
privateUserId = new EditTextPreference(context) { privateUserId = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { @Override
Utils.setEditTextDialogTheme(builder); protected void showDialog(Bundle state) {
try {
Context context = getContext();
EditText editText = getEditText();
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { // Set initial EditText value to the current persisted value or empty string.
String initialValue = getText() != null ? getText() : "";
editText.setText(initialValue);
editText.setSelection(initialValue.length()); // Move cursor to end.
// Create custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action. Persist the EditText value when OK is clicked.
String newValue = editText.getText().toString();
if (callChangeListener(newValue)) {
setText(newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
() -> {
// Neutral button action (Copy).
try { try {
Utils.setClipboard(getEditText().getText()); Utils.setClipboard(getEditText().getText());
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex); Logger.printException(() -> "Copy settings failure", ex);
} }
}); },
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as cancelable.
dialogPair.first.setCancelable(true);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
} }
}; };
privateUserId.setTitle(str("revanced_sb_general_uuid")); privateUserId.setTitle(str("revanced_sb_general_uuid"));
@ -407,11 +461,15 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(Settings.SB_API_URL.get()); editText.setText(Settings.SB_API_URL.get());
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { // Create a custom dialog.
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Settings.SB_API_URL.resetToDefault(); context,
Utils.showToastLong(str("revanced_sb_api_url_reset")); str("revanced_sb_general_api_url"), // Title.
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { null, // No message, EditText replaces it.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action.
String serverAddress = editText.getText().toString(); String serverAddress = editText.getText().toString();
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) { if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
Utils.showToastLong(str("revanced_sb_api_url_invalid")); Utils.showToastLong(str("revanced_sb_api_url_invalid"));
@ -419,39 +477,74 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
Settings.SB_API_URL.save(serverAddress); Settings.SB_API_URL.save(serverAddress);
Utils.showToastLong(str("revanced_sb_api_url_changed")); Utils.showToastLong(str("revanced_sb_api_url_changed"));
} }
} },
}; () -> {}, // Cancel button action (dismiss dialog).
new AlertDialog.Builder(context) str("revanced_settings_reset"), // Neutral (Reset) button text.
.setTitle(apiUrl.getTitle()) () -> {
.setView(editText) // Neutral button action.
.setNegativeButton(android.R.string.cancel, null) Settings.SB_API_URL.resetToDefault();
.setNeutralButton(str("revanced_settings_reset"), urlChangeListener) Utils.showToastLong(str("revanced_sb_api_url_reset"));
.setPositiveButton(android.R.string.ok, urlChangeListener) },
.show(); true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
return true; return true;
}); });
generalCategory.addPreference(apiUrl); generalCategory.addPreference(apiUrl);
importExport = new EditTextPreference(context) { importExport = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { @Override
Utils.setEditTextDialogTheme(builder); protected void showDialog(Bundle state) {
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
try { try {
Utils.setClipboard(getEditText().getText()); Context context = getContext();
EditText editText = getEditText();
// Create a custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("revanced_sb_settings_ie"), // Title.
null, // No message, EditText replaces it.
editText, // Pass the EditText.
str("revanced_settings_import"), // OK button text.
() -> {
// OK button action. Trigger OnPreferenceChangeListener.
String newValue = editText.getText().toString();
if (getOnPreferenceChangeListener() != null) {
getOnPreferenceChangeListener().onPreferenceChange(this, newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
() -> {
// Neutral button action (Copy).
try {
Utils.setClipboard(editText.getText());
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex); Logger.printException(() -> "Copy settings failure", ex);
} }
}); },
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
} }
}; };
importExport.setTitle(str("revanced_sb_settings_ie")); importExport.setTitle(str("revanced_sb_settings_ie"));
// Summary is set in updateUI() // Summary is set in updateUI().
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT EditText editText = importExport.getEditText();
editText.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
importExport.getEditText().setAutofillHints((String) null); editText.setAutofillHints((String) null);
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
// Set preference listeners.
importExport.setOnPreferenceClickListener(preference1 -> { importExport.setOnPreferenceClickListener(preference1 -> {
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings()); importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
return true; return true;

View File

@ -3,7 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
import static android.text.Html.fromHtml; import static android.text.Html.fromHtml;
import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.StringRef.str;
import android.app.AlertDialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -11,6 +11,8 @@ import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceCategory; import android.preference.PreferenceCategory;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -194,14 +196,26 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
updateStatsSelfSaved.run(); updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> { preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext()) Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
.setTitle(str("revanced_sb_stats_self_saved_reset_title")) preference.getContext(),
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { str("revanced_sb_stats_self_saved_reset_title"), // Title.
null, // No message.
null, // No EditText.
null, // OK button text.
() -> {
// OK button action.
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault(); Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault(); Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
updateStatsSelfSaved.run(); updateStatsSelfSaved.run();
}) },
.setNegativeButton(android.R.string.no, null).show(); () -> {}, // Cancel button action (dismiss only).
null, // No neutral button.
null, // No neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
return true; return true;
}); });

View File

@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true org.gradle.parallel = true
android.useAndroidX = true android.useAndroidX = true
kotlin.code.style = official kotlin.code.style = official
version = 5.27.0 version = 5.28.0-dev.8

View File

@ -694,6 +694,7 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens
} }
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt { public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch; public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch; public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun settingsPatch$default (Ljava/util/List;Ljava/util/Set;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch; public static synthetic fun settingsPatch$default (Ljava/util/List;Ljava/util/Set;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
@ -921,6 +922,10 @@ public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginP
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatchKt {
public static final fun getChangeLyricsProviderPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt { public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
@ -953,6 +958,10 @@ public final class app/revanced/patches/swissid/integritycheck/RemoveGooglePlayI
public static final fun getRemoveGooglePlayIntegrityCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getRemoveGooglePlayIntegrityCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
public final class app/revanced/patches/threads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatchKt { public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatchKt {
public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
@ -1674,6 +1683,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
@ -1683,6 +1693,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V

View File

@ -1,7 +1,7 @@
package app.revanced.patches.idaustria.detection.signature package app.revanced.patches.idaustria.detection.signature
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused") @Suppress("unused")
val spoofSignaturePatch = bytecodePatch( val spoofSignaturePatch = bytecodePatch(
@ -24,12 +24,6 @@ val spoofSignaturePatch = bytecodePatch(
"77ef1be61b2c01ebdabddcbf53cc4b6fd9a3c445606ee77b3758162c80ad8f8137b3c6864e92db904807dcb2be9d7717dd21" + "77ef1be61b2c01ebdabddcbf53cc4b6fd9a3c445606ee77b3758162c80ad8f8137b3c6864e92db904807dcb2be9d7717dd21" +
"bf42c121d620ddfb7914f7a95c713d9e1c1b7bdb4a03d618e40cf7e9e235c0b5687e03b7ab3,publicExponent=10001}" "bf42c121d620ddfb7914f7a95c713d9e1c1b7bdb4a03d618e40cf7e9e235c0b5687e03b7ab3,publicExponent=10001}"
spoofSignatureFingerprint.method.addInstructions( spoofSignatureFingerprint.method.returnEarly(expectedSignature)
0,
"""
const-string v0, "$expectedSignature"
return-object v0
""",
)
} }
} }

View File

@ -1,9 +1,16 @@
package app.revanced.patches.instagram.ads package app.revanced.patches.instagram.ads
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.meta.ads.adInjectorFingerprint
import app.revanced.util.returnEarly
@Deprecated("Patch was moved to different package: app.revanced.patches.meta.ads.hideAdsPatch")
@Suppress("unused") @Suppress("unused")
val hideAdsPatch = bytecodePatch { val hideAdsPatch = bytecodePatch(
dependsOn(app.revanced.patches.meta.ads.hideAdsPatch) name = "Hide ads",
) {
compatibleWith("com.instagram.android")
execute {
adInjectorFingerprint.method.returnEarly(false)
}
} }

View File

@ -11,3 +11,15 @@ internal val getMobileConfigBoolFingerprint = fingerprint {
classDef.interfaces.contains("Lcom/facebook/mobileconfig/factory/MobileConfigUnsafeContext;") classDef.interfaces.contains("Lcom/facebook/mobileconfig/factory/MobileConfigUnsafeContext;")
} }
} }
internal val metaAIKillSwitchCheckFingerprint = fingerprint {
strings("SearchAiagentImplementationsKillSwitch")
opcodes(Opcode.CONST_WIDE)
}
internal val extensionMethodFingerprint = fingerprint {
strings("REPLACED_BY_PATCH")
custom { method, classDef ->
method.name == EXTENSION_METHOD_NAME && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}

View File

@ -2,11 +2,14 @@ package app.revanced.patches.messenger.metaai
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.messenger.misc.extension.sharedExtensionPatch import app.revanced.patches.messenger.misc.extension.sharedExtensionPatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;" internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;"
internal const val EXTENSION_METHOD_NAME = "overrideBooleanFlag"
@Suppress("unused") @Suppress("unused")
val removeMetaAIPatch = bytecodePatch( val removeMetaAIPatch = bytecodePatch(
@ -25,10 +28,25 @@ val removeMetaAIPatch = bytecodePatch(
addInstructions( addInstructions(
returnIndex, returnIndex,
""" """
invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBooleanFlag(JZ)Z invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->$EXTENSION_METHOD_NAME(JZ)Z
move-result v$returnRegister move-result v$returnRegister
""" """
) )
} }
// Extract the common starting digits of Meta AI flag IDs from a flag found in code.
val relevantDigits = with(metaAIKillSwitchCheckFingerprint) {
method.getInstruction<WideLiteralInstruction>(patternMatch!!.startIndex).wideLiteral
}.toString().substring(0, 7)
// Replace placeholder in the extension method.
with(extensionMethodFingerprint) {
method.replaceInstruction(
stringMatches!!.first().index,
"""
const-string v1, "$relevantDigits"
"""
)
}
} }
} }

View File

@ -2,4 +2,4 @@ package app.revanced.patches.messenger.misc.extension
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch("messenger", mainActivityOnCreateHook) val sharedExtensionPatch = sharedExtensionPatch("messenger", messengerApplicationOnCreateHook)

View File

@ -2,6 +2,8 @@ package app.revanced.patches.messenger.misc.extension
import app.revanced.patches.shared.misc.extension.extensionHook import app.revanced.patches.shared.misc.extension.extensionHook
internal val mainActivityOnCreateHook = extensionHook { internal val messengerApplicationOnCreateHook = extensionHook {
strings("MainActivity_onCreate_begin") custom { method, classDef ->
method.name == "onCreate" && classDef.endsWith("/MessengerApplication;")
}
} }

View File

@ -3,18 +3,9 @@ package app.revanced.patches.meta.ads
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly import app.revanced.util.returnEarly
@Deprecated("Instead use the Instagram or Threads specific hide ads patch")
@Suppress("unused") @Suppress("unused")
val hideAdsPatch = bytecodePatch( val hideAdsPatch = bytecodePatch {
name = "Hide ads",
) {
/**
* Patch is identical for both Instagram and Threads app.
*/
compatibleWith(
"com.instagram.android",
"com.instagram.barcelona",
)
execute { execute {
adInjectorFingerprint.method.returnEarly(false) adInjectorFingerprint.method.returnEarly(false)
} }

View File

@ -1,7 +1,7 @@
package app.revanced.patches.nunl.firebase package app.revanced.patches.nunl.firebase
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused") @Suppress("unused")
val spoofCertificatePatch = bytecodePatch( val spoofCertificatePatch = bytecodePatch(
@ -12,13 +12,7 @@ val spoofCertificatePatch = bytecodePatch(
execute { execute {
getFingerprintHashForPackageFingerprints.forEach { fingerprint -> getFingerprintHashForPackageFingerprints.forEach { fingerprint ->
fingerprint.method.addInstructions( fingerprint.method.returnEarly("eae41fc018df2731a9b6ae1ac327da44a288667b")
0,
"""
const-string v0, "eae41fc018df2731a9b6ae1ac327da44a288667b"
return-object v0
""",
)
} }
} }
} }

View File

@ -1,12 +1,7 @@
package app.revanced.patches.pandora.ads package app.revanced.patches.pandora.ads
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.pandora.shared.constructUserDataFingerprint import app.revanced.util.returnEarly
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused") @Suppress("unused")
val disableAudioAdsPatch = bytecodePatch( val disableAudioAdsPatch = bytecodePatch(
@ -15,16 +10,7 @@ val disableAudioAdsPatch = bytecodePatch(
compatibleWith("com.pandora.android") compatibleWith("com.pandora.android")
execute { execute {
constructUserDataFingerprint.method.apply { getIsAdSupportedFingerprint.method.returnEarly(false)
// First match is "hasAudioAds". requestAudioAdFingerprint.method.returnEarly()
val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
val hasAudioAdsRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
addInstruction(
moveResultIndex + 1,
"const/4 v$hasAudioAdsRegister, 0"
)
}
} }
} }

View File

@ -0,0 +1,15 @@
package app.revanced.patches.pandora.ads
import app.revanced.patcher.fingerprint
internal val getIsAdSupportedFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getIsAdSupported" && classDef.endsWith("UserData;")
}
}
internal val requestAudioAdFingerprint = fingerprint {
custom { method, classDef ->
method.name == "requestAudioAdFromAdSDK" && classDef.endsWith("ContentServiceOpsImpl;")
}
}

View File

@ -1,12 +1,7 @@
package app.revanced.patches.pandora.misc package app.revanced.patches.pandora.misc
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.pandora.shared.constructUserDataFingerprint import app.revanced.util.returnEarly
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused") @Suppress("unused")
val enableUnlimitedSkipsPatch = bytecodePatch( val enableUnlimitedSkipsPatch = bytecodePatch(
@ -15,17 +10,6 @@ val enableUnlimitedSkipsPatch = bytecodePatch(
compatibleWith("com.pandora.android") compatibleWith("com.pandora.android")
execute { execute {
constructUserDataFingerprint.method.apply { skipLimitBehaviorFingerprint.method.returnEarly("unlimited")
// Last match is "skipLimitBehavior".
val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
val moveResultObjectIndex =
indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
val skipLimitBehaviorRegister = getInstruction<OneRegisterInstruction>(moveResultObjectIndex).registerA
addInstruction(
moveResultObjectIndex + 1,
"const-string v$skipLimitBehaviorRegister, \"unlimited\""
)
}
} }
} }

View File

@ -0,0 +1,9 @@
package app.revanced.patches.pandora.misc
import app.revanced.patcher.fingerprint
internal val skipLimitBehaviorFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getSkipLimitBehavior" && classDef.endsWith("UserData;")
}
}

View File

@ -1,7 +0,0 @@
package app.revanced.patches.pandora.shared
import app.revanced.patcher.fingerprint
internal val constructUserDataFingerprint = fingerprint {
strings("hasAudioAds", "skipLimitBehavior")
}

View File

@ -1,8 +1,8 @@
package app.revanced.patches.photomath.detection.deviceid package app.revanced.patches.photomath.detection.deviceid
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch
import app.revanced.util.returnEarly
import kotlin.random.Random import kotlin.random.Random
@Suppress("unused") @Suppress("unused")
@ -15,12 +15,6 @@ val getDeviceIdPatch = bytecodePatch(
compatibleWith("com.microblink.photomath") compatibleWith("com.microblink.photomath")
execute { execute {
getDeviceIdFingerprint.method.replaceInstructions( getDeviceIdFingerprint.method.returnEarly(Random.nextLong().toString(16))
0,
"""
const-string v0, "${Random.nextLong().toString(16)}"
return-object v0
""",
)
} }
} }

View File

@ -3,6 +3,7 @@ package app.revanced.patches.piccomafr.misc
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.stringOption import app.revanced.patcher.patch.stringOption
import app.revanced.util.returnEarly
@Suppress("unused") @Suppress("unused")
val spoofAndroidDeviceIdPatch = bytecodePatch( val spoofAndroidDeviceIdPatch = bytecodePatch(
@ -39,12 +40,6 @@ val spoofAndroidDeviceIdPatch = bytecodePatch(
) { it!!.matches("[A-Fa-f0-9]{16}".toRegex()) } ) { it!!.matches("[A-Fa-f0-9]{16}".toRegex()) }
execute { execute {
getAndroidIdFingerprint.method.addInstructions( getAndroidIdFingerprint.method.returnEarly(androidDeviceId!!)
0,
"""
const-string v0, "$androidDeviceId"
return-object v0
""",
)
} }
} }

View File

@ -1,9 +1,9 @@
package app.revanced.patches.reddit.customclients.boostforreddit.api package app.revanced.patches.reddit.customclients.boostforreddit.api
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.spoofClientPatch import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com") { clientIdOption -> val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com") { clientIdOption ->
@ -14,13 +14,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com")
execute { execute {
// region Patch client id. // region Patch client id.
getClientIdFingerprint.method.addInstructions( getClientIdFingerprint.method.returnEarly(clientId!!)
0,
"""
const-string v0, "$clientId"
return-object v0
""",
)
// endregion // endregion

View File

@ -1,9 +1,8 @@
package app.revanced.patches.reddit.customclients.joeyforreddit.api package app.revanced.patches.reddit.customclients.joeyforreddit.api
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
import app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy.disablePiracyDetectionPatch import app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy.disablePiracyDetectionPatch
import app.revanced.patches.reddit.customclients.spoofClientPatch import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.util.returnEarly
val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/authorize_callback") { clientIdOption -> val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/authorize_callback") { clientIdOption ->
dependsOn(disablePiracyDetectionPatch) dependsOn(disablePiracyDetectionPatch)
@ -19,13 +18,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/a
execute { execute {
// region Patch client id. // region Patch client id.
getClientIdFingerprint.method.addInstructions( getClientIdFingerprint.method.returnEarly(clientId!!)
0,
"""
const-string v0, "$clientId"
return-object v0
""",
)
// endregion // endregion
@ -35,13 +28,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/a
val randomName = (0..100000).random() val randomName = (0..100000).random()
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
authUtilityUserAgentFingerprint.method.replaceInstructions( authUtilityUserAgentFingerprint.method.returnEarly(userAgent)
0,
"""
const-string v0, "$userAgent"
return-object v0
""",
)
// endregion // endregion
} }

View File

@ -2,12 +2,12 @@ package app.revanced.patches.reddit.customclients.redditisfun.api
import app.revanced.patcher.Fingerprint import app.revanced.patcher.Fingerprint
import app.revanced.patcher.Match import app.revanced.patcher.Match
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.spoofClientPatch import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.iface.reference.StringReference
@ -54,13 +54,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "redditisfun://auth") { cl
val randomName = (0..100000).random() val randomName = (0..100000).random()
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
getUserAgentFingerprint.method.addInstructions( getUserAgentFingerprint.method.returnEarly(userAgent)
0,
"""
const-string v0, "$userAgent"
return-object v0
""",
)
// endregion // endregion

View File

@ -1,7 +1,7 @@
package app.revanced.patches.reddit.customclients.slide.api package app.revanced.patches.reddit.customclients.slide.api
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patches.reddit.customclients.spoofClientPatch import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.util.returnEarly
val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") { clientIdOption -> val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") { clientIdOption ->
compatibleWith("me.ccrama.redditslide") compatibleWith("me.ccrama.redditslide")
@ -9,12 +9,6 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") {
val clientId by clientIdOption val clientId by clientIdOption
execute { execute {
getClientIdFingerprint.method.addInstructions( getClientIdFingerprint.method.returnEarly(clientId!!)
0,
"""
const-string v0, "$clientId"
return-object v0
""",
)
} }
} }

View File

@ -1,14 +1,14 @@
package app.revanced.patches.reddit.customclients.sync.syncforreddit.api package app.revanced.patches.reddit.customclients.sync.syncforreddit.api
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.spoofClientPatch import app.revanced.patches.reddit.customclients.spoofClientPatch
import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference import com.android.tools.smali.dexlib2.iface.reference.StringReference
import java.util.* import java.util.Base64
val spoofClientPatch = spoofClientPatch( val spoofClientPatch = spoofClientPatch(
redirectUri = "http://redditsync/auth", redirectUri = "http://redditsync/auth",
@ -28,13 +28,8 @@ val spoofClientPatch = spoofClientPatch(
getBearerTokenFingerprint.match(getAuthorizationStringFingerprint.originalClassDef).method.apply { getBearerTokenFingerprint.match(getAuthorizationStringFingerprint.originalClassDef).method.apply {
val auth = Base64.getEncoder().encodeToString("$clientId:".toByteArray(Charsets.UTF_8)) val auth = Base64.getEncoder().encodeToString("$clientId:".toByteArray(Charsets.UTF_8))
addInstructions( returnEarly("Basic $auth")
0,
"""
const-string v0, "Basic $auth"
return-object v0
""",
)
val occurrenceIndex = val occurrenceIndex =
getAuthorizationStringFingerprint.stringMatches!!.first().index getAuthorizationStringFingerprint.stringMatches!!.first().index
@ -63,23 +58,19 @@ val spoofClientPatch = spoofClientPatch(
val randomName = (0..100000).random() val randomName = (0..100000).random()
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
getUserAgentFingerprint.method.replaceInstruction( getUserAgentFingerprint.method.returnEarly(userAgent)
0,
"""
const-string v0, "$userAgent"
return-object v0
""",
)
// endregion // endregion
// region Patch Imgur API URL. // region Patch Imgur API URL.
val apiUrlIndex = imgurImageAPIFingerprint.stringMatches!!.first().index imgurImageAPIFingerprint.let {
imgurImageAPIFingerprint.method.replaceInstruction( val apiUrlIndex = it.stringMatches!!.first().index
it.method.replaceInstruction(
apiUrlIndex, apiUrlIndex,
"const-string v1, \"https://api.imgur.com/3/image\"", "const-string v1, \"https://api.imgur.com/3/image\"",
) )
}
// endregion // endregion
} }

View File

@ -84,7 +84,7 @@ fun checkEnvironmentPatch(
fun invokeCheck() = mainActivityOnCreateFingerprint.method.addInstruction( fun invokeCheck() = mainActivityOnCreateFingerprint.method.addInstruction(
0, 0,
"invoke-static/range { p0 .. p0 },$EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V", "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V",
) )
setPatchInfo() setPatchInfo()

View File

@ -3,11 +3,11 @@ package app.revanced.patches.shared.misc.extension
import app.revanced.patcher.Fingerprint import app.revanced.patcher.Fingerprint
import app.revanced.patcher.FingerprintBuilder import app.revanced.patcher.FingerprintBuilder
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
import java.net.URLDecoder import java.net.URLDecoder
import java.util.jar.JarFile import java.util.jar.JarFile
@ -80,14 +80,7 @@ fun sharedExtensionPatch(
} }
val manifestValue = getPatchesManifestEntry("Version") val manifestValue = getPatchesManifestEntry("Version")
returnEarly(manifestValue)
addInstructions(
0,
"""
const-string v0, "$manifestValue"
return-object v0
""",
)
} }
} }
} }

View File

@ -0,0 +1,23 @@
package app.revanced.patches.shared.misc.settings
import app.revanced.patcher.fingerprint
import app.revanced.patches.shared.misc.extension.EXTENSION_CLASS_DESCRIPTOR
import com.android.tools.smali.dexlib2.AccessFlags
internal val themeLightColorResourceNameFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("Ljava/lang/String;")
parameters()
custom { method, classDef ->
method.name == "getThemeLightColorResourceName" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}
internal val themeDarkColorResourceNameFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("Ljava/lang/String;")
parameters()
custom { method, classDef ->
method.name == "getThemeDarkColorResourceName" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.patches.shared.misc.settings package app.revanced.patches.shared.misc.settings
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.resources.addResource import app.revanced.patches.all.misc.resources.addResource
import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResources
@ -13,6 +14,7 @@ import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources import app.revanced.util.copyResources
import app.revanced.util.getNode import app.revanced.util.getNode
import app.revanced.util.insertFirst import app.revanced.util.insertFirst
import app.revanced.util.returnEarly
import org.w3c.dom.Node import org.w3c.dom.Node
// TODO: Delete this on next major version bump. // TODO: Delete this on next major version bump.
@ -22,6 +24,30 @@ fun settingsPatch (
preferences: Set<BasePreference>, preferences: Set<BasePreference>,
) = settingsPatch(listOf(rootPreference), preferences) ) = settingsPatch(listOf(rootPreference), preferences)
private var themeForegroundColor : String? = null
private var themeBackgroundColor : String? = null
/**
* Sets the default theme colors used in various ReVanced specific settings menus.
* By default these colors are white and black, but instead can be set to the
* same color the target app uses for it's own settings.
*/
fun overrideThemeColors(foregroundColor: String, backgroundColor: String) {
themeForegroundColor = foregroundColor
themeBackgroundColor = backgroundColor
}
private val settingsColorPatch = bytecodePatch {
finalize {
if (themeForegroundColor != null) {
themeLightColorResourceNameFingerprint.method.returnEarly(themeForegroundColor!!)
}
if (themeBackgroundColor != null) {
themeDarkColorResourceNameFingerprint.method.returnEarly(themeBackgroundColor!!)
}
}
}
/** /**
* A resource patch that adds settings to a settings fragment. * A resource patch that adds settings to a settings fragment.
* *
@ -33,12 +59,28 @@ fun settingsPatch (
rootPreferences: List<Pair<BasePreference, String>>? = null, rootPreferences: List<Pair<BasePreference, String>>? = null,
preferences: Set<BasePreference>, preferences: Set<BasePreference>,
) = resourcePatch { ) = resourcePatch {
dependsOn(addResourcesPatch) dependsOn(addResourcesPatch, settingsColorPatch)
execute { execute {
copyResources( copyResources(
"settings", "settings",
ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"), ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"),
ResourceGroup("drawable",
// CustomListPreference resources.
"revanced_ic_dialog_alert.xml",
"revanced_settings_arrow_time.xml",
"revanced_settings_circle_background.xml",
"revanced_settings_cursor.xml",
"revanced_settings_custom_checkmark.xml",
"revanced_settings_search_icon.xml",
"revanced_settings_toolbar_arrow_left.xml",
),
ResourceGroup("layout",
"revanced_custom_list_item_checked.xml",
// Color picker.
"revanced_color_dot_widget.xml",
"revanced_color_picker.xml",
)
) )
addResources("shared", "misc.settings.settingsResourcePatch") addResources("shared", "misc.settings.settingsResourcePatch")

View File

@ -12,7 +12,7 @@ import org.w3c.dom.Document
* @param summaryKey The preference summary key. * @param summaryKey The preference summary key.
* @param icon The preference icon resource name. * @param icon The preference icon resource name.
* @param layout Layout declaration. * @param layout Layout declaration.
* @param tag The preference tag. * @param tag The preference class type.
* @param entriesKey The entries array key. * @param entriesKey The entries array key.
* @param entryValuesKey The entry values array key. * @param entryValuesKey The entry values array key.
*/ */
@ -20,10 +20,12 @@ import org.w3c.dom.Document
class ListPreference( class ListPreference(
key: String? = null, key: String? = null,
titleKey: String = "${key}_title", titleKey: String = "${key}_title",
summaryKey: String? = "${key}_summary", /** Summary key is ignored and will be removed soon */
//@Deprecated
summaryKey: String? = null,
icon: String? = null, icon: String? = null,
layout: String? = null, layout: String? = null,
tag: String = "ListPreference", tag: String = "app.revanced.extension.shared.settings.preference.CustomDialogListPreference",
val entriesKey: String? = "${key}_entries", val entriesKey: String? = "${key}_entries",
val entryValuesKey: String? = "${key}_entry_values" val entryValuesKey: String? = "${key}_entry_values"
) : BasePreference(key, titleKey, summaryKey, icon, layout, tag) { ) : BasePreference(key, titleKey, summaryKey, icon, layout, tag) {

View File

@ -72,7 +72,7 @@ val hideCreateButtonPatch = bytecodePatch(
if (oldNavigationBarAddItemMethod != null) { if (oldNavigationBarAddItemMethod != null) {
// In case an older version of the app is being patched, hook the old method which adds navigation bar items. // In case an older version of the app is being patched, hook the old method which adds navigation bar items.
// Return null early if the navigation bar item title resource id is the old Create button title resource id. // Return early if the navigation bar item title resource id is the old Create button title resource id.
oldNavigationBarAddItemFingerprint.methodOrNull?.apply { oldNavigationBarAddItemFingerprint.methodOrNull?.apply {
val getNavigationBarItemTitleStringIndex = indexOfFirstInstructionOrThrow { val getNavigationBarItemTitleStringIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>() val reference = getReference<MethodReference>()
@ -89,6 +89,16 @@ val hideCreateButtonPatch = bytecodePatch(
val isOldCreateButtonDescriptor = val isOldCreateButtonDescriptor =
"$EXTENSION_CLASS_DESCRIPTOR->isOldCreateButton(I)Z" "$EXTENSION_CLASS_DESCRIPTOR->isOldCreateButton(I)Z"
val returnEarlyInstruction = if (returnType == "V") {
// In older implementations the method return value is void.
"return-void"
} else {
// In newer implementations
// return null because the method return value is a BottomNavigationItemView.
"const/4 v0, 0\n" +
"return-object v0"
}
addInstructionsWithLabels( addInstructionsWithLabels(
0, 0,
""" """
@ -98,9 +108,7 @@ val hideCreateButtonPatch = bytecodePatch(
# If this navigation bar item is not the Create button, jump to the normal method logic. # If this navigation bar item is not the Create button, jump to the normal method logic.
if-eqz v0, :normal-method-logic if-eqz v0, :normal-method-logic
# Return null early because this method return value is a BottomNavigationItemView. $returnEarlyInstruction
const/4 v0, 0
return-object v0
""", """,
ExternalLabel("normal-method-logic", firstInstruction) ExternalLabel("normal-method-logic", firstInstruction)
) )

View File

@ -0,0 +1,123 @@
package app.revanced.patches.spotify.misc.lyrics
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
import java.net.InetAddress
import java.net.URI
import java.net.URISyntaxException
import java.net.UnknownHostException
import java.util.logging.Logger
@Suppress("unused")
val changeLyricsProviderPatch = bytecodePatch(
name = "Change lyrics provider",
description = "Changes the lyrics provider to a custom one.",
use = false,
) {
compatibleWith("com.spotify.music")
val lyricsProviderHost by stringOption(
key = "lyricsProviderHost",
default = "lyrics.natanchiodi.fr",
title = "Lyrics provider host",
description = "The domain name or IP address of a custom lyrics provider.",
required = false,
) {
// Fix bad data if the user enters a URL (https://whatever.com/path).
val host = try {
URI(it!!).host ?: it
} catch (e: URISyntaxException) {
return@stringOption false
}
// Do a courtesy check if the host can be resolved.
// If it does not resolve, then print a warning but use the host anyway.
// Unresolvable hosts should not be rejected, since the patching environment
// may not allow network connections or the network may be down.
try {
InetAddress.getByName(host)
} catch (e: UnknownHostException) {
Logger.getLogger(this::class.java.name).warning(
"Host \"$host\" did not resolve to any domain."
)
}
true
}
execute {
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
Logger.getLogger(this::class.java.name).severe(
"Change lyrics provider patch is not supported for this target version."
)
return@execute
}
val httpClientBuilderMethod = httpClientBuilderFingerprint.originalMethod
// region Create a modified copy of the HTTP client builder method with the custom lyrics provider host.
val patchedHttpClientBuilderMethod = with(httpClientBuilderMethod) {
val invokeBuildUrlIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.returnType == "Lokhttp3/HttpUrl;"
}
val setUrlBuilderHostIndex = indexOfFirstInstructionReversedOrThrow(invokeBuildUrlIndex) {
val reference = getReference<MethodReference>()
reference?.definingClass == "Lokhttp3/HttpUrl${"$"}Builder;" &&
reference.parameterTypes.firstOrNull() == "Ljava/lang/String;"
}
val hostRegister = getInstruction<FiveRegisterInstruction>(setUrlBuilderHostIndex).registerD
MutableMethod(this).apply {
name = "rv_getCustomLyricsProviderHttpClient"
addInstruction(
setUrlBuilderHostIndex,
"const-string v$hostRegister, \"$lyricsProviderHost\""
)
// Add the patched method to the class.
httpClientBuilderFingerprint.classDef.methods.add(this)
}
}
//endregion
// region Replace the call to the HTTP client builder method used exclusively for lyrics by the modified one.
getLyricsHttpClientFingerprint(httpClientBuilderMethod).method.apply {
val getLyricsHttpClientIndex = indexOfFirstInstructionOrThrow {
getReference<MethodReference>() == httpClientBuilderMethod
}
val getLyricsHttpClientInstruction = getInstruction<BuilderInstruction35c>(getLyricsHttpClientIndex)
// Replace the original method call with a call to our patched method.
replaceInstruction(
getLyricsHttpClientIndex,
BuilderInstruction35c(
getLyricsHttpClientInstruction.opcode,
getLyricsHttpClientInstruction.registerCount,
getLyricsHttpClientInstruction.registerC,
getLyricsHttpClientInstruction.registerD,
getLyricsHttpClientInstruction.registerE,
getLyricsHttpClientInstruction.registerF,
getLyricsHttpClientInstruction.registerG,
ImmutableMethodReference(
patchedHttpClientBuilderMethod.definingClass,
patchedHttpClientBuilderMethod.name, // Only difference from the original method.
patchedHttpClientBuilderMethod.parameters,
patchedHttpClientBuilderMethod.returnType
)
)
)
}
//endregion
}
}

View File

@ -0,0 +1,21 @@
package app.revanced.patches.spotify.misc.lyrics
import app.revanced.patcher.fingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val httpClientBuilderFingerprint = fingerprint {
strings("client == null", "scheduler == null")
}
internal fun getLyricsHttpClientFingerprint(httpClientBuilderMethodReference: MethodReference) =
fingerprint {
returns(httpClientBuilderMethodReference.returnType)
parameters()
custom { method, _ ->
method.indexOfFirstInstruction {
getReference<MethodReference>() == httpClientBuilderMethodReference
} >= 0
}
}

View File

@ -3,6 +3,7 @@ package app.revanced.patches.spotify.misc.privacy
import app.revanced.patcher.fingerprint import app.revanced.patcher.fingerprint
import app.revanced.util.literal import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val shareCopyUrlFingerprint = fingerprint { internal val shareCopyUrlFingerprint = fingerprint {
returns("Ljava/lang/Object;") returns("Ljava/lang/Object;")
@ -23,9 +24,15 @@ internal val shareCopyUrlLegacyFingerprint = fingerprint {
} }
internal val formatAndroidShareSheetUrlFingerprint = fingerprint { internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("Ljava/lang/String;") returns("Ljava/lang/String;")
parameters("L", "Ljava/lang/String;") parameters("L", "Ljava/lang/String;")
opcodes(
Opcode.GOTO,
Opcode.IF_EQZ,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT_OBJECT,
Opcode.RETURN_OBJECT
)
literal { literal {
'\n'.code.toLong() '\n'.code.toLong()
} }

View File

@ -8,6 +8,7 @@ import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@ -56,7 +57,15 @@ val sanitizeSharingLinksPatch = bytecodePatch(
shareUrlParameter = "p2" shareUrlParameter = "p2"
} else { } else {
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
shareUrlParameter = "p1" val methodAccessFlags = formatAndroidShareSheetUrlFingerprint.originalMethod.accessFlags
shareUrlParameter = if (AccessFlags.STATIC.isSet(methodAccessFlags)) {
// In newer implementations the method is static, so p0 is not `this`.
"p1"
} else {
// In older implementations the method is not static, making it so p0 is `this`.
// For that reason, add one to the parameter register.
"p2"
}
} }
shareSheetFingerprint.method.addInstructions( shareSheetFingerprint.method.addInstructions(

View File

@ -0,0 +1,16 @@
package app.revanced.patches.threads
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.meta.ads.adInjectorFingerprint
import app.revanced.util.returnEarly
@Suppress("unused")
val hideAdsPatch = bytecodePatch(
name = "Hide ads",
) {
compatibleWith("com.instagram.barcelona"("382.0.0.51.85"))
execute {
adInjectorFingerprint.method.returnEarly(false)
}
}

View File

@ -25,7 +25,7 @@ val embeddedAdsPatch = bytecodePatch(
addResources("twitch", "ad.embedded.embeddedAdsPatch") addResources("twitch", "ad.embedded.embeddedAdsPatch")
PreferenceScreen.ADS.SURESTREAM.addPreferences( PreferenceScreen.ADS.SURESTREAM.addPreferences(
ListPreference("revanced_block_embedded_ads", summaryKey = null), ListPreference("revanced_block_embedded_ads"),
) )
// Inject OkHttp3 application interceptor // Inject OkHttp3 application interceptor

View File

@ -34,10 +34,7 @@ val showDeletedMessagesPatch = bytecodePatch(
addResources("twitch", "chat.antidelete.showDeletedMessagesPatch") addResources("twitch", "chat.antidelete.showDeletedMessagesPatch")
PreferenceScreen.CHAT.GENERAL.addPreferences( PreferenceScreen.CHAT.GENERAL.addPreferences(
ListPreference( ListPreference("revanced_show_deleted_messages")
key = "revanced_show_deleted_messages",
summaryKey = null,
),
) )
// Spoiler mode: Force set hasModAccess member to true in constructor // Spoiler mode: Force set hasModAccess member to true in constructor

View File

@ -1,7 +1,7 @@
package app.revanced.patches.warnwetter.misc.firebasegetcert package app.revanced.patches.warnwetter.misc.firebasegetcert
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
val firebaseGetCertPatch = bytecodePatch( val firebaseGetCertPatch = bytecodePatch(
description = "Spoofs the X-Android-Cert header.", description = "Spoofs the X-Android-Cert header.",
@ -10,13 +10,7 @@ val firebaseGetCertPatch = bytecodePatch(
execute { execute {
listOf(getRegistrationCertFingerprint, getMessagingCertFingerprint).forEach { match -> listOf(getRegistrationCertFingerprint, getMessagingCertFingerprint).forEach { match ->
match.method.addInstructions( match.method.returnEarly("0799DDF0414D3B3475E88743C91C0676793ED450")
0,
"""
const-string v0, "0799DDF0414D3B3475E88743C91C0676793ED450"
return-object v0
""",
)
} }
} }
} }

View File

@ -77,8 +77,6 @@ val hideAdsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -25,8 +25,6 @@ val hideGetPremiumPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -23,8 +23,6 @@ val videoAdsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -53,8 +53,6 @@ val copyVideoUrlPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -24,8 +24,6 @@ val removeViewerDiscretionDialogPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -11,10 +11,14 @@ import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPref
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.shared.misc.settings.preference.TextPreference import app.revanced.patches.shared.misc.settings.preference.TextPreference
import app.revanced.patches.youtube.misc.playercontrols.* import app.revanced.patches.youtube.misc.playercontrols.addBottomControl
import app.revanced.patches.youtube.misc.playercontrols.initializeBottomControl
import app.revanced.patches.youtube.misc.playercontrols.injectVisibilityCheckCall
import app.revanced.patches.youtube.misc.playercontrols.playerControlsPatch
import app.revanced.patches.youtube.misc.playercontrols.playerControlsResourcePatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.mainActivityFingerprint import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
import app.revanced.patches.youtube.video.information.videoInformationPatch import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.util.ResourceGroup import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources import app.revanced.util.copyResources
@ -68,8 +72,6 @@ val downloadsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",
@ -83,12 +85,10 @@ val downloadsPatch = bytecodePatch(
injectVisibilityCheckCall(BUTTON_DESCRIPTOR) injectVisibilityCheckCall(BUTTON_DESCRIPTOR)
// Main activity is used to launch downloader intent. // Main activity is used to launch downloader intent.
mainActivityFingerprint.method.apply { mainActivityOnCreateFingerprint.method.addInstruction(
addInstruction( 1,
implementation!!.instructions.lastIndex, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V"
"invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V",
) )
}
offlineVideoEndpointFingerprint.method.apply { offlineVideoEndpointFingerprint.method.apply {
addInstructionsWithLabels( addInstructionsWithLabels(

View File

@ -20,8 +20,6 @@ val seekbarPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -18,8 +18,7 @@ private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/SeekbarThumbnailsPatch;" "Lapp/revanced/extension/youtube/patches/SeekbarThumbnailsPatch;"
val seekbarThumbnailsPatch = bytecodePatch( val seekbarThumbnailsPatch = bytecodePatch(
description = "Adds an option to use high quality fullscreen seekbar thumbnails. " + description = "Adds an option to use high quality fullscreen seekbar thumbnails."
"Patching 19.16.39 adds an option to restore old seekbar thumbnails.",
) { ) {
dependsOn( dependsOn(
sharedExtensionPatch, sharedExtensionPatch,

View File

@ -14,7 +14,7 @@ import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater import app.revanced.patches.youtube.misc.playservice.is_19_43_or_greater
import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.mainActivityFingerprint import app.revanced.patches.youtube.shared.mainActivityConstructorFingerprint
import app.revanced.util.* import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
@ -43,10 +43,7 @@ private val swipeControlsResourcePatch = resourcePatch {
SwitchPreference("revanced_swipe_haptic_feedback"), SwitchPreference("revanced_swipe_haptic_feedback"),
SwitchPreference("revanced_swipe_save_and_restore_brightness"), SwitchPreference("revanced_swipe_save_and_restore_brightness"),
SwitchPreference("revanced_swipe_lowest_value_enable_auto_brightness"), SwitchPreference("revanced_swipe_lowest_value_enable_auto_brightness"),
ListPreference( ListPreference("revanced_swipe_overlay_style"),
"revanced_swipe_overlay_style",
summaryKey = null,
),
TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER), TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER),
TextPreference("revanced_swipe_overlay_progress_brightness_color", TextPreference("revanced_swipe_overlay_progress_brightness_color",
tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference", tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
@ -91,8 +88,6 @@ val swipeControlsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",
@ -103,7 +98,7 @@ val swipeControlsPatch = bytecodePatch(
execute { execute {
val wrapperClass = swipeControlsHostActivityFingerprint.classDef val wrapperClass = swipeControlsHostActivityFingerprint.classDef
val targetClass = mainActivityFingerprint.classDef val targetClass = mainActivityConstructorFingerprint.classDef
// Inject the wrapper class from the extension into the class hierarchy of MainActivity. // Inject the wrapper class from the extension into the class hierarchy of MainActivity.
wrapperClass.setSuperClass(targetClass.superclass) wrapperClass.setSuperClass(targetClass.superclass)

View File

@ -24,8 +24,6 @@ val autoCaptionsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -43,8 +43,6 @@ val customBrandingPatch = resourcePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -41,8 +41,6 @@ val changeHeaderPatch = resourcePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -22,8 +22,6 @@ val hideButtonsPatch = resourcePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -40,8 +40,6 @@ val navigationButtonsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -58,8 +58,6 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -33,8 +33,6 @@ val changeFormFactorPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",
@ -47,10 +45,7 @@ val changeFormFactorPatch = bytecodePatch(
addResources("youtube", "layout.formfactor.changeFormFactorPatch") addResources("youtube", "layout.formfactor.changeFormFactorPatch")
PreferenceScreen.GENERAL_LAYOUT.addPreferences( PreferenceScreen.GENERAL_LAYOUT.addPreferences(
ListPreference( ListPreference("revanced_change_form_factor")
"revanced_change_form_factor",
summaryKey = null,
)
) )
hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)

View File

@ -59,8 +59,6 @@ val hideEndscreenCardsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -31,8 +31,6 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -29,8 +29,6 @@ val disableFullscreenAmbientModePatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -125,8 +125,6 @@ val hideLayoutComponentsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -57,8 +57,6 @@ val hideInfoCardsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -24,8 +24,6 @@ val hidePlayerFlyoutMenuPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -48,8 +48,6 @@ val hideRelatedVideoOverlayPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -29,8 +29,6 @@ val disableRollingNumberAnimationPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -170,8 +170,6 @@ val hideShortsComponentsPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -21,8 +21,6 @@ val hideTimestampPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39",
"19.25.37",
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",

View File

@ -153,22 +153,6 @@ val miniplayerPatch = bytecodePatch(
compatibleWith( compatibleWith(
"com.google.android.youtube"( "com.google.android.youtube"(
"19.16.39", // First with modern miniplayers.
// 19.17.41 // Works without issues, but no reason to recommend over 19.16.
// 19.18.41 // Works without issues, but no reason to recommend over 19.16.
// 19.19.39 // Last bug free version with smaller Modern 1 miniplayer, but no reason to recommend over 19.16.
// 19.20.35 // Cannot swipe to expand.
// 19.21.40 // Cannot swipe to expand.
// 19.22.43 // Cannot swipe to expand.
// 19.23.40 // First with Modern 1 drag and drop, Cannot swipe to expand.
// 19.24.45 // First with larger Modern 1, Cannot swipe to expand.
"19.25.37", // First with double tap, last with skip forward/back buttons, last with swipe to expand/close, and last before double tap to expand seems to be required.
// 19.26.42 // Modern 1 Pause/play button are always hidden. Unusable.
// 19.28.42 // First with custom miniplayer size, screen flickers when swiping to maximize Modern 1. Swipe to close miniplayer is broken.
// 19.29.42 // All modern players are broken and ignore tapping the miniplayer video.
// 19.30.39 // Modern 3 is less broken when double tap expand is enabled, but cannot swipe to expand when double tap is off.
// 19.31.36 // All Modern 1 buttons are missing. Unusable.
// 19.32.36 // 19.32+ and beyond all work without issues.
"19.34.42", "19.34.42",
"19.43.41", "19.43.41",
"19.47.53", "19.47.53",
@ -184,23 +168,18 @@ val miniplayerPatch = bytecodePatch(
preferences += preferences +=
if (is_20_03_or_greater) { if (is_20_03_or_greater) {
ListPreference( ListPreference("revanced_miniplayer_type")
"revanced_miniplayer_type",
summaryKey = null,
)
} else if (is_19_43_or_greater) { } else if (is_19_43_or_greater) {
ListPreference( ListPreference(
"revanced_miniplayer_type", key = "revanced_miniplayer_type",
summaryKey = null,
entriesKey = "revanced_miniplayer_type_legacy_19_43_entries", entriesKey = "revanced_miniplayer_type_legacy_19_43_entries",
entryValuesKey = "revanced_miniplayer_type_legacy_19_43_entry_values", entryValuesKey = "revanced_miniplayer_type_legacy_19_43_entry_values"
) )
} else { } else {
ListPreference( ListPreference(
"revanced_miniplayer_type", key = "revanced_miniplayer_type",
summaryKey = null,
entriesKey = "revanced_miniplayer_type_legacy_19_16_entries", entriesKey = "revanced_miniplayer_type_legacy_19_16_entries",
entryValuesKey = "revanced_miniplayer_type_legacy_19_16_entry_values", entryValuesKey = "revanced_miniplayer_type_legacy_19_16_entry_values"
) )
} }

Some files were not shown because too many files have changed in this diff Show More