feat(YouTube): Add Change form factor, Remove Change layout patch

This commit is contained in:
inotia00
2025-03-14 18:37:56 +09:00
parent 1b3ebe1a58
commit 86e5c1b45c
22 changed files with 579 additions and 212 deletions

View File

@ -24,9 +24,6 @@ import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
/**
* @noinspection rawtypes
*/
@SuppressWarnings("unused")
public abstract class Setting<T> {
@ -128,6 +125,7 @@ public abstract class Setting<T> {
/**
* @return All settings that have been created, sorted by keys.
* @noinspection Java8ListSort
*/
@NonNull
private static List<Setting<?>> allLoadedSettingsSorted() {
@ -171,7 +169,6 @@ public abstract class Setting<T> {
/**
* Confirmation message to display, if the user tries to change the setting from the default value.
* Currently this works only for Boolean setting types.
*/
@Nullable
public final StringRef userDialogMessage;
@ -271,6 +268,7 @@ public abstract class Setting<T> {
* <p>
* This method will be deleted in the future.
*/
@SuppressWarnings("rawtypes")
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
@ -452,6 +450,7 @@ public abstract class Setting<T> {
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
//noinspection rawtypes
for (Setting setting : SETTINGS) {
String key = setting.getImportExportKey();
if (json.has(key)) {

View File

@ -12,6 +12,7 @@ import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
@ -21,6 +22,9 @@ import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
@ -48,10 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
if (str == null) {
return;
}
Setting<?> setting = Setting.getSettingFromPath(str);
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
if (setting == null) {
return;
}
@ -59,24 +60,23 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (pref == null) {
return;
}
Logger.printDebug(() -> "Preference changed: " + setting.key);
if (!settingImportInProgress && !showingUserDialogMessage) {
if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
// Do not change the setting yet, to allow preserving whatever
// list/text value was previously set if it needs to be reverted.
showSettingUserDialogConfirmation(pref, setting);
return;
} else if (setting.rebootApp) {
showRestartDialog(getContext());
}
}
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
} else if (setting.rebootApp) {
showRestartDialog(getActivity());
}
}
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
@ -90,14 +90,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
final int id = getXmlIdentifier("revanced_prefs");
final int identifier = getXmlIdentifier("revanced_prefs");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
if (id == 0) return;
addPreferencesFromResource(id);
Utils.sortPreferenceGroups(getPreferenceScreen());
PreferenceScreen screen = getPreferenceScreen();
Utils.sortPreferenceGroups(screen);
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
}
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
Utils.verifyOnMainThread();
final var context = getActivity();
@ -107,12 +109,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
.setTitle(android.R.string.dialog_alert_title)
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
// User confirmed, save to the Setting.
updatePreference(pref, setting, true, false);
// Update availability of other preferences that may be changed.
updateUIAvailability();
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
// Restore whatever the setting was before the change.
updatePreference(pref, setting, true, true);
})
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
@ -123,7 +132,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true, true);
updatePreferenceScreen(getPreferenceScreen(), true,true);
}
/**
@ -133,24 +142,48 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
updatePreferenceScreen(getPreferenceScreen(), false, false);
}
/**
* @return If the preference is currently set to the default value of the Setting.
*/
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
if (pref instanceof SwitchPreference switchPref) {
return switchPref.isChecked() == (Boolean) setting.defaultValue;
}
if (pref instanceof EditTextPreference editPreference) {
return editPreference.getText().equals(setting.defaultValue.toString());
}
if (pref instanceof ListPreference listPref) {
return listPref.getValue().equals(setting.defaultValue.toString());
}
throw new IllegalStateException("Must override method to handle "
+ "preference type: " + pref.getClass());
}
/**
* Syncs all UI Preferences to any {@link Setting} they represent.
*/
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i);
if (pref instanceof PreferenceScreen preferenceScreen) {
updatePreferenceScreen(preferenceScreen, syncSettingValue, applySettingToPreference);
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
if (pref instanceof PreferenceGroup subGroup) {
updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
} else if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && (pref instanceof SwitchPreference
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
// Probably a typo in the patches preference declaration.
Logger.printException(() -> "Preference key has no setting: " + key);
}
}
}
@ -166,26 +199,26 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
protected void syncSettingWithPreference(@NonNull Preference pref,
@NonNull Setting<?> setting,
boolean applySettingToPreference) {
if (pref instanceof SwitchPreference switchPreference) {
if (pref instanceof SwitchPreference switchPref) {
BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) {
switchPreference.setChecked(boolSetting.get());
switchPref.setChecked(boolSetting.get());
} else {
BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked());
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference editTextPreference) {
} else if (pref instanceof EditTextPreference editPreference) {
if (applySettingToPreference) {
editTextPreference.setText(setting.get().toString());
editPreference.setText(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, editTextPreference.getText());
Setting.privateSetValueFromString(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference listPreference) {
} else if (pref instanceof ListPreference listPref) {
if (applySettingToPreference) {
listPreference.setValue(setting.get().toString());
listPref.setValue(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, listPreference.getValue());
Setting.privateSetValueFromString(setting, listPref.getValue());
}
updateListPreferenceSummary(listPreference, setting);
updateListPreferenceSummary(listPref, setting);
} else {
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
}
@ -194,7 +227,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Updates a UI Preference with the {@link Setting} that backs it.
*
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/

View File

@ -3,6 +3,8 @@ package app.revanced.extension.shared.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.content.ClipboardManager;
import android.content.Context;
@ -13,6 +15,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
@ -43,11 +46,11 @@ import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import kotlin.text.Regex;
@SuppressWarnings("deprecation")
public class Utils {
@ -529,6 +532,81 @@ public class Utils {
}
}
/**
* Ignore this class. It must be public to satisfy Android requirements.
*/
public static final class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/**
* Safe to call from any thread
*/
@ -737,14 +815,14 @@ public class Utils {
}
}
private static final Regex punctuationRegex = new Regex("\\p{P}+");
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
/**
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
*/
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
if (original == null) return "";
return punctuationRegex.replace(original, "").toLowerCase();
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
}
/**
@ -763,8 +841,8 @@ public class Utils {
Preference preference = group.getPreference(i);
final Sort preferenceSort;
if (preference instanceof PreferenceGroup preferenceGroup) {
sortPreferenceGroups(preferenceGroup);
if (preference instanceof PreferenceGroup subGroup) {
sortPreferenceGroups(subGroup);
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
} else {
// Allow individual preferences to set a key sorting.
@ -774,13 +852,16 @@ public class Utils {
final String sortValue;
switch (preferenceSort) {
case BY_TITLE ->
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
case BY_KEY -> sortValue = preference.getKey();
case UNSORTED -> {
case BY_TITLE:
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
break;
case BY_KEY:
sortValue = preference.getKey();
break;
case UNSORTED:
continue; // Keep original sorting.
}
default -> throw new IllegalStateException();
default:
throw new IllegalStateException();
}
preferences.put(sortValue, preference);
@ -790,7 +871,7 @@ public class Utils {
for (Preference pref : preferences.values()) {
int order = index++;
// If the preference is a PreferenceScreen or is an intent preference, move to the top.
// Move any screens, intents, and the one off About preference to the top.
if (pref instanceof PreferenceScreen || pref.getIntent() != null) {
// Arbitrary high number.
order -= 1000;
@ -799,4 +880,32 @@ public class Utils {
pref.setOrder(order);
}
}
/**
* Set all preferences to multiline titles if the device is not using an English variant.
* The English strings are heavily scrutinized and all titles fit on screen
* except 2 or 3 preference strings and those do not affect readability.
* <p>
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
* and visually it looks better to clip the text and keep all titles 1 line.
*/
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
if (!isSDKAbove(26)) {
return;
}
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
return;
}
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
pref.setSingleLineTitle(false);
if (pref instanceof PreferenceGroup subGroup) {
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
}
}
}
}

View File

@ -189,14 +189,13 @@ public final class AlternativeThumbnailsPatch {
// Unknown tab, treat as the home tab;
return homeOption;
}
if (selectedNavButton == NavigationButton.HOME) {
return homeOption;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
return subscriptionsOption;
}
// A library tab variant is active.
return libraryOption;
return switch (selectedNavButton) {
case SUBSCRIPTIONS, NOTIFICATIONS -> subscriptionsOption;
case LIBRARY -> libraryOption;
// Home or explore tab.
default -> homeOption;
};
}
/**

View File

@ -556,14 +556,13 @@ public final class KeywordContentFilter extends Filter {
if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home.
}
if (selectedNavButton == NavigationButton.HOME) {
return hideHome;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) {
return hideSubscriptions;
}
// User is in the Library or Notifications tab.
return false;
return switch (selectedNavButton) {
case HOME, EXPLORE -> hideHome;
case SUBSCRIPTIONS -> hideSubscriptions;
// User is in the Library or notifications.
default -> false;
};
}
private void updateStats(boolean videoWasHidden, @Nullable String keyword) {

View File

@ -0,0 +1,151 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.view.View;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public class ChangeFormFactorPatch {
public enum FormFactor {
/**
* Unmodified, and same as un-patched.
*/
DEFAULT(null, null, null),
/**
* <pre>
* Some changes include:
* - Explore tab is present.
* - watch history is missing.
* - feed thumbnails fade in.
*/
UNKNOWN(0, null, null),
SMALL(1, null, TRUE),
SMALL_WIDTH_DP(1, 480, TRUE),
LARGE(2, null, FALSE),
LARGE_WIDTH_DP(2, 600, FALSE),
/**
* Cars with 'Google built-in'.
* Layout seems identical to {@link #UNKNOWN}
* even when using an Android Automotive device.
*/
AUTOMOTIVE(3, null, null),
WEARABLE(4, null, null);
@Nullable
final Integer formFactorType;
@Nullable
final Integer widthDp;
@Nullable
final Boolean setMinimumDp;
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
this.formFactorType = formFactorType;
this.widthDp = widthDp;
this.setMinimumDp = setMinimumDp;
}
private boolean setMinimumDp() {
return BooleanUtils.isTrue(setMinimumDp);
}
}
private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
@Nullable
private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType;
private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
/**
* Injection point.
*/
public static int getFormFactor(int original) {
if (FORM_FACTOR_TYPE == null) return original;
if (USING_AUTOMOTIVE_TYPE) {
// Do not change if the player is opening or is opened,
// otherwise the video description cannot be opened.
PlayerType current = PlayerType.getCurrent();
if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
Logger.printDebug(() -> "Using original form factor for player");
return original;
}
if (!RootView.isSearchBarActive()) {
// Automotive type shows error 400 when opening a channel page and using some explore tab.
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
// Work around the issue by using the original form factor if not in search and the
// navigation back button is present.
if (RootView.isBackButtonVisible()) {
Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
return original;
}
// Do not change library tab otherwise watch history is hidden.
// Do this check last since the current navigation button is required.
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
return original;
}
}
}
return FORM_FACTOR_TYPE;
}
/**
* Injection point.
*/
public static int getWidthDp(int original) {
if (FORM_FACTOR_TYPE == null) return original;
Integer widthDp = FORM_FACTOR.widthDp;
if (widthDp == null) {
return original;
}
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
if (smallestScreenWidthDp == 0) {
return original;
}
return FORM_FACTOR.setMinimumDp()
? Math.min(smallestScreenWidthDp, widthDp)
: Math.max(smallestScreenWidthDp, widthDp);
}
public static boolean phoneLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 1);
}
public static boolean tabletLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 2);
}
/**
* Injection point.
*/
public static void navigationTabCreated(NavigationButton button, View tabView) {
// On first startup of the app the navigation buttons are fetched and updated.
// If the user immediately opens the 'You' or opens a video, then the call to
// update the navigtation buttons will use the non automotive form factor
// and the explore tab is missing.
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
// For now, always hide the explore tab.
if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
tabView.setVisibility(View.GONE);
}
}
}

View File

@ -1,79 +0,0 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Objects;
import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class LayoutSwitchPatch {
public enum FormFactor {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL(null, null, null),
SMALL_FORM_FACTOR(1, null, TRUE),
SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
LARGE_FORM_FACTOR(2, null, FALSE),
LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
@Nullable
final Integer formFactorType;
@Nullable
final Integer widthDp;
@Nullable
final Boolean setMinimumDp;
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
this.formFactorType = formFactorType;
this.widthDp = widthDp;
this.setMinimumDp = setMinimumDp;
}
private boolean setMinimumDp() {
return BooleanUtils.isTrue(setMinimumDp);
}
}
private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
public static int getFormFactor(int original) {
Integer formFactorType = FORM_FACTOR.formFactorType;
return formFactorType == null
? original
: formFactorType;
}
public static int getWidthDp(int original) {
Integer widthDp = FORM_FACTOR.widthDp;
if (widthDp == null) {
return original;
}
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
if (smallestScreenWidthDp == 0) {
return original;
}
return FORM_FACTOR.setMinimumDp()
? Math.min(smallestScreenWidthDp, widthDp)
: Math.max(smallestScreenWidthDp, widthDp);
}
public static boolean phoneLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 1);
}
public static boolean tabletLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 2);
}
}

View File

@ -32,9 +32,9 @@ import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeT
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode;
import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
@ -154,7 +154,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
public static final EnumSetting<FormFactor> CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));

View File

@ -75,7 +75,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
if (str == null) return;
Setting<?> setting = Setting.getSettingFromPath(str);
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
if (setting == null) return;

View File

@ -8,7 +8,7 @@ import android.preference.Preference;
import android.preference.SwitchPreference;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch;
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
@ -82,7 +82,7 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
*/
private static void TabletLayoutLinks() {
final boolean isTablet = ExtendedUtils.isTablet() &&
!LayoutSwitchPatch.phoneLayoutEnabled();
!ChangeFormFactorPatch.phoneLayoutEnabled();
enableDisablePreferences(
isTablet,

View File

@ -223,6 +223,10 @@ public final class NavigationBar {
* This tab will never be in a selected state, even if the create video UI is on screen.
*/
CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
/**
* Only shown to automotive layout.
*/
EXPLORE("TAB_EXPLORE"),
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
/**
* Notifications tab. Only present when

View File

@ -2,9 +2,48 @@ package app.revanced.extension.youtube.shared;
import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible;
import android.graphics.drawable.Drawable;
import android.widget.FrameLayout;
import java.lang.ref.WeakReference;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public final class RootView {
/**
* Interface to call obfuscated methods in AppCompat Toolbar class.
*/
public interface AppCompatToolbarPatchInterface {
Drawable patch_getToolbarIcon();
}
private static volatile WeakReference<AppCompatToolbarPatchInterface> toolbarResultsRef
= new WeakReference<>(null);
/**
* Injection point.
*/
public static void setToolbar(FrameLayout layout) {
AppCompatToolbarPatchInterface toolbar = Utils.getChildView(layout, false, (view) ->
view instanceof AppCompatToolbarPatchInterface
);
if (toolbar == null) {
Logger.printException(() -> "Could not find toolbar");
return;
}
toolbarResultsRef = new WeakReference<>(toolbar);
}
public static boolean isBackButtonVisible() {
AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get();
return toolbar != null && toolbar.patch_getToolbarIcon() != null;
}
/**
* @return If the search bar is on screen. This includes if the player
* is on screen and the search results are behind the player (and not visible).