diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index f623d8a57..98b14a097 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -72,6 +72,7 @@ body:
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
+ - **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index f49436ec6..13d436ba2 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -72,6 +72,7 @@ body:
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
+ - **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef513ef33..902aa6cba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,61 @@
+## [5.40.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.40.0...v5.40.1-dev.1) (2025-09-21)
+
+
+### Bug Fixes
+
+* **YouTube - Return YouTube Dislike:** Do not show error toast if API returns 401 status ([#5949](https://github.com/ReVanced/revanced-patches/issues/5949)) ([58d088a](https://github.com/ReVanced/revanced-patches/commit/58d088ab307440a6912a867246da799b7dd6499b))
+* **YouTube - Settings:** Use an overlay to show search results ([#5806](https://github.com/ReVanced/revanced-patches/issues/5806)) ([ece8076](https://github.com/ReVanced/revanced-patches/commit/ece8076f7cefd752b97515014bc50fe4fd80171e))
+
+# [5.40.0](https://github.com/ReVanced/revanced-patches/compare/v5.39.0...v5.40.0) (2025-09-21)
+
+
+### Bug Fixes
+
+* **Instagram - Limit feed to followed profiles:** Change patch to default off ([767f1e3](https://github.com/ReVanced/revanced-patches/commit/767f1e3695327bdbc4daea8b50a80d4c0a38456a))
+* **Spoof video streams:** Resolve occasional playback stuttering ([5c7c8b5](https://github.com/ReVanced/revanced-patches/commit/5c7c8b536416ec53cd98f7d59d11850aa1b70f11))
+* **YouTube - Force original audio:** Show UI setting summary if spoofing to Android Studio ([b7026b7](https://github.com/ReVanced/revanced-patches/commit/b7026b70865bc44de07b30f84ba8b8b608930d5b))
+* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b))
+* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93))
+
+
+### Features
+
+* **Instagram:** Add `Limit feed to followed profiles` patch ([#5908](https://github.com/ReVanced/revanced-patches/issues/5908)) ([8ba9a19](https://github.com/ReVanced/revanced-patches/commit/8ba9a19ade24c5fe9bd6d4e49772b7663522780e))
+* **Viber - Hide ads:** Support latest app target ([#5863](https://github.com/ReVanced/revanced-patches/issues/5863)) ([e6cce85](https://github.com/ReVanced/revanced-patches/commit/e6cce8554116df3c0ea6dbb7440c59c9e73d8334))
+* **YouTube - Hide video action buttons:** Add "Hide comments" button ([db796fb](https://github.com/ReVanced/revanced-patches/commit/db796fb8830b813e1ed626d491c4a797171e69e7))
+* **YouTube Music:** Add `Enable debugging` patch ([#5939](https://github.com/ReVanced/revanced-patches/issues/5939)) ([418f594](https://github.com/ReVanced/revanced-patches/commit/418f5945c213313f9a77cac9a5c326d89c754dfd))
+* **YouTube Music:** Add `Hide cast button` and `Navigation bar` patches ([#5934](https://github.com/ReVanced/revanced-patches/issues/5934)) ([651d358](https://github.com/ReVanced/revanced-patches/commit/651d3580967a252b57cbf4afbba02d6a4601ccfe))
+* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7))
+* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440))
+
+# [5.40.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.10...v5.40.0-dev.11) (2025-09-20)
+
+
+### Bug Fixes
+
+* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93))
+
+# [5.40.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.9...v5.40.0-dev.10) (2025-09-20)
+
+
+### Bug Fixes
+
+* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b))
+
+# [5.40.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.8...v5.40.0-dev.9) (2025-09-20)
+
+
+### Features
+
+* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7))
+
+# [5.40.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.7...v5.40.0-dev.8) (2025-09-20)
+
+
+### Features
+
+* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440))
+
# [5.40.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.6...v5.40.0-dev.7) (2025-09-20)
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java
deleted file mode 100644
index 8d3e022b1..000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideUpgradeButtonPatch.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package app.revanced.extension.music.patches;
-
-import app.revanced.extension.music.settings.Settings;
-
-@SuppressWarnings("unused")
-public class HideUpgradeButtonPatch {
-
- /**
- * Injection point
- */
- public static boolean hideUpgradeButton() {
- return Settings.HIDE_UPGRADE_BUTTON.get();
- }
-}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java
deleted file mode 100644
index 57395a0a2..000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/GoogleApiActivityHook.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package app.revanced.extension.music.settings;
-
-import android.app.Activity;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.preference.PreferenceFragment;
-import android.view.View;
-
-import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseActivityHook;
-
-/**
- * Hooks GoogleApiActivity to inject a custom ReVancedPreferenceFragment with a toolbar.
- */
-public class GoogleApiActivityHook extends BaseActivityHook {
- /**
- * Injection point
- *
- * Creates an instance of GoogleApiActivityHook for use in static initialization.
- */
- @SuppressWarnings("unused")
- public static GoogleApiActivityHook createInstance() {
- // Must touch the Music settings to ensure the class is loaded and
- // the values can be found when setting the UI preferences.
- // Logging anything under non debug ensures this is set.
- Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
-
- // YT Music always uses dark mode.
- Utils.setIsDarkModeEnabled(true);
-
- return new GoogleApiActivityHook();
- }
-
- /**
- * Sets the fixed theme for the activity.
- */
- @Override
- protected void customizeActivityTheme(Activity activity) {
- // Override the default YouTube Music theme to increase start padding of list items.
- // Custom style located in resources/music/values/style.xml
- activity.setTheme(Utils.getResourceIdentifier(ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
- }
-
- /**
- * Returns the resource ID for the YouTube Music settings layout.
- */
- @Override
- protected int getContentViewResourceId() {
- return Utils.getResourceIdentifier(ResourceType.LAYOUT, "revanced_music_settings_with_toolbar");
- }
-
- /**
- * Returns the fixed background color for the toolbar.
- */
- @Override
- protected int getToolbarBackgroundColor() {
- return Utils.getResourceColor("ytm_color_black");
- }
-
- /**
- * Returns the navigation icon with a color filter applied.
- */
- @Override
- protected Drawable getNavigationIcon() {
- Drawable navigationIcon = ReVancedPreferenceFragment.getBackButtonDrawable();
- navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
- return navigationIcon;
- }
-
- /**
- * Returns the click listener that finishes the activity when the navigation icon is clicked.
- */
- @Override
- protected View.OnClickListener getNavigationClickListener(Activity activity) {
- return view -> activity.finish();
- }
-
- /**
- * Creates a new ReVancedPreferenceFragment for the activity.
- */
- @Override
- protected PreferenceFragment createPreferenceFragment() {
- return new ReVancedPreferenceFragment();
- }
-}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java
new file mode 100644
index 000000000..bb19d2497
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java
@@ -0,0 +1,126 @@
+package app.revanced.extension.music.settings;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.preference.PreferenceFragment;
+import android.view.View;
+import android.widget.Toolbar;
+
+import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
+import app.revanced.extension.music.settings.search.MusicSearchViewController;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.BaseActivityHook;
+
+/**
+ * Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search.
+ */
+public class MusicActivityHook extends BaseActivityHook {
+
+ @SuppressLint("StaticFieldLeak")
+ public static MusicSearchViewController searchViewController;
+
+ /**
+ * Injection point.
+ */
+ @SuppressWarnings("unused")
+ public static void initialize(Activity parentActivity) {
+ // Must touch the Music settings to ensure the class is loaded and
+ // the values can be found when setting the UI preferences.
+ // Logging anything under non debug ensures this is set.
+ Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
+
+ // YT Music always uses dark mode.
+ Utils.setIsDarkModeEnabled(true);
+
+ BaseActivityHook.initialize(new MusicActivityHook(), parentActivity);
+ }
+
+ /**
+ * Sets the fixed theme for the activity.
+ */
+ @Override
+ protected void customizeActivityTheme(Activity activity) {
+ // Override the default YouTube Music theme to increase start padding of list items.
+ // Custom style located in resources/music/values/style.xml
+ activity.setTheme(Utils.getResourceIdentifierOrThrow(
+ "Theme.ReVanced.YouTubeMusic.Settings", "style"));
+ }
+
+ /**
+ * Returns the resource ID for the YouTube Music settings layout.
+ */
+ @Override
+ protected int getContentViewResourceId() {
+ return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
+ }
+
+ /**
+ * Returns the fixed background color for the toolbar.
+ */
+ @Override
+ protected int getToolbarBackgroundColor() {
+ return Utils.getResourceColor("ytm_color_black");
+ }
+
+ /**
+ * Returns the navigation icon with a color filter applied.
+ */
+ @Override
+ protected Drawable getNavigationIcon() {
+ Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable();
+ navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
+ return navigationIcon;
+ }
+
+ /**
+ * Returns the click listener that finishes the activity when the navigation icon is clicked.
+ */
+ @Override
+ protected View.OnClickListener getNavigationClickListener(Activity activity) {
+ return view -> {
+ if (searchViewController != null && searchViewController.isSearchActive()) {
+ searchViewController.closeSearch();
+ } else {
+ activity.finish();
+ }
+ };
+ }
+
+ /**
+ * Adds search view components to the toolbar for {@link MusicPreferenceFragment}.
+ *
+ * @param activity The activity hosting the toolbar.
+ * @param toolbar The configured toolbar.
+ * @param fragment The PreferenceFragment associated with the activity.
+ */
+ @Override
+ protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
+ if (fragment instanceof MusicPreferenceFragment) {
+ searchViewController = MusicSearchViewController.addSearchViewComponents(
+ activity, toolbar, (MusicPreferenceFragment) fragment);
+ }
+ }
+
+ /**
+ * Creates a new {@link MusicPreferenceFragment} for the activity.
+ */
+ @Override
+ protected PreferenceFragment createPreferenceFragment() {
+ return new MusicPreferenceFragment();
+ }
+
+ /**
+ * Injection point.
+ *
+ * Overrides {@link Activity#finish()} of the injection Activity.
+ *
+ * @return if the original activity finish method should be allowed to run.
+ */
+ @SuppressWarnings("unused")
+ public static boolean handleFinish() {
+ return MusicSearchViewController.handleFinish(searchViewController);
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
index 431ec7de0..4feb13d9c 100644
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java
@@ -2,7 +2,6 @@ package app.revanced.extension.music.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
-
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -15,7 +14,6 @@ public class Settings extends BaseSettings {
// Ads
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true);
- public static final BooleanSetting HIDE_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_upgrade_button", TRUE, true);
// General
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, false);
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java
new file mode 100644
index 000000000..1ebae16df
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java
@@ -0,0 +1,80 @@
+package app.revanced.extension.music.settings.preference;
+
+import android.app.Dialog;
+import android.preference.PreferenceScreen;
+import android.widget.Toolbar;
+
+import app.revanced.extension.music.settings.MusicActivityHook;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
+
+/**
+ * Preference fragment for ReVanced settings.
+ */
+@SuppressWarnings("deprecation")
+public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
+ /**
+ * The main PreferenceScreen used to display the current set of preferences.
+ */
+ private PreferenceScreen preferenceScreen;
+
+ /**
+ * Initializes the preference fragment.
+ */
+ @Override
+ protected void initialize() {
+ super.initialize();
+
+ try {
+ preferenceScreen = getPreferenceScreen();
+ Utils.sortPreferenceGroups(preferenceScreen);
+ setPreferenceScreenToolbar(preferenceScreen);
+ } catch (Exception ex) {
+ Logger.printException(() -> "initialize failure", ex);
+ }
+ }
+
+ /**
+ * Called when the fragment starts.
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+ try {
+ // Initialize search controller if needed
+ if (MusicActivityHook.searchViewController != null) {
+ // Trigger search data collection after fragment is ready.
+ MusicActivityHook.searchViewController.initializeSearchData();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onStart failure", ex);
+ }
+ }
+
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
+ @Override
+ protected void customizeToolbar(Toolbar toolbar) {
+ MusicActivityHook.setToolbarLayoutParams(toolbar);
+ }
+
+ /**
+ * Perform actions after toolbar setup.
+ */
+ @Override
+ protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
+ if (MusicActivityHook.searchViewController != null
+ && MusicActivityHook.searchViewController.isSearchActive()) {
+ toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch());
+ }
+ }
+
+ /**
+ * Returns the preference screen for external access by SearchViewController.
+ */
+ public PreferenceScreen getPreferenceScreenForSearch() {
+ return preferenceScreen;
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java
deleted file mode 100644
index 67ca69ba4..000000000
--- a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package app.revanced.extension.music.settings.preference;
-
-import android.widget.Toolbar;
-
-import app.revanced.extension.music.settings.GoogleApiActivityHook;
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
-
-/**
- * Preference fragment for ReVanced settings.
- */
-@SuppressWarnings({"deprecation", "NewApi"})
-public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
-
- /**
- * Initializes the preference fragment.
- */
- @Override
- protected void initialize() {
- super.initialize();
-
- try {
- Utils.sortPreferenceGroups(getPreferenceScreen());
- setPreferenceScreenToolbar(getPreferenceScreen());
- } catch (Exception ex) {
- Logger.printException(() -> "initialize failure", ex);
- }
- }
-
- /**
- * Sets toolbar for all nested preference screens.
- */
- @Override
- protected void customizeToolbar(Toolbar toolbar) {
- GoogleApiActivityHook.setToolbarLayoutParams(toolbar);
- }
-}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java
new file mode 100644
index 000000000..65ccd4ea1
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java
@@ -0,0 +1,28 @@
+package app.revanced.extension.music.settings.search;
+
+import android.content.Context;
+import android.preference.PreferenceScreen;
+
+import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
+import app.revanced.extension.shared.settings.search.BaseSearchViewController;
+import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
+
+import java.util.List;
+
+/**
+ * Music-specific search results adapter.
+ */
+@SuppressWarnings("deprecation")
+public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter {
+
+ public MusicSearchResultsAdapter(Context context, List items,
+ BaseSearchViewController.BasePreferenceFragment fragment,
+ BaseSearchViewController searchViewController) {
+ super(context, items, fragment, searchViewController);
+ }
+
+ @Override
+ protected PreferenceScreen getMainPreferenceScreen() {
+ return fragment.getPreferenceScreenForSearch();
+ }
+}
diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java
new file mode 100644
index 000000000..6681a2f02
--- /dev/null
+++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java
@@ -0,0 +1,71 @@
+package app.revanced.extension.music.settings.search;
+
+import android.app.Activity;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.view.View;
+import android.widget.Toolbar;
+
+import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
+import app.revanced.extension.shared.settings.search.*;
+
+/**
+ * Music-specific search view controller implementation.
+ */
+@SuppressWarnings("deprecation")
+public class MusicSearchViewController extends BaseSearchViewController {
+
+ public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
+ MusicPreferenceFragment fragment) {
+ return new MusicSearchViewController(activity, toolbar, fragment);
+ }
+
+ private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) {
+ super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
+ }
+
+ @Override
+ protected BaseSearchResultsAdapter createSearchResultsAdapter() {
+ return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
+ }
+
+ @Override
+ protected boolean isSpecialPreferenceGroup(Preference preference) {
+ // Music doesn't have SponsorBlock, so no special groups.
+ return false;
+ }
+
+ @Override
+ protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
+ // Music doesn't have special preferences.
+ // This method can be empty or handle music-specific preferences if any.
+ }
+
+ // Static method for handling Activity finish
+ public static boolean handleFinish(MusicSearchViewController searchViewController) {
+ if (searchViewController != null && searchViewController.isSearchActive()) {
+ searchViewController.closeSearch();
+ return true;
+ }
+ return false;
+ }
+
+ // Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface.
+ private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment {
+
+ @Override
+ public PreferenceScreen getPreferenceScreenForSearch() {
+ return fragment.getPreferenceScreenForSearch();
+ }
+
+ @Override
+ public View getView() {
+ return fragment.getView();
+ }
+
+ @Override
+ public Activity getActivity() {
+ return fragment.getActivity();
+ }
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java
index 3f9f0af11..978ee7131 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java
@@ -27,6 +27,7 @@ import java.util.Locale;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
+import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings("unused")
public class GmsCoreSupport {
@@ -80,17 +81,17 @@ public class GmsCoreSupport {
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
str("gms_core_dialog_title"), // Title.
- str(dialogMessageRef), // Message.
- null, // No EditText.
- str(positiveButtonTextRef), // OK button text.
+ 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.
+ null, // No Cancel button action.
+ null, // No Neutral button text.
+ null, // No Neutral button action.
+ true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
index a71037ec7..92903b9f9 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
@@ -4,6 +4,8 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.app.DialogFragment;
+import android.content.ClipData;
+import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -12,9 +14,6 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
-import android.graphics.Typeface;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
@@ -23,9 +22,6 @@ import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.TypedValue;
@@ -37,13 +33,9 @@ import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
-import android.widget.Button;
-import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
@@ -71,6 +63,7 @@ import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
+@SuppressWarnings("NewApi")
public class Utils {
@SuppressLint("StaticFieldLeak")
@@ -304,42 +297,108 @@ public class Utils {
/**
* @return zero, if the resource is not found.
*/
- public static int getResourceIdentifier(ResourceType type, String resourceIdentifierName) {
+ @SuppressLint("DiscouragedApi")
+ public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
+ return context.getResources().getIdentifier(resourceIdentifierName,
+ type == null ? null : type.value, context.getPackageName());
+ }
+
+ public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
+ final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
+ if (resourceId == 0) {
+ throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ + " type: " + type);
+ }
+ return resourceId;
+ }
+
+ /**
+ * @return zero, if the resource is not found.
+ * @see #getResourceIdentifierOrThrow(ResourceType, String)
+ */
+ public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) {
return getResourceIdentifier(getContext(), type, resourceIdentifierName);
}
/**
* @return zero, if the resource is not found.
+ * @see #getResourceIdentifier(ResourceType, String)
*/
- @SuppressLint("DiscouragedApi")
- public static int getResourceIdentifier(Context context, ResourceType type, String resourceIdentifierName) {
- return context.getResources().getIdentifier(resourceIdentifierName, type.value, context.getPackageName());
+ public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) {
+ return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName);
+ }
+
+ /**
+ * @return The resource identifier, or throws an exception if not found.
+ */
+ @Deprecated
+ public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
+ final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
+ if (resourceId == 0) {
+ throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ + " type: " + type);
+ }
+ return resourceId;
+ }
+
+
+ /**
+ * Instead use {@link #getResourceIdentifierOrThrow(ResourceType, String)}
+ */
+ @Deprecated
+ public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String stringType) {
+ return getResourceIdentifierOrThrow(getContext(), resourceIdentifierName, stringType);
+ }
+
+ /**
+ * Instead use {@link #getResourceIdentifier(ResourceType, String)}
+ */
+ @Deprecated
+ public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String stringType) {
+ return getResourceIdentifier(getContext(), resourceIdentifierName, stringType);
+ }
+
+ /**
+ * Instead use {@link #getResourceIdentifier(Context, ResourceType, String)}
+ */
+ @Deprecated
+ public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String stringType) {
+ // Find ResourceType with same name as type parameter string
+ ResourceType convertedType = null;
+ for (ResourceType type : ResourceType.values()) {
+ if (type.value.equals(stringType)) {
+ convertedType = type;
+ break;
+ }
+ }
+
+ return getResourceIdentifierOrThrow(context, convertedType, resourceIdentifierName);
}
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getInteger(getResourceIdentifier(ResourceType.INTEGER, resourceIdentifierName));
+ return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName));
}
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
- return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(ResourceType.ANIM, resourceIdentifierName));
+ return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName));
}
@ColorInt
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
//noinspection deprecation
- return getContext().getResources().getColor(getResourceIdentifier(ResourceType.COLOR, resourceIdentifierName));
+ return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName));
}
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(ResourceType.DIMEN, resourceIdentifierName));
+ return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
}
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getDimension(getResourceIdentifier(ResourceType.DIMEN, resourceIdentifierName));
+ return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
}
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
- return getContext().getResources().getStringArray(getResourceIdentifier(ResourceType.ARRAY, resourceIdentifierName));
+ return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
}
public interface MatchFilter {
@@ -350,13 +409,9 @@ public class Utils {
* Includes sub children.
*/
public static R getChildViewByResourceName(View view, String str) {
- var child = view.findViewById(Utils.getResourceIdentifier(ResourceType.ID, str));
- if (child != null) {
- //noinspection unchecked
- return (R) child;
- }
-
- throw new IllegalArgumentException("View with resource name not found: " + str);
+ var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str));
+ //noinspection unchecked
+ return (R) child;
}
/**
@@ -442,9 +497,9 @@ public class Utils {
}
public static void setClipboard(CharSequence text) {
- android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
+ ClipboardManager clipboard = (ClipboardManager) context
.getSystemService(Context.CLIPBOARD_SERVICE);
- android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
+ ClipData clip = ClipData.newPlainText("ReVanced", text);
clipboard.setPrimaryClip(clip);
}
@@ -604,7 +659,13 @@ public class Utils {
showToast(messageToToast, Toast.LENGTH_LONG);
}
- private static void showToast(String messageToToast, int toastDuration) {
+ /**
+ * Safe to call from any thread.
+ *
+ * @param messageToToast Message to show.
+ * @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}.
+ */
+ public static void showToast(String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
Context currentContext = context;
@@ -746,396 +807,32 @@ public class Utils {
}
/**
- * Creates a custom dialog with a styled layout, including a title, message, buttons, and an
- * optional EditText. The dialog's appearance adapts to the app's dark mode setting, with
- * rounded corners and customizable button actions. Buttons adjust dynamically to their text
- * content and are arranged in a single row if they fit within 80% of the screen width,
- * with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
- * If buttons do not fit, each is placed on a separate row, all aligned to the right.
+ * Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming.
+ * The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP.
+ * The default dialog background is removed to allow for custom styling.
*
- * @param context Context used to create the dialog.
- * @param title Title text of the dialog.
- * @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
- * @param editText EditText to include in the dialog, or null if no EditText is needed.
- * @param okButtonText OK button text, or null to use the default "OK" string.
- * @param onOkClick Action to perform when the OK button is clicked.
- * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
- * @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
- * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
- * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
- * @return The Dialog and its main LinearLayout container.
+ * @param window The {@link Window} object to configure.
+ * @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}).
+ * @param yOffsetDip The vertical offset from the gravity position in DIP.
+ * @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100).
+ * @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount.
*/
- @SuppressWarnings("ExtractMethodRecommender")
- public static Pair createCustomDialog(
- Context context, String title, CharSequence message, @Nullable EditText editText,
- String okButtonText, Runnable onOkClick, Runnable onCancelClick,
- @Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
- boolean dismissDialogOnNeutralClick
- ) {
- Logger.printDebug(() -> "Creating custom dialog with title: " + title);
-
- Dialog dialog = new Dialog(context);
- dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
-
- // Preset size constants.
- final int dip4 = dipToPixels(4);
- final int dip8 = dipToPixels(8);
- final int dip16 = dipToPixels(16);
- final int dip24 = dipToPixels(24);
-
- // Create main layout.
- LinearLayout mainLayout = new LinearLayout(context);
- mainLayout.setOrientation(LinearLayout.VERTICAL);
- mainLayout.setPadding(dip24, dip16, dip24, dip24);
- // Set rounded rectangle background.
- ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
- createCornerRadii(28), null, null));
- mainBackground.getPaint().setColor(getDialogBackgroundColor()); // Dialog background.
- mainLayout.setBackground(mainBackground);
-
- // Title.
- if (!TextUtils.isEmpty(title)) {
- TextView titleView = new TextView(context);
- titleView.setText(title);
- titleView.setTypeface(Typeface.DEFAULT_BOLD);
- titleView.setTextSize(18);
- titleView.setTextColor(getAppForegroundColor());
- titleView.setGravity(Gravity.CENTER);
- LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
- layoutParams.setMargins(0, 0, 0, dip16);
- titleView.setLayoutParams(layoutParams);
- mainLayout.addView(titleView);
- }
-
- // Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
- ScrollView contentScrollView = null;
- LinearLayout contentContainer;
- if (message != null || editText != null) {
- contentScrollView = new ScrollView(context);
- contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
- contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
- if (editText != null) {
- ShapeDrawable scrollViewBackground = new ShapeDrawable(new RoundRectShape(
- createCornerRadii(10), null, null));
- scrollViewBackground.getPaint().setColor(getEditTextBackground());
- contentScrollView.setPadding(dip8, dip8, dip8, dip8);
- contentScrollView.setBackground(scrollViewBackground);
- contentScrollView.setClipToOutline(true);
- }
- LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f // Weight to take available space.
- );
- contentScrollView.setLayoutParams(contentParams);
- contentContainer = new LinearLayout(context);
- contentContainer.setOrientation(LinearLayout.VERTICAL);
- contentScrollView.addView(contentContainer);
-
- // Message (if not replaced by EditText).
- if (editText == null) {
- TextView messageView = new TextView(context);
- messageView.setText(message); // Supports Spanned (HTML).
- messageView.setTextSize(16);
- messageView.setTextColor(getAppForegroundColor());
- // Enable HTML link clicking if the message contains links.
- if (message instanceof Spanned) {
- messageView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- LinearLayout.LayoutParams messageParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
- messageView.setLayoutParams(messageParams);
- contentContainer.addView(messageView);
- }
-
- // EditText (if provided).
- if (editText != null) {
- // Remove EditText from its current parent, if any.
- ViewGroup parent = (ViewGroup) editText.getParent();
- if (parent != null) {
- parent.removeView(editText);
- }
- // Style the EditText to match the dialog theme.
- editText.setTextColor(getAppForegroundColor());
- editText.setBackgroundColor(Color.TRANSPARENT);
- editText.setPadding(0, 0, 0, 0);
- LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- contentContainer.addView(editText, editTextParams);
- }
- }
-
- // Button container.
- LinearLayout buttonContainer = new LinearLayout(context);
- buttonContainer.setOrientation(LinearLayout.VERTICAL);
- buttonContainer.removeAllViews();
- LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- );
- buttonContainerParams.setMargins(0, dip16, 0, 0);
- buttonContainer.setLayoutParams(buttonContainerParams);
-
- // Lists to track buttons.
- List buttons = new ArrayList<>();
- List buttonWidths = new ArrayList<>();
-
- // Create buttons in order: Neutral, Cancel, OK.
- if (neutralButtonText != null && onNeutralClick != null) {
- Button neutralButton = addButton(
- context,
- neutralButtonText,
- onNeutralClick,
- false,
- dismissDialogOnNeutralClick,
- dialog
- );
- buttons.add(neutralButton);
- neutralButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
- buttonWidths.add(neutralButton.getMeasuredWidth());
- }
-
- if (onCancelClick != null) {
- Button cancelButton = addButton(
- context,
- context.getString(android.R.string.cancel),
- onCancelClick,
- false,
- true,
- dialog
- );
- buttons.add(cancelButton);
- cancelButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
- buttonWidths.add(cancelButton.getMeasuredWidth());
- }
-
- if (onOkClick != null) {
- Button okButton = addButton(
- context,
- okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
- onOkClick,
- true,
- true,
- dialog
- );
- buttons.add(okButton);
- okButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
- buttonWidths.add(okButton.getMeasuredWidth());
- }
-
- // Handle button layout.
- int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
- int totalWidth = 0;
- for (Integer width : buttonWidths) {
- totalWidth += width;
- }
- if (buttonWidths.size() > 1) {
- totalWidth += (buttonWidths.size() - 1) * dip8; // Add margins for gaps.
- }
-
- if (buttons.size() == 1) {
- // Single button: stretch to full width.
- Button singleButton = buttons.get(0);
- LinearLayout singleContainer = new LinearLayout(context);
- singleContainer.setOrientation(LinearLayout.HORIZONTAL);
- singleContainer.setGravity(Gravity.CENTER);
- ViewGroup parent = (ViewGroup) singleButton.getParent();
- if (parent != null) {
- parent.removeView(singleButton);
- }
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- dipToPixels(36)
- );
- params.setMargins(0, 0, 0, 0);
- singleButton.setLayoutParams(params);
- singleContainer.addView(singleButton);
- buttonContainer.addView(singleContainer);
- } else if (buttons.size() > 1) {
- // Check if buttons fit in one row.
- if (totalWidth <= screenWidth * 0.8) {
- // Single row: Neutral, Cancel, OK.
- LinearLayout rowContainer = new LinearLayout(context);
- rowContainer.setOrientation(LinearLayout.HORIZONTAL);
- rowContainer.setGravity(Gravity.CENTER);
- rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ));
-
- // Add all buttons with proportional weights and specific margins.
- for (int i = 0; i < buttons.size(); i++) {
- Button button = buttons.get(i);
- ViewGroup parent = (ViewGroup) button.getParent();
- if (parent != null) {
- parent.removeView(button);
- }
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- 0,
- dipToPixels(36),
- buttonWidths.get(i) // Use measured width as weight.
- );
- // Set margins based on button type and combination.
- if (buttons.size() == 2) {
- // Neutral + OK or Cancel + OK.
- if (i == 0) { // Neutral or Cancel.
- params.setMargins(0, 0, dip4, 0);
- } else { // OK
- params.setMargins(dip4, 0, 0, 0);
- }
- } else if (buttons.size() == 3) {
- if (i == 0) { // Neutral.
- params.setMargins(0, 0, dip4, 0);
- } else if (i == 1) { // Cancel
- params.setMargins(dip4, 0, dip4, 0);
- } else { // OK
- params.setMargins(dip4, 0, 0, 0);
- }
- }
- button.setLayoutParams(params);
- rowContainer.addView(button);
- }
- buttonContainer.addView(rowContainer);
- } else {
- // Multiple rows: OK, Cancel, Neutral.
- List reorderedButtons = new ArrayList<>();
- // Reorder: OK, Cancel, Neutral.
- if (onOkClick != null) {
- reorderedButtons.add(buttons.get(buttons.size() - 1));
- }
- if (onCancelClick != null) {
- reorderedButtons.add(buttons.get((neutralButtonText != null && onNeutralClick != null) ? 1 : 0));
- }
- if (neutralButtonText != null && onNeutralClick != null) {
- reorderedButtons.add(buttons.get(0));
- }
-
- // Add each button in its own row with spacers.
- for (int i = 0; i < reorderedButtons.size(); i++) {
- Button button = reorderedButtons.get(i);
- LinearLayout singleContainer = new LinearLayout(context);
- singleContainer.setOrientation(LinearLayout.HORIZONTAL);
- singleContainer.setGravity(Gravity.CENTER);
- singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- dipToPixels(36)
- ));
- ViewGroup parent = (ViewGroup) button.getParent();
- if (parent != null) {
- parent.removeView(button);
- }
- LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- dipToPixels(36)
- );
- buttonParams.setMargins(0, 0, 0, 0);
- button.setLayoutParams(buttonParams);
- singleContainer.addView(button);
- buttonContainer.addView(singleContainer);
-
- // Add a spacer between the buttons (except the last one).
- // Adding a margin between buttons is not suitable, as it conflicts with the single row layout.
- if (i < reorderedButtons.size() - 1) {
- View spacer = new View(context);
- LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- dipToPixels(8)
- );
- spacer.setLayoutParams(spacerParams);
- buttonContainer.addView(spacer);
- }
- }
- }
- }
-
- // Add ScrollView to main layout only if content exist.
- if (contentScrollView != null) {
- mainLayout.addView(contentScrollView);
- }
- mainLayout.addView(buttonContainer);
- dialog.setContentView(mainLayout);
-
- // Set dialog window attributes.
- Window window = dialog.getWindow();
- if (window != null) {
- setDialogWindowParameters(window);
- }
-
- return new Pair<>(dialog, mainLayout);
- }
-
- public static void setDialogWindowParameters(Window window) {
+ public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
WindowManager.LayoutParams params = window.getAttributes();
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
- int portraitWidth = (int) (displayMetrics.widthPixels * 0.9);
+ int portraitWidth = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels);
- if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
- portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.9);
- }
- params.width = portraitWidth;
+ params.width = (int) (portraitWidth * (widthPercentage / 100.0f)); // Set width based on parameters.
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- params.gravity = Gravity.CENTER;
- window.setAttributes(params);
- window.setBackgroundDrawable(null); // Remove default dialog background.
- }
+ params.gravity = gravity;
+ params.y = yOffsetDip > 0 ? dipToPixels(yOffsetDip) : 0;
+ if (dimAmount) {
+ params.dimAmount = 0f;
+ }
- /**
- * Adds a styled button to a dialog's button container with customizable text, click behavior, and appearance.
- * The button's background and text colors adapt to the app's dark mode setting. Buttons stretch to full width
- * when on separate rows or proportionally based on content when in a single row (Neutral, Cancel, OK order).
- * When wrapped to separate rows, buttons are ordered OK, Cancel, Neutral.
- *
- * @param context Context to create the button and access resources.
- * @param buttonText Button text to display.
- * @param onClick Action to perform when the button is clicked, or null if no action is required.
- * @param isOkButton If this is the OK button, which uses distinct background and text colors.
- * @param dismissDialog If the dialog should be dismissed when the button is clicked.
- * @param dialog The Dialog to dismiss when the button is clicked.
- * @return The created Button.
- */
- private static Button addButton(Context context, String buttonText, Runnable onClick,
- boolean isOkButton, boolean dismissDialog, Dialog dialog) {
- Button button = new Button(context, null, 0);
- button.setText(buttonText);
- button.setTextSize(14);
- button.setAllCaps(false);
- button.setSingleLine(true);
- button.setEllipsize(android.text.TextUtils.TruncateAt.END);
- button.setGravity(Gravity.CENTER);
-
- ShapeDrawable background = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
- int backgroundColor = isOkButton
- ? getOkButtonBackgroundColor() // Background color for OK button (inversion).
- : getCancelOrNeutralButtonBackgroundColor(); // Background color for Cancel or Neutral buttons.
- background.getPaint().setColor(backgroundColor);
- button.setBackground(background);
-
- button.setTextColor(isDarkModeEnabled()
- ? (isOkButton ? Color.BLACK : Color.WHITE)
- : (isOkButton ? Color.WHITE : Color.BLACK));
-
- // Set internal padding.
- final int dip16 = dipToPixels(16);
- button.setPadding(dip16, 0, dip16, 0);
-
- button.setOnClickListener(v -> {
- if (onClick != null) {
- onClick.run();
- }
- if (dismissDialog) {
- dialog.dismiss();
- }
- });
-
- return button;
+ window.setAttributes(params); // Apply window attributes.
+ window.setBackgroundDrawable(null); // Remove default dialog background
}
/**
@@ -1322,9 +1019,9 @@ public class Utils {
/**
* Sort a PreferenceGroup and all it's sub groups by title or key.
- *
+ *
* Sort order is determined by the preferences key {@link Sort} suffix.
- *
+ *
* If a preference has no key or no {@link Sort} suffix,
* then the preferences are left unsorted.
*/
@@ -1387,7 +1084,7 @@ public class Utils {
* 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.
- *
+ *
* 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.
*/
@@ -1496,9 +1193,9 @@ public class Utils {
blue = Math.round(blue + (255 - blue) * t);
} else {
// Darken or no change: Scale toward black.
- red *= factor;
- green *= factor;
- blue *= factor;
+ red = Math.round(red * factor);
+ green = Math.round(green * factor);
+ blue = Math.round(blue * factor);
}
// Ensure values are within [0, 255].
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java
index a66be8418..7a3a2e1d4 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java
@@ -26,6 +26,7 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.ui.CustomDialog;
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
@@ -93,7 +94,7 @@ abstract class Check {
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
activity,
str("revanced_check_environment_failed_title"), // Title.
message, // Message.
@@ -127,8 +128,8 @@ abstract class Check {
// Add icon to the dialog.
ImageView iconView = new ImageView(activity);
- iconView.setImageResource(Utils.getResourceIdentifier(
- ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
+ iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
+ "revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
@@ -159,8 +160,8 @@ abstract class Check {
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);
+ if (buttonContainer.getChildCount() == 1
+ && buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) {
// Neutral button is the first child (index 0).
ignoreButton = (Button) rowContainer.getChildAt(0);
// OK button is the last child.
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
index 1385f4ce8..d29e5e085 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java
@@ -1,7 +1,10 @@
package app.revanced.extension.shared.settings;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.content.Context;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceFragment;
import android.util.TypedValue;
@@ -22,6 +25,15 @@ import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragme
@SuppressWarnings({"deprecation", "NewApi"})
public abstract class BaseActivityHook extends Activity {
+ private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
+ getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
+ private static final int ID_REVANCED_TOOLBAR_PARENT =
+ getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
+ public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
+ getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
+ private static final int STRING_REVANCED_SETTINGS_TITLE =
+ getResourceIdentifierOrThrow("revanced_settings_title", "string");
+
/**
* Layout parameters for the toolbar, extracted from the dummy toolbar.
*/
@@ -56,13 +68,27 @@ public abstract class BaseActivityHook extends Activity {
activity.getFragmentManager()
.beginTransaction()
- .replace(Utils.getResourceIdentifier(ResourceType.ID, "revanced_settings_fragments"), fragment)
+ .replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
.commit();
} catch (Exception ex) {
Logger.printException(() -> "initialize failure", ex);
}
}
+ /**
+ * Injection point.
+ * Overrides the ReVanced settings language.
+ */
+ @SuppressWarnings("unused")
+ public static Context getAttachBaseContext(Context original) {
+ AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
+ if (language == AppLanguage.DEFAULT) {
+ return original;
+ }
+
+ return Utils.getContext();
+ }
+
/**
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
*/
@@ -70,8 +96,7 @@ public abstract class BaseActivityHook extends Activity {
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
// Replace dummy placeholder toolbar.
// This is required to fix submenu title alignment issue with Android ASOP 15+
- ViewGroup toolBarParent = activity.findViewById(
- Utils.getResourceIdentifier(ResourceType.ID, "revanced_toolbar_parent"));
+ ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
toolbarLayoutParams = dummyToolbar.getLayoutParams();
toolBarParent.removeView(dummyToolbar);
@@ -83,7 +108,7 @@ public abstract class BaseActivityHook extends Activity {
toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(getNavigationIcon());
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
- toolbar.setTitle(Utils.getResourceIdentifier(ResourceType.STRING, "revanced_settings_title"));
+ toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
final int margin = Utils.dipToPixels(16);
toolbar.setTitleMarginStart(margin);
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
index be4b58227..0dd948aa8 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
@@ -25,6 +25,9 @@ public class BaseSettings {
*/
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
+ public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
+ public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
+
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final EnumSetting SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java
index bbb590558..1f24a7489 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java
@@ -1,18 +1,27 @@
package app.revanced.extension.shared.settings;
+import static app.revanced.extension.shared.StringRef.str;
+
import android.content.Context;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.StringRef;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.*;
-
-import static app.revanced.extension.shared.StringRef.str;
public abstract class Setting {
@@ -23,24 +32,49 @@ public abstract class Setting {
*/
public interface Availability {
boolean isAvailable();
+
+ /**
+ * @return parent settings (dependencies) of this availability.
+ */
+ default List> getParentSettings() {
+ return Collections.emptyList();
+ }
}
/**
* Availability based on a single parent setting being enabled.
*/
public static Availability parent(BooleanSetting parent) {
- return parent::get;
+ return new Availability() {
+ @Override
+ public boolean isAvailable() {
+ return parent.get();
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return Collections.singletonList(parent);
+ }
+ };
}
/**
* Availability based on all parents being enabled.
*/
public static Availability parentsAll(BooleanSetting... parents) {
- return () -> {
- for (BooleanSetting parent : parents) {
- if (!parent.get()) return false;
+ return new Availability() {
+ @Override
+ public boolean isAvailable() {
+ for (BooleanSetting parent : parents) {
+ if (!parent.get()) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return Collections.unmodifiableList(Arrays.asList(parents));
}
- return true;
};
}
@@ -48,11 +82,19 @@ public abstract class Setting {
* Availability based on any parent being enabled.
*/
public static Availability parentsAny(BooleanSetting... parents) {
- return () -> {
- for (BooleanSetting parent : parents) {
- if (parent.get()) return true;
+ return new Availability() {
+ @Override
+ public boolean isAvailable() {
+ for (BooleanSetting parent : parents) {
+ if (parent.get()) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return Collections.unmodifiableList(Arrays.asList(parents));
}
- return false;
};
}
@@ -112,6 +154,7 @@ public abstract class Setting {
* @return All settings that have been created, sorted by keys.
*/
private static List> allLoadedSettingsSorted() {
+ //noinspection ComparatorCombinators
Collections.sort(SETTINGS, (Setting> o1, Setting> o2) -> o1.key.compareTo(o2.key));
return allLoadedSettings();
}
@@ -207,9 +250,7 @@ public abstract class Setting {
SETTINGS.add(this);
if (PATH_TO_SETTINGS.put(key, this) != null) {
- // Debug setting may not be created yet so using Logger may cause an initialization crash.
- // Show a toast instead.
- Utils.showToastLong(this.getClass().getSimpleName()
+ Logger.printException(() -> this.getClass().getSimpleName()
+ " error: Duplicate Setting key found: " + key);
}
@@ -231,10 +272,10 @@ public abstract class Setting {
/**
* Migrate an old Setting value previously stored in a different SharedPreference.
- *
+ *
* This method will be deleted in the future.
*/
- @SuppressWarnings("rawtypes")
+ @SuppressWarnings({"rawtypes", "NewApi"})
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
@@ -254,7 +295,7 @@ public abstract class Setting {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
- // Remove otherwise it'll show a toast on every launch
+ // Remove otherwise it'll show a toast on every launch.
oldPrefs.preferences.edit().remove(settingKey).apply();
return;
}
@@ -273,7 +314,7 @@ public abstract class Setting {
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
- *
+ *
* This intentionally is a static method to deter
* accidental usage when {@link #save(Object)} was intended.
*/
@@ -349,6 +390,14 @@ public abstract class Setting {
return availability == null || availability.isAvailable();
}
+ /**
+ * Get the parent Settings that this setting depends on.
+ * @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist.
+ */
+ public List> getParentSettings() {
+ return availability == null ? Collections.emptyList() : availability.getParentSettings();
+ }
+
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
@@ -467,9 +516,12 @@ public abstract class Setting {
callback.settingsImported(alertDialogContext);
}
- Utils.showToastLong(numberOfSettingsImported == 0
- ? str("revanced_settings_import_reset")
- : str("revanced_settings_import_success", numberOfSettingsImported));
+ // Use a delay, otherwise the toast can move about on screen from the dismissing dialog.
+ final int numberOfSettingsImportedFinal = numberOfSettingsImported;
+ Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0
+ ? str("revanced_settings_import_reset")
+ : str("revanced_settings_import_success", numberOfSettingsImportedFinal)),
+ 150);
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
index 649583318..e829b8b1f 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
@@ -28,6 +28,7 @@ import app.revanced.extension.shared.Utils;
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.ui.CustomDialog;
@SuppressWarnings("deprecation")
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
@@ -125,7 +126,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
showingUserDialogMessage = true;
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
confirmDialogTitle, // Title.
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
@@ -249,7 +250,8 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
Setting.privateSetValueFromString(setting, listPref.getValue());
}
updateListPreferenceSummary(listPref, setting);
- } else {
+ } else if (!pref.getClass().equals(Preference.class)) {
+ // Ignore root preference class because there is no data to sync.
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
}
}
@@ -303,7 +305,8 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
restartDialogButtonText = str("revanced_settings_restart");
}
- Pair dialogPair = Utils.createCustomDialog(context,
+ Pair dialogPair = CustomDialog.create(
+ context,
restartDialogTitle, // Title.
restartDialogMessage, // Message.
null, // No EditText.
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java
new file mode 100644
index 000000000..204697228
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java
@@ -0,0 +1,78 @@
+package app.revanced.extension.shared.settings.preference;
+
+import android.content.Context;
+import android.preference.Preference;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.text.style.BulletSpan;
+import android.util.AttributeSet;
+
+/**
+ * Formats the summary text bullet points into Spanned text for better presentation.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public class BulletPointPreference extends Preference {
+
+ public static SpannedString formatIntoBulletPoints(CharSequence source) {
+ SpannableStringBuilder builder = new SpannableStringBuilder(source);
+
+ int lineStart = 0;
+ int length = builder.length();
+
+ while (lineStart < length) {
+ int lineEnd = TextUtils.indexOf(builder, '\n', lineStart);
+ if (lineEnd < 0) lineEnd = length;
+
+ // Apply BulletSpan only if the line starts with the '•' character.
+ if (lineEnd > lineStart && builder.charAt(lineStart) == '•') {
+ int deleteEnd = lineStart + 1; // remove the bullet itself
+
+ // If there's a single space right after the bullet, remove that too.
+ if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') {
+ deleteEnd++;
+ }
+
+ builder.delete(lineStart, deleteEnd);
+
+ // Apply the BulletSpan to the remainder of that line.
+ builder.setSpan(new BulletSpan(20),
+ lineStart,
+ lineEnd - (deleteEnd - lineStart), // adjust for deleted chars.
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ // Update total length and lineEnd after deletion.
+ length = builder.length();
+ final int removed = deleteEnd - lineStart;
+ lineEnd -= removed;
+ }
+
+ lineStart = lineEnd + 1;
+ }
+
+ return new SpannedString(builder);
+ }
+
+ public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BulletPointPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BulletPointPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ super.setSummary(formatIntoBulletPoints(summary));
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java
new file mode 100644
index 000000000..ccbbf1eef
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java
@@ -0,0 +1,45 @@
+package app.revanced.extension.shared.settings.preference;
+
+import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints;
+
+import android.content.Context;
+import android.preference.SwitchPreference;
+import android.util.AttributeSet;
+
+/**
+ * Formats the summary text bullet points into Spanned text for better presentation.
+ */
+@SuppressWarnings({"unused", "deprecation"})
+public class BulletPointSwitchPreference extends SwitchPreference {
+
+ public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BulletPointSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BulletPointSwitchPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ super.setSummary(formatIntoBulletPoints(summary));
+ }
+
+ @Override
+ public void setSummaryOn(CharSequence summaryOn) {
+ super.setSummaryOn(formatIntoBulletPoints(summaryOn));
+ }
+
+ @Override
+ public void setSummaryOff(CharSequence summaryOff) {
+ super.setSummaryOff(formatIntoBulletPoints(summaryOff));
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java
index 5c8b5837e..696a3d296 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java
@@ -1,8 +1,8 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
import android.app.Dialog;
import android.content.Context;
@@ -13,20 +13,20 @@ import android.os.Bundle;
import android.preference.EditTextPreference;
import android.text.Editable;
import android.text.InputType;
-import android.text.SpannableString;
-import android.text.Spanned;
import android.text.TextWatcher;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.util.Pair;
+import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
-import android.widget.*;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
import java.util.Locale;
import java.util.regex.Pattern;
@@ -36,6 +36,8 @@ import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
+import app.revanced.extension.shared.ui.ColorDot;
+import app.revanced.extension.shared.ui.CustomDialog;
/**
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
@@ -44,100 +46,98 @@ import app.revanced.extension.shared.settings.StringSetting;
*/
@SuppressWarnings({"unused", "deprecation"})
public class ColorPickerPreference extends EditTextPreference {
+ /** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */
+ public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7;
+ public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9;
- /**
- * Character to show the color appearance.
- */
- public static final String COLOR_DOT_STRING = "⬤";
-
- /**
- * Length of a valid color string of format #RRGGBB.
- */
- public static final int COLOR_STRING_LENGTH = 7;
-
- /**
- * Matches everything that is not a hex number/letter.
- */
+ /** Matches everything that is not a hex number/letter. */
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
- /**
- * Alpha for dimming when the preference is disabled.
- */
- private static final float DISABLED_ALPHA = 0.5f; // 50%
+ /** Alpha for dimming when the preference is disabled. */
+ public static final float DISABLED_ALPHA = 0.5f; // 50%
- /**
- * View displaying a colored dot in the widget area.
- */
+ /** View displaying a colored dot in the widget area. */
private View widgetColorDot;
- /**
- * Current color in RGB format (without alpha).
- */
+ /** Dialog View displaying a colored dot for the selected color preview in the dialog. */
+ private View dialogColorDot;
+
+ /** Current color, including alpha channel if opacity slider is enabled. */
@ColorInt
private int currentColor;
- /**
- * Associated setting for storing the color value.
- */
+ /** Associated setting for storing the color value. */
private StringSetting colorSetting;
- /**
- * Dialog TextWatcher for the EditText to monitor color input changes.
- */
+ /** Dialog TextWatcher for the EditText to monitor color input changes. */
private TextWatcher colorTextWatcher;
- /**
- * Dialog TextView displaying a colored dot for the selected color preview in the dialog.
- */
- private TextView dialogColorPreview;
+ /** Dialog color picker view. */
+ protected ColorPickerView dialogColorPickerView;
- /**
- * Dialog color picker view.
- */
- private ColorPickerView dialogColorPickerView;
+ /** Listener for color changes. */
+ protected OnColorChangeListener colorChangeListener;
+
+ /** Whether the opacity slider is enabled. */
+ private boolean opacitySliderEnabled = false;
+
+ public static final int ID_REVANCED_COLOR_PICKER_VIEW =
+ getResourceIdentifierOrThrow(ResourceType.ID, "revanced_color_picker_view");
+ public static final int ID_PREFERENCE_COLOR_DOT =
+ getResourceIdentifierOrThrow(ResourceType.ID, "preference_color_dot");
+ public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
+ getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_dot_widget");
+ public static final int LAYOUT_REVANCED_COLOR_PICKER =
+ getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_picker");
/**
* Removes non valid hex characters, converts to all uppercase,
* and adds # character to the start if not present.
*/
- public static String cleanupColorCodeString(String colorString) {
- // Remove non-hex chars, convert to uppercase, and ensure correct length
+ public static String cleanupColorCodeString(String colorString, boolean includeAlpha) {
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
.replaceAll("").toUpperCase(Locale.ROOT);
- if (result.length() < COLOR_STRING_LENGTH) {
+ int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
+ if (result.length() < maxLength) {
return result;
}
- return result.substring(0, COLOR_STRING_LENGTH);
+ return result.substring(0, maxLength);
}
/**
- * @param color RGB color, without an alpha channel.
- * @return #RRGGBB hex color string
+ * @param color Color, with or without alpha channel.
+ * @param includeAlpha Whether to include the alpha channel in the output string.
+ * @return #RRGGBB or #AARRGGBB hex color string
*/
- public static String getColorString(@ColorInt int color) {
- String colorString = String.format("#%06X", color);
- if ((color & 0xFF000000) != 0) {
- // Likely a bug somewhere.
- Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
+ public static String getColorString(@ColorInt int color, boolean includeAlpha) {
+ if (includeAlpha) {
+ return String.format("#%08X", color);
}
- return colorString;
+ color = color & 0x00FFFFFF; // Mask to strip alpha.
+ return String.format("#%06X", color);
}
/**
- * Creates a Spanned object for a colored dot using SpannableString.
- *
- * @param color The RGB color (without alpha).
- * @return A Spanned object with the colored dot.
+ * Interface for notifying color changes.
*/
- public static Spanned getColorDot(@ColorInt int color) {
- SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
- spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return spannable;
+ public interface OnColorChangeListener {
+ void onColorChanged(String key, int newColor);
+ }
+
+ /**
+ * Sets the listener for color changes.
+ */
+ public void setOnColorChangeListener(OnColorChangeListener listener) {
+ this.colorChangeListener = listener;
+ }
+
+ /**
+ * Enables or disables the opacity slider in the color picker dialog.
+ */
+ public void setOpacitySliderEnabled(boolean enabled) {
+ this.opacitySliderEnabled = enabled;
}
public ColorPickerPreference(Context context) {
@@ -159,9 +159,13 @@ public class ColorPickerPreference extends EditTextPreference {
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
*/
private void init() {
- colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
- if (colorSetting == null) {
- Logger.printException(() -> "Could not find color setting for: " + getKey());
+ if (getKey() != null) {
+ colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
+ if (colorSetting == null) {
+ Logger.printException(() -> "Could not find color setting for: " + getKey());
+ }
+ } else {
+ Logger.printDebug(() -> "initialized without key, settings will be loaded later");
}
EditText editText = getEditText();
@@ -172,28 +176,29 @@ public class ColorPickerPreference extends EditTextPreference {
}
// Set the widget layout to a custom layout containing the colored dot.
- setWidgetLayoutResource(getResourceIdentifier(
- ResourceType.LAYOUT, "revanced_color_dot_widget"));
+ setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
}
/**
* Sets the selected color and updates the UI and settings.
- *
- * @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
- * @throws IllegalArgumentException If the color string is invalid.
*/
@Override
- public final void setText(String colorString) {
+ public void setText(String colorString) {
try {
Logger.printDebug(() -> "setText: " + colorString);
super.setText(colorString);
- currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
+ currentColor = Color.parseColor(colorString);
if (colorSetting != null) {
- colorSetting.save(getColorString(currentColor));
+ colorSetting.save(getColorString(currentColor, opacitySliderEnabled));
}
- updateColorPreview();
+ updateDialogColorDot();
updateWidgetColorDot();
+
+ // Notify the listener about the color change.
+ if (colorChangeListener != null) {
+ colorChangeListener.onColorChanged(getKey(), currentColor);
+ }
} catch (IllegalArgumentException ex) {
// This code is reached if the user pastes settings json with an invalid color
// since this preference is updated with the new setting text.
@@ -205,40 +210,8 @@ public class ColorPickerPreference extends EditTextPreference {
}
}
- @Override
- protected void onBindView(View view) {
- super.onBindView(view);
-
- widgetColorDot = view.findViewById(getResourceIdentifier(
- ResourceType.ID,
- "revanced_color_dot_widget"));
- widgetColorDot.setBackgroundResource(getResourceIdentifier(
- ResourceType.DRAWABLE,
- "revanced_settings_circle_background"));
- widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
- widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
- }
-
- /**
- * Updates the color preview TextView with a colored dot.
- */
- private void updateColorPreview() {
- if (dialogColorPreview != null) {
- dialogColorPreview.setText(getColorDot(currentColor));
- }
- }
-
- private void updateWidgetColorDot() {
- if (widgetColorDot != null) {
- widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
- widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
- }
- }
-
/**
* Creates a TextWatcher to monitor changes in the EditText for color input.
- *
- * @return A TextWatcher that updates the color preview on valid input.
*/
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
return new TextWatcher() {
@@ -254,15 +227,16 @@ public class ColorPickerPreference extends EditTextPreference {
public void afterTextChanged(Editable edit) {
try {
String colorString = edit.toString();
-
- String sanitizedColorString = cleanupColorCodeString(colorString);
+ String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled);
if (!sanitizedColorString.equals(colorString)) {
edit.replace(0, colorString.length(), sanitizedColorString);
return;
}
- if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
- // User is still typing out the color.
+ int expectedLength = opacitySliderEnabled
+ ? COLOR_STRING_LENGTH_WITH_ALPHA
+ : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
+ if (sanitizedColorString.length() != expectedLength) {
return;
}
@@ -270,7 +244,7 @@ public class ColorPickerPreference extends EditTextPreference {
if (currentColor != newColor) {
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
currentColor = newColor;
- updateColorPreview();
+ updateDialogColorDot();
updateWidgetColorDot();
colorPickerView.setColor(newColor);
}
@@ -283,32 +257,68 @@ public class ColorPickerPreference extends EditTextPreference {
}
/**
- * Creates a Dialog with a color preview and EditText for hex color input.
+ * Hook for subclasses to add a custom view to the top of the dialog.
*/
+ @Nullable
+ protected View createExtraDialogContentView(Context context) {
+ return null; // Default implementation returns no extra view.
+ }
+
+ /**
+ * Hook for subclasses to handle the OK button click.
+ */
+ protected void onDialogOkClicked() {
+ // Default implementation does nothing.
+ }
+
+ /**
+ * Hook for subclasses to handle the Neutral button click.
+ */
+ protected void onDialogNeutralClicked() {
+ // Default implementation.
+ try {
+ final int defaultColor = Color.parseColor(colorSetting.defaultValue);
+ dialogColorPickerView.setColor(defaultColor);
+ } catch (Exception ex) {
+ Logger.printException(() -> "Reset button failure", ex);
+ }
+ }
+
@Override
protected void showDialog(Bundle state) {
Context context = getContext();
+ // Create content container for all dialog views.
+ LinearLayout contentContainer = new LinearLayout(context);
+ contentContainer.setOrientation(LinearLayout.VERTICAL);
+
+ // Add extra view from subclass if it exists.
+ View extraView = createExtraDialogContentView(context);
+ if (extraView != null) {
+ contentContainer.addView(extraView);
+ }
+
// Inflate color picker view.
- View colorPicker = LayoutInflater.from(context).inflate(getResourceIdentifier(
- ResourceType.LAYOUT, "revanced_color_picker"), null);
- dialogColorPickerView = colorPicker.findViewById(getResourceIdentifier(
- ResourceType.ID, "revanced_color_picker_view"));
+ View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null);
+ dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW);
+ dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled);
dialogColorPickerView.setColor(currentColor);
+ contentContainer.addView(colorPicker);
// Horizontal layout for preview and EditText.
LinearLayout inputLayout = new LinearLayout(context);
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
+ inputLayout.setGravity(Gravity.CENTER_VERTICAL);
- dialogColorPreview = new TextView(context);
+ dialogColorDot = new View(context);
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
+ dipToPixels(20),
+ dipToPixels(20)
);
- 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();
+ previewParams.setMargins(dipToPixels(16), 0, dipToPixels(10), 0);
+ dialogColorDot.setLayoutParams(previewParams);
+ inputLayout.addView(dialogColorDot);
+ updateDialogColorDot();
EditText editText = getEditText();
ViewParent parent = editText.getParent();
@@ -319,7 +329,7 @@ public class ColorPickerPreference extends EditTextPreference {
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
- String currentColorString = getColorString(currentColor);
+ String currentColorString = getColorString(currentColor, opacitySliderEnabled);
editText.setText(currentColorString);
editText.setSelection(currentColorString.length());
editText.setTypeface(Typeface.MONOSPACE);
@@ -338,16 +348,12 @@ public class ColorPickerPreference extends EditTextPreference {
paddingView.setLayoutParams(params);
inputLayout.addView(paddingView);
- // Create content container for color picker and input layout.
- LinearLayout contentContainer = new LinearLayout(context);
- contentContainer.setOrientation(LinearLayout.VERTICAL);
- contentContainer.addView(colorPicker);
contentContainer.addView(inputLayout);
// Create ScrollView to wrap the content container.
ScrollView contentScrollView = new ScrollView(context);
- contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
- contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
+ contentScrollView.setVerticalScrollBarEnabled(false);
+ contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
@@ -356,51 +362,43 @@ public class ColorPickerPreference extends EditTextPreference {
contentScrollView.setLayoutParams(scrollViewParams);
contentScrollView.addView(contentContainer);
- // Create custom dialog.
- final int originalColor = currentColor & 0x00FFFFFF;
- Pair dialogPair = Utils.createCustomDialog(
+ final int originalColor = currentColor;
+ Pair dialogPair = CustomDialog.create(
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.
+ getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"),
+ null,
+ null,
+ null,
+ () -> { // OK button action.
try {
String colorString = editText.getText().toString();
- if (colorString.length() != COLOR_STRING_LENGTH) {
+ int expectedLength = opacitySliderEnabled
+ ? COLOR_STRING_LENGTH_WITH_ALPHA
+ : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
+ if (colorString.length() != expectedLength) {
Utils.showToastShort(str("revanced_settings_color_invalid"));
- setText(getColorString(originalColor));
+ setText(getColorString(originalColor, opacitySliderEnabled));
return;
}
setText(colorString);
+
+ onDialogOkClicked();
} catch (Exception ex) {
// Should never happen due to a bad color string,
// since the text is validated and fixed while the user types.
Logger.printException(() -> "OK button failure", ex);
}
},
- () -> {
- // Cancel button action.
+ () -> { // Cancel button action.
try {
- // Restore the original color.
- setText(getColorString(originalColor));
+ setText(getColorString(originalColor, opacitySliderEnabled));
} catch (Exception ex) {
Logger.printException(() -> "Cancel button failure", ex);
}
},
str("revanced_settings_reset_color"), // Neutral button text.
- () -> {
- // Neutral button action.
- try {
- final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
- // Setting view color causes listener callback into this class.
- dialogColorPickerView.setColor(defaultColor);
- } catch (Exception ex) {
- Logger.printException(() -> "Reset button failure", ex);
- }
- },
- false // Do not dismiss dialog when onNeutralClick.
+ this::onDialogNeutralClicked, // Neutral button action.
+ false // Do not dismiss dialog.
);
// Add the ScrollView to the dialog's main layout.
@@ -416,13 +414,13 @@ public class ColorPickerPreference extends EditTextPreference {
return;
}
- String updatedColorString = getColorString(color);
+ String updatedColorString = getColorString(color, opacitySliderEnabled);
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
currentColor = color;
editText.setText(updatedColorString);
editText.setSelection(updatedColorString.length());
- updateColorPreview();
+ updateDialogColorDot();
updateWidgetColorDot();
});
@@ -441,7 +439,7 @@ public class ColorPickerPreference extends EditTextPreference {
colorTextWatcher = null;
}
- dialogColorPreview = null;
+ dialogColorDot = null;
dialogColorPickerView = null;
}
@@ -450,4 +448,32 @@ public class ColorPickerPreference extends EditTextPreference {
super.setEnabled(enabled);
updateWidgetColorDot();
}
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
+ updateWidgetColorDot();
+ }
+
+ private void updateWidgetColorDot() {
+ if (widgetColorDot == null) return;
+
+ ColorDot.applyColorDot(
+ widgetColorDot,
+ currentColor,
+ widgetColorDot.isEnabled()
+ );
+ }
+
+ private void updateDialogColorDot() {
+ if (dialogColorDot == null) return;
+
+ ColorDot.applyColorDot(
+ dialogColorDot,
+ currentColor,
+ true
+ );
+ }
}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java
index 4d8efb2d2..810ddb3a6 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java
@@ -23,57 +23,73 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
/**
- * A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
+ * A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
+ * and an optional opacity slider.
* This implementation is density-independent and responsive across different screen sizes and DPIs.
- *
*
- * This view displays two main components for color selection:
+ * This view displays three main components for color selection:
*
* Hue Bar: A horizontal bar at the bottom that allows the user to select the hue component of the color.
- * Saturation-Value Selector: 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.
+ * Saturation-Value Selector: 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.
+ * Opacity Slider: An optional horizontal bar below the hue bar that allows the user to adjust
+ * the opacity (alpha channel) of the color.
*
- *
*
- * The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
- * saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
- *
+ * The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar,
+ * opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
*
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
*/
public class ColorPickerView extends View {
-
/**
* Interface definition for a callback to be invoked when the selected color changes.
*/
public interface OnColorChangedListener {
/**
* Called when the selected color has changed.
- *
- * Important: Callback color uses RGB format with zero alpha channel.
- *
- * @param color The new selected color.
*/
void onColorChanged(@ColorInt int color);
}
- /** Expanded touch area for the hue bar to increase the touch-sensitive area. */
+ /** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
public static final float TOUCH_EXPANSION = dipToPixels(20f);
+ /** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
+
+ /** Padding around the view. */
private static final float VIEW_PADDING = dipToPixels(16);
+
+ /** Height of the hue bar. */
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
+
+ /** Height of the opacity slider. */
+ private static final float OPACITY_BAR_HEIGHT = dipToPixels(12);
+
+ /** Corner radius for the hue bar. */
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
+
+ /** Corner radius for the opacity slider. */
+ private static final float OPACITY_CORNER_RADIUS = dipToPixels(6);
+
+ /** Radius of the selector handles. */
private static final float SELECTOR_RADIUS = dipToPixels(12);
+
+ /** Stroke width for the selector handle outlines. */
private static final float SELECTOR_STROKE_WIDTH = 8;
+
/**
- * Hue fill radius. Use slightly smaller radius for the selector handle fill,
+ * Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill,
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
*/
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
+
/** Thin dark outline stroke width for the selector rings. */
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
+
+ /** Radius for the outer edge of the selector rings, including stroke width. */
public static final float SELECTOR_EDGE_RADIUS =
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
@@ -85,6 +101,7 @@ public class ColorPickerView extends View {
@ColorInt
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
+ /** Precomputed array of hue colors for the hue bar (0-360 degrees). */
private static final int[] HUE_COLORS = new int[361];
static {
for (int i = 0; i < 361; i++) {
@@ -92,11 +109,16 @@ public class ColorPickerView extends View {
}
}
- /** Hue bar. */
+ /** Paint for the hue bar. */
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- /** Saturation-value selector. */
+
+ /** Paint for the opacity slider. */
+ private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ /** Paint for the saturation-value selector. */
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- /** Draggable selector. */
+
+ /** Paint for the draggable selector handles. */
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
{
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
@@ -104,6 +126,10 @@ public class ColorPickerView extends View {
/** Bounds of the hue bar. */
private final RectF hueRect = new RectF();
+
+ /** Bounds of the opacity slider. */
+ private final RectF opacityRect = new RectF();
+
/** Bounds of the saturation-value selector. */
private final RectF saturationValueRect = new RectF();
@@ -112,21 +138,35 @@ public class ColorPickerView extends View {
/** Current hue value (0-360). */
private float hue = 0f;
+
/** Current saturation value (0-1). */
private float saturation = 1f;
+
/** Current value (brightness) value (0-1). */
private float value = 1f;
- /** The currently selected color in RGB format with no alpha channel. */
+ /** Current opacity value (0-1). */
+ private float opacity = 1f;
+
+ /** The currently selected color, including alpha channel if opacity slider is enabled. */
@ColorInt
private int selectedColor;
+ /** Listener for color change events. */
private OnColorChangedListener colorChangedListener;
- /** Track if we're currently dragging the hue or saturation handle. */
+ /** Tracks if the hue selector is being dragged. */
private boolean isDraggingHue;
+
+ /** Tracks if the saturation-value selector is being dragged. */
private boolean isDraggingSaturation;
+ /** Tracks if the opacity selector is being dragged. */
+ private boolean isDraggingOpacity;
+
+ /** Flag to enable/disable the opacity slider. */
+ private boolean opacitySliderEnabled = false;
+
public ColorPickerView(Context context) {
super(context);
}
@@ -139,12 +179,32 @@ public class ColorPickerView extends View {
super(context, attrs, defStyleAttr);
}
+ /**
+ * Enables or disables the opacity slider.
+ */
+ public void setOpacitySliderEnabled(boolean enabled) {
+ if (opacitySliderEnabled != enabled) {
+ opacitySliderEnabled = enabled;
+ if (!enabled) {
+ opacity = 1f; // Reset to fully opaque when disabled.
+ updateSelectedColor();
+ }
+ updateOpacityShader();
+ requestLayout(); // Trigger re-measure to account for opacity slider.
+ invalidate();
+ }
+ }
+
+ /**
+ * Measures the view, ensuring a consistent aspect ratio and minimum dimensions.
+ */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
- final int minWidth = Utils.dipToPixels(250);
- final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
+ final int minWidth = dipToPixels(250);
+ final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
+ + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
int width = resolveSize(minWidth, widthMeasureSpec);
int height = resolveSize(minHeight, heightMeasureSpec);
@@ -154,7 +214,8 @@ public class ColorPickerView extends View {
height = Math.max(height, minHeight);
// Adjust height to maintain desired aspect ratio if possible.
- final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
+ final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
+ + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
height = desiredHeight;
}
@@ -163,17 +224,16 @@ public class ColorPickerView extends View {
}
/**
- * Called when the size of the view changes.
- * This method calculates and sets the bounds of the hue bar and saturation-value selector.
- * It also creates the necessary shaders for the gradients.
+ * Updates the view's layout when its size changes, recalculating bounds and shaders.
*/
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
- // Calculate bounds with hue bar at the bottom.
+ // Calculate bounds with hue bar and optional opacity bar at the bottom.
final float effectiveWidth = width - (2 * VIEW_PADDING);
- final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
+ final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS
+ - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0);
// Adjust rectangles to account for padding and density-independent dimensions.
saturationValueRect.set(
@@ -185,18 +245,28 @@ public class ColorPickerView extends View {
hueRect.set(
VIEW_PADDING,
- height - VIEW_PADDING - HUE_BAR_HEIGHT,
+ height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0),
VIEW_PADDING + effectiveWidth,
- height - VIEW_PADDING
+ height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0)
);
+ if (opacitySliderEnabled) {
+ opacityRect.set(
+ VIEW_PADDING,
+ height - VIEW_PADDING - OPACITY_BAR_HEIGHT,
+ VIEW_PADDING + effectiveWidth,
+ height - VIEW_PADDING
+ );
+ }
+
// Update the shaders.
updateHueShader();
updateSaturationValueShader();
+ updateOpacityShader();
}
/**
- * Updates the hue full spectrum (0-360 degrees).
+ * Updates the shader for the hue bar to reflect the color gradient.
*/
private void updateHueShader() {
LinearGradient hueShader = new LinearGradient(
@@ -211,8 +281,29 @@ public class ColorPickerView extends View {
}
/**
- * Updates the shader for the saturation-value selector based on the currently selected hue.
- * This method creates a combined shader that blends a saturation gradient with a value gradient.
+ * Updates the shader for the opacity slider to reflect the current RGB color with varying opacity.
+ */
+ private void updateOpacityShader() {
+ if (!opacitySliderEnabled) {
+ opacityPaint.setShader(null);
+ return;
+ }
+
+ // Create a linear gradient for opacity from transparent to opaque, using the current RGB color.
+ int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
+ LinearGradient opacityShader = new LinearGradient(
+ opacityRect.left, opacityRect.top,
+ opacityRect.right, opacityRect.top,
+ rgbColor & 0x00FFFFFF, // Fully transparent
+ rgbColor | 0xFF000000, // Fully opaque
+ Shader.TileMode.CLAMP
+ );
+
+ opacityPaint.setShader(opacityShader);
+ }
+
+ /**
+ * Updates the shader for the saturation-value selector to reflect the current hue.
*/
private void updateSaturationValueShader() {
// Create a saturation-value gradient based on the current hue.
@@ -232,7 +323,6 @@ public class ColorPickerView extends View {
);
// Create a linear gradient for the value (brightness) from white to black (vertical).
- //noinspection ExtractMethodRecommender
LinearGradient valShader = new LinearGradient(
saturationValueRect.left, saturationValueRect.top,
saturationValueRect.left, saturationValueRect.bottom,
@@ -249,11 +339,7 @@ public class ColorPickerView extends View {
}
/**
- * Draws the color picker view on the canvas.
- * This method draws the saturation-value selector, the hue bar with rounded corners,
- * and the draggable handles.
- *
- * @param canvas The canvas on which to draw.
+ * Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles.
*/
@Override
protected void onDraw(Canvas canvas) {
@@ -263,49 +349,67 @@ public class ColorPickerView extends View {
// Draw the hue bar.
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
+ // Draw the opacity bar if enabled.
+ if (opacitySliderEnabled) {
+ canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint);
+ }
+
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
final float hueSelectorY = hueRect.centerY();
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
- // Draw the saturation and hue selector handle filled with the selected color.
+ // Draw the saturation and hue selector handles filled with their respective colors (fully opaque).
hsvArray[0] = hue;
- final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
+ final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle.
+ final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle.
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
selectorPaint.setColor(hueHandleColor);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
- selectorPaint.setColor(selectedColor | 0xFF000000);
+ selectorPaint.setColor(satHandleColor);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
+ if (opacitySliderEnabled) {
+ final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
+ final float opacitySelectorY = opacityRect.centerY();
+ selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity.
+ canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
+ }
+
// Draw white outlines for the handles.
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
selectorPaint.setStyle(Paint.Style.STROKE);
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
+ if (opacitySliderEnabled) {
+ final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
+ final float opacitySelectorY = opacityRect.centerY();
+ canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint);
+ }
// Draw thin dark outlines for the handles at the outer edge of the white outline.
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
+ if (opacitySliderEnabled) {
+ final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
+ final float opacitySelectorY = opacityRect.centerY();
+ canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
+ }
}
/**
- * Handles touch events on the view.
- * This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
- * updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
- *
- * In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
- * is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
+ * Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
- @SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
+ @SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
@@ -314,13 +418,19 @@ public class ColorPickerView extends View {
final int action = event.getAction();
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
- // Define touch expansion for the hue bar.
+ // Define touch expansion for the hue and opacity bars.
RectF expandedHueRect = new RectF(
hueRect.left,
hueRect.top - TOUCH_EXPANSION,
hueRect.right,
hueRect.bottom + TOUCH_EXPANSION
);
+ RectF expandedOpacityRect = opacitySliderEnabled ? new RectF(
+ opacityRect.left,
+ opacityRect.top - TOUCH_EXPANSION,
+ opacityRect.right,
+ opacityRect.bottom + TOUCH_EXPANSION
+ ) : new RectF();
switch (action) {
case MotionEvent.ACTION_DOWN:
@@ -331,7 +441,10 @@ public class ColorPickerView extends View {
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
- // Create hit areas for both handles.
+ final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0;
+ final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0;
+
+ // Create hit areas for all handles.
RectF hueHitRect = new RectF(
hueSelectorX - SELECTOR_RADIUS,
hueSelectorY - SELECTOR_RADIUS,
@@ -344,14 +457,23 @@ public class ColorPickerView extends View {
satSelectorX + SELECTOR_RADIUS,
valSelectorY + SELECTOR_RADIUS
);
+ RectF opacityHitRect = opacitySliderEnabled ? new RectF(
+ opacitySelectorX - SELECTOR_RADIUS,
+ opacitySelectorY - SELECTOR_RADIUS,
+ opacitySelectorX + SELECTOR_RADIUS,
+ opacitySelectorY + SELECTOR_RADIUS
+ ) : new RectF();
- // 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 bar areas.
if (hueHitRect.contains(x, y)) {
isDraggingHue = true;
updateHueFromTouch(x);
} else if (satValHitRect.contains(x, y)) {
isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y);
+ } else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) {
+ isDraggingOpacity = true;
+ updateOpacityFromTouch(x);
} else if (expandedHueRect.contains(x, y)) {
// Handle touch within the expanded hue bar area.
isDraggingHue = true;
@@ -359,6 +481,9 @@ public class ColorPickerView extends View {
} else if (saturationValueRect.contains(x, y)) {
isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y);
+ } else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) {
+ isDraggingOpacity = true;
+ updateOpacityFromTouch(x);
}
break;
@@ -368,6 +493,8 @@ public class ColorPickerView extends View {
updateHueFromTouch(x);
} else if (isDraggingSaturation) {
updateSaturationValueFromTouch(x, y);
+ } else if (isDraggingOpacity) {
+ updateOpacityFromTouch(x);
}
break;
@@ -375,6 +502,7 @@ public class ColorPickerView extends View {
case MotionEvent.ACTION_CANCEL:
isDraggingHue = false;
isDraggingSaturation = false;
+ isDraggingOpacity = false;
break;
}
} catch (Exception ex) {
@@ -385,9 +513,7 @@ public class ColorPickerView extends View {
}
/**
- * Updates the hue value based on touch position, clamping to valid range.
- *
- * @param x The x-coordinate of the touch position.
+ * Updates the hue value based on a touch event.
*/
private void updateHueFromTouch(float x) {
// Clamp x to the hue rectangle bounds.
@@ -399,14 +525,12 @@ public class ColorPickerView extends View {
hue = updatedHue;
updateSaturationValueShader();
+ updateOpacityShader();
updateSelectedColor();
}
/**
- * Updates saturation and value based on touch position, clamping to valid range.
- *
- * @param x The x-coordinate of the touch position.
- * @param y The y-coordinate of the touch position.
+ * Updates the saturation and value based on a touch event.
*/
private void updateSaturationValueFromTouch(float x, float y) {
// Clamp x and y to the saturation-value rectangle bounds.
@@ -421,14 +545,34 @@ public class ColorPickerView extends View {
}
saturation = updatedSaturation;
value = updatedValue;
+ updateOpacityShader();
updateSelectedColor();
}
/**
- * Updates the selected color and notifies listeners.
+ * Updates the opacity value based on a touch event.
+ */
+ private void updateOpacityFromTouch(float x) {
+ if (!opacitySliderEnabled) {
+ return;
+ }
+ final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right);
+ final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width();
+ if (opacity == updatedOpacity) {
+ return;
+ }
+ opacity = updatedOpacity;
+ updateSelectedColor();
+ }
+
+ /**
+ * Updates the selected color based on the current hue, saturation, value, and opacity.
*/
private void updateSelectedColor() {
- final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
+ final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
+ final int updatedColor = opacitySliderEnabled
+ ? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24)
+ : (rgbColor & 0x00FFFFFF) | 0xFF000000;
if (selectedColor != updatedColor) {
selectedColor = updatedColor;
@@ -444,19 +588,16 @@ public class ColorPickerView extends View {
}
/**
- * Sets the currently selected color.
- *
- * @param color The color to set in either ARGB or RGB format.
+ * Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly.
*/
public void setColor(@ColorInt int color) {
- color &= 0x00FFFFFF;
if (selectedColor == color) {
return;
}
// Update the selected color.
selectedColor = color;
- Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
+ Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled));
// Convert the ARGB color to HSV values.
float[] hsv = new float[3];
@@ -466,9 +607,11 @@ public class ColorPickerView extends View {
hue = hsv[0];
saturation = hsv[1];
value = hsv[2];
+ opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f;
// Update the saturation-value shader based on the new hue.
updateSaturationValueShader();
+ updateOpacityShader();
// Notify the listener if it's set.
if (colorChangedListener != null) {
@@ -481,8 +624,6 @@ public class ColorPickerView extends View {
/**
* Gets the currently selected color.
- *
- * @return The selected color in RGB format with no alpha channel.
*/
@ColorInt
public int getColor() {
@@ -490,9 +631,7 @@ public class ColorPickerView extends View {
}
/**
- * Sets the listener to be notified when the selected color changes.
- *
- * @param listener The listener to set.
+ * Sets a listener to be notified when the selected color changes.
*/
public void setOnColorChangedListener(OnColorChangedListener listener) {
colorChangedListener = listener;
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java
new file mode 100644
index 000000000..5e24f7bf3
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java
@@ -0,0 +1,34 @@
+package app.revanced.extension.shared.settings.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * Extended ColorPickerPreference that enables the opacity slider for color selection.
+ */
+@SuppressWarnings("unused")
+public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference {
+
+ public ColorPickerWithOpacitySliderPreference(Context context) {
+ super(context);
+ init();
+ }
+
+ public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ /**
+ * Initialize the preference with opacity slider enabled.
+ */
+ private void init() {
+ // Enable the opacity slider for alpha channel support.
+ setOpacitySliderEnabled(true);
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java
index e1b6de8c8..ff728838b 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java
@@ -1,5 +1,7 @@
package app.revanced.extension.shared.settings.preference;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
@@ -9,19 +11,86 @@ import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.*;
+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 androidx.annotation.Nullable;
-import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.ui.CustomDialog;
/**
- * A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
+ * A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator,
+ * supports a static summary and highlighted entries for search functionality.
*/
@SuppressWarnings({"unused", "deprecation"})
public class CustomDialogListPreference extends ListPreference {
+ public static final int ID_REVANCED_CHECK_ICON =
+ getResourceIdentifierOrThrow("revanced_check_icon", "id");
+ public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
+ getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
+ public static final int ID_REVANCED_ITEM_TEXT =
+ getResourceIdentifierOrThrow("revanced_item_text", "id");
+ public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
+ getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
+
+ private String staticSummary = null;
+ private CharSequence[] highlightedEntriesForDialog = null;
+
+ /**
+ * Set a static summary that will not be overwritten by value changes.
+ */
+ public void setStaticSummary(String summary) {
+ this.staticSummary = summary;
+ }
+
+ /**
+ * Returns the static summary if set, otherwise null.
+ */
+ @Nullable
+ public String getStaticSummary() {
+ return staticSummary;
+ }
+
+ /**
+ * Always return static summary if set.
+ */
+ @Override
+ public CharSequence getSummary() {
+ if (staticSummary != null) {
+ return staticSummary;
+ }
+ return super.getSummary();
+ }
+
+ /**
+ * Sets highlighted entries for display in the dialog.
+ * These entries are used only for the current dialog and are automatically cleared.
+ */
+ public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) {
+ this.highlightedEntriesForDialog = highlightedEntries;
+ }
+
+ /**
+ * Clears highlighted entries after the dialog is closed.
+ */
+ public void clearHighlightedEntriesForDialog() {
+ this.highlightedEntriesForDialog = null;
+ }
+
+ /**
+ * Returns entries for display in the dialog.
+ * If highlighted entries exist, they are used; otherwise, the original entries are returned.
+ */
+ private CharSequence[] getEntriesForDialog() {
+ return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries();
+ }
+
/**
* Custom ArrayAdapter to handle checkmark visibility.
*/
@@ -36,8 +105,10 @@ public class CustomDialogListPreference extends ListPreference {
final CharSequence[] entryValues;
String selectedValue;
- public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
- 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;
@@ -54,22 +125,16 @@ public class CustomDialogListPreference extends ListPreference {
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(layoutResourceId, parent, false);
holder = new SubViewDataContainer();
- holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
- ResourceType.ID,
- "revanced_check_icon"));
- holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
- ResourceType.ID,
- "revanced_check_icon_placeholder"));
- holder.itemText = view.findViewById(Utils.getResourceIdentifier(
- ResourceType.ID,
- "revanced_item_text"));
+ holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
+ holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
+ holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
view.setTag(holder);
} else {
holder = (SubViewDataContainer) view.getTag();
}
- // Set text.
- holder.itemText.setText(getItem(position));
+ CharSequence itemText = getItem(position);
+ holder.itemText.setText(itemText);
holder.itemText.setTextColor(Utils.getAppForegroundColor());
// Show or hide checkmark and placeholder.
@@ -107,6 +172,9 @@ public class CustomDialogListPreference extends ListPreference {
protected void showDialog(Bundle state) {
Context context = getContext();
+ CharSequence[] entriesToShow = getEntriesForDialog();
+ CharSequence[] entryValues = getEntryValues();
+
// Create ListView.
ListView listView = new ListView(context);
listView.setId(android.R.id.list);
@@ -115,9 +183,9 @@ public class CustomDialogListPreference extends ListPreference {
// Create custom adapter for the ListView.
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
context,
- Utils.getResourceIdentifier(ResourceType.LAYOUT, "revanced_custom_list_item_checked"),
- getEntries(),
- getEntryValues(),
+ LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
+ entriesToShow,
+ entryValues,
getValue()
);
listView.setAdapter(adapter);
@@ -125,7 +193,6 @@ public class CustomDialogListPreference extends ListPreference {
// 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);
@@ -136,19 +203,23 @@ public class CustomDialogListPreference extends ListPreference {
}
// Create the custom dialog without OK button.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
getTitle() != null ? getTitle().toString() : "",
null,
null,
- null, // No OK button text.
- null, // No OK button action.
- () -> {}, // Cancel button action (just dismiss).
+ null,
+ null,
+ this::clearHighlightedEntriesForDialog, // Cancel button action.
null,
null,
true
);
+ Dialog dialog = dialogPair.first;
+ // Add a listener to clear when the dialog is closed in any way.
+ dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog());
+
// Add the ListView to the main layout.
LinearLayout mainLayout = dialogPair.second;
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
@@ -160,16 +231,28 @@ public class CustomDialogListPreference extends ListPreference {
// Handle item click to select value and dismiss dialog.
listView.setOnItemClickListener((parent, view, position, id) -> {
- String selectedValue = getEntryValues()[position].toString();
+ String selectedValue = entryValues[position].toString();
if (callChangeListener(selectedValue)) {
setValue(selectedValue);
+
+ // Update summaries from the original entries (without highlighting).
+ if (staticSummary == null) {
+ CharSequence[] originalEntries = getEntries();
+ if (originalEntries != null && position < originalEntries.length) {
+ setSummary(originalEntries[position]);
+ }
+ }
+
adapter.setSelectedValue(selectedValue);
adapter.notifyDataSetChanged();
}
- dialogPair.first.dismiss();
+
+ // Clear highlighted entries before closing.
+ clearHighlightedEntriesForDialog();
+ dialog.dismiss();
});
// Show the dialog.
- dialogPair.first.show();
+ dialog.show();
}
}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java
index 45c1d36b4..1c8580241 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java
@@ -1,7 +1,6 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.Dialog;
import android.content.Context;
@@ -10,21 +9,17 @@ import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
-import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue;
-import android.view.View;
-import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
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.Utils;
import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
@@ -82,7 +77,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
EditText editText = getEditText();
// Create a custom dialog with the EditText.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
str("revanced_pref_import_export_title"), // Title.
null, // No message (EditText replaces it).
@@ -98,6 +93,20 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
true // Dismiss dialog when onNeutralClick.
);
+ // If there are no settings yet, then show the on screen keyboard and bring focus to
+ // the edit text. This makes it easier to paste saved settings after a reinstall.
+ dialogPair.first.setOnShowListener(dialogInterface -> {
+ if (existingSettings.isEmpty()) {
+ editText.postDelayed(() -> {
+ editText.requestFocus();
+
+ InputMethodManager inputMethodManager = (InputMethodManager)
+ editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
+ }, 100);
+ }
+ });
+
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java
index d639d39be..be14e4a63 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java
@@ -17,6 +17,7 @@ import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.util.AttributeSet;
+import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.webkit.WebView;
@@ -228,10 +229,10 @@ class WebViewDialog extends Dialog {
setContentView(mainLayout);
- // Set dialog window attributes
+ // Set dialog window attributes.
Window window = getWindow();
if (window != null) {
- Utils.setDialogWindowParameters(window);
+ Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
}
}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
index 13de26bfa..c6f323ceb 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
@@ -16,8 +16,8 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference {
@@ -66,7 +66,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
// Create custom dialog.
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
index a60a46d5e..130805d35 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java
@@ -116,7 +116,7 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
*/
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
- final int backButtonResource = Utils.getResourceIdentifier(
+ final int backButtonResource = Utils.getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_toolbar_arrow_left");
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
customizeBackButtonDrawable(drawable);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java
similarity index 95%
rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java
rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java
index 9570883cb..8b9a76f03 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/UrlLinkPreference.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/UrlLinkPreference.java
@@ -1,4 +1,4 @@
-package app.revanced.extension.youtube.settings.preference;
+package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.content.Intent;
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java
new file mode 100644
index 000000000..9b5c9464c
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java
@@ -0,0 +1,365 @@
+package app.revanced.extension.shared.settings.search;
+
+import android.graphics.Color;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+
+import androidx.annotation.ColorInt;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
+import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
+import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
+
+/**
+ * Abstract base class for search result items, defining common fields and behavior.
+ */
+public abstract class BaseSearchResultItem {
+ // Enum to represent view types.
+ public enum ViewType {
+ REGULAR,
+ SWITCH,
+ LIST,
+ COLOR_PICKER,
+ GROUP_HEADER,
+ NO_RESULTS,
+ URL_LINK;
+
+ // Get the corresponding layout resource ID.
+ public int getLayoutResourceId() {
+ return switch (this) {
+ case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
+ case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
+ case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
+ case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
+ case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
+ case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
+ };
+ }
+
+ private static int getResourceIdentifier(String name) {
+ // Placeholder for actual resource identifier retrieval.
+ return Utils.getResourceIdentifierOrThrow(name, "layout");
+ }
+ }
+
+ final String navigationPath;
+ final List navigationKeys;
+ final ViewType preferenceType;
+ CharSequence highlightedTitle;
+ CharSequence highlightedSummary;
+ boolean highlightingApplied;
+
+ BaseSearchResultItem(String navPath, List navKeys, ViewType type) {
+ this.navigationPath = navPath;
+ this.navigationKeys = new ArrayList<>(navKeys != null ? navKeys : Collections.emptyList());
+ this.preferenceType = type;
+ this.highlightedTitle = "";
+ this.highlightedSummary = "";
+ this.highlightingApplied = false;
+ }
+
+ abstract boolean matchesQuery(String query);
+ abstract void applyHighlighting(Pattern queryPattern);
+ abstract void clearHighlighting();
+
+ // Shared method for highlighting text with search query.
+ protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
+ if (TextUtils.isEmpty(text)) return text;
+
+ final int adjustedColor = Utils.adjustColorBrightness(
+ Utils.getAppBackgroundColor(), 0.95f, 1.20f);
+ BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
+ SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+
+ Matcher matcher = queryPattern.matcher(text);
+ while (matcher.find()) {
+ spannable.setSpan(highlightSpan, matcher.start(), matcher.end(),
+ SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return spannable;
+ }
+
+ /**
+ * Search result item for group headers (navigation path only).
+ */
+ public static class GroupHeaderItem extends BaseSearchResultItem {
+ GroupHeaderItem(String navPath, List navKeys) {
+ super(navPath, navKeys, ViewType.GROUP_HEADER);
+ this.highlightedTitle = navPath;
+ }
+
+ @Override
+ boolean matchesQuery(String query) {
+ return false; // Headers are not directly searchable.
+ }
+
+ @Override
+ void applyHighlighting(Pattern queryPattern) {}
+
+ @Override
+ void clearHighlighting() {}
+ }
+
+ /**
+ * Search result item for preferences, handling type-specific data and search text.
+ */
+ @SuppressWarnings("deprecation")
+ public static class PreferenceSearchItem extends BaseSearchResultItem {
+ public final Preference preference;
+ final String searchableText;
+ final CharSequence originalTitle;
+ final CharSequence originalSummary;
+ final CharSequence originalSummaryOn;
+ final CharSequence originalSummaryOff;
+ final CharSequence[] originalEntries;
+ private CharSequence[] highlightedEntries;
+ private boolean entriesHighlightingApplied;
+
+ @ColorInt
+ private int color;
+
+ // Store last applied highlighting pattern to reapply when needed.
+ Pattern lastQueryPattern;
+
+ PreferenceSearchItem(Preference pref, String navPath, List navKeys) {
+ super(navPath, navKeys, determineType(pref));
+ this.preference = pref;
+ this.originalTitle = pref.getTitle() != null ? pref.getTitle() : "";
+ this.originalSummary = pref.getSummary();
+ this.highlightedTitle = this.originalTitle;
+ this.highlightedSummary = this.originalSummary != null ? this.originalSummary : "";
+ this.color = 0;
+ this.lastQueryPattern = null;
+
+ // Initialize type-specific fields.
+ FieldInitializationResult result = initTypeSpecificFields(pref);
+ this.originalSummaryOn = result.summaryOn;
+ this.originalSummaryOff = result.summaryOff;
+ this.originalEntries = result.entries;
+
+ // Build searchable text.
+ this.searchableText = buildSearchableText(pref);
+ }
+
+ private static class FieldInitializationResult {
+ CharSequence summaryOn = null;
+ CharSequence summaryOff = null;
+ CharSequence[] entries = null;
+ }
+
+ private static ViewType determineType(Preference pref) {
+ if (pref instanceof SwitchPreference) return ViewType.SWITCH;
+ if (pref instanceof ListPreference) return ViewType.LIST;
+ if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
+ if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK;
+ if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
+ return ViewType.REGULAR;
+ }
+
+ private FieldInitializationResult initTypeSpecificFields(Preference pref) {
+ FieldInitializationResult result = new FieldInitializationResult();
+
+ if (pref instanceof SwitchPreference switchPref) {
+ result.summaryOn = switchPref.getSummaryOn();
+ result.summaryOff = switchPref.getSummaryOff();
+ } else if (pref instanceof ColorPickerPreference colorPref) {
+ String colorString = colorPref.getText();
+ this.color = TextUtils.isEmpty(colorString) ? 0 : Color.parseColor(colorString);
+ } else if (pref instanceof ListPreference listPref) {
+ result.entries = listPref.getEntries();
+ if (result.entries != null) {
+ this.highlightedEntries = new CharSequence[result.entries.length];
+ System.arraycopy(result.entries, 0, this.highlightedEntries, 0, result.entries.length);
+ }
+ }
+
+ this.entriesHighlightingApplied = false;
+ return result;
+ }
+
+ private String buildSearchableText(Preference pref) {
+ StringBuilder searchBuilder = new StringBuilder();
+ String key = pref.getKey();
+ String normalizedKey = "";
+ if (key != null) {
+ // Normalize preference key by removing the common "revanced_" prefix
+ // so that users can search by the meaningful part only.
+ normalizedKey = key.startsWith("revanced_")
+ ? key.substring("revanced_".length())
+ : key;
+ }
+ appendText(searchBuilder, normalizedKey);
+ appendText(searchBuilder, originalTitle);
+ appendText(searchBuilder, originalSummary);
+
+ // Add type-specific searchable content.
+ if (pref instanceof ListPreference) {
+ if (originalEntries != null) {
+ for (CharSequence entry : originalEntries) {
+ appendText(searchBuilder, entry);
+ }
+ }
+ } else if (pref instanceof SwitchPreference) {
+ appendText(searchBuilder, originalSummaryOn);
+ appendText(searchBuilder, originalSummaryOff);
+ } else if (pref instanceof ColorPickerPreference) {
+ appendText(searchBuilder, ColorPickerPreference.getColorString(color, false));
+ }
+
+ // Include navigation path in searchable text.
+ appendText(searchBuilder, navigationPath);
+
+ return searchBuilder.toString();
+ }
+
+ private void appendText(StringBuilder builder, CharSequence text) {
+ if (!TextUtils.isEmpty(text)) {
+ if (builder.length() > 0) builder.append(" ");
+ builder.append(Utils.removePunctuationToLowercase(text));
+ }
+ }
+
+ /**
+ * Gets the current effective summary for this preference, considering state-dependent summaries.
+ */
+ public CharSequence getCurrentEffectiveSummary() {
+ if (preference instanceof CustomDialogListPreference customPref) {
+ String staticSum = customPref.getStaticSummary();
+ if (staticSum != null) {
+ return staticSum;
+ }
+ }
+ if (preference instanceof SwitchPreference switchPref) {
+ boolean currentState = switchPref.isChecked();
+ return currentState
+ ? (originalSummaryOn != null ? originalSummaryOn :
+ originalSummary != null ? originalSummary : "")
+ : (originalSummaryOff != null ? originalSummaryOff :
+ originalSummary != null ? originalSummary : "");
+ } else if (preference instanceof ListPreference listPref) {
+ String value = listPref.getValue();
+ CharSequence[] entries = listPref.getEntries();
+ CharSequence[] entryValues = listPref.getEntryValues();
+ if (value != null && entries != null && entryValues != null) {
+ for (int i = 0, length = entries.length; i < length; i++) {
+ if (value.equals(entryValues[i].toString())) {
+ return originalEntries != null && i < originalEntries.length && originalEntries[i] != null
+ ? originalEntries[i]
+ : originalSummary != null ? originalSummary : "";
+ }
+ }
+ }
+ return originalSummary != null ? originalSummary : "";
+ }
+ return originalSummary != null ? originalSummary : "";
+ }
+
+ /**
+ * Checks if this search result item matches the provided query.
+ * Uses case-insensitive matching against the searchable text.
+ */
+ @Override
+ boolean matchesQuery(String query) {
+ return searchableText.contains(Utils.removePunctuationToLowercase(query));
+ }
+
+ /**
+ * Get highlighted entries to show in dialog.
+ */
+ public CharSequence[] getHighlightedEntries() {
+ return highlightedEntries;
+ }
+
+ /**
+ * Whether highlighting is applied to entries.
+ */
+ public boolean isEntriesHighlightingApplied() {
+ return entriesHighlightingApplied;
+ }
+
+ /**
+ * Highlights the search query in the title and summary.
+ */
+ @Override
+ void applyHighlighting(Pattern queryPattern) {
+ this.lastQueryPattern = queryPattern;
+ // Highlight the title.
+ highlightedTitle = highlightSearchQuery(originalTitle, queryPattern);
+
+ // Get the current effective summary and highlight it.
+ CharSequence currentSummary = getCurrentEffectiveSummary();
+ highlightedSummary = highlightSearchQuery(currentSummary, queryPattern);
+
+ // Highlight the entries.
+ if (preference instanceof ListPreference && originalEntries != null) {
+ highlightedEntries = new CharSequence[originalEntries.length];
+ for (int i = 0, length = originalEntries.length; i < length; i++) {
+ if (originalEntries[i] != null) {
+ highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
+ } else {
+ highlightedEntries[i] = null;
+ }
+ }
+ entriesHighlightingApplied = true;
+ }
+
+ highlightingApplied = true;
+ }
+
+ /**
+ * Clears all search query highlighting and restores original state completely.
+ */
+ @Override
+ void clearHighlighting() {
+ if (!highlightingApplied) return;
+
+ // Restore original title.
+ highlightedTitle = originalTitle;
+
+ // Restore current effective summary without highlighting.
+ highlightedSummary = getCurrentEffectiveSummary();
+
+ // Restore original entries.
+ if (originalEntries != null && highlightedEntries != null) {
+ System.arraycopy(originalEntries, 0, highlightedEntries, 0,
+ Math.min(originalEntries.length, highlightedEntries.length));
+ }
+
+ entriesHighlightingApplied = false;
+ highlightingApplied = false;
+ lastQueryPattern = null;
+ }
+
+ /**
+ * Refreshes highlighting for dynamic summaries (like switch preferences).
+ * Should be called when the preference state changes.
+ */
+ public void refreshHighlighting() {
+ if (highlightingApplied && lastQueryPattern != null) {
+ CharSequence currentSummary = getCurrentEffectiveSummary();
+ highlightedSummary = highlightSearchQuery(currentSummary, lastQueryPattern);
+ }
+ }
+
+ public void setColor(int newColor) {
+ this.color = newColor;
+ }
+
+ @ColorInt
+ public int getColor() {
+ return color;
+ }
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java
new file mode 100644
index 000000000..f1893a232
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java
@@ -0,0 +1,621 @@
+package app.revanced.extension.shared.settings.search;
+
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+import static app.revanced.extension.shared.settings.search.BaseSearchViewController.DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
+
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
+import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
+import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
+import app.revanced.extension.shared.ui.ColorDot;
+
+/**
+ * Abstract adapter for displaying search results in overlay ListView with ViewHolder pattern.
+ */
+@SuppressWarnings("deprecation")
+public abstract class BaseSearchResultsAdapter extends ArrayAdapter {
+ protected final LayoutInflater inflater;
+ protected final BaseSearchViewController.BasePreferenceFragment fragment;
+ protected final BaseSearchViewController searchViewController;
+ protected AnimatorSet currentAnimator;
+ protected abstract PreferenceScreen getMainPreferenceScreen();
+
+ protected static final int BLINK_DURATION = 400;
+ protected static final int PAUSE_BETWEEN_BLINKS = 100;
+
+ protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
+ "preference_title", "id");
+ protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
+ "preference_summary", "id");
+ protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
+ "preference_path", "id");
+ protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
+ "preference_switch", "id");
+ protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
+ "preference_color_dot", "id");
+
+ protected static class RegularViewHolder {
+ TextView titleView;
+ TextView summaryView;
+ }
+
+ protected static class SwitchViewHolder {
+ TextView titleView;
+ TextView summaryView;
+ Switch switchWidget;
+ }
+
+ protected static class ColorViewHolder {
+ TextView titleView;
+ TextView summaryView;
+ View colorDot;
+ }
+
+ protected static class GroupHeaderViewHolder {
+ TextView pathView;
+ }
+
+ protected static class NoResultsViewHolder {
+ TextView titleView;
+ TextView summaryView;
+ ImageView iconView;
+ }
+
+ public BaseSearchResultsAdapter(Context context, List items,
+ BaseSearchViewController.BasePreferenceFragment fragment,
+ BaseSearchViewController searchViewController) {
+ super(context, 0, items);
+ this.inflater = LayoutInflater.from(context);
+ this.fragment = fragment;
+ this.searchViewController = searchViewController;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ BaseSearchResultItem item = getItem(position);
+ return item == null ? 0 : item.preferenceType.ordinal();
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return BaseSearchResultItem.ViewType.values().length;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ BaseSearchResultItem item = getItem(position);
+ if (item == null) return new View(getContext());
+ // Use the ViewType enum.
+ BaseSearchResultItem.ViewType viewType = item.preferenceType;
+ // Create or reuse preference view based on type.
+ return createPreferenceView(item, convertView, viewType, parent);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ BaseSearchResultItem item = getItem(position);
+ // Disable for NO_RESULTS items to prevent ripple/selection.
+ return item != null && item.preferenceType != BaseSearchResultItem.ViewType.NO_RESULTS;
+ }
+
+ /**
+ * Creates or reuses a view for the given SearchResultItem.
+ *
+ * Thanks to {@link #getItemViewType(int)} and {@link #getViewTypeCount()}, ListView knows
+ * how many different row types exist and keeps a separate "recycling pool" for each.
+ * That means convertView passed here is ALWAYS of the correct type for this position.
+ * So only need to check if (view == null), and if so – inflate a new layout and create the proper ViewHolder.
+ */
+ protected View createPreferenceView(BaseSearchResultItem item, View convertView,
+ BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = inflateViewForType(viewType, parent);
+ createViewHolderForType(view, viewType);
+ }
+
+ // Retrieve the cached ViewHolder.
+ Object holder = view.getTag();
+ bindDataToViewHolder(item, holder, viewType, view);
+ return view;
+ }
+
+ protected View inflateViewForType(BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
+ return inflater.inflate(viewType.getLayoutResourceId(), parent, false);
+ }
+
+ protected void createViewHolderForType(View view, BaseSearchResultItem.ViewType viewType) {
+ switch (viewType) {
+ case REGULAR, LIST, URL_LINK -> {
+ RegularViewHolder regularHolder = new RegularViewHolder();
+ regularHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
+ regularHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
+ view.setTag(regularHolder);
+ }
+ case SWITCH -> {
+ SwitchViewHolder switchHolder = new SwitchViewHolder();
+ switchHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
+ switchHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
+ switchHolder.switchWidget = view.findViewById(ID_PREFERENCE_SWITCH);
+ view.setTag(switchHolder);
+ }
+ case COLOR_PICKER -> {
+ ColorViewHolder colorHolder = new ColorViewHolder();
+ colorHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
+ colorHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
+ colorHolder.colorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
+ view.setTag(colorHolder);
+ }
+ case GROUP_HEADER -> {
+ GroupHeaderViewHolder groupHolder = new GroupHeaderViewHolder();
+ groupHolder.pathView = view.findViewById(ID_PREFERENCE_PATH);
+ view.setTag(groupHolder);
+ }
+ case NO_RESULTS -> {
+ NoResultsViewHolder noResultsHolder = new NoResultsViewHolder();
+ noResultsHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
+ noResultsHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
+ noResultsHolder.iconView = view.findViewById(android.R.id.icon);
+ view.setTag(noResultsHolder);
+ }
+ default -> throw new IllegalStateException("Unknown viewType: " + viewType);
+ }
+ }
+
+ protected void bindDataToViewHolder(BaseSearchResultItem item, Object holder,
+ BaseSearchResultItem.ViewType viewType, View view) {
+ switch (viewType) {
+ case REGULAR, URL_LINK, LIST -> bindRegularViewHolder(item, (RegularViewHolder) holder, view);
+ case SWITCH -> bindSwitchViewHolder(item, (SwitchViewHolder) holder, view);
+ case COLOR_PICKER -> bindColorViewHolder(item, (ColorViewHolder) holder, view);
+ case GROUP_HEADER -> bindGroupHeaderViewHolder(item, (GroupHeaderViewHolder) holder, view);
+ case NO_RESULTS -> bindNoResultsViewHolder(item, (NoResultsViewHolder) holder);
+ default -> throw new IllegalStateException("Unknown viewType: " + viewType);
+ }
+ }
+
+ protected void bindRegularViewHolder(BaseSearchResultItem item, RegularViewHolder holder, View view) {
+ BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
+ prefItem.refreshHighlighting();
+ holder.titleView.setText(item.highlightedTitle);
+ holder.summaryView.setText(item.highlightedSummary);
+ holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
+ setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
+ () -> {
+ handlePreferenceClick(prefItem.preference);
+ if (prefItem.preference instanceof ListPreference) {
+ prefItem.refreshHighlighting();
+ holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
+ holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
+ notifyDataSetChanged();
+ }
+ },
+ () -> navigateAndScrollToPreference(item));
+ }
+
+ protected void bindSwitchViewHolder(BaseSearchResultItem item, SwitchViewHolder holder, View view) {
+ BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
+ SwitchPreference switchPref = (SwitchPreference) prefItem.preference;
+ holder.titleView.setText(item.highlightedTitle);
+ holder.switchWidget.setBackground(null); // Remove ripple/highlight.
+ // Sync switch state with preference without animation.
+ boolean currentState = switchPref.isChecked();
+ if (holder.switchWidget.isChecked() != currentState) {
+ holder.switchWidget.setChecked(currentState);
+ holder.switchWidget.jumpDrawablesToCurrentState();
+ }
+ prefItem.refreshHighlighting();
+ holder.summaryView.setText(prefItem.highlightedSummary);
+ holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
+ setupPreferenceView(view, holder.titleView, holder.summaryView, switchPref,
+ () -> {
+ boolean newState = !switchPref.isChecked();
+ switchPref.setChecked(newState);
+ holder.switchWidget.setChecked(newState);
+ prefItem.refreshHighlighting();
+ holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
+ holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
+ if (switchPref.getOnPreferenceChangeListener() != null) {
+ switchPref.getOnPreferenceChangeListener().onPreferenceChange(switchPref, newState);
+ }
+ notifyDataSetChanged();
+ },
+ () -> navigateAndScrollToPreference(item));
+ holder.switchWidget.setEnabled(switchPref.isEnabled());
+ }
+
+ protected void bindColorViewHolder(BaseSearchResultItem item, ColorViewHolder holder, View view) {
+ BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
+ holder.titleView.setText(item.highlightedTitle);
+ holder.summaryView.setText(item.highlightedSummary);
+ holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
+ ColorDot.applyColorDot(holder.colorDot, prefItem.getColor(), prefItem.preference.isEnabled());
+ setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
+ () -> handlePreferenceClick(prefItem.preference),
+ () -> navigateAndScrollToPreference(item));
+ }
+
+ protected void bindGroupHeaderViewHolder(BaseSearchResultItem item, GroupHeaderViewHolder holder, View view) {
+ holder.pathView.setText(item.highlightedTitle);
+ view.setOnClickListener(v -> navigateToTargetScreen(item));
+ }
+
+ protected void bindNoResultsViewHolder(BaseSearchResultItem item, NoResultsViewHolder holder) {
+ holder.titleView.setText(item.highlightedTitle);
+ holder.summaryView.setText(item.highlightedSummary);
+ holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
+ holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
+ }
+
+ /**
+ * Sets up a preference view with click listeners and proper enabled state handling.
+ */
+ protected void setupPreferenceView(View view, TextView titleView, TextView summaryView, Preference preference,
+ Runnable onClickAction, Runnable onLongClickAction) {
+ boolean enabled = preference.isEnabled();
+
+ // To enable long-click navigation for disabled settings, manually control the enabled state of the title
+ // and summary and disable the ripple effect instead of using 'view.setEnabled(enabled)'.
+
+ titleView.setEnabled(enabled);
+ summaryView.setEnabled(enabled);
+
+ if (!enabled) view.setBackground(null); // Disable ripple effect.
+
+ // In light mode, alpha 0.5 is applied to a disabled title automatically,
+ // but in dark mode it needs to be applied manually.
+ if (Utils.isDarkModeEnabled()) {
+ titleView.setAlpha(enabled ? 1.0f : ColorPickerPreference.DISABLED_ALPHA);
+ }
+ // Set up click and long-click listeners.
+ view.setOnClickListener(enabled ? v -> onClickAction.run() : null);
+ view.setOnLongClickListener(v -> {
+ onLongClickAction.run();
+ return true;
+ });
+ }
+
+ /**
+ * Navigates to the settings screen containing the given search result item and triggers scrolling.
+ */
+ protected void navigateAndScrollToPreference(BaseSearchResultItem item) {
+ // No navigation for URL_LINK items.
+ if (item.preferenceType == BaseSearchResultItem.ViewType.URL_LINK) return;
+
+ PreferenceScreen targetScreen = navigateToTargetScreen(item);
+ if (targetScreen == null) return;
+ if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) return;
+
+ Preference targetPreference = prefItem.preference;
+
+ fragment.getView().post(() -> {
+ ListView listView = targetScreen == getMainPreferenceScreen()
+ ? getPreferenceListView()
+ : targetScreen.getDialog().findViewById(android.R.id.list);
+
+ if (listView == null) return;
+
+ int targetPosition = findPreferencePosition(targetPreference, listView);
+ if (targetPosition == -1) return;
+
+ int firstVisible = listView.getFirstVisiblePosition();
+ int lastVisible = listView.getLastVisiblePosition();
+
+ if (targetPosition >= firstVisible && targetPosition <= lastVisible) {
+ // The preference is already visible, but still scroll it to the bottom of the list for consistency.
+ View child = listView.getChildAt(targetPosition - firstVisible);
+ if (child != null) {
+ // Calculate how much to scroll so the item is aligned at the bottom.
+ int scrollAmount = child.getBottom() - listView.getHeight();
+ if (scrollAmount > 0) {
+ // Perform smooth scroll animation for better user experience.
+ listView.smoothScrollBy(scrollAmount, 300);
+ }
+ }
+ // Highlight the preference once it is positioned.
+ highlightPreferenceAtPosition(listView, targetPosition);
+ } else {
+ // The preference is outside of the current visible range, scroll to it from the top.
+ listView.smoothScrollToPositionFromTop(targetPosition, 0);
+
+ Handler handler = new Handler(Looper.getMainLooper());
+ // Fallback runnable in case the OnScrollListener does not trigger.
+ Runnable fallback = () -> {
+ listView.setOnScrollListener(null);
+ highlightPreferenceAtPosition(listView, targetPosition);
+ };
+ // Post fallback with a small delay.
+ handler.postDelayed(fallback, 350);
+
+ listView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ private boolean isScrolling = false;
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL || scrollState == SCROLL_STATE_FLING) {
+ // Mark that scrolling has started.
+ isScrolling = true;
+ }
+ if (scrollState == SCROLL_STATE_IDLE && isScrolling) {
+ // Scrolling is finished, cleanup listener and cancel fallback.
+ isScrolling = false;
+ listView.setOnScrollListener(null);
+ handler.removeCallbacks(fallback);
+ // Highlight the target preference when scrolling is done.
+ highlightPreferenceAtPosition(listView, targetPosition);
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
+ });
+ }
+ });
+ }
+
+ /**
+ * Navigates to the final PreferenceScreen using preference keys or titles as fallback.
+ */
+ protected PreferenceScreen navigateToTargetScreen(BaseSearchResultItem item) {
+ PreferenceScreen currentScreen = getMainPreferenceScreen();
+ Preference targetPref = null;
+
+ // Try key-based navigation first.
+ if (item.navigationKeys != null && !item.navigationKeys.isEmpty()) {
+ String finalKey = item.navigationKeys.get(item.navigationKeys.size() - 1);
+ targetPref = findPreferenceByKey(currentScreen, finalKey);
+ }
+
+ // Fallback to title-based navigation.
+ if (targetPref == null && !TextUtils.isEmpty(item.navigationPath)) {
+ String[] pathSegments = item.navigationPath.split(" > ");
+ String finalSegment = pathSegments[pathSegments.length - 1].trim();
+ if (!TextUtils.isEmpty(finalSegment)) {
+ targetPref = findPreferenceByTitle(currentScreen, finalSegment);
+ }
+ }
+
+ if (targetPref instanceof PreferenceScreen targetScreen) {
+ handlePreferenceClick(targetScreen);
+ return targetScreen;
+ }
+
+ return currentScreen;
+ }
+
+ /**
+ * Recursively searches for a preference by title in a preference group.
+ */
+ protected Preference findPreferenceByTitle(PreferenceGroup group, String title) {
+ for (int i = 0; i < group.getPreferenceCount(); i++) {
+ Preference pref = group.getPreference(i);
+ CharSequence prefTitle = pref.getTitle();
+ if (prefTitle != null && (prefTitle.toString().trim().equalsIgnoreCase(title)
+ || normalizeString(prefTitle.toString()).equals(normalizeString(title)))) {
+ return pref;
+ }
+ if (pref instanceof PreferenceGroup) {
+ Preference found = findPreferenceByTitle((PreferenceGroup) pref, title);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Normalizes string for comparison (removes extra characters, spaces etc).
+ */
+ protected String normalizeString(String input) {
+ if (TextUtils.isEmpty(input)) return "";
+ return input.trim().toLowerCase().replaceAll("\\s+", " ").replaceAll("[^\\w\\s]", "");
+ }
+
+ /**
+ * Gets the ListView from the PreferenceFragment.
+ */
+ protected ListView getPreferenceListView() {
+ View fragmentView = fragment.getView();
+ if (fragmentView != null) {
+ ListView listView = findListViewInViewGroup(fragmentView);
+ if (listView != null) {
+ return listView;
+ }
+ }
+ return fragment.getActivity().findViewById(android.R.id.list);
+ }
+
+ /**
+ * Recursively searches for a ListView in a ViewGroup.
+ */
+ protected ListView findListViewInViewGroup(View view) {
+ if (view instanceof ListView) {
+ return (ListView) view;
+ }
+ if (view instanceof ViewGroup group) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ ListView result = findListViewInViewGroup(group.getChildAt(i));
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds the position of a preference in the ListView adapter.
+ */
+ protected int findPreferencePosition(Preference targetPreference, ListView listView) {
+ ListAdapter adapter = listView.getAdapter();
+ if (adapter == null) {
+ return -1;
+ }
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ Object item = adapter.getItem(i);
+ if (item == targetPreference) {
+ return i;
+ }
+ if (item instanceof Preference pref && targetPreference.getKey() != null) {
+ if (targetPreference.getKey().equals(pref.getKey())) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Highlights a preference at the specified position with a blink effect.
+ */
+ protected void highlightPreferenceAtPosition(ListView listView, int position) {
+ int firstVisible = listView.getFirstVisiblePosition();
+ if (position < firstVisible || position > listView.getLastVisiblePosition()) {
+ return;
+ }
+
+ View itemView = listView.getChildAt(position - firstVisible);
+ if (itemView != null) {
+ blinkView(itemView);
+ }
+ }
+
+ /**
+ * Creates a smooth double-blink effect on a view's background without affecting the text.
+ * @param view The View to apply the animation to.
+ */
+ protected void blinkView(View view) {
+ // If a previous animation is still running, cancel it to prevent conflicts.
+ if (currentAnimator != null && currentAnimator.isRunning()) {
+ currentAnimator.cancel();
+ }
+ int startColor = Utils.getAppBackgroundColor();
+ int highlightColor = Utils.adjustColorBrightness(
+ startColor,
+ Utils.isDarkModeEnabled() ? 1.25f : 0.8f
+ );
+ // Animator for transitioning from the start color to the highlight color.
+ ObjectAnimator fadeIn = ObjectAnimator.ofObject(
+ view,
+ "backgroundColor",
+ new ArgbEvaluator(),
+ startColor,
+ highlightColor
+ );
+ fadeIn.setDuration(BLINK_DURATION);
+ // Animator to return to the start color.
+ ObjectAnimator fadeOut = ObjectAnimator.ofObject(
+ view,
+ "backgroundColor",
+ new ArgbEvaluator(),
+ highlightColor,
+ startColor
+ );
+ fadeOut.setDuration(BLINK_DURATION);
+
+ currentAnimator = new AnimatorSet();
+ // Create the sequence: fadeIn -> fadeOut -> (pause) -> fadeIn -> fadeOut.
+ AnimatorSet firstBlink = new AnimatorSet();
+ firstBlink.playSequentially(fadeIn, fadeOut);
+ AnimatorSet secondBlink = new AnimatorSet();
+ secondBlink.playSequentially(fadeIn.clone(), fadeOut.clone()); // Use clones for the second blink.
+
+ currentAnimator.play(secondBlink).after(firstBlink).after(PAUSE_BETWEEN_BLINKS);
+ currentAnimator.start();
+ }
+
+ /**
+ * Recursively finds a preference by key in a preference group.
+ */
+ protected Preference findPreferenceByKey(PreferenceGroup group, String key) {
+ if (group == null || TextUtils.isEmpty(key)) {
+ return null;
+ }
+
+ // First search on current level.
+ for (int i = 0; i < group.getPreferenceCount(); i++) {
+ Preference pref = group.getPreference(i);
+ if (key.equals(pref.getKey())) {
+ return pref;
+ }
+ if (pref instanceof PreferenceGroup) {
+ Preference found = findPreferenceByKey((PreferenceGroup) pref, key);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Handles preference click actions by invoking the preference's performClick method via reflection.
+ */
+ @SuppressWarnings("all")
+ private void handlePreferenceClick(Preference preference) {
+ try {
+ if (preference instanceof CustomDialogListPreference listPref) {
+ BaseSearchResultItem.PreferenceSearchItem searchItem =
+ searchViewController.findSearchItemByPreference(preference);
+ if (searchItem != null && searchItem.isEntriesHighlightingApplied()) {
+ listPref.setHighlightedEntriesForDialog(searchItem.getHighlightedEntries());
+ }
+ }
+
+ Method m = Preference.class.getDeclaredMethod("performClick", PreferenceScreen.class);
+ m.setAccessible(true);
+ m.invoke(preference, fragment.getPreferenceScreenForSearch());
+ } catch (Exception e) {
+ Logger.printException(() -> "Failed to invoke performClick()", e);
+ }
+ }
+
+ /**
+ * Checks if a preference has navigation capability (can open a new screen).
+ */
+ boolean hasNavigationCapability(Preference preference) {
+ // PreferenceScreen always allows navigation.
+ if (preference instanceof PreferenceScreen) return true;
+ // UrlLinkPreference does not navigate to a new screen, it opens an external URL.
+ if (preference instanceof UrlLinkPreference) return false;
+ // Other group types that might have their own screens.
+ if (preference instanceof PreferenceGroup) {
+ // Check if it has its own fragment or intent.
+ return preference.getIntent() != null || preference.getFragment() != null;
+ }
+ return false;
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java
new file mode 100644
index 000000000..814ae29c3
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java
@@ -0,0 +1,653 @@
+package app.revanced.extension.shared.settings.search;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.GradientDrawable;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.SearchView;
+import android.widget.Toolbar;
+
+import androidx.annotation.ColorInt;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.AppLanguage;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
+import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
+import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
+
+/**
+ * Abstract controller for managing the overlay search view in ReVanced settings.
+ * Subclasses must implement app-specific preference handling.
+ */
+@SuppressWarnings("deprecation")
+public abstract class BaseSearchViewController {
+ protected SearchView searchView;
+ protected FrameLayout searchContainer;
+ protected FrameLayout overlayContainer;
+ protected final Toolbar toolbar;
+ protected final Activity activity;
+ protected final BasePreferenceFragment fragment;
+ protected final CharSequence originalTitle;
+ protected BaseSearchResultsAdapter searchResultsAdapter;
+ protected final List allSearchItems;
+ protected final List filteredSearchItems;
+ protected final Map keyToSearchItem;
+ protected final InputMethodManager inputMethodManager;
+ protected SearchHistoryManager searchHistoryManager;
+ protected boolean isSearchActive;
+ protected boolean isShowingSearchHistory;
+
+ protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
+
+ protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow("revanced_search_view", "id");
+ protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow("revanced_search_view_container", "id");
+ protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow("action_search", "id");
+ protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
+ public static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON =
+ getResourceIdentifierOrThrow("revanced_settings_search_icon", "drawable");
+ protected static final int MENU_REVANCED_SEARCH_MENU =
+ getResourceIdentifierOrThrow("revanced_search_menu", "menu");
+
+ /**
+ * Constructs a new BaseSearchViewController instance.
+ *
+ * @param activity The activity hosting the search view.
+ * @param toolbar The toolbar containing the search action.
+ * @param fragment The preference fragment to manage search preferences.
+ */
+ protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePreferenceFragment fragment) {
+ this.activity = activity;
+ this.toolbar = toolbar;
+ this.fragment = fragment;
+ this.originalTitle = toolbar.getTitle();
+ this.allSearchItems = new ArrayList<>();
+ this.filteredSearchItems = new ArrayList<>();
+ this.keyToSearchItem = new HashMap<>();
+ this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ this.isShowingSearchHistory = false;
+
+ // Initialize components
+ initializeSearchView();
+ initializeOverlayContainer();
+ initializeSearchHistoryManager();
+ setupToolbarMenu();
+ setupListeners();
+ }
+
+ /**
+ * Initializes the search view with proper configurations, such as background, query hint, and RTL support.
+ */
+ private void initializeSearchView() {
+ // Retrieve SearchView and container from XML.
+ searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
+ EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
+ "android:id/search_src_text", null));
+ // Disable fullscreen keyboard mode.
+ searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+
+ searchContainer = activity.findViewById(ID_REVANCED_SEARCH_VIEW_CONTAINER);
+
+ // Set background and query hint.
+ searchView.setBackground(createBackgroundDrawable());
+ searchView.setQueryHint(str("revanced_settings_search_hint"));
+
+ // Configure RTL support based on app language.
+ AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
+ if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
+ searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
+ searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
+ }
+ }
+
+ /**
+ * Initializes the overlay container for displaying search results and history.
+ */
+ private void initializeOverlayContainer() {
+ // Create overlay container for search results and history.
+ overlayContainer = new FrameLayout(activity);
+ overlayContainer.setVisibility(View.GONE);
+ overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
+ overlayContainer.setElevation(Utils.dipToPixels(8));
+
+ // Container for search results.
+ FrameLayout searchResultsContainer = new FrameLayout(activity);
+ searchResultsContainer.setVisibility(View.VISIBLE);
+
+ // Create a ListView for the results.
+ ListView searchResultsListView = new ListView(activity);
+ searchResultsListView.setDivider(null);
+ searchResultsListView.setDividerHeight(0);
+ searchResultsAdapter = createSearchResultsAdapter();
+ searchResultsListView.setAdapter(searchResultsAdapter);
+
+ // Add results list into container.
+ searchResultsContainer.addView(searchResultsListView, new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT));
+
+ // Add results container into overlay.
+ overlayContainer.addView(searchResultsContainer, new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT));
+
+ // Add overlay to the main content container.
+ FrameLayout mainContainer = activity.findViewById(ID_REVANCED_SETTINGS_FRAGMENTS);
+ if (mainContainer != null) {
+ FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ overlayParams.gravity = Gravity.TOP;
+ mainContainer.addView(overlayContainer, overlayParams);
+ }
+ }
+
+ /**
+ * Initializes the search history manager with the specified overlay container and listener.
+ */
+ private void initializeSearchHistoryManager() {
+ searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> {
+ searchView.setQuery(query, true);
+ hideSearchHistory();
+ });
+ }
+
+ // Abstract methods that subclasses must implement.
+ protected abstract BaseSearchResultsAdapter createSearchResultsAdapter();
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ protected abstract boolean isSpecialPreferenceGroup(Preference preference);
+ protected abstract void setupSpecialPreferenceListeners(BaseSearchResultItem item);
+
+ // Abstract interface for preference fragments.
+ public interface BasePreferenceFragment {
+ PreferenceScreen getPreferenceScreenForSearch();
+ android.view.View getView();
+ Activity getActivity();
+ }
+
+ /**
+ * Determines whether a preference should be included in the search index.
+ *
+ * @param preference The preference to evaluate.
+ * @param currentDepth The current depth in the preference hierarchy.
+ * @param includeDepth The maximum depth to include in the search index.
+ * @return True if the preference should be included, false otherwise.
+ */
+ protected boolean shouldIncludePreference(Preference preference, int currentDepth, int includeDepth) {
+ return includeDepth <= currentDepth
+ && !(preference instanceof PreferenceCategory)
+ && !isSpecialPreferenceGroup(preference)
+ && !(preference instanceof PreferenceScreen);
+ }
+
+ /**
+ * Sets up the toolbar menu for the search action.
+ */
+ protected void setupToolbarMenu() {
+ toolbar.inflateMenu(MENU_REVANCED_SEARCH_MENU);
+ toolbar.setOnMenuItemClickListener(item -> {
+ if (item.getItemId() == ID_ACTION_SEARCH && !isSearchActive) {
+ openSearch();
+ return true;
+ }
+ return false;
+ });
+ }
+
+ /**
+ * Configures listeners for the search view and toolbar navigation.
+ */
+ protected void setupListeners() {
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ try {
+ String queryTrimmed = query.trim();
+ if (!queryTrimmed.isEmpty()) {
+ searchHistoryManager.saveSearchQuery(queryTrimmed);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onQueryTextSubmit failure", ex);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ try {
+ Logger.printDebug(() -> "Search query: " + newText);
+
+ String trimmedText = newText.trim();
+ if (!isSearchActive) {
+ Logger.printDebug(() -> "Search is not active, skipping query processing");
+ return true;
+ }
+
+ if (trimmedText.isEmpty()) {
+ // If empty query: show history.
+ hideSearchResults();
+ showSearchHistory();
+ } else {
+ // If has search text: hide history and show search results.
+ hideSearchHistory();
+ filterAndShowResults(newText);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onQueryTextChange failure", ex);
+ }
+ return true;
+ }
+ });
+ // Set navigation click listener.
+ toolbar.setNavigationOnClickListener(view -> {
+ if (isSearchActive) {
+ closeSearch();
+ } else {
+ activity.finish();
+ }
+ });
+ }
+
+ /**
+ * Initializes search data by collecting all searchable preferences from the fragment.
+ * This method should be called after the preference fragment is fully loaded.
+ * Runs on the UI thread to ensure proper access to preference components.
+ */
+ public void initializeSearchData() {
+ allSearchItems.clear();
+ keyToSearchItem.clear();
+ // Wait until fragment is properly initialized.
+ activity.runOnUiThread(() -> {
+ try {
+ PreferenceScreen screen = fragment.getPreferenceScreenForSearch();
+ if (screen != null) {
+ collectSearchablePreferences(screen);
+ for (BaseSearchResultItem item : allSearchItems) {
+ if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
+ String key = prefItem.preference.getKey();
+ if (key != null) {
+ keyToSearchItem.put(key, item);
+ }
+ }
+ }
+ setupPreferenceListeners();
+ Logger.printDebug(() -> "Collected " + allSearchItems.size() + " searchable preferences");
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "Failed to initialize search data", ex);
+ }
+ });
+ }
+
+ /**
+ * Sets up listeners for preferences to keep search results in sync when preference values change.
+ */
+ protected void setupPreferenceListeners() {
+ for (BaseSearchResultItem item : allSearchItems) {
+ // Skip non-preference items.
+ if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) continue;
+ Preference pref = prefItem.preference;
+
+ if (pref instanceof ColorPickerPreference colorPref) {
+ colorPref.setOnColorChangeListener((prefKey, newColor) -> {
+ BaseSearchResultItem.PreferenceSearchItem searchItem =
+ (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(prefKey);
+ if (searchItem != null) {
+ searchItem.setColor(newColor);
+ refreshSearchResults();
+ }
+ });
+ } else if (pref instanceof CustomDialogListPreference listPref) {
+ listPref.setOnPreferenceChangeListener((preference, newValue) -> {
+ BaseSearchResultItem.PreferenceSearchItem searchItem =
+ (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(preference.getKey());
+ if (searchItem == null) return true;
+
+ int index = listPref.findIndexOfValue(newValue.toString());
+ if (index >= 0) {
+ // Check if a static summary is set.
+ boolean isStaticSummary = listPref.getStaticSummary() != null;
+ if (!isStaticSummary) {
+ // Only update summary if it is not static.
+ CharSequence newSummary = listPref.getEntries()[index];
+ listPref.setSummary(newSummary);
+ }
+ }
+
+ listPref.clearHighlightedEntriesForDialog();
+ searchItem.refreshHighlighting();
+ refreshSearchResults();
+ return true;
+ });
+ }
+
+ // Let subclasses handle special preferences.
+ setupSpecialPreferenceListeners(item);
+ }
+ }
+
+ /**
+ * Collects searchable preferences from a preference group.
+ */
+ protected void collectSearchablePreferences(PreferenceGroup group) {
+ collectSearchablePreferencesWithKeys(group, "", new ArrayList<>(), 1, 0);
+ }
+
+ /**
+ * Collects searchable preferences with their navigation paths and keys.
+ *
+ * @param group The preference group to collect from.
+ * @param parentPath The navigation path of the parent group.
+ * @param parentKeys The keys of parent preferences.
+ * @param includeDepth The maximum depth to include in the search index.
+ * @param currentDepth The current depth in the preference hierarchy.
+ */
+ protected void collectSearchablePreferencesWithKeys(PreferenceGroup group, String parentPath,
+ List parentKeys, int includeDepth, int currentDepth) {
+ if (group == null) return;
+
+ for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
+ Preference preference = group.getPreference(i);
+
+ // Add to search results only if it is not a category, special group, or PreferenceScreen.
+ if (shouldIncludePreference(preference, currentDepth, includeDepth)) {
+ allSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(
+ preference, parentPath, parentKeys));
+ }
+
+ // If the preference is a group, recurse into it.
+ if (preference instanceof PreferenceGroup subGroup) {
+ String newPath = parentPath;
+ List newKeys = new ArrayList<>(parentKeys);
+
+ // Append the group title to the path and save key for navigation.
+ if (!isSpecialPreferenceGroup(preference)
+ && !(preference instanceof NoTitlePreferenceCategory)) {
+ CharSequence title = preference.getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ newPath = TextUtils.isEmpty(parentPath)
+ ? title.toString()
+ : parentPath + " > " + title;
+ }
+
+ // Add key for navigation if this is a PreferenceScreen or group with navigation capability.
+ String key = preference.getKey();
+ if (!TextUtils.isEmpty(key) && (preference instanceof PreferenceScreen
+ || searchResultsAdapter.hasNavigationCapability(preference))) {
+ newKeys.add(key);
+ }
+ }
+
+ collectSearchablePreferencesWithKeys(subGroup, newPath, newKeys, includeDepth, currentDepth + 1);
+ }
+ }
+ }
+
+ /**
+ * Filters all search items based on the provided query and displays results in the overlay.
+ * Applies highlighting to matching text and shows a "no results" message if nothing matches.
+ */
+ protected void filterAndShowResults(String query) {
+ hideSearchHistory();
+ // Keep track of the previously displayed items to clear their highlights.
+ List previouslyDisplayedItems = new ArrayList<>(filteredSearchItems);
+
+ filteredSearchItems.clear();
+
+ String queryLower = Utils.removePunctuationToLowercase(query);
+ Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
+
+ // Clear highlighting only for items that were previously visible.
+ // This avoids iterating through all items on every keystroke during filtering.
+ for (BaseSearchResultItem item : previouslyDisplayedItems) {
+ item.clearHighlighting();
+ }
+
+ // Collect matched items first.
+ List matched = new ArrayList<>();
+ int matchCount = 0;
+ for (BaseSearchResultItem item : allSearchItems) {
+ if (matchCount >= MAX_SEARCH_RESULTS) break; // Stop after collecting max results.
+ if (item.matchesQuery(queryLower)) {
+ item.applyHighlighting(queryPattern);
+ matched.add(item);
+ matchCount++;
+ }
+ }
+
+ // Build filteredSearchItems, inserting parent enablers for disabled dependents.
+ Set addedParentKeys = new HashSet<>(2 * matched.size());
+ for (BaseSearchResultItem item : matched) {
+ if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
+ String key = prefItem.preference.getKey();
+ Setting> setting = (key != null) ? Setting.getSettingFromPath(key) : null;
+ if (setting != null && !setting.isAvailable()) {
+ List> parentSettings = setting.getParentSettings();
+ for (Setting> parentSetting : parentSettings) {
+ BaseSearchResultItem parentItem = keyToSearchItem.get(parentSetting.key);
+ if (parentItem != null && !addedParentKeys.contains(parentSetting.key)) {
+ if (!parentItem.matchesQuery(queryLower)) {
+ // Apply highlighting to parent items even if they don't match the query.
+ // This ensures they get their current effective summary calculated.
+ parentItem.applyHighlighting(queryPattern);
+ filteredSearchItems.add(parentItem);
+ }
+ addedParentKeys.add(parentSetting.key);
+ }
+ }
+ }
+ filteredSearchItems.add(item);
+ if (key != null) {
+ addedParentKeys.add(key);
+ }
+ }
+ }
+
+ if (!filteredSearchItems.isEmpty()) {
+ //noinspection ComparatorCombinators
+ Collections.sort(filteredSearchItems, (o1, o2) ->
+ o1.navigationPath.compareTo(o2.navigationPath)
+ );
+ List displayItems = new ArrayList<>();
+ String currentPath = null;
+ for (BaseSearchResultItem item : filteredSearchItems) {
+ if (!item.navigationPath.equals(currentPath)) {
+ BaseSearchResultItem header = new BaseSearchResultItem.GroupHeaderItem(item.navigationPath, item.navigationKeys);
+ displayItems.add(header);
+ currentPath = item.navigationPath;
+ }
+ displayItems.add(item);
+ }
+ filteredSearchItems.clear();
+ filteredSearchItems.addAll(displayItems);
+ }
+ // Show "No results found" if search results are empty.
+ if (filteredSearchItems.isEmpty()) {
+ Preference noResultsPreference = new Preference(activity);
+ noResultsPreference.setKey("no_results_placeholder");
+ noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
+ noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
+ noResultsPreference.setSelectable(false);
+ noResultsPreference.setIcon(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
+ filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
+ }
+
+ searchResultsAdapter.notifyDataSetChanged();
+ overlayContainer.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Opens the search interface by showing the search view and hiding the menu item.
+ * Configures the UI for search mode, shows the keyboard, and displays search suggestions.
+ */
+ protected void openSearch() {
+ isSearchActive = true;
+ toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(false);
+ toolbar.setTitle("");
+ searchContainer.setVisibility(View.VISIBLE);
+ searchView.requestFocus();
+ // Configure soft input mode to adjust layout and show keyboard.
+ activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+ | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ inputMethodManager.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
+ // Always show search history when opening search.
+ showSearchHistory();
+ }
+
+ /**
+ * Closes the search interface and restores the normal UI state.
+ * Hides the overlay, clears search results, dismisses the keyboard, and removes highlighting.
+ */
+ public void closeSearch() {
+ isSearchActive = false;
+ isShowingSearchHistory = false;
+
+ searchHistoryManager.hideSearchHistoryContainer();
+ overlayContainer.setVisibility(View.GONE);
+
+ filteredSearchItems.clear();
+
+ searchContainer.setVisibility(View.GONE);
+ toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(true);
+ toolbar.setTitle(originalTitle);
+ searchView.setQuery("", false);
+ // Hide keyboard and reset soft input mode.
+ inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
+ activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
+ // Clear highlighting for all search items.
+ for (BaseSearchResultItem item : allSearchItems) {
+ item.clearHighlighting();
+ }
+
+ searchResultsAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Shows the search history if enabled.
+ */
+ protected void showSearchHistory() {
+ if (searchHistoryManager.isSearchHistoryEnabled()) {
+ overlayContainer.setVisibility(View.VISIBLE);
+ searchHistoryManager.showSearchHistory();
+ isShowingSearchHistory = true;
+ } else {
+ hideAllOverlays();
+ }
+ }
+
+ /**
+ * Hides the search history container.
+ */
+ protected void hideSearchHistory() {
+ searchHistoryManager.hideSearchHistoryContainer();
+ isShowingSearchHistory = false;
+ }
+
+ /**
+ * Hides all overlay containers, including search results and history.
+ */
+ protected void hideAllOverlays() {
+ hideSearchHistory();
+ hideSearchResults();
+ }
+
+ /**
+ * Hides the search results overlay and clears the filtered results.
+ */
+ protected void hideSearchResults() {
+ overlayContainer.setVisibility(View.GONE);
+ filteredSearchItems.clear();
+ searchResultsAdapter.notifyDataSetChanged();
+ for (BaseSearchResultItem item : allSearchItems) {
+ item.clearHighlighting();
+ }
+ }
+
+ /**
+ * Refreshes the search results display if the search is active and history is not shown.
+ */
+ protected void refreshSearchResults() {
+ if (isSearchActive && !isShowingSearchHistory) {
+ searchResultsAdapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Finds a search item corresponding to the given preference.
+ *
+ * @param preference The preference to find a search item for.
+ * @return The corresponding PreferenceSearchItem, or null if not found.
+ */
+ public BaseSearchResultItem.PreferenceSearchItem findSearchItemByPreference(Preference preference) {
+ // First, search in filtered results.
+ for (BaseSearchResultItem item : filteredSearchItems) {
+ if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
+ if (prefItem.preference == preference) {
+ return prefItem;
+ }
+ }
+ }
+ // If not found, search in all items.
+ for (BaseSearchResultItem item : allSearchItems) {
+ if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
+ if (prefItem.preference == preference) {
+ return prefItem;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the background color for search view components based on current theme.
+ */
+ @ColorInt
+ public static int getSearchViewBackground() {
+ return Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), Utils.isDarkModeEnabled() ? 1.11f : 0.95f);
+ }
+
+ /**
+ * Creates a rounded background drawable for the main search view.
+ */
+ protected static GradientDrawable createBackgroundDrawable() {
+ GradientDrawable background = new GradientDrawable();
+ background.setShape(GradientDrawable.RECTANGLE);
+ background.setCornerRadius(Utils.dipToPixels(28));
+ background.setColor(getSearchViewBackground());
+ return background;
+ }
+
+ /**
+ * Return if a search is currently active.
+ */
+ public boolean isSearchActive() {
+ return isSearchActive;
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java
new file mode 100644
index 000000000..41fb20d97
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java
@@ -0,0 +1,377 @@
+package app.revanced.extension.shared.settings.search;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_ENTRIES;
+import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_HISTORY;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.LinkedList;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.settings.preference.BulletPointPreference;
+import app.revanced.extension.shared.ui.CustomDialog;
+
+/**
+ * Manager for search history functionality.
+ */
+public class SearchHistoryManager {
+ /**
+ * Interface for handling history item selection.
+ */
+ private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
+
+ private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
+ "clear_history_button", "id");
+ private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
+ "history_text", "id");
+ private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
+ "delete_icon", "id");
+ private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
+ "empty_history_title", "id");
+ private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
+ "empty_history_summary", "id");
+ private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
+ "search_history_header", "id");
+ private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
+ "revanced_settings_search_tips_summary", "id");
+ private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
+ "revanced_preference_search_history_screen", "layout");
+ private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
+ "revanced_preference_search_history_item", "layout");
+ private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
+ "search_history_list", "id");
+
+ private final Deque searchHistory;
+ private final Activity activity;
+ private final SearchHistoryAdapter searchHistoryAdapter;
+ private final boolean showSettingsSearchHistory;
+ private final FrameLayout searchHistoryContainer;
+
+ public interface OnSelectHistoryItemListener {
+ void onSelectHistoryItem(String query);
+ }
+
+ /**
+ * Constructor for SearchHistoryManager.
+ *
+ * @param activity The parent activity.
+ * @param overlayContainer The overlay container to hold the search history container.
+ * @param onSelectHistoryItemAction Callback for when a history item is selected.
+ */
+ SearchHistoryManager(Activity activity, FrameLayout overlayContainer,
+ OnSelectHistoryItemListener onSelectHistoryItemAction) {
+ this.activity = activity;
+ this.showSettingsSearchHistory = SETTINGS_SEARCH_HISTORY.get();
+ this.searchHistory = new LinkedList<>();
+
+ // Initialize search history from settings.
+ if (showSettingsSearchHistory) {
+ String entries = SETTINGS_SEARCH_ENTRIES.get();
+ if (!entries.isBlank()) {
+ searchHistory.addAll(Arrays.asList(entries.split("\n")));
+ }
+ } else {
+ // Clear old saved history if the feature is disabled.
+ SETTINGS_SEARCH_ENTRIES.resetToDefault();
+ }
+
+ // Create search history container.
+ this.searchHistoryContainer = new FrameLayout(activity);
+ searchHistoryContainer.setVisibility(View.GONE);
+
+ // Inflate search history layout.
+ LayoutInflater inflater = LayoutInflater.from(activity);
+ View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN, searchHistoryContainer, false);
+ searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT));
+
+ // Add history container to overlay.
+ FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT);
+ overlayParams.gravity = Gravity.TOP;
+ overlayContainer.addView(searchHistoryContainer, overlayParams);
+
+ // Find the LinearLayout for the history list within the container.
+ LinearLayout searchHistoryListView = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
+ if (searchHistoryListView == null) {
+ throw new IllegalStateException("Search history list view not found in container");
+ }
+
+ // Set up history adapter. Use a copy of the search history.
+ this.searchHistoryAdapter = new SearchHistoryAdapter(activity, searchHistoryListView,
+ new ArrayList<>(searchHistory), onSelectHistoryItemAction);
+
+ // Set up clear history button.
+ TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
+ clearHistoryButton.setOnClickListener(v -> createAndShowDialog(
+ str("revanced_settings_search_clear_history"),
+ str("revanced_settings_search_clear_history_message"),
+ this::clearAllSearchHistory
+ ));
+
+ // Set up search tips summary.
+ CharSequence text = BulletPointPreference.formatIntoBulletPoints(
+ str("revanced_settings_search_tips_summary"));
+ TextView tipsSummary = historyView.findViewById(ID_SEARCH_TIPS_SUMMARY);
+ tipsSummary.setText(text);
+ }
+
+ /**
+ * Shows search history screen - either with history items or empty history message.
+ */
+ public void showSearchHistory() {
+ if (!showSettingsSearchHistory) {
+ return;
+ }
+
+ // Find all view elements.
+ TextView emptyHistoryTitle = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_TITLE);
+ TextView emptyHistorySummary = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_SUMMARY);
+ TextView historyHeader = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_HEADER);
+ LinearLayout historyList = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
+ TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
+
+ if (searchHistory.isEmpty()) {
+ // Show empty history state.
+ showEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
+ hideHistoryViews(historyHeader, historyList, clearHistoryButton);
+ } else {
+ // Show history list state.
+ hideEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
+ showHistoryViews(historyHeader, historyList, clearHistoryButton);
+
+ // Update adapter with current history.
+ searchHistoryAdapter.clear();
+ searchHistoryAdapter.addAll(searchHistory);
+ searchHistoryAdapter.notifyDataSetChanged();
+ }
+
+ // Show the search history container.
+ showSearchHistoryContainer();
+ }
+
+ /**
+ * Saves a search query to the history, maintaining the size limit.
+ */
+ public void saveSearchQuery(String query) {
+ if (!showSettingsSearchHistory) return;
+
+ searchHistory.remove(query); // Remove if already exists to update position.
+ searchHistory.addFirst(query); // Add to the most recent.
+
+ // Remove extra old entries.
+ while (searchHistory.size() > MAX_HISTORY_SIZE) {
+ String last = searchHistory.removeLast();
+ Logger.printDebug(() -> "Removing search history query: " + last);
+ }
+
+ saveSearchHistory();
+ }
+
+ /**
+ * Saves the search history to shared preferences.
+ */
+ protected void saveSearchHistory() {
+ Logger.printDebug(() -> "Saving search history: " + searchHistory);
+ SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory));
+ }
+
+ /**
+ * Removes a search query from the history.
+ */
+ public void removeSearchQuery(String query) {
+ searchHistory.remove(query);
+ saveSearchHistory();
+ }
+
+ /**
+ * Clears all search history.
+ */
+ public void clearAllSearchHistory() {
+ searchHistory.clear();
+ saveSearchHistory();
+ searchHistoryAdapter.clear();
+ searchHistoryAdapter.notifyDataSetChanged();
+ showSearchHistory();
+ }
+
+ /**
+ * Checks if search history feature is enabled.
+ */
+ public boolean isSearchHistoryEnabled() {
+ return showSettingsSearchHistory;
+ }
+
+ /**
+ * Shows the search history container and overlay.
+ */
+ public void showSearchHistoryContainer() {
+ searchHistoryContainer.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Hides the search history container.
+ */
+ public void hideSearchHistoryContainer() {
+ searchHistoryContainer.setVisibility(View.GONE);
+ }
+
+ /**
+ * Helper method to show empty history views.
+ */
+ protected void showEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
+ emptyTitle.setVisibility(View.VISIBLE);
+ emptyTitle.setText(str("revanced_settings_search_empty_history_title"));
+ emptySummary.setVisibility(View.VISIBLE);
+ emptySummary.setText(str("revanced_settings_search_empty_history_summary"));
+ }
+
+ /**
+ * Helper method to hide empty history views.
+ */
+ protected void hideEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
+ emptyTitle.setVisibility(View.GONE);
+ emptySummary.setVisibility(View.GONE);
+ }
+
+ /**
+ * Helper method to show history list views.
+ */
+ protected void showHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
+ header.setVisibility(View.VISIBLE);
+ list.setVisibility(View.VISIBLE);
+ clearButton.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Helper method to hide history list views.
+ */
+ protected void hideHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
+ header.setVisibility(View.GONE);
+ list.setVisibility(View.GONE);
+ clearButton.setVisibility(View.GONE);
+ }
+
+ /**
+ * Creates and shows a dialog with the specified title, message, and confirmation action.
+ *
+ * @param title The title of the dialog.
+ * @param message The message to display in the dialog.
+ * @param confirmAction The action to perform when the dialog is confirmed.
+ */
+ protected void createAndShowDialog(String title, String message, Runnable confirmAction) {
+ Pair dialogPair = CustomDialog.create(
+ activity,
+ title,
+ message,
+ null,
+ null,
+ confirmAction,
+ () -> {},
+ null,
+ null,
+ false
+ );
+
+ Dialog dialog = dialogPair.first;
+ dialog.setCancelable(true);
+ dialog.show();
+ }
+
+
+ /**
+ * Custom adapter for search history items.
+ */
+ protected class SearchHistoryAdapter {
+ protected final Collection history;
+ protected final LayoutInflater inflater;
+ protected final LinearLayout container;
+ protected final OnSelectHistoryItemListener onSelectHistoryItemListener;
+
+ public SearchHistoryAdapter(Context context, LinearLayout container, Collection history,
+ OnSelectHistoryItemListener listener) {
+ this.history = history;
+ this.inflater = LayoutInflater.from(context);
+ this.container = container;
+ this.onSelectHistoryItemListener = listener;
+ }
+
+ /**
+ * Updates the container with current history items.
+ */
+ public void notifyDataSetChanged() {
+ container.removeAllViews();
+ for (String query : history) {
+ View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM, container, false);
+
+ TextView historyText = view.findViewById(ID_HISTORY_TEXT);
+ ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
+
+ historyText.setText(query);
+
+ // Set click listener for main item (select query).
+ view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(query));
+
+ // Set click listener for delete icon.
+ deleteIcon.setOnClickListener(v -> createAndShowDialog(
+ query,
+ str("revanced_settings_search_remove_message"),
+ () -> {
+ removeSearchQuery(query);
+ remove(query);
+ notifyDataSetChanged();
+ }
+ ));
+
+ container.addView(view);
+ }
+ }
+
+ /**
+ * Clears all views from the container and history list.
+ */
+ public void clear() {
+ history.clear();
+ container.removeAllViews();
+ }
+
+ /**
+ * Adds all provided history items to the container.
+ */
+ public void addAll(Collection items) {
+ history.addAll(items);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Removes a query from the history and updates the container.
+ */
+ public void remove(String query) {
+ history.remove(query);
+ if (history.isEmpty()) {
+ // If history is now empty, show the empty history state.
+ showSearchHistory();
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java
new file mode 100644
index 000000000..f0eeef408
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java
@@ -0,0 +1,61 @@
+package app.revanced.extension.shared.ui;
+
+import static app.revanced.extension.shared.Utils.adjustColorBrightness;
+import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.shared.Utils.getAppBackgroundColor;
+import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
+import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA;
+
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+
+public class ColorDot {
+ private static final int STROKE_WIDTH = dipToPixels(1.5f); // Stroke width in dp.
+
+ /**
+ * Creates a circular drawable with a main fill and a stroke.
+ * Stroke adapts to dark/light theme and transparency, applied only when color is transparent or matches app background.
+ */
+ public static GradientDrawable createColorDotDrawable(@ColorInt int color) {
+ final boolean isDarkTheme = isDarkModeEnabled();
+ final boolean isTransparent = Color.alpha(color) == 0;
+ final int opaqueColor = color | 0xFF000000;
+ final int appBackground = getAppBackgroundColor();
+ final int strokeColor;
+ final int strokeWidth;
+
+ // Determine stroke color.
+ if (isTransparent || (opaqueColor == appBackground)) {
+ final int baseColor = isTransparent ? appBackground : opaqueColor;
+ strokeColor = adjustColorBrightness(baseColor, isDarkTheme ? 1.2f : 0.8f);
+ strokeWidth = STROKE_WIDTH;
+ } else {
+ strokeColor = 0;
+ strokeWidth = 0;
+ }
+
+ // Create circular drawable with conditional stroke.
+ GradientDrawable circle = new GradientDrawable();
+ circle.setShape(GradientDrawable.OVAL);
+ circle.setColor(color);
+ circle.setStroke(strokeWidth, strokeColor);
+
+ return circle;
+ }
+
+ /**
+ * Applies the color dot drawable to the target view.
+ */
+ public static void applyColorDot(View targetView, @ColorInt int color, boolean enabled) {
+ if (targetView == null) return;
+ targetView.setBackground(createColorDotDrawable(color));
+ targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
+ if (!isDarkModeEnabled()) {
+ targetView.setClipToOutline(true);
+ targetView.setElevation(dipToPixels(2));
+ }
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java
new file mode 100644
index 000000000..43a698ed1
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java
@@ -0,0 +1,472 @@
+package app.revanced.extension.shared.ui;
+
+import static app.revanced.extension.shared.Utils.dipToPixels;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A utility class for creating a customizable dialog with a title, message or EditText, and up to three buttons (OK, Cancel, Neutral).
+ * The dialog supports themed colors, rounded corners, and dynamic button layout based on screen width. It is dismissible by default.
+ */
+public class CustomDialog {
+ private final Context context;
+ private final Dialog dialog;
+ private final LinearLayout mainLayout;
+ private final int dip4, dip8, dip16, dip24, dip36;
+
+ /**
+ * Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
+ * The dialog's appearance adapts to the app's dark mode setting, with rounded corners and customizable button actions.
+ * Buttons adjust dynamically to their text content and are arranged in a single row if they fit within 80% of the
+ * screen width, with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
+ * If buttons do not fit, each is placed on a separate row, all aligned to the right.
+ *
+ * @param context Context used to create the dialog.
+ * @param title Title text of the dialog.
+ * @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
+ * @param editText EditText to include in the dialog, or null if no EditText is needed.
+ * @param okButtonText OK button text, or null to use the default "OK" string.
+ * @param onOkClick Action to perform when the OK button is clicked.
+ * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
+ * @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
+ * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
+ * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
+ * @return The Dialog and its main LinearLayout container.
+ */
+ public static Pair create(Context context, String title, CharSequence message,
+ @Nullable EditText editText, String okButtonText,
+ Runnable onOkClick, Runnable onCancelClick,
+ @Nullable String neutralButtonText,
+ @Nullable Runnable onNeutralClick,
+ boolean dismissDialogOnNeutralClick) {
+ Logger.printDebug(() -> "Creating custom dialog with title: " + title);
+ CustomDialog customDialog = new CustomDialog(context, title, message, editText,
+ okButtonText, onOkClick, onCancelClick,
+ neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
+ return new Pair<>(customDialog.dialog, customDialog.mainLayout);
+ }
+
+ /**
+ * Initializes a custom dialog with the specified parameters.
+ *
+ * @param context Context used to create the dialog.
+ * @param title Title text of the dialog.
+ * @param message Message text of the dialog, or null if replaced by EditText.
+ * @param editText EditText to include in the dialog, or null if no EditText is needed.
+ * @param okButtonText OK button text, or null to use the default "OK" string.
+ * @param onOkClick Action to perform when the OK button is clicked.
+ * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
+ * @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
+ * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
+ * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
+ */
+ private CustomDialog(Context context, String title, CharSequence message, @Nullable EditText editText,
+ String okButtonText, Runnable onOkClick, Runnable onCancelClick,
+ @Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
+ boolean dismissDialogOnNeutralClick) {
+ this.context = context;
+ this.dialog = new Dialog(context);
+ this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
+
+ // Preset size constants.
+ dip4 = dipToPixels(4);
+ dip8 = dipToPixels(8);
+ dip16 = dipToPixels(16);
+ dip24 = dipToPixels(24);
+ dip36 = dipToPixels(36);
+
+ // Create main layout.
+ mainLayout = createMainLayout();
+ addTitle(title);
+ addContent(message, editText);
+ addButtons(okButtonText, onOkClick, onCancelClick, neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
+
+ // Set dialog content and window attributes.
+ dialog.setContentView(mainLayout);
+ Window window = dialog.getWindow();
+ if (window != null) {
+ Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
+ }
+ }
+
+ /**
+ * Creates the main layout for the dialog with vertical orientation and rounded corners.
+ *
+ * @return The configured LinearLayout for the dialog.
+ */
+ private LinearLayout createMainLayout() {
+ LinearLayout layout = new LinearLayout(context);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(dip24, dip16, dip24, dip24);
+
+ // Set rounded rectangle background.
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(28), null, null));
+ // Dialog background.
+ background.getPaint().setColor(Utils.getDialogBackgroundColor());
+ layout.setBackground(background);
+
+ return layout;
+ }
+
+ /**
+ * Adds a title to the dialog if provided.
+ *
+ * @param title The title text to display.
+ */
+ private void addTitle(String title) {
+ if (TextUtils.isEmpty(title)) return;
+
+ TextView titleView = new TextView(context);
+ titleView.setText(title);
+ titleView.setTypeface(Typeface.DEFAULT_BOLD);
+ titleView.setTextSize(18);
+ titleView.setTextColor(Utils.getAppForegroundColor());
+ titleView.setGravity(Gravity.CENTER);
+
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ params.setMargins(0, 0, 0, dip16);
+ titleView.setLayoutParams(params);
+
+ mainLayout.addView(titleView);
+ }
+
+ /**
+ * Adds a message or EditText to the dialog within a ScrollView.
+ *
+ * @param message The message text to display (supports Spanned for HTML), or null if replaced by EditText.
+ * @param editText The EditText to include, or null if no EditText is needed.
+ */
+ private void addContent(CharSequence message, @Nullable EditText editText) {
+ // Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
+ if (message == null && editText == null) return;
+
+ ScrollView scrollView = new ScrollView(context);
+ // Disable the vertical scrollbar.
+ scrollView.setVerticalScrollBarEnabled(false);
+ scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+
+ LinearLayout contentContainer = new LinearLayout(context);
+ contentContainer.setOrientation(LinearLayout.VERTICAL);
+ scrollView.addView(contentContainer);
+
+ // EditText (if provided).
+ if (editText != null) {
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(10), null, null));
+ background.getPaint().setColor(Utils.getEditTextBackground());
+ scrollView.setPadding(dip8, dip8, dip8, dip8);
+ scrollView.setBackground(background);
+ scrollView.setClipToOutline(true);
+
+ // Remove EditText from its current parent, if any.
+ ViewGroup parent = (ViewGroup) editText.getParent();
+ if (parent != null) parent.removeView(editText);
+ // Style the EditText to match the dialog theme.
+ editText.setTextColor(Utils.getAppForegroundColor());
+ editText.setBackgroundColor(Color.TRANSPARENT);
+ editText.setPadding(0, 0, 0, 0);
+ contentContainer.addView(editText, new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT));
+ // Message (if not replaced by EditText).
+ } else {
+ TextView messageView = new TextView(context);
+ // Supports Spanned (HTML).
+ messageView.setText(message);
+ messageView.setTextSize(16);
+ messageView.setTextColor(Utils.getAppForegroundColor());
+ // Enable HTML link clicking if the message contains links.
+ if (message instanceof Spanned) {
+ messageView.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ contentContainer.addView(messageView, new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ }
+
+ // Weight to take available space.
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 0,
+ 1.0f);
+ scrollView.setLayoutParams(params);
+ // Add ScrollView to main layout only if content exist.
+ mainLayout.addView(scrollView);
+ }
+
+ /**
+ * Adds buttons to the dialog, arranging them dynamically based on their widths.
+ *
+ * @param okButtonText OK button text, or null to use the default "OK" string.
+ * @param onOkClick Action for the OK button click.
+ * @param onCancelClick Action for the Cancel button click, or null if no Cancel button.
+ * @param neutralButtonText Neutral button text, or null if no Neutral button.
+ * @param onNeutralClick Action for the Neutral button click, or null if no Neutral button.
+ * @param dismissDialogOnNeutralClick If the dialog should dismiss on Neutral button click.
+ */
+ private void addButtons(String okButtonText, Runnable onOkClick, Runnable onCancelClick,
+ @Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
+ boolean dismissDialogOnNeutralClick) {
+ // Button container.
+ LinearLayout buttonContainer = new LinearLayout(context);
+ buttonContainer.setOrientation(LinearLayout.VERTICAL);
+ LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ buttonContainerParams.setMargins(0, dip16, 0, 0);
+ buttonContainer.setLayoutParams(buttonContainerParams);
+
+ List buttons = new ArrayList<>();
+ List buttonWidths = new ArrayList<>();
+
+ // Create buttons in order: Neutral, Cancel, OK.
+ if (neutralButtonText != null && onNeutralClick != null) {
+ Button neutralButton = createButton(neutralButtonText, onNeutralClick, false, dismissDialogOnNeutralClick);
+ buttons.add(neutralButton);
+ buttonWidths.add(measureButtonWidth(neutralButton));
+ }
+ if (onCancelClick != null) {
+ Button cancelButton = createButton(context.getString(android.R.string.cancel), onCancelClick, false, true);
+ buttons.add(cancelButton);
+ buttonWidths.add(measureButtonWidth(cancelButton));
+ }
+ if (onOkClick != null) {
+ Button okButton = createButton(
+ okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
+ onOkClick, true, true);
+ buttons.add(okButton);
+ buttonWidths.add(measureButtonWidth(okButton));
+ }
+
+ // Handle button layout.
+ layoutButtons(buttonContainer, buttons, buttonWidths);
+ mainLayout.addView(buttonContainer);
+ }
+
+ /**
+ * Creates a styled button with customizable text, click behavior, and appearance.
+ *
+ * @param text The button text to display.
+ * @param onClick The action to perform on button click.
+ * @param isOkButton If this is the OK button, which uses distinct styling.
+ * @param dismissDialog If the dialog should dismiss when the button is clicked.
+ * @return The created Button.
+ */
+ private Button createButton(String text, Runnable onClick, boolean isOkButton, boolean dismissDialog) {
+ Button button = new Button(context, null, 0);
+ button.setText(text);
+ button.setTextSize(14);
+ button.setAllCaps(false);
+ button.setSingleLine(true);
+ button.setEllipsize(TextUtils.TruncateAt.END);
+ button.setGravity(Gravity.CENTER);
+ // Set internal padding.
+ button.setPadding(dip16, 0, dip16, 0);
+
+ // Background color for OK button (inversion).
+ // Background color for Cancel or Neutral buttons.
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(20), null, null));
+ background.getPaint().setColor(isOkButton
+ ? Utils.getOkButtonBackgroundColor()
+ : Utils.getCancelOrNeutralButtonBackgroundColor());
+ button.setBackground(background);
+
+ button.setTextColor(Utils.isDarkModeEnabled()
+ ? (isOkButton ? Color.BLACK : Color.WHITE)
+ : (isOkButton ? Color.WHITE : Color.BLACK));
+
+ button.setOnClickListener(v -> {
+ if (onClick != null) onClick.run();
+ if (dismissDialog) dialog.dismiss();
+ });
+
+ return button;
+ }
+
+ /**
+ * Measures the width of a button.
+ */
+ private int measureButtonWidth(Button button) {
+ button.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ return button.getMeasuredWidth();
+ }
+
+ /**
+ * Arranges buttons in the dialog, either in a single row or multiple rows based on their total width.
+ *
+ * @param buttonContainer The container for the buttons.
+ * @param buttons The list of buttons to arrange.
+ * @param buttonWidths The measured widths of the buttons.
+ */
+ private void layoutButtons(LinearLayout buttonContainer, List buttons, List buttonWidths) {
+ if (buttons.isEmpty()) return;
+
+ // Check if buttons fit in one row.
+ int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ int totalWidth = 0;
+ for (Integer width : buttonWidths) {
+ totalWidth += width;
+ }
+ if (buttonWidths.size() > 1) {
+ // Add margins for gaps.
+ totalWidth += (buttonWidths.size() - 1) * dip8;
+ }
+
+ // Single button: stretch to full width.
+ if (buttons.size() == 1) {
+ layoutSingleButton(buttonContainer, buttons.get(0));
+ } else if (totalWidth <= screenWidth * 0.8) {
+ // Single row: Neutral, Cancel, OK.
+ layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
+ } else {
+ // Multiple rows: OK, Cancel, Neutral.
+ layoutButtonsInColumns(buttonContainer, buttons);
+ }
+ }
+
+ /**
+ * Arranges a single button, stretching it to full width.
+ *
+ * @param buttonContainer The container for the button.
+ * @param button The button to arrange.
+ */
+ private void layoutSingleButton(LinearLayout buttonContainer, Button button) {
+ LinearLayout singleContainer = new LinearLayout(context);
+ singleContainer.setOrientation(LinearLayout.HORIZONTAL);
+ singleContainer.setGravity(Gravity.CENTER);
+
+ ViewGroup parent = (ViewGroup) button.getParent();
+ if (parent != null) parent.removeView(button);
+
+ button.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dip36));
+ singleContainer.addView(button);
+ buttonContainer.addView(singleContainer);
+ }
+
+ /**
+ * Arranges buttons in a single horizontal row with proportional widths.
+ *
+ * @param buttonContainer The container for the buttons.
+ * @param buttons The list of buttons to arrange.
+ * @param buttonWidths The measured widths of the buttons.
+ */
+ private void layoutButtonsInRow(LinearLayout buttonContainer, List buttons, List buttonWidths) {
+ LinearLayout rowContainer = new LinearLayout(context);
+ rowContainer.setOrientation(LinearLayout.HORIZONTAL);
+ rowContainer.setGravity(Gravity.CENTER);
+ rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT));
+
+ // Add all buttons with proportional weights and specific margins.
+ for (int i = 0; i < buttons.size(); i++) {
+ Button button = getButton(buttons, buttonWidths, i);
+ rowContainer.addView(button);
+ }
+
+ buttonContainer.addView(rowContainer);
+ }
+
+ @NotNull
+ private Button getButton(List buttons, List buttonWidths, int i) {
+ Button button = buttons.get(i);
+ ViewGroup parent = (ViewGroup) button.getParent();
+ if (parent != null) parent.removeView(button);
+
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ 0, dip36, buttonWidths.get(i));
+
+ // Set margins based on button type and combination.
+ if (buttons.size() == 2) {
+ // Neutral + OK or Cancel + OK.
+ params.setMargins(i == 0 ? 0 : dip4, 0, i == 0 ? dip4 : 0, 0);
+ } else if (buttons.size() == 3) {
+ // Neutral.
+ // Cancel.
+ // OK.
+ params.setMargins(i == 0 ? 0 : dip4, 0, i == 2 ? 0 : dip4, 0);
+ }
+
+ button.setLayoutParams(params);
+ return button;
+ }
+
+ /**
+ * Arranges buttons in separate rows, ordered OK, Cancel, Neutral.
+ *
+ * @param buttonContainer The container for the buttons.
+ * @param buttons The list of buttons to arrange.
+ */
+ private void layoutButtonsInColumns(LinearLayout buttonContainer, List buttons) {
+ // Reorder: OK, Cancel, Neutral.
+ List reorderedButtons = new ArrayList<>();
+ if (buttons.size() == 3) {
+ reorderedButtons.add(buttons.get(2)); // OK
+ reorderedButtons.add(buttons.get(1)); // Cancel
+ reorderedButtons.add(buttons.get(0)); // Neutral
+ } else if (buttons.size() == 2) {
+ reorderedButtons.add(buttons.get(1)); // OK or Cancel
+ reorderedButtons.add(buttons.get(0)); // Neutral or Cancel
+ }
+
+ for (int i = 0; i < reorderedButtons.size(); i++) {
+ Button button = reorderedButtons.get(i);
+ LinearLayout singleContainer = new LinearLayout(context);
+ singleContainer.setOrientation(LinearLayout.HORIZONTAL);
+ singleContainer.setGravity(Gravity.CENTER);
+ singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dip36));
+
+ ViewGroup parent = (ViewGroup) button.getParent();
+ if (parent != null) parent.removeView(button);
+
+ button.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dip36));
+ singleContainer.addView(button);
+ buttonContainer.addView(singleContainer);
+
+ // Add a spacer between the buttons (except the last one).
+ if (i < reorderedButtons.size() - 1) {
+ View spacer = new View(context);
+ LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ dip8);
+ spacer.setLayoutParams(spacerParams);
+ buttonContainer.addView(spacer);
+ }
+ }
+ }
+}
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/SheetBottomDialog.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/SheetBottomDialog.java
new file mode 100644
index 000000000..483d413b2
--- /dev/null
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/SheetBottomDialog.java
@@ -0,0 +1,463 @@
+package app.revanced.extension.shared.ui;
+
+import static app.revanced.extension.shared.Utils.dipToPixels;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.LinearLayout;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.widget.ScrollView;
+import android.widget.ListView;
+import android.widget.Scroller;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import app.revanced.extension.shared.Utils;
+
+/**
+ * A utility class for creating a bottom sheet dialog that slides up from the bottom of the screen.
+ * The dialog supports drag-to-dismiss functionality, animations, and nested scrolling for scrollable content.
+ */
+public class SheetBottomDialog {
+
+ /**
+ * Creates a {@link SlideDialog} that slides up from the bottom of the screen with a specified content view.
+ * The dialog supports drag-to-dismiss functionality, allowing the user to drag it downward to close it,
+ * with proper handling of nested scrolling for scrollable content (e.g., {@link ListView}).
+ * It includes side margins, a top spacer for drag interaction, and can be dismissed by touching outside.
+ *
+ * @param context The context used to create the dialog.
+ * @param contentView The {@link View} to be displayed inside the dialog, such as a {@link LinearLayout}
+ * containing a {@link ListView}, buttons, or other UI elements.
+ * @param animationDuration The duration of the slide-in and slide-out animations in milliseconds.
+ * @return A configured {@link SlideDialog} instance ready to be shown.
+ * @throws IllegalArgumentException If contentView is null.
+ */
+ public static SlideDialog createSlideDialog(@NonNull Context context, @NonNull View contentView, int animationDuration) {
+ SlideDialog dialog = new SlideDialog(context);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.setCancelable(true);
+
+ // Create wrapper layout for side margins.
+ LinearLayout wrapperLayout = new LinearLayout(context);
+ wrapperLayout.setOrientation(LinearLayout.VERTICAL);
+
+ // Create drag container.
+ DraggableLinearLayout dragContainer = new DraggableLinearLayout(context, animationDuration);
+ dragContainer.setOrientation(LinearLayout.VERTICAL);
+ dragContainer.setDialog(dialog);
+
+ // Add top spacer.
+ View spacer = new View(context);
+ final int dip40 = dipToPixels(40);
+ LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, dip40);
+ spacer.setLayoutParams(spacerParams);
+ spacer.setClickable(true);
+ dragContainer.addView(spacer);
+
+ // Add content view.
+ ViewGroup parent = (ViewGroup) contentView.getParent();
+ if (parent != null) parent.removeView(contentView);
+ dragContainer.addView(contentView);
+
+ // Add drag container to wrapper layout.
+ wrapperLayout.addView(dragContainer);
+
+ dialog.setContentView(wrapperLayout);
+
+ // Configure dialog window.
+ Window window = dialog.getWindow();
+ if (window != null) {
+ Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 0, 100, false);
+ }
+
+ // Set up animation on drag container.
+ dialog.setAnimView(dragContainer);
+ dialog.setAnimationDuration(animationDuration);
+
+ return dialog;
+ }
+
+ /**
+ * Creates a {@link DraggableLinearLayout} with a rounded background and a centered handle bar,
+ * styled for use as the main layout in a {@link SlideDialog}. The layout has vertical orientation,
+ * includes padding, and supports drag-to-dismiss functionality with proper handling of nested scrolling
+ * for scrollable content (e.g., {@link ListView}) or clickable elements (e.g., buttons, {@link android.widget.SeekBar}).
+ *
+ * @param context The context used to create the layout.
+ * @param backgroundColor The background color for the layout as an {@link Integer}, or null to use
+ * the default dialog background color.
+ * @return A configured {@link DraggableLinearLayout} with a handle bar and styled background.
+ */
+ public static DraggableLinearLayout createMainLayout(@NonNull Context context, @Nullable Integer backgroundColor) {
+ // Preset size constants.
+ final int dip4 = dipToPixels(4); // Handle bar height.
+ final int dip8 = dipToPixels(8); // Dialog padding.
+ final int dip40 = dipToPixels(40); // Handle bar width.
+
+ DraggableLinearLayout mainLayout = new DraggableLinearLayout(context);
+ mainLayout.setOrientation(LinearLayout.VERTICAL);
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ layoutParams.setMargins(dip8, 0, dip8, dip8);
+ mainLayout.setLayoutParams(layoutParams);
+
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(12), null, null));
+ int color = (backgroundColor != null) ? backgroundColor : Utils.getDialogBackgroundColor();
+ background.getPaint().setColor(color);
+ mainLayout.setBackground(background);
+
+ // Add handle bar.
+ LinearLayout handleContainer = new LinearLayout(context);
+ LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ containerParams.setMargins(0, dip8, 0, 0);
+ handleContainer.setLayoutParams(containerParams);
+ handleContainer.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
+ View handleBar = new View(context);
+ ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(4), null, null));
+ handleBackground.getPaint().setColor(Utils.adjustColorBrightness(color, 0.9f, 1.25f));
+ LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
+ handleBar.setLayoutParams(handleParams);
+ handleBar.setBackground(handleBackground);
+
+ handleContainer.addView(handleBar);
+ mainLayout.addView(handleContainer);
+
+ return mainLayout;
+ }
+
+ /**
+ * A custom {@link LinearLayout} that provides drag-to-dismiss functionality for a {@link SlideDialog}.
+ * This layout intercepts touch events to allow dragging the dialog downward to dismiss it when the
+ * content cannot scroll upward. It ensures compatibility with scrollable content (e.g., {@link ListView},
+ * {@link ScrollView}) and clickable elements (e.g., buttons, {@link android.widget.SeekBar}) by prioritizing
+ * their touch events to prevent conflicts.
+ *
+ * Dragging is enabled only after the dialog's slide-in animation completes. The dialog is dismissed
+ * if dragged beyond 50% of its height or with a downward fling velocity exceeding 800 px/s.
+ */
+ public static class DraggableLinearLayout extends LinearLayout {
+ private static final int MIN_FLING_VELOCITY = 800; // px/s
+ private static final float DISMISS_HEIGHT_FRACTION = 0.5f; // 50% of height.
+
+ private float initialTouchRawY; // Raw Y on ACTION_DOWN.
+ private float dragOffset; // Current drag translation.
+ private boolean isDragging;
+ private boolean isDragEnabled;
+
+ private final int animationDuration;
+ private final Scroller scroller;
+ private final VelocityTracker velocityTracker;
+ private final Runnable settleRunnable;
+
+ private SlideDialog dialog;
+ private float dismissThreshold;
+
+ /**
+ * Constructs a new {@link DraggableLinearLayout} with the specified context.
+ */
+ public DraggableLinearLayout(@NonNull Context context) {
+ this(context, 0);
+ }
+
+ /**
+ * Constructs a new {@link DraggableLinearLayout} with the specified context and animation duration.
+ *
+ * @param context The context used to initialize the layout.
+ * @param animDuration The duration of the drag animation in milliseconds.
+ */
+ public DraggableLinearLayout(@NonNull Context context, int animDuration) {
+ super(context);
+ scroller = new Scroller(context, new DecelerateInterpolator());
+ velocityTracker = VelocityTracker.obtain();
+ animationDuration = animDuration;
+ settleRunnable = this::runSettleAnimation;
+
+ setClickable(true);
+
+ // Enable drag only after slide-in animation finishes.
+ isDragEnabled = false;
+ postDelayed(() -> isDragEnabled = true, animationDuration + 50);
+ }
+
+ /**
+ * Sets the {@link SlideDialog} associated with this layout for dismissal.
+ */
+ public void setDialog(@NonNull SlideDialog dialog) {
+ this.dialog = dialog;
+ }
+
+ /**
+ * Updates the dismissal threshold when the layout's size changes.
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ dismissThreshold = h * DISMISS_HEIGHT_FRACTION;
+ }
+
+ /**
+ * Intercepts touch events to initiate dragging when the content cannot scroll upward and the
+ * touch movement exceeds the system's touch slop.
+ */
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!isDragEnabled) return false;
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ initialTouchRawY = ev.getRawY();
+ isDragging = false;
+ scroller.forceFinished(true);
+ removeCallbacks(settleRunnable);
+ velocityTracker.clear();
+ velocityTracker.addMovement(ev);
+ dragOffset = getTranslationY();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ float dy = ev.getRawY() - initialTouchRawY;
+ if (dy > ViewConfiguration.get(getContext()).getScaledTouchSlop()
+ && !canChildScrollUp()) {
+ isDragging = true;
+ return true; // Intercept touches for drag.
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Handles touch events to perform dragging or trigger dismissal/return animations based on
+ * drag distance or fling velocity.
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (!isDragEnabled) return super.onTouchEvent(ev);
+ velocityTracker.addMovement(ev);
+
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ if (isDragging) {
+ float deltaY = ev.getRawY() - initialTouchRawY;
+ dragOffset = Math.max(0, deltaY); // Prevent upward drag.
+ setTranslationY(dragOffset); // 1:1 following finger.
+ }
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ velocityTracker.computeCurrentVelocity(1000);
+ float velocityY = velocityTracker.getYVelocity();
+
+ if (dragOffset > dismissThreshold || velocityY > MIN_FLING_VELOCITY) {
+ startDismissAnimation();
+ } else {
+ startReturnAnimation();
+ }
+ isDragging = false;
+ return true;
+ }
+ // Consume the touch event to prevent focus changes on child views.
+ return true;
+ }
+
+ /**
+ * Starts an animation to dismiss the dialog by sliding it downward.
+ */
+ private void startDismissAnimation() {
+ scroller.startScroll(0, (int) dragOffset,
+ 0, getHeight() - (int) dragOffset, animationDuration);
+ post(settleRunnable);
+ }
+
+ /**
+ * Starts an animation to return the dialog to its original position.
+ */
+ private void startReturnAnimation() {
+ scroller.startScroll(0, (int) dragOffset,
+ 0, -(int) dragOffset, animationDuration);
+ post(settleRunnable);
+ }
+
+ /**
+ * Runs the settle animation, updating the layout's translation until the animation completes.
+ * Dismisses the dialog if the drag offset reaches the view's height.
+ */
+ private void runSettleAnimation() {
+ if (scroller.computeScrollOffset()) {
+ dragOffset = scroller.getCurrY();
+ setTranslationY(dragOffset);
+
+ if (dragOffset >= getHeight() && dialog != null) {
+ dialog.dismiss();
+ scroller.forceFinished(true);
+ } else {
+ post(settleRunnable);
+ }
+ } else {
+ dragOffset = getTranslationY();
+ }
+ }
+
+ /**
+ * Checks if any child view can scroll upward, preventing drag if scrolling is possible.
+ *
+ * @return True if a child can scroll upward, false otherwise.
+ */
+ private boolean canChildScrollUp() {
+ View target = findScrollableChild(this);
+ return target != null && target.canScrollVertically(-1);
+ }
+
+ /**
+ * Recursively searches for a scrollable child view within the given view group.
+ *
+ * @param group The view group to search.
+ * @return The scrollable child view, or null if none found.
+ */
+ private View findScrollableChild(ViewGroup group) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View child = group.getChildAt(i);
+ if (child.canScrollVertically(-1)) return child;
+ if (child instanceof ViewGroup) {
+ View scroll = findScrollableChild((ViewGroup) child);
+ if (scroll != null) return scroll;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A custom dialog that slides up from the bottom of the screen with animation. It supports
+ * drag-to-dismiss functionality and ensures smooth dismissal animations without overlapping
+ * dismiss calls. The dialog animates a specified view during show and dismiss operations.
+ */
+ public static class SlideDialog extends Dialog {
+ private View animView;
+ private boolean isDismissing = false;
+ private int duration;
+ private final int screenHeight;
+
+ /**
+ * Constructs a new {@link SlideDialog} with the specified context.
+ */
+ public SlideDialog(@NonNull Context context) {
+ super(context);
+ screenHeight = context.getResources().getDisplayMetrics().heightPixels;
+ }
+
+ /**
+ * Sets the view to animate during show and dismiss operations.
+ */
+ public void setAnimView(@NonNull View view) {
+ this.animView = view;
+ }
+
+ /**
+ * Sets the duration of the slide-in and slide-out animations.
+ */
+ public void setAnimationDuration(int duration) {
+ this.duration = duration;
+ }
+
+ /**
+ * Displays the dialog with a slide-up animation for the animated view, if set.
+ */
+ @Override
+ public void show() {
+ super.show();
+ if (animView == null) return;
+
+ animView.setTranslationY(screenHeight);
+ animView.animate()
+ .translationY(0)
+ .setDuration(duration)
+ .setListener(null)
+ .start();
+ }
+
+ /**
+ * Cancels the dialog, triggering a dismissal animation.
+ */
+ @Override
+ public void cancel() {
+ dismiss();
+ }
+
+ /**
+ * Dismisses the dialog with a slide-down animation for the animated view, if set.
+ * Ensures that dismissal is not triggered multiple times concurrently.
+ */
+ @Override
+ public void dismiss() {
+ if (isDismissing) return;
+ isDismissing = true;
+
+ Window window = getWindow();
+ if (window == null) {
+ super.dismiss();
+ isDismissing = false;
+ return;
+ }
+
+ WindowManager.LayoutParams params = window.getAttributes();
+ float startDim = params != null ? params.dimAmount : 0f;
+
+ // Animate dimming effect.
+ ValueAnimator dimAnimator = ValueAnimator.ofFloat(startDim, 0f);
+ dimAnimator.setDuration(duration);
+ dimAnimator.addUpdateListener(animation -> {
+ if (params != null) {
+ params.dimAmount = (float) animation.getAnimatedValue();
+ window.setAttributes(params);
+ }
+ });
+
+ if (animView == null) {
+ dimAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SlideDialog.super.dismiss();
+ isDismissing = false;
+ }
+ });
+ dimAnimator.start();
+ return;
+ }
+
+ dimAnimator.start();
+ animView.animate()
+ .translationY(screenHeight)
+ .setDuration(duration)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SlideDialog.super.dismiss();
+ isDismissing = false;
+ }
+ })
+ .start();
+ }
+ }
+}
diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/TikTokActivityHook.java
similarity index 86%
rename from extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java
rename to extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/TikTokActivityHook.java
index 11304eb1e..b9d6bc8ec 100644
--- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java
+++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/TikTokActivityHook.java
@@ -7,29 +7,29 @@ import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
+
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
-import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment;
+import app.revanced.extension.tiktok.settings.preference.TikTokPreferenceFragment;
+
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
- * Hooks AdPersonalizationActivity.
- *
- * This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
- *
- * @noinspection unused
+ * Hooks AdPersonalizationActivity to inject a custom {@link TikTokPreferenceFragment}.
*/
-public class AdPersonalizationActivityHook {
+@SuppressWarnings({"deprecation", "NewApi", "unused"})
+public class TikTokActivityHook {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
try {
Class> entryClazz = Class.forName(entryClazzName);
Class> entryInfoClazz = Class.forName(entryInfoClazzName);
Constructor> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
Constructor> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
- Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
+ Object buttonInfo = entryInfoConstructor.newInstance(
+ "ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
return entryConstructor.newInstance(buttonInfo);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
@@ -62,7 +62,7 @@ public class AdPersonalizationActivityHook {
linearLayout.addView(fragment);
base.setContentView(linearLayout);
- PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment();
+ PreferenceFragment preferenceFragment = new TikTokPreferenceFragment();
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
return true;
diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java
index f387c68f5..5f090f7cd 100644
--- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java
+++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java
@@ -121,7 +121,7 @@ public class DownloadPathPreference extends DialogPreference {
}
private int findIndexOf(String str) {
- for (int i = 0; i < entryValues.length; i++) {
+ for (int i = 0, length = entryValues.length; i < length; i++) {
if (str.equals(entryValues[i])) return i;
}
return -1;
diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java
index 7348ccc70..14b2f1a72 100644
--- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java
+++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java
@@ -15,7 +15,7 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
/**
* Because resources cannot be added to TikTok,
* these strings are copied from the shared strings.xml file.
- *
+ *
* Changes here must also be made in strings.xml
*/
private final Map aboutStrings = Map.of(
diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TikTokPreferenceFragment.java
similarity index 97%
rename from extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java
rename to extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TikTokPreferenceFragment.java
index c1c48b04c..cf7223333 100644
--- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TikTokPreferenceFragment.java
@@ -14,7 +14,7 @@ import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPref
* Preference fragment for ReVanced settings
*/
@SuppressWarnings("deprecation")
-public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
+public class TikTokPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void syncSettingWithPreference(@NonNull Preference pref,
diff --git a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/TwitchActivityHook.java
similarity index 70%
rename from extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java
rename to extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/TwitchActivityHook.java
index b071d2cf1..8886ecbe4 100644
--- a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java
+++ b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/TwitchActivityHook.java
@@ -1,9 +1,12 @@
package app.revanced.extension.twitch.settings;
+import static app.revanced.extension.twitch.Utils.getStringId;
+
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;
@@ -11,22 +14,21 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
-import app.revanced.extension.twitch.settings.preference.ReVancedPreferenceFragment;
+import app.revanced.extension.twitch.settings.preference.TwitchPreferenceFragment;
+
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
import tv.twitch.android.settings.SettingsActivity;
/**
- * Hooks AppCompatActivity.
- *
- * This class is responsible for injecting our own fragment by replacing the AppCompatActivity.
- * @noinspection unused
+ * Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
*/
-public class AppCompatActivityHook {
+@SuppressWarnings({"deprecation", "NewApi", "unused"})
+public class TwitchActivityHook {
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
/**
- * Launches SettingsActivity and show ReVanced settings
+ * Launches SettingsActivity and show ReVanced settings.
*/
public static void startSettingsActivity() {
Logger.printDebug(() -> "Launching ReVanced settings");
@@ -44,27 +46,27 @@ public class AppCompatActivityHook {
}
/**
- * Helper for easy access in smali
- * @return Returns string resource id
+ * Helper for easy access in smali.
+ * @return Returns string resource id.
*/
public static int getReVancedSettingsString() {
- return app.revanced.extension.twitch.Utils.getStringId("revanced_settings");
+ return getStringId("revanced_settings");
}
/**
- * Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated
- * @return Returns a modified list of menu groups
+ * Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated.
+ * @return Returns a modified list of menu groups.
*/
public static List handleSettingMenuCreation(List settingGroups, Object revancedEntry) {
List groups = new ArrayList<>(settingGroups);
if (groups.isEmpty()) {
- // Create new menu group if none exist yet
+ // Create new menu group if none exist yet.
List items = new ArrayList<>();
items.add(revancedEntry);
groups.add(new SettingsMenuGroup(items));
} else {
- // Add to last menu group
+ // Add to last menu group.
int groupIdx = groups.size() - 1;
List items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
items.add(revancedEntry);
@@ -76,8 +78,8 @@ public class AppCompatActivityHook {
}
/**
- * Intercepts settings menu group onclick events
- * @return Returns true if handled, otherwise false
+ * Intercepts settings menu group onclick events.
+ * @return Returns true if handled, otherwise false.
*/
@SuppressWarnings("rawtypes")
public static boolean handleSettingMenuOnClick(Enum item) {
@@ -91,20 +93,20 @@ public class AppCompatActivityHook {
}
/**
- * Intercepts fragment loading in SettingsActivity.onCreate
- * @return Returns true if the revanced settings have been requested by the user, otherwise false
+ * Intercepts fragment loading in SettingsActivity.onCreate.
+ * @return Returns true if the ReVanced settings have been requested by the user, otherwise false.
*/
- public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) {
+ public static boolean handleSettingsCreation(AppCompatActivity base) {
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
Logger.printDebug(() -> "Revanced settings not requested");
- return false; // User wants to enter another settings fragment
+ return false; // User wants to enter another settings fragment.
}
Logger.printDebug(() -> "ReVanced settings requested");
- ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment();
+ TwitchPreferenceFragment fragment = new TwitchPreferenceFragment();
ActionBar supportActionBar = base.getSupportActionBar();
if (supportActionBar != null)
- supportActionBar.setTitle(app.revanced.extension.twitch.Utils.getStringId("revanced_settings"));
+ supportActionBar.setTitle(getStringId("revanced_settings"));
base.getFragmentManager()
.beginTransaction()
diff --git a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java
index 5cef4a0b7..0b6cb1af1 100644
--- a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java
+++ b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java
@@ -7,6 +7,7 @@ import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
+@SuppressWarnings({"deprecation", "unused"})
public class CustomPreferenceCategory extends PreferenceCategory {
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
diff --git a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/TwitchPreferenceFragment.java
similarity index 82%
rename from extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java
rename to extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/TwitchPreferenceFragment.java
index d77d06e19..4be39aaae 100644
--- a/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/twitch/src/main/java/app/revanced/extension/twitch/settings/preference/TwitchPreferenceFragment.java
@@ -5,9 +5,9 @@ import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragm
import app.revanced.extension.twitch.settings.Settings;
/**
- * Preference fragment for ReVanced settings
+ * Preference fragment for ReVanced settings.
*/
-public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
+public class TwitchPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void initialize() {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java
index 0b07c17b9..57531bf46 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java
@@ -18,6 +18,8 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@@ -64,6 +66,17 @@ public final class AlternativeThumbnailsPatch {
public boolean isAvailable() {
return usingDeArrowAnywhere();
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(
+ ALT_THUMBNAIL_HOME,
+ ALT_THUMBNAIL_SUBSCRIPTIONS,
+ ALT_THUMBNAIL_LIBRARY,
+ ALT_THUMBNAIL_PLAYER,
+ ALT_THUMBNAIL_SEARCH
+ );
+ }
}
public static final class StillImagesAvailability implements Setting.Availability {
@@ -79,6 +92,17 @@ public final class AlternativeThumbnailsPatch {
public boolean isAvailable() {
return usingStillImagesAnywhere();
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(
+ ALT_THUMBNAIL_HOME,
+ ALT_THUMBNAIL_SUBSCRIPTIONS,
+ ALT_THUMBNAIL_LIBRARY,
+ ALT_THUMBNAIL_PLAYER,
+ ALT_THUMBNAIL_SEARCH
+ );
+ }
}
public enum ThumbnailOption {
@@ -450,29 +474,29 @@ public final class AlternativeThumbnailsPatch {
}
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
- switch (quality) {
- case SDDEFAULT:
- // SD alt images have somewhat worse quality with washed out color and poor contrast.
- // But the 720 images look much better and don't suffer from these issues.
- // For unknown reasons, the 720 thumbnails are used only for the home feed,
- // while SD is used for the search and subscription feed
- // (even though search and subscriptions use the exact same layout as the home feed).
- // Of note, this image quality issue only appears with the alt thumbnail images,
- // and the regular thumbnails have identical color/contrast quality for all sizes.
- // Fix this by falling thru and upgrading SD to 720.
- case HQ720:
+ return switch (quality) {
+ // SD alt images have somewhat worse quality with washed out color and poor contrast.
+ // But the 720 images look much better and don't suffer from these issues.
+ // For unknown reasons, the 720 thumbnails are used only for the home feed,
+ // while SD is used for the search and subscription feed
+ // (even though search and subscriptions use the exact same layout as the home feed).
+ // Of note, this image quality issue only appears with the alt thumbnail images,
+ // and the regular thumbnails have identical color/contrast quality for all sizes.
+ // Fix this by falling thru and upgrading SD to 720.
+ case SDDEFAULT, HQ720 -> { // SD is max resolution for fast alt images.
if (useFastQuality) {
- return SDDEFAULT; // SD is max resolution for fast alt images.
+ yield SDDEFAULT;
}
- return HQ720;
- case MAXRESDEFAULT:
+ yield HQ720;
+ }
+ case MAXRESDEFAULT -> {
if (useFastQuality) {
- return SDDEFAULT;
+ yield SDDEFAULT;
}
- return MAXRESDEFAULT;
- default:
- return quality;
- }
+ yield MAXRESDEFAULT;
+ }
+ default -> quality;
+ };
}
final String originalName;
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java
index 14872dcfc..9748a3d68 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java
@@ -12,6 +12,8 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
+import java.util.List;
+
@SuppressWarnings("unused")
public final class ChangeStartPagePatch {
@@ -87,6 +89,11 @@ public final class ChangeStartPagePatch {
public boolean isAvailable() {
return Settings.CHANGE_START_PAGE.get() != StartPage.DEFAULT;
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(Settings.CHANGE_START_PAGE);
+ }
}
/**
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java
index 5a7acdd37..62a0a0de8 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java
@@ -13,6 +13,7 @@ import java.net.UnknownHostException;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
@@ -68,7 +69,7 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
Utils.runOnMainThread(() -> {
try {
// Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java
index 17115ec93..4114d525c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java
@@ -27,11 +27,11 @@ public final class DownloadsPatch {
/**
* Injection point.
- *
+ *
* Called from the in app download hook,
* for both the player action button (below the video)
* and the 'Download video' flyout option for feed videos.
- *
+ *
* Appears to always be called from the main thread.
*/
public static boolean inAppDownloadButtonOnClick(String videoId) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java
index 2f666f990..7bf99f479 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java
@@ -1,11 +1,12 @@
package app.revanced.extension.youtube.patches;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@@ -28,15 +29,6 @@ public final class HidePlayerOverlayButtonsPatch {
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
}
- /**
- * Injection point.
- */
- public static boolean getCastButtonOverrideV2(boolean original) {
- if (Settings.HIDE_CAST_BUTTON.get()) return false;
-
- return original;
- }
-
/**
* Injection point.
*/
@@ -47,11 +39,11 @@ public final class HidePlayerOverlayButtonsPatch {
private static final boolean HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED
= Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get();
- private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID =
- Utils.getResourceIdentifier(ResourceType.ID, "player_control_previous_button_touch_area");
+ private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
+ "player_control_previous_button_touch_area", "id");
- private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID =
- Utils.getResourceIdentifier(ResourceType.ID, "player_control_next_button_touch_area");
+ private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
+ "player_control_next_button_touch_area", "id");
/**
* Injection point.
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java
index e726c3616..ec0342dd0 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java
@@ -12,13 +12,15 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
+import java.util.List;
+
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
-@SuppressWarnings({"unused", "SpellCheckingInspection"})
+@SuppressWarnings("SpellCheckingInspection")
public final class MiniplayerPatch {
/**
@@ -174,6 +176,14 @@ public final class MiniplayerPatch {
public boolean isAvailable() {
return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(
+ Settings.MINIPLAYER_TYPE,
+ Settings.MINIPLAYER_DRAG_AND_DROP
+ );
+ }
}
public static final class MiniplayerHideOverlayButtonsAvailability implements Setting.Availability {
@@ -186,11 +196,59 @@ public final class MiniplayerPatch {
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|| (IS_19_29_OR_GREATER && type == MODERN_3);
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(
+ Settings.MINIPLAYER_TYPE,
+ Settings.MINIPLAYER_DOUBLE_TAP_ACTION,
+ Settings.MINIPLAYER_DRAG_AND_DROP
+ );
+ }
+ }
+
+ public static final class MiniplayerAnyModernAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
+ return type == MODERN_1 || type == MODERN_2 || type == MODERN_3 || type == MODERN_4;
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(Settings.MINIPLAYER_TYPE);
+ }
+ }
+
+ public static final class MiniplayerHideSubtextsAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
+ return type == MODERN_3 || type == MODERN_4;
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(Settings.MINIPLAYER_TYPE);
+ }
+ }
+
+ public static final class MiniplayerHideRewindOrOverlayOpacityAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
+ return type == MODERN_1;
+ }
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(Settings.MINIPLAYER_TYPE);
+ }
}
/**
* Injection point.
- *
+ *
* Enables a handler that immediately closes the miniplayer when the video is minimized,
* effectively disabling the miniplayer.
*/
@@ -379,4 +437,4 @@ public final class MiniplayerPatch {
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
}
}
-}
\ No newline at end of file
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java
index e9a469026..30722c087 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java
@@ -3,6 +3,8 @@ package app.revanced.extension.youtube.patches;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
+import java.util.List;
+
@SuppressWarnings("unused")
public class SeekbarThumbnailsPatch {
@@ -11,6 +13,11 @@ public class SeekbarThumbnailsPatch {
public boolean isAvailable() {
return VersionCheckPatch.IS_19_17_OR_GREATER || !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS);
+ }
}
private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java
index 8358824f4..1cf7e4d16 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java
@@ -14,6 +14,7 @@ import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
+import app.revanced.extension.shared.ui.CustomDialog;
import org.json.JSONArray;
import java.io.IOException;
@@ -125,7 +126,7 @@ public final class AnnouncementsPatch {
Utils.runOnMainThread(() -> {
// Create the custom dialog and show the announcement.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
finalTitle, // Title.
finalMessage, // Message.
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
index cbf2930bb..0cdf7b873 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
@@ -1,10 +1,13 @@
package app.revanced.extension.youtube.patches.components;
+import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
+import java.util.List;
+
@SuppressWarnings("unused")
public class PlayerFlyoutMenuItemsFilter extends Filter {
@@ -17,6 +20,11 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
// without a restart the setting will show as available when it's not.
return AVAILABLE_ON_LAUNCH && !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams();
}
+
+ @Override
+ public List> getParentSettings() {
+ return List.of(BaseSettings.SPOOF_VIDEO_STREAMS);
+ }
}
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
index 52e977e34..ba4aaa6e6 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
@@ -2,11 +2,11 @@ package app.revanced.extension.youtube.patches.playback.speed;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.fadeInDuration;
+import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.getDialogBackgroundColor;
import android.annotation.SuppressLint;
-import android.app.Dialog;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
@@ -20,19 +20,9 @@ import android.graphics.drawable.shapes.RoundRectShape;
import android.icu.text.NumberFormat;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.animation.Animation;
-import android.view.animation.TranslateAnimation;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.GridLayout;
-import android.widget.LinearLayout;
-import android.widget.SeekBar;
-import android.widget.TextView;
+import android.widget.*;
import java.lang.ref.WeakReference;
import java.util.Arrays;
@@ -40,6 +30,7 @@ import java.util.function.Function;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.ui.SheetBottomDialog;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter;
import app.revanced.extension.youtube.settings.Settings;
@@ -97,7 +88,7 @@ public class CustomPlaybackSpeedPatch {
/**
* Weak reference to the currently open dialog.
*/
- private static WeakReference currentDialog = new WeakReference<>(null);
+ private static WeakReference currentDialog;
static {
// Use same 2 digit format as built in speed picker,
@@ -268,368 +259,239 @@ public class CustomPlaybackSpeedPatch {
*/
@SuppressLint("SetTextI18n")
public static void showModernCustomPlaybackSpeedDialog(Context context) {
- // Create a dialog without a theme for custom appearance.
- Dialog dialog = new Dialog(context);
- dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
+ try {
+ // Create main layout.
+ SheetBottomDialog.DraggableLinearLayout mainLayout =
+ SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor());
- // Store the dialog reference.
- currentDialog = new WeakReference<>(dialog);
+ // Preset size constants.
+ final int dip4 = dipToPixels(4);
+ final int dip8 = dipToPixels(8);
+ final int dip12 = dipToPixels(12);
+ final int dip20 = dipToPixels(20);
+ final int dip32 = dipToPixels(32);
+ final int dip60 = dipToPixels(60);
- // Enable dismissing the dialog when tapping outside.
- dialog.setCanceledOnTouchOutside(true);
+ // Display current playback speed.
+ TextView currentSpeedText = new TextView(context);
+ float currentSpeed = VideoInformation.getPlaybackSpeed();
+ // Initially show with only 0 minimum digits, so 1.0 shows as 1x.
+ currentSpeedText.setText(formatSpeedStringX(currentSpeed));
+ currentSpeedText.setTextColor(Utils.getAppForegroundColor());
+ currentSpeedText.setTextSize(16);
+ currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
+ currentSpeedText.setGravity(Gravity.CENTER);
+ LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ textParams.setMargins(0, dip20, 0, 0);
+ currentSpeedText.setLayoutParams(textParams);
+ // Add current speed text view to main layout.
+ mainLayout.addView(currentSpeedText);
- // Create main vertical LinearLayout for dialog content.
- LinearLayout mainLayout = new LinearLayout(context);
- mainLayout.setOrientation(LinearLayout.VERTICAL);
+ // Create horizontal layout for slider and +/- buttons.
+ LinearLayout sliderLayout = new LinearLayout(context);
+ sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
+ sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
- // Preset size constants.
- final int dip4 = dipToPixels(4); // Height for handle bar.
- final int dip5 = dipToPixels(5);
- final int dip6 = dipToPixels(6); // Padding for mainLayout from bottom.
- final int dip8 = dipToPixels(8); // Padding for mainLayout from left and right.
- final int dip20 = dipToPixels(20);
- final int dip32 = dipToPixels(32); // Height for in-rows speed buttons.
- final int dip36 = dipToPixels(36); // Height for minus and plus buttons.
- final int dip40 = dipToPixels(40); // Width for handle bar.
- final int dip60 = dipToPixels(60); // Height for speed button container.
+ // Create +/- buttons.
+ Button minusButton = createStyledButton(context, false, dip8, dip8);
+ Button plusButton = createStyledButton(context, true, dip8, dip8);
- mainLayout.setPadding(dip5, dip8, dip5, dip8);
+ // Create slider for speed adjustment.
+ SeekBar speedSlider = new SeekBar(context);
+ speedSlider.setFocusable(true);
+ speedSlider.setFocusableInTouchMode(true);
+ speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
+ speedSlider.setProgress(speedToProgressValue(currentSpeed));
+ speedSlider.getProgressDrawable().setColorFilter(
+ Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
+ speedSlider.getThumb().setColorFilter(
+ Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
+ LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
+ 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
+ speedSlider.setLayoutParams(sliderParams);
- // Set rounded rectangle background for the main layout.
- RoundRectShape roundRectShape = new RoundRectShape(
- Utils.createCornerRadii(12), null, null);
- ShapeDrawable background = new ShapeDrawable(roundRectShape);
- background.getPaint().setColor(Utils.getDialogBackgroundColor());
- mainLayout.setBackground(background);
+ // Add -/+ and slider views to slider layout.
+ sliderLayout.addView(minusButton);
+ sliderLayout.addView(speedSlider);
+ sliderLayout.addView(plusButton);
- // Add handle bar at the top.
- View handleBar = new View(context);
- ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(4), null, null));
- handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
- handleBar.setBackground(handleBackground);
- LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
- dip40, // handle bar width.
- dip4 // handle bar height.
- );
- handleParams.gravity = Gravity.CENTER_HORIZONTAL; // Center horizontally.
- handleParams.setMargins(0, 0, 0, dip20); // 20dp bottom margins.
- handleBar.setLayoutParams(handleParams);
- // Add handle bar view to main layout.
- mainLayout.addView(handleBar);
+ // Add slider layout to main layout.
+ mainLayout.addView(sliderLayout);
- // Display current playback speed.
- TextView currentSpeedText = new TextView(context);
- float currentSpeed = VideoInformation.getPlaybackSpeed();
- // Initially show with only 0 minimum digits, so 1.0 shows as 1x
- currentSpeedText.setText(formatSpeedStringX(currentSpeed));
- currentSpeedText.setTextColor(Utils.getAppForegroundColor());
- currentSpeedText.setTextSize(16);
- currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
- currentSpeedText.setGravity(Gravity.CENTER);
- LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
- textParams.setMargins(0, 0, 0, 0);
- currentSpeedText.setLayoutParams(textParams);
- // Add current speed text view to main layout.
- mainLayout.addView(currentSpeedText);
+ Function userSelectedSpeed = newSpeed -> {
+ final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
+ if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
+ // Nothing has changed. New speed rounds to the current speed.
+ return null;
+ }
- // Create horizontal layout for slider and +/- buttons.
- LinearLayout sliderLayout = new LinearLayout(context);
- sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
- sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
- sliderLayout.setPadding(dip5, dip5, dip5, dip5); // 5dp padding.
+ currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
+ speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
- // Create minus button.
- Button minusButton = new Button(context, null, 0); // Disable default theme style.
- minusButton.setText(""); // No text on button.
- ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(20), null, null));
- minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
- minusButton.setBackground(minusBackground);
- OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
- minusButton.setForeground(minusDrawable);
- LinearLayout.LayoutParams minusParams = new LinearLayout.LayoutParams(dip36, dip36);
- minusParams.setMargins(0, 0, dip5, 0); // 5dp to slider.
- minusButton.setLayoutParams(minusParams);
-
- // Create slider for speed adjustment.
- SeekBar speedSlider = new SeekBar(context);
- speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
- speedSlider.setProgress(speedToProgressValue(currentSpeed));
- speedSlider.getProgressDrawable().setColorFilter(
- Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
- speedSlider.getThumb().setColorFilter(
- Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
- LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
- 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
- sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
- speedSlider.setLayoutParams(sliderParams);
-
- // Create plus button.
- Button plusButton = new Button(context, null, 0); // Disable default theme style.
- plusButton.setText(""); // No text on button.
- ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(20), null, null));
- plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
- plusButton.setBackground(plusBackground);
- OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
- plusButton.setForeground(plusDrawable);
- LinearLayout.LayoutParams plusParams = new LinearLayout.LayoutParams(dip36, dip36);
- plusParams.setMargins(dip5, 0, 0, 0); // 5dp to slider.
- plusButton.setLayoutParams(plusParams);
-
- // Add -/+ and slider views to slider layout.
- sliderLayout.addView(minusButton);
- sliderLayout.addView(speedSlider);
- sliderLayout.addView(plusButton);
-
- LinearLayout.LayoutParams sliderLayoutParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
- sliderLayoutParams.setMargins(0, 0, 0, dip5); // 5dp bottom margin.
- sliderLayout.setLayoutParams(sliderLayoutParams);
-
- // Add slider layout to main layout.
- mainLayout.addView(sliderLayout);
-
- Function userSelectedSpeed = newSpeed -> {
- final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
- if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
- // Nothing has changed. New speed rounds to the current speed.
+ RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
+ VideoInformation.overridePlaybackSpeed(roundedSpeed);
return null;
- }
+ };
- currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
- speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
-
- RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
- VideoInformation.overridePlaybackSpeed(roundedSpeed);
- return null;
- };
-
- // Set listener for slider to update playback speed.
- speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (fromUser) {
- // Convert from progress value to video playback speed.
- userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
+ // Set listener for slider to update playback speed.
+ speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ // Convert from progress value to video playback speed.
+ userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
+ }
}
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+
+ minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
+ (float) (VideoInformation.getPlaybackSpeed() - SPEED_ADJUSTMENT_CHANGE)));
+ plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
+ (float) (VideoInformation.getPlaybackSpeed() + SPEED_ADJUSTMENT_CHANGE)));
+
+ // Create GridLayout for preset speed buttons.
+ GridLayout gridLayout = new GridLayout(context);
+ gridLayout.setColumnCount(5); // 5 columns for speed buttons.
+ gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
+ gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
+ LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ gridParams.setMargins(dip4, dip12, dip4, dip12); // Speed buttons container.
+ gridLayout.setLayoutParams(gridParams);
+
+ // For button use 1 digit minimum.
+ speedFormatter.setMinimumFractionDigits(1);
+
+ // Add buttons for each preset playback speed.
+ for (float speed : customPlaybackSpeeds) {
+ // Container for button and optional label.
+ FrameLayout buttonContainer = new FrameLayout(context);
+
+ // Set layout parameters for each grid cell.
+ GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
+ containerParams.width = 0; // Equal width for columns.
+ containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
+ containerParams.setMargins(dip4, 0, dip4, 0); // Button margins.
+ containerParams.height = dip60; // Fixed height for button and label.
+ buttonContainer.setLayoutParams(containerParams);
+
+ // Create speed button.
+ Button speedButton = new Button(context, null, 0);
+ speedButton.setText(speedFormatter.format(speed));
+ speedButton.setTextColor(Utils.getAppForegroundColor());
+ speedButton.setTextSize(12);
+ speedButton.setAllCaps(false);
+ speedButton.setGravity(Gravity.CENTER);
+
+ ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(20), null, null));
+ buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
+ speedButton.setBackground(buttonBackground);
+ speedButton.setPadding(dip4, dip4, dip4, dip4);
+
+ // Center button vertically and stretch horizontally in container.
+ FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
+ speedButton.setLayoutParams(buttonParams);
+
+ // Add speed buttons view to buttons container layout.
+ buttonContainer.addView(speedButton);
+
+ // Add "Normal" label for 1.0x speed.
+ if (speed == 1.0f) {
+ TextView normalLabel = new TextView(context);
+ // Use same 'Normal' string as stock YouTube.
+ normalLabel.setText(str("normal_playback_rate_label"));
+ normalLabel.setTextColor(Utils.getAppForegroundColor());
+ normalLabel.setTextSize(10);
+ normalLabel.setGravity(Gravity.CENTER);
+
+ FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
+ Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
+ labelParams.bottomMargin = 0; // Position label below button.
+ normalLabel.setLayoutParams(labelParams);
+
+ buttonContainer.addView(normalLabel);
+ }
+
+ speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
+
+ gridLayout.addView(buttonContainer);
}
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {}
+ // Restore 2 digit minimum.
+ speedFormatter.setMinimumFractionDigits(2);
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {}
- });
+ // Add in-rows speed buttons layout to main layout.
+ mainLayout.addView(gridLayout);
- minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
- (float) (VideoInformation.getPlaybackSpeed() - SPEED_ADJUSTMENT_CHANGE)));
- plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
- (float) (VideoInformation.getPlaybackSpeed() + SPEED_ADJUSTMENT_CHANGE)));
+ // Create dialog.
+ SheetBottomDialog.SlideDialog dialog = SheetBottomDialog.createSlideDialog(context, mainLayout, fadeInDuration);
+ currentDialog = new WeakReference<>(dialog);
- // Create GridLayout for preset speed buttons.
- GridLayout gridLayout = new GridLayout(context);
- gridLayout.setColumnCount(5); // 5 columns for speed buttons.
- gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
- gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
- LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
- gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
- gridLayout.setLayoutParams(gridParams);
+ // Create observer for PlayerType changes.
+ Function1 playerTypeObserver = new Function1<>() {
+ @Override
+ public Unit invoke(PlayerType type) {
+ SheetBottomDialog.SlideDialog current = currentDialog.get();
+ if (current == null || !current.isShowing()) {
+ // Should never happen.
+ PlayerType.getOnChange().removeObserver(this);
+ Logger.printException(() -> "Removing player type listener as dialog is null or closed");
+ } else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
+ current.dismiss();
+ Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
+ }
+ return Unit.INSTANCE;
+ }
+ };
- // For button use 1 digit minimum.
- speedFormatter.setMinimumFractionDigits(1);
+ // Add observer to dismiss dialog when entering PiP mode.
+ PlayerType.getOnChange().addObserver(playerTypeObserver);
- // Add buttons for each preset playback speed.
- for (float speed : customPlaybackSpeeds) {
- // Container for button and optional label.
- FrameLayout buttonContainer = new FrameLayout(context);
+ // Remove observer when dialog is dismissed.
+ dialog.setOnDismissListener(d -> {
+ PlayerType.getOnChange().removeObserver(playerTypeObserver);
+ Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
+ });
- // Set layout parameters for each grid cell.
- GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
- containerParams.width = 0; // Equal width for columns.
- containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
- containerParams.setMargins(dip5, 0, dip5, 0); // Button margins.
- containerParams.height = dip60; // Fixed height for button and label.
- buttonContainer.setLayoutParams(containerParams);
+ dialog.show(); // Show the dialog.
- // Create speed button.
- Button speedButton = new Button(context, null, 0);
- speedButton.setText(speedFormatter.format(speed));
- speedButton.setTextColor(Utils.getAppForegroundColor());
- speedButton.setTextSize(12);
- speedButton.setAllCaps(false);
- speedButton.setGravity(Gravity.CENTER);
-
- ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(20), null, null));
- buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
- speedButton.setBackground(buttonBackground);
- speedButton.setPadding(dip5, dip5, dip5, dip5);
-
- // Center button vertically and stretch horizontally in container.
- FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
- speedButton.setLayoutParams(buttonParams);
-
- // Add speed buttons view to buttons container layout.
- buttonContainer.addView(speedButton);
-
- // Add "Normal" label for 1.0x speed.
- if (speed == 1.0f) {
- TextView normalLabel = new TextView(context);
- // Use same 'Normal' string as stock YouTube.
- normalLabel.setText(str("normal_playback_rate_label"));
- normalLabel.setTextColor(Utils.getAppForegroundColor());
- normalLabel.setTextSize(10);
- normalLabel.setGravity(Gravity.CENTER);
-
- FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
- Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
- labelParams.bottomMargin = 0; // Position label below button.
- normalLabel.setLayoutParams(labelParams);
-
- buttonContainer.addView(normalLabel);
- }
-
- speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
-
- gridLayout.addView(buttonContainer);
+ } catch (Exception ex) {
+ Logger.printException(() -> "showModernCustomPlaybackSpeedDialog failure", ex);
}
+ }
- // Restore 2 digit minimum.
- speedFormatter.setMinimumFractionDigits(2);
-
- // Add in-rows speed buttons layout to main layout.
- mainLayout.addView(gridLayout);
-
- // Wrap mainLayout in another LinearLayout for side margins.
- LinearLayout wrapperLayout = new LinearLayout(context);
- wrapperLayout.setOrientation(LinearLayout.VERTICAL);
- wrapperLayout.setPadding(dip8, 0, dip8, 0); // 8dp side margins.
- wrapperLayout.addView(mainLayout);
- dialog.setContentView(wrapperLayout);
-
- // Configure dialog window to appear at the bottom.
- Window window = dialog.getWindow();
- if (window != null) {
- WindowManager.LayoutParams params = window.getAttributes();
- params.gravity = Gravity.BOTTOM; // Position at bottom of screen.
- params.y = dip6; // 6dp margin from bottom.
- // In landscape, use the smaller dimension (height) as portrait width.
- int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
- if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
- portraitWidth = Math.min(
- portraitWidth,
- context.getResources().getDisplayMetrics().heightPixels);
- }
- params.width = portraitWidth; // Use portrait width.
- params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- window.setAttributes(params);
- 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.
- Function1 playerTypeObserver = new Function1<>() {
- @Override
- public Unit invoke(PlayerType type) {
- Dialog current = currentDialog.get();
- if (current == null || !current.isShowing()) {
- // Should never happen.
- PlayerType.getOnChange().removeObserver(this);
- Logger.printException(() -> "Removing player type listener as dialog is null or closed");
- } else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
- current.dismiss();
- Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
- }
- return Unit.INSTANCE;
- }
- };
-
- // Add observer to dismiss dialog when entering PiP mode.
- PlayerType.getOnChange().addObserver(playerTypeObserver);
-
- // Remove observer when dialog is dismissed.
- dialog.setOnDismissListener(d -> {
- PlayerType.getOnChange().removeObserver(playerTypeObserver);
- Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
- });
-
- dialog.show(); // Display the dialog.
+ /**
+ * Creates a styled button with a plus or minus symbol.
+ *
+ * @param context The Android context used to create the button.
+ * @param isPlus True to display a plus symbol, false to display a minus symbol.
+ * @param marginStart The start margin in pixels (left for LTR, right for RTL).
+ * @param marginEnd The end margin in pixels (right for LTR, left for RTL).
+ * @return A configured {@link Button} with the specified styling and layout parameters.
+ */
+ private static Button createStyledButton(Context context, boolean isPlus, int marginStart, int marginEnd) {
+ Button button = new Button(context, null, 0); // Disable default theme style.
+ button.setText(""); // No text on button.
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(20), null, null));
+ background.getPaint().setColor(getAdjustedBackgroundColor(false));
+ button.setBackground(background);
+ button.setForeground(new OutlineSymbolDrawable(isPlus)); // Plus or minus symbol.
+ final int dip36 = dipToPixels(36);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dip36, dip36);
+ params.setMargins(marginStart, 0, marginEnd, 0); // Set margins.
+ button.setLayoutParams(params);
+ return button;
}
/**
@@ -674,10 +536,9 @@ public class CustomPlaybackSpeedPatch {
* for light themes to ensure visual contrast.
*/
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
- final int baseColor = Utils.getDialogBackgroundColor();
final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
- return Utils.adjustColorBrightness(baseColor, lightThemeFactor, darkThemeFactor);
+ return Utils.adjustColorBrightness(getDialogBackgroundColor(), lightThemeFactor, darkThemeFactor);
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
index 1cffc4948..de70304bb 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java
@@ -152,13 +152,10 @@ public final class SeekbarColorPatch {
String seekbarStyle = get9BitStyleIdentifier(customSeekbarColor);
Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle);
- final int styleIdentifierDefault = Utils.getResourceIdentifier(
+ final int styleIdentifierDefault = Utils.getResourceIdentifierOrThrow(
ResourceType.STYLE,
seekbarStyle
);
- if (styleIdentifierDefault == 0) {
- throw new RuntimeException("Seekbar style not found: " + seekbarStyle);
- }
Resources.Theme theme = Utils.getContext().getResources().newTheme();
theme.applyStyle(styleIdentifierDefault, true);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index 6b9681295..7fc068ab5 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -21,6 +21,7 @@ import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.ReplacementSpan;
+import android.widget.Toast;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -508,6 +509,11 @@ public class ReturnYouTubeDislike {
try {
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (votingData == null) {
+ // Method automatically prevents showing multiple toasts if the connection failed.
+ // This call is needed here in case the api call did succeed but took too long.
+ ReturnYouTubeDislikeApi.handleConnectionError(
+ str("revanced_ryd_failure_connection_timeout"),
+ null, null, Toast.LENGTH_SHORT);
Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
return original;
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
index 1ebeea223..18d46282c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
@@ -4,8 +4,8 @@ import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute;
import android.util.Base64;
+import android.widget.Toast;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
@@ -30,15 +30,15 @@ import app.revanced.extension.youtube.settings.Settings;
public class ReturnYouTubeDislikeApi {
/**
- * {@link #fetchVotes(String)} TCP connection timeout
+ * {@link #fetchVotes(String)} TCP connection timeout.
*/
- private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2 * 1000; // 2 Seconds.
+ private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 3 * 1000; // 3 Seconds.
/**
* {@link #fetchVotes(String)} HTTP read timeout.
* To locally debug and force timeouts, change this to a very small number (ie: 100)
*/
- private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds.
+ private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 7 * 1000; // 7 Seconds.
/**
* Default connection and response timeout for voting and registration.
@@ -53,6 +53,15 @@ public class ReturnYouTubeDislikeApi {
*/
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
+ /**
+ * RYD API sometimes returns 401 (authorization error), even though the user id is valid.
+ * There is no known fix for this (resetting to a different user id does not fix it),
+ * so instead just quietly ignore the error.
+ *
+ * See RYD bug report .
+ */
+ private static final int HTTP_STATUS_CODE_UNAUTHORIZED = 401;
+
/**
* Indicates a client rate limit has been reached and the client must back off.
*/
@@ -232,14 +241,20 @@ public class ReturnYouTubeDislikeApi {
}
}
- private static void handleConnectionError(@NonNull String toastMessage,
- @Nullable Exception ex,
- boolean showLongToast) {
+ /**
+ * @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}.
+ */
+ public static void handleConnectionError(String toastMessage,
+ @Nullable Integer responseCode,
+ @Nullable Exception ex,
+ @Nullable Integer toastDuration) {
if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) {
- if (showLongToast) {
- Utils.showToastLong(toastMessage);
- } else {
- Utils.showToastShort(toastMessage);
+ if (responseCode != null && responseCode == HTTP_STATUS_CODE_UNAUTHORIZED) {
+ Logger.printInfo(() -> "Ignoring status code " + HTTP_STATUS_CODE_UNAUTHORIZED
+ + " (API authorization erorr)");
+ return; // Do not set api failure field.
+ } else if (toastDuration != null) {
+ Utils.showToast(toastMessage, toastDuration);
}
}
lastApiCallFailed = true;
@@ -297,13 +312,13 @@ public class ReturnYouTubeDislikeApi {
} else {
// Unexpected response code. Most likely RYD is temporarily broken.
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
- null, true);
+ responseCode, null, Toast.LENGTH_LONG);
}
connection.disconnect(); // Something went wrong, might as well disconnect.
} catch (SocketTimeoutException ex) {
- handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false);
+ handleConnectionError((str("revanced_ryd_failure_connection_timeout")), null, ex, Toast.LENGTH_SHORT);
} catch (IOException ex) {
- handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true);
+ handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), null, ex, Toast.LENGTH_LONG);
} catch (Exception ex) {
// should never happen
Logger.printException(() -> "fetchVotes failure", ex);
@@ -344,13 +359,14 @@ public class ReturnYouTubeDislikeApi {
String solution = solvePuzzle(challenge, difficulty);
return confirmRegistration(userId, solution);
}
+
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
- null, true);
+ responseCode, null, Toast.LENGTH_LONG);
connection.disconnect();
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), null, ex, Toast.LENGTH_SHORT);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true);
+ handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), null, ex, Toast.LENGTH_LONG);
} catch (Exception ex) {
Logger.printException(() -> "Failed to register user", ex); // should never happen
}
@@ -393,12 +409,12 @@ public class ReturnYouTubeDislikeApi {
Logger.printInfo(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''");
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
- null, true);
+ responseCode, null, Toast.LENGTH_LONG);
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), null, ex, Toast.LENGTH_SHORT);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"),
- ex, true);
+ null, ex, Toast.LENGTH_LONG);
} catch (Exception ex) {
Logger.printException(() -> "Failed to confirm registration for user: " + userId
+ "solution: " + solution, ex);
@@ -469,12 +485,12 @@ public class ReturnYouTubeDislikeApi {
Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote
+ " response code was: " + responseCode);
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
- null, true);
+ responseCode, null, Toast.LENGTH_LONG);
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), null, ex, Toast.LENGTH_SHORT);
} catch (IOException ex) {
- handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true);
+ handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), null, ex, Toast.LENGTH_LONG);
} catch (Exception ex) {
// should never happen
Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
@@ -518,12 +534,12 @@ public class ReturnYouTubeDislikeApi {
Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'");
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode),
- null, true);
+ responseCode, null, Toast.LENGTH_LONG);
} catch (SocketTimeoutException ex) {
- handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
+ handleConnectionError(str("revanced_ryd_failure_connection_timeout"), null, ex, Toast.LENGTH_SHORT);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"),
- ex, true);
+ null, ex, Toast.LENGTH_LONG);
} catch (Exception ex) {
Logger.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution, ex); // should never happen
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java
index c37b472d3..8aa739d20 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ui/ReturnYouTubeDislikeAboutPreference.java
@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.returnyoutubedislike.ui;
import android.content.Context;
import android.util.AttributeSet;
-import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
+import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
/**
* Allows tapping the RYD about preference to open the website.
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java
deleted file mode 100644
index 844294d15..000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/SearchViewController.java
+++ /dev/null
@@ -1,421 +0,0 @@
-package app.revanced.extension.youtube.settings;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.getResourceIdentifier;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.graphics.drawable.GradientDrawable;
-import android.util.Pair;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.ArrayAdapter;
-import android.widget.AutoCompleteTextView;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.SearchView;
-import android.widget.TextView;
-import android.widget.Toolbar;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Deque;
-import java.util.LinkedList;
-import java.util.List;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.AppLanguage;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.StringSetting;
-import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
-
-/**
- * Controller for managing the search view in ReVanced settings.
- */
-@SuppressWarnings({"deprecated", "DiscouragedApi"})
-public class SearchViewController {
- private static final int MAX_HISTORY_SIZE = 5;
-
- private final SearchView searchView;
- private final FrameLayout searchContainer;
- private final Toolbar toolbar;
- private final Activity activity;
- private boolean isSearchActive;
- private final CharSequence originalTitle;
- private final Deque searchHistory;
- private final AutoCompleteTextView autoCompleteTextView;
- private final boolean showSettingsSearchHistory;
- private int currentOrientation;
-
- /**
- * Creates a background drawable for the SearchView with rounded corners.
- */
- private static GradientDrawable createBackgroundDrawable(Context context) {
- GradientDrawable background = new GradientDrawable();
- background.setShape(GradientDrawable.RECTANGLE);
- background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
- background.setColor(getSearchViewBackground());
- return background;
- }
-
- /**
- * Creates a background drawable for suggestion items with rounded corners.
- */
- private static GradientDrawable createSuggestionBackgroundDrawable() {
- GradientDrawable background = new GradientDrawable();
- background.setShape(GradientDrawable.RECTANGLE);
- background.setColor(getSearchViewBackground());
- return background;
- }
-
- @ColorInt
- public static int getSearchViewBackground() {
- return Utils.isDarkModeEnabled()
- ? Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), 1.11f)
- : Utils.adjustColorBrightness(Utils.getThemeLightColor(), 0.95f);
- }
-
- /**
- * Adds search view components to the activity.
- */
- public static SearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
- return new SearchViewController(activity, toolbar, fragment);
- }
-
- private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
- this.activity = activity;
- this.toolbar = toolbar;
- this.originalTitle = toolbar.getTitle();
- this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
- this.searchHistory = new LinkedList<>();
- this.currentOrientation = activity.getResources().getConfiguration().orientation;
- StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
- if (showSettingsSearchHistory) {
- String entries = searchEntries.get();
- if (!entries.isBlank()) {
- searchHistory.addAll(Arrays.asList(entries.split("\n")));
- }
- } else {
- // Clear old saved history if the user turns off the feature.
- searchEntries.resetToDefault();
- }
-
- // Retrieve SearchView and container from XML.
- searchView = activity.findViewById(getResourceIdentifier(
- ResourceType.ID,
- "revanced_search_view"));
- searchContainer = activity.findViewById(getResourceIdentifier(
- ResourceType.ID,
- "revanced_search_view_container"));
-
- // Initialize AutoCompleteTextView.
- autoCompleteTextView = searchView.findViewById(
- searchView.getContext().getResources().getIdentifier(
- "android:id/search_src_text", null, null));
-
- // Disable fullscreen keyboard mode.
- autoCompleteTextView.setImeOptions(autoCompleteTextView.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
-
- // Set background and query hint.
- searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
- searchView.setQueryHint(str("revanced_settings_search_hint"));
-
- // Configure RTL support based on app language.
- AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
- if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
- searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
- searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
- }
-
- // Set up search history suggestions.
- if (showSettingsSearchHistory) {
- setupSearchHistory();
- }
-
- // Set up query text listener.
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- try {
- String queryTrimmed = query.trim();
- if (!queryTrimmed.isEmpty()) {
- saveSearchQuery(queryTrimmed);
- }
- // Hide suggestions on submit.
- if (showSettingsSearchHistory && autoCompleteTextView != null) {
- autoCompleteTextView.dismissDropDown();
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onQueryTextSubmit failure", ex);
- }
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String newText) {
- try {
- Logger.printDebug(() -> "Search query: " + newText);
- fragment.filterPreferences(newText);
- // Prevent suggestions from showing during text input.
- if (showSettingsSearchHistory && autoCompleteTextView != null) {
- if (!newText.isEmpty()) {
- autoCompleteTextView.dismissDropDown();
- autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
- } else {
- autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
- }
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onQueryTextChange failure", ex);
- }
- return true;
- }
- });
-
- // Set menu and search icon.
- final int actionSearchId = getResourceIdentifier(ResourceType.ID, "action_search");
- toolbar.inflateMenu(getResourceIdentifier(ResourceType.MENU, "revanced_search_menu"));
- MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
-
- // Set menu item click listener.
- toolbar.setOnMenuItemClickListener(item -> {
- try {
- if (item.getItemId() == actionSearchId) {
- if (!isSearchActive) {
- openSearch();
- }
- return true;
- }
- } catch (Exception ex) {
- Logger.printException(() -> "menu click failure", ex);
- }
- return false;
- });
-
- // Set navigation click listener.
- toolbar.setNavigationOnClickListener(view -> {
- try {
- if (isSearchActive) {
- closeSearch();
- } else {
- activity.finish();
- }
- } catch (Exception ex) {
- Logger.printException(() -> "navigation click failure", ex);
- }
- });
- }
-
- /**
- * Sets up the search history suggestions for the SearchView with custom adapter.
- */
- private void setupSearchHistory() {
- if (autoCompleteTextView != null) {
- SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
- autoCompleteTextView.setAdapter(adapter);
- autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
- autoCompleteTextView.setLongClickable(true);
-
- // Show suggestions only when search bar is active and query is empty.
- autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
- if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
- autoCompleteTextView.showDropDown();
- }
- });
- }
- }
-
- /**
- * Saves a search query to the search history.
- * @param query The search query to save.
- */
- private void saveSearchQuery(String query) {
- if (!showSettingsSearchHistory) {
- return;
- }
- searchHistory.remove(query); // Remove if already exists to update position.
- searchHistory.addFirst(query); // Add to the most recent.
-
- // Remove extra old entries.
- while (searchHistory.size() > MAX_HISTORY_SIZE) {
- String last = searchHistory.removeLast();
- Logger.printDebug(() -> "Removing search history query: " + last);
- }
-
- saveSearchHistory();
-
- updateSearchHistoryAdapter();
- }
-
- /**
- * Removes a search query from the search history.
- * @param query The search query to remove.
- */
- private void removeSearchQuery(String query) {
- searchHistory.remove(query);
-
- saveSearchHistory();
-
- updateSearchHistoryAdapter();
- }
-
- /**
- * Save the search history to the shared preferences.
- */
- private void saveSearchHistory() {
- Logger.printDebug(() -> "Saving search history: " + searchHistory);
-
- Settings.SETTINGS_SEARCH_ENTRIES.save(
- String.join("\n", searchHistory)
- );
- }
-
- /**
- * Updates the search history adapter with the latest history.
- */
- private void updateSearchHistoryAdapter() {
- if (autoCompleteTextView == null) {
- return;
- }
-
- SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
- if (adapter != null) {
- adapter.clear();
- adapter.addAll(searchHistory);
- adapter.notifyDataSetChanged();
- }
- }
-
- public void handleOrientationChange(int newOrientation) {
- if (newOrientation != currentOrientation) {
- currentOrientation = newOrientation;
- if (autoCompleteTextView != null) {
- autoCompleteTextView.dismissDropDown();
- Logger.printDebug(() -> "Orientation changed, search history dismissed");
- }
- }
- }
-
- /**
- * Opens the search view and shows the keyboard.
- */
- private void openSearch() {
- isSearchActive = true;
- toolbar.getMenu().findItem(getResourceIdentifier(
- ResourceType.ID,
- "action_search")).setVisible(false);
- toolbar.setTitle("");
- searchContainer.setVisibility(View.VISIBLE);
- searchView.requestFocus();
-
- // Show keyboard.
- InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
-
- // Show suggestions with a slight delay.
- if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
- searchView.postDelayed(() -> {
- if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
- autoCompleteTextView.showDropDown();
- }
- }, 100); // 100ms delay to ensure focus is stable.
- }
- }
-
- /**
- * Closes the search view and hides the keyboard.
- */
- public void closeSearch() {
- isSearchActive = false;
- toolbar.getMenu().findItem(getResourceIdentifier(
- ResourceType.ID,
- "action_search")).setVisible(true);
- toolbar.setTitle(originalTitle);
- searchContainer.setVisibility(View.GONE);
- searchView.setQuery("", false);
-
- // Hide keyboard.
- InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
- }
-
- public static boolean handleBackPress() {
- if (LicenseActivityHook.searchViewController != null
- && LicenseActivityHook.searchViewController.isSearchActive()) {
- LicenseActivityHook.searchViewController.closeSearch();
- return true;
- }
- return false;
- }
-
- public boolean isSearchActive() {
- return isSearchActive;
- }
-
- /**
- * Custom ArrayAdapter for search history.
- */
- private class SearchHistoryAdapter extends ArrayAdapter {
- public SearchHistoryAdapter(Context context, List history) {
- super(context, 0, history);
- }
-
- @NonNull
- @Override
- public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
- if (convertView == null) {
- convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
- ResourceType.LAYOUT,
- "revanced_search_suggestion_item"), null);
- }
-
- // Apply rounded corners programmatically.
- convertView.setBackground(createSuggestionBackgroundDrawable());
- String query = getItem(position);
-
- // Set query text.
- TextView textView = convertView.findViewById(getResourceIdentifier(
- ResourceType.ID,
- "suggestion_text"));
- if (textView != null) {
- textView.setText(query);
- }
-
- // Set click listener for inserting query into SearchView.
- convertView.setOnClickListener(v -> {
- searchView.setQuery(query, true); // Insert selected query and submit.
- });
-
- // Set long click listener for deletion confirmation.
- convertView.setOnLongClickListener(v -> {
- Pair dialogPair = Utils.createCustomDialog(
- activity,
- query, // Title.
- str("revanced_settings_search_remove_message"), // Message.
- null, // No EditText.
- null, // OK button text.
- () -> 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 convertView;
- }
- }
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 3c24f617b..f53dd8c30 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -2,7 +2,6 @@ package app.revanced.extension.youtube.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
-import static app.revanced.extension.shared.settings.Setting.Availability;
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.settings.Setting.parentsAll;
@@ -12,14 +11,13 @@ import static app.revanced.extension.youtube.patches.ChangeHeaderPatch.HeaderLog
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
+import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerAnyModernAvailability;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHideOverlayButtonsAvailability;
+import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability;
+import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHideSubtextsAvailability;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL;
-import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
-import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_2;
-import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
-import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_4;
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.TABLET;
import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
@@ -181,16 +179,15 @@ public class Settings extends BaseSettings {
// Miniplayer
public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.DEFAULT, true);
- private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4);
- public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN);
- public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN);
+ public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, new MiniplayerAnyModernAvailability());
+ public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, new MiniplayerAnyModernAvailability());
public static final BooleanSetting MINIPLAYER_HORIZONTAL_DRAG = new BooleanSetting("revanced_miniplayer_horizontal_drag", FALSE, true, new MiniplayerHorizontalDragAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_OVERLAY_BUTTONS = new BooleanSetting("revanced_miniplayer_hide_overlay_buttons", FALSE, true, new MiniplayerHideOverlayButtonsAvailability());
- public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3, MODERN_4));
- public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, MINIPLAYER_TYPE.availability(MODERN_1));
- public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN);
- public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN);
- public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
+ public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, new MiniplayerHideSubtextsAvailability());
+ public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, new MiniplayerHideRewindOrOverlayOpacityAvailability());
+ public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, new MiniplayerAnyModernAvailability());
+ public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, new MiniplayerAnyModernAvailability());
+ public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, new MiniplayerHideRewindOrOverlayOpacityAvailability());
// External downloader
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
@@ -256,8 +253,6 @@ public class Settings extends BaseSettings {
// General layout
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
- public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
- public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
@@ -385,9 +380,9 @@ public class Settings extends BaseSettings {
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
- public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#FFFFFF", true,
+ public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#BFFFFFFF", true,
parent(SWIPE_BRIGHTNESS));
- public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#FFFFFF", true,
+ public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#BFFFFFFF", true,
parent(SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
@@ -409,7 +404,7 @@ public class Settings extends BaseSettings {
// SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
- public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
+ public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
public static final BooleanSetting SB_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED));
@@ -425,46 +420,36 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED));
- public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app");
+ public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app", parent(SB_ENABLED));
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
- public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0);
- public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L);
+ public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0, parent(SB_ENABLED));
+ public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L, parent(SB_ENABLED));
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false);
public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false);
- public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
- public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
- public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
- public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
- public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_HOOK = new StringSetting("sb_hook", IGNORE.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_HOOK_COLOR = new StringSetting("sb_hook_color", "#395699");
- public static final FloatSetting SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
- public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
- public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", IGNORE.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
- public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", IGNORE.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
- public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
- public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
- public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
- public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
- public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
+ public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#CC00D400");
+ public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#CCFFFF00");
+ public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CCCC00FF");
+ public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#CCFF1684");
+ public static final StringSetting SB_CATEGORY_HOOK = new StringSetting("sb_hook", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_HOOK_COLOR = new StringSetting("sb_hook_color", "#CC395699");
+ public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#CC00FFFF");
+ public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#CC0202ED");
+ public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#CC008FD6");
+ public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#CC7300FF");
+ public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
+ public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#CCFF9900");
+ // Dummy setting. Category is not exposed in the UI nor does it ever change.
+ public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue, false, false);
+ public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false);
// Deprecated migrations
private static final BooleanSetting DEPRECATED_HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
@@ -475,6 +460,17 @@ public class Settings extends BaseSettings {
private static final BooleanSetting DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
private static final BooleanSetting DEPRECATED_AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f, false, false);
+ public static final FloatSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f, false, false);
+
static {
// region Migration
@@ -546,6 +542,18 @@ public class Settings extends BaseSettings {
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
+ // Migrate old saved data. Must be done here before the settings can be used by any other code.
+ applyOldSbOpacityToColor(SB_CATEGORY_SPONSOR_COLOR, DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_SELF_PROMO_COLOR, DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_INTERACTION_COLOR, DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_HIGHLIGHT_COLOR, DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_HOOK_COLOR, DEPRECATED_SB_CATEGORY_HOOK_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_INTRO_COLOR, DEPRECATED_SB_CATEGORY_INTRO_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_OUTRO_COLOR, DEPRECATED_SB_CATEGORY_OUTRO_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_PREVIEW_COLOR, DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_FILLER_COLOR, DEPRECATED_SB_CATEGORY_FILLER_OPACITY);
+ applyOldSbOpacityToColor(SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY);
+
// endregion
// region SB import/export callbacks
@@ -554,4 +562,13 @@ public class Settings extends BaseSettings {
// endregion
}
+
+ private static void applyOldSbOpacityToColor(StringSetting colorSetting, FloatSetting opacitySetting) {
+ String colorString = colorSetting.get();
+ if (colorString.length() >= 8) {
+ return; // Color is already #ARGB
+ }
+
+ colorSetting.save(SponsorBlockSettings.migrateOldColorString(colorString, opacitySetting.get()));
+ }
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java
similarity index 66%
rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
rename to extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java
index 0c10dd884..3d59ca513 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/YouTubeActivityHook.java
@@ -2,8 +2,6 @@ package app.revanced.extension.youtube.settings;
import android.annotation.SuppressLint;
import android.app.Activity;
-import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceFragment;
import android.view.View;
@@ -11,18 +9,17 @@ import android.widget.Toolbar;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseActivityHook;
-import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
-import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
+import app.revanced.extension.youtube.settings.preference.YouTubePreferenceFragment;
+import app.revanced.extension.youtube.settings.search.YouTubeSearchViewController;
/**
- * Hooks LicenseActivity to inject a custom ReVancedPreferenceFragment with a toolbar and search functionality.
+ * Hooks LicenseActivity to inject a custom {@link YouTubePreferenceFragment} with a toolbar and search functionality.
*/
@SuppressWarnings("deprecation")
-public class LicenseActivityHook extends BaseActivityHook {
+public class YouTubeActivityHook extends BaseActivityHook {
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
@@ -30,16 +27,14 @@ public class LicenseActivityHook extends BaseActivityHook {
* Controller for managing search view components in the toolbar.
*/
@SuppressLint("StaticFieldLeak")
- public static SearchViewController searchViewController;
+ public static YouTubeSearchViewController searchViewController;
/**
- * Injection point
- *
- * Creates an instance of LicenseActivityHook for use in static initialization.
+ * Injection point.
*/
@SuppressWarnings("unused")
- public static LicenseActivityHook createInstance() {
- return new LicenseActivityHook();
+ public static void initialize(Activity parentActivity) {
+ BaseActivityHook.initialize(new YouTubeActivityHook(), parentActivity);
}
/**
@@ -50,7 +45,7 @@ public class LicenseActivityHook extends BaseActivityHook {
final var theme = Utils.isDarkModeEnabled()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
- activity.setTheme(Utils.getResourceIdentifier(ResourceType.STYLE, theme));
+ activity.setTheme(Utils.getResourceIdentifierOrThrow(ResourceType.STYLE, theme));
}
/**
@@ -58,7 +53,7 @@ public class LicenseActivityHook extends BaseActivityHook {
*/
@Override
protected int getContentViewResourceId() {
- return Utils.getResourceIdentifier(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
+ return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**
@@ -77,7 +72,7 @@ public class LicenseActivityHook extends BaseActivityHook {
*/
@Override
protected Drawable getNavigationIcon() {
- return ReVancedPreferenceFragment.getBackButtonDrawable();
+ return YouTubePreferenceFragment.getBackButtonDrawable();
}
/**
@@ -89,7 +84,7 @@ public class LicenseActivityHook extends BaseActivityHook {
}
/**
- * Adds search view components to the toolbar for ReVancedPreferenceFragment.
+ * Adds search view components to the toolbar for {@link YouTubePreferenceFragment}.
*
* @param activity The activity hosting the toolbar.
* @param toolbar The configured toolbar.
@@ -97,32 +92,18 @@ public class LicenseActivityHook extends BaseActivityHook {
*/
@Override
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
- if (fragment instanceof ReVancedPreferenceFragment) {
- searchViewController = SearchViewController.addSearchViewComponents(
- activity, toolbar, (ReVancedPreferenceFragment) fragment);
+ if (fragment instanceof YouTubePreferenceFragment) {
+ searchViewController = YouTubeSearchViewController.addSearchViewComponents(
+ activity, toolbar, (YouTubePreferenceFragment) fragment);
}
}
/**
- * Creates a new ReVancedPreferenceFragment for the activity.
+ * Creates a new {@link YouTubePreferenceFragment} for the activity.
*/
@Override
protected PreferenceFragment createPreferenceFragment() {
- return new ReVancedPreferenceFragment();
- }
-
- /**
- * Injection point.
- * Overrides the ReVanced settings language.
- */
- @SuppressWarnings("unused")
- public static Context getAttachBaseContext(Context original) {
- AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
- if (language == AppLanguage.DEFAULT) {
- return original;
- }
-
- return Utils.getContext();
+ return new YouTubePreferenceFragment();
}
/**
@@ -165,12 +146,14 @@ public class LicenseActivityHook extends BaseActivityHook {
}
/**
- * Handles configuration changes, such as orientation, to update the search view.
+ * Injection point.
+ *
+ * Overrides {@link Activity#finish()} of the injection Activity.
+ *
+ * @return if the original activity finish method should be allowed to run.
*/
@SuppressWarnings("unused")
- public static void handleConfigurationChanged(Activity activity, Configuration newConfig) {
- if (searchViewController != null) {
- searchViewController.handleOrientationChange(newConfig.orientation);
- }
+ public static boolean handleFinish() {
+ return YouTubeSearchViewController.handleFinish(searchViewController);
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
index e5a16947f..fd213ca0e 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java
@@ -2,6 +2,7 @@ package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
+import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
/**
* Allows tapping the DeArrow about preference to open the DeArrow website.
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java
index 5e25ae10c..4ed2198ee 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java
@@ -34,9 +34,9 @@ import java.util.Objects;
import java.util.function.Function;
import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
+import app.revanced.extension.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
/**
@@ -211,7 +211,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null;
adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter(
context,
- Utils.getResourceIdentifier(ResourceType.LAYOUT, "revanced_custom_list_item_checked"),
+ LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
getEntries(),
getEntryValues(),
usingCustomDownloader
@@ -303,7 +303,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
contentLayout.addView(editText);
// Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
getTitle() != null ? getTitle().toString() : "",
null,
@@ -313,7 +313,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
String newValue = editText.getText().toString().trim();
if (newValue.isEmpty()) {
// Show dialog if EditText is empty.
- Utils.createCustomDialog(
+ CustomDialog.create(
context,
str("revanced_external_downloader_name_title"),
str("revanced_external_downloader_empty_warning"),
@@ -416,7 +416,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
? str("revanced_external_downloader_not_installed_warning", downloader.name)
: str("revanced_external_downloader_package_not_found_warning", packageName);
- Utils.createCustomDialog(
+ CustomDialog.create(
context,
str("revanced_external_downloader_not_found_title"),
message,
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
deleted file mode 100644
index cd82aacca..000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ /dev/null
@@ -1,487 +0,0 @@
-package app.revanced.extension.youtube.settings.preference;
-
-import static app.revanced.extension.shared.StringRef.str;
-import static app.revanced.extension.shared.Utils.getResourceIdentifier;
-
-import android.app.Dialog;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.BackgroundColorSpan;
-import android.widget.Toolbar;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.Nullable;
-
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.BaseSettings;
-import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
-import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
-import app.revanced.extension.youtube.settings.LicenseActivityHook;
-import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
-
-/**
- * Preference fragment for ReVanced settings.
- */
-@SuppressWarnings({"deprecation", "NewApi"})
-public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
-
- /**
- * The main PreferenceScreen used to display the current set of preferences.
- * This screen is manipulated during initialization and filtering to show or hide preferences.
- */
- private PreferenceScreen preferenceScreen;
-
- /**
- * A copy of the original PreferenceScreen created during initialization.
- * Used to restore the preference structure to its initial state after filtering or other modifications.
- */
- private PreferenceScreen originalPreferenceScreen;
-
- /**
- * Used for searching preferences. A Collection of all preferences including nested preferences.
- * Root preferences are excluded (no need to search what's on the root screen),
- * but their sub preferences are included.
- */
- private final List> allPreferences = new ArrayList<>();
-
- /**
- * Initializes the preference fragment, copying the original screen to allow full restoration.
- */
- @Override
- protected void initialize() {
- super.initialize();
-
- try {
- preferenceScreen = getPreferenceScreen();
- Utils.sortPreferenceGroups(preferenceScreen);
-
- // Store the original structure for restoration after filtering.
- originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
- for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) {
- originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i));
- }
-
- setPreferenceScreenToolbar(preferenceScreen);
- } catch (Exception ex) {
- Logger.printException(() -> "initialize failure", ex);
- }
- }
-
- /**
- * Called when the fragment starts, ensuring all preferences are collected after initialization.
- */
- @Override
- public void onStart() {
- super.onStart();
- try {
- if (allPreferences.isEmpty()) {
- // Must collect preferences on start and not in initialize since
- // legacy SB settings are not loaded yet.
- Logger.printDebug(() -> "Collecting preferences to search");
-
- // Do not show root menu preferences in search results.
- // Instead search for everything that's not shown when search is not active.
- collectPreferences(preferenceScreen, 1, 0);
- }
- } catch (Exception ex) {
- Logger.printException(() -> "onStart failure", ex);
- }
- }
-
- /**
- * Sets toolbar for all nested preference screens.
- */
- @Override
- protected void customizeToolbar(Toolbar toolbar) {
- LicenseActivityHook.setToolbarLayoutParams(toolbar);
- }
-
- /**
- * Perform actions after toolbar setup.
- */
- @Override
- protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
- if (LicenseActivityHook.searchViewController != null
- && LicenseActivityHook.searchViewController.isSearchActive()) {
- toolbar.post(() -> LicenseActivityHook.searchViewController.closeSearch());
- }
- }
-
- /**
- * Recursively collects all preferences from the screen or group.
- *
- * @param includeDepth Menu depth to start including preferences.
- * A value of 0 adds all preferences.
- */
- private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) {
- for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
- Preference preference = group.getPreference(i);
- if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory)
- && !(preference instanceof SponsorBlockPreferenceGroup)) {
-
- AbstractPreferenceSearchData> data;
- if (preference instanceof SwitchPreference switchPref) {
- data = new SwitchPreferenceSearchData(switchPref);
- } else if (preference instanceof ListPreference listPref) {
- data = new ListPreferenceSearchData(listPref);
- } else {
- data = new PreferenceSearchData(preference);
- }
-
- allPreferences.add(data);
- }
-
- if (preference instanceof PreferenceGroup subGroup) {
- collectPreferences(subGroup, includeDepth, currentDepth + 1);
- }
- }
- }
-
- /**
- * Filters the preferences using the given query string and applies highlighting.
- */
- public void filterPreferences(String query) {
- preferenceScreen.removeAll();
-
- if (TextUtils.isEmpty(query)) {
- // Restore original preferences and their titles/summaries/entries.
- for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) {
- preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i));
- }
-
- for (AbstractPreferenceSearchData> data : allPreferences) {
- data.clearHighlighting();
- }
-
- return;
- }
-
- // Navigation path -> Category
- Map categoryMap = new HashMap<>();
- String queryLower = Utils.removePunctuationToLowercase(query);
-
- Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
- Pattern.CASE_INSENSITIVE);
-
- for (AbstractPreferenceSearchData> data : allPreferences) {
- if (data.matchesSearchQuery(queryLower)) {
- data.applyHighlighting(queryLower, queryPattern);
-
- String navigationPath = data.navigationPath;
- PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
- PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
- newGroup.setTitle(navigationPath);
- preferenceScreen.addPreference(newGroup);
- return newGroup;
- });
- group.addPreference(data.preference);
- }
- }
-
- // Show 'No results found' if search results are empty.
- if (categoryMap.isEmpty()) {
- Preference noResultsPreference = new Preference(preferenceScreen.getContext());
- noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
- noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
- noResultsPreference.setSelectable(false);
- // Set icon for the placeholder preference.
- noResultsPreference.setLayoutResource(getResourceIdentifier(
- ResourceType.LAYOUT,
- "revanced_preference_with_icon_no_search_result"));
- noResultsPreference.setIcon(getResourceIdentifier(
- ResourceType.DRAWABLE,
- "revanced_settings_search_icon"));
- preferenceScreen.addPreference(noResultsPreference);
- }
- }
-}
-
-@SuppressWarnings("deprecation")
-class AbstractPreferenceSearchData {
- /**
- * @return The navigation path for the given preference, such as "Player > Action buttons".
- */
- private static String getPreferenceNavigationString(Preference preference) {
- Deque pathElements = new ArrayDeque<>();
-
- while (true) {
- preference = preference.getParent();
-
- if (preference == null) {
- if (pathElements.isEmpty()) {
- return "";
- }
- Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
- return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
- }
-
- if (!(preference instanceof NoTitlePreferenceCategory)
- && !(preference instanceof SponsorBlockPreferenceGroup)) {
- CharSequence title = preference.getTitle();
- if (title != null && title.length() > 0) {
- pathElements.addFirst(title);
- }
- }
- }
- }
-
- /**
- * Highlights the search query in the given text by applying color span.
- * @param text The original text to process.
- * @param queryPattern The search query to highlight.
- * @return The text with highlighted query matches as a SpannableStringBuilder.
- */
- static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
- if (TextUtils.isEmpty(text)) {
- return text;
- }
-
- final int adjustedColor = Utils.adjustColorBrightness(Utils.getAppBackgroundColor(),
- 0.95f, 1.20f);
- BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
-
- SpannableStringBuilder spannable = new SpannableStringBuilder(text);
- Matcher matcher = queryPattern.matcher(text);
-
- while (matcher.find()) {
- spannable.setSpan(
- highlightSpan,
- matcher.start(),
- matcher.end(),
- SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
- );
- }
-
- return spannable;
- }
-
- final T preference;
- final String key;
- final String navigationPath;
- boolean highlightingApplied;
-
- @Nullable
- CharSequence originalTitle;
- @Nullable
- String searchTitle;
-
- AbstractPreferenceSearchData(T pref) {
- preference = pref;
- key = Utils.removePunctuationToLowercase(pref.getKey());
- navigationPath = getPreferenceNavigationString(pref);
- }
-
- @CallSuper
- void updateSearchDataIfNeeded() {
- if (highlightingApplied) {
- // Must clear, otherwise old highlighting is still applied.
- clearHighlighting();
- }
-
- CharSequence title = preference.getTitle();
- if (originalTitle != title) { // Check using reference equality.
- originalTitle = title;
- searchTitle = Utils.removePunctuationToLowercase(title);
- }
- }
-
- @CallSuper
- boolean matchesSearchQuery(String query) {
- updateSearchDataIfNeeded();
-
- return key.contains(query)
- || searchTitle != null && searchTitle.contains(query);
- }
-
- @CallSuper
- void applyHighlighting(String query, Pattern queryPattern) {
- preference.setTitle(highlightSearchQuery(originalTitle, queryPattern));
- highlightingApplied = true;
- }
-
- @CallSuper
- void clearHighlighting() {
- if (highlightingApplied) {
- preference.setTitle(originalTitle);
- highlightingApplied = false;
- }
- }
-}
-
-/**
- * Regular preference type that only uses the base preference summary.
- * Should only be used if a more specific data class does not exist.
- */
-@SuppressWarnings("deprecation")
-class PreferenceSearchData extends AbstractPreferenceSearchData {
- @Nullable
- CharSequence originalSummary;
- @Nullable
- String searchSummary;
-
- PreferenceSearchData(Preference pref) {
- super(pref);
- }
-
- void updateSearchDataIfNeeded() {
- super.updateSearchDataIfNeeded();
-
- CharSequence summary = preference.getSummary();
- if (originalSummary != summary) {
- originalSummary = summary;
- searchSummary = Utils.removePunctuationToLowercase(summary);
- }
- }
-
- boolean matchesSearchQuery(String query) {
- return super.matchesSearchQuery(query)
- || searchSummary != null && searchSummary.contains(query);
- }
-
- @Override
- void applyHighlighting(String query, Pattern queryPattern) {
- super.applyHighlighting(query, queryPattern);
-
- preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
- }
-
- @CallSuper
- void clearHighlighting() {
- if (highlightingApplied) {
- preference.setSummary(originalSummary);
- }
-
- super.clearHighlighting();
- }
-}
-
-/**
- * Switch preference type that uses summaryOn and summaryOff.
- */
-@SuppressWarnings("deprecation")
-class SwitchPreferenceSearchData extends AbstractPreferenceSearchData {
- @Nullable
- CharSequence originalSummaryOn, originalSummaryOff;
- @Nullable
- String searchSummaryOn, searchSummaryOff;
-
- SwitchPreferenceSearchData(SwitchPreference pref) {
- super(pref);
- }
-
- void updateSearchDataIfNeeded() {
- super.updateSearchDataIfNeeded();
-
- CharSequence summaryOn = preference.getSummaryOn();
- if (originalSummaryOn != summaryOn) {
- originalSummaryOn = summaryOn;
- searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
- }
-
- CharSequence summaryOff = preference.getSummaryOff();
- if (originalSummaryOff != summaryOff) {
- originalSummaryOff = summaryOff;
- searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
- }
- }
-
- boolean matchesSearchQuery(String query) {
- return super.matchesSearchQuery(query)
- || searchSummaryOn != null && searchSummaryOn.contains(query)
- || searchSummaryOff != null && searchSummaryOff.contains(query);
- }
-
- @Override
- void applyHighlighting(String query, Pattern queryPattern) {
- super.applyHighlighting(query, queryPattern);
-
- preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
- preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
- }
-
- @CallSuper
- void clearHighlighting() {
- if (highlightingApplied) {
- preference.setSummaryOn(originalSummaryOn);
- preference.setSummaryOff(originalSummaryOff);
- }
-
- super.clearHighlighting();
- }
-}
-
-/**
- * List preference type that uses entries.
- */
-@SuppressWarnings("deprecation")
-class ListPreferenceSearchData extends AbstractPreferenceSearchData {
- @Nullable
- CharSequence[] originalEntries;
- @Nullable
- String searchEntries;
-
- ListPreferenceSearchData(ListPreference pref) {
- super(pref);
- }
-
- void updateSearchDataIfNeeded() {
- super.updateSearchDataIfNeeded();
-
- CharSequence[] entries = preference.getEntries();
- if (originalEntries != entries) {
- originalEntries = entries;
- searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
- }
- }
-
- boolean matchesSearchQuery(String query) {
- return super.matchesSearchQuery(query)
- || searchEntries != null && searchEntries.contains(query);
- }
-
- @Override
- void applyHighlighting(String query, Pattern queryPattern) {
- super.applyHighlighting(query, queryPattern);
-
- if (originalEntries != null) {
- final int length = originalEntries.length;
- CharSequence[] highlightedEntries = new CharSequence[length];
-
- for (int i = 0; i < length; i++) {
- highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
-
- // Cannot highlight the summary text, because ListPreference uses
- // the toString() of the summary CharSequence which strips away all formatting.
- }
-
- preference.setEntries(highlightedEntries);
- }
- }
-
- @CallSuper
- void clearHighlighting() {
- if (highlightingApplied) {
- preference.setEntries(originalEntries);
- }
-
- super.clearHighlighting();
- }
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofAudioSelectorListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofAudioSelectorListPreference.java
index d27230421..00c3f2007 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofAudioSelectorListPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofAudioSelectorListPreference.java
@@ -6,7 +6,9 @@ import android.content.Context;
import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.SortedListPreference;
+import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
+import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "unused"})
public class SpoofAudioSelectorListPreference extends SortedListPreference {
@@ -14,10 +16,14 @@ public class SpoofAudioSelectorListPreference extends SortedListPreference {
private final boolean available;
{
- if (SpoofVideoStreamsPatch.getLanguageOverride() != null) {
+ final boolean isAndroidStudio = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_CREATOR;
+
+ if (isAndroidStudio || SpoofVideoStreamsPatch.getLanguageOverride() != null) {
available = false;
super.setEnabled(false);
- super.setSummary(str("revanced_spoof_video_streams_language_not_available"));
+ super.setSummary(str(isAndroidStudio
+ ? "revanced_spoof_video_streams_language_android_studio"
+ : "revanced_spoof_video_streams_language_not_available"));
} else {
available = true;
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
index ae84c9dc2..b1e5471cf 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java
@@ -14,6 +14,7 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.shared.settings.preference.BulletPointPreference;
import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.youtube.settings.Settings;
@@ -95,9 +96,11 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
+ '\n' + str("revanced_spoof_video_streams_about_kids_videos");
} else if (clientType == ClientType.ANDROID_CREATOR) {
summary += '\n' + str("revanced_spoof_video_streams_about_no_av1")
+ + '\n' + str("revanced_spoof_video_streams_about_no_force_original_audio")
+ '\n' + str("revanced_spoof_video_streams_about_kids_videos");
}
- setSummary(summary);
+ // Use better formatting for bullet points.
+ setSummary(BulletPointPreference.formatIntoBulletPoints(summary));
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/YouTubePreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/YouTubePreferenceFragment.java
new file mode 100644
index 000000000..1cd1c80c4
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/YouTubePreferenceFragment.java
@@ -0,0 +1,80 @@
+package app.revanced.extension.youtube.settings.preference;
+
+import android.app.Dialog;
+import android.preference.PreferenceScreen;
+import android.widget.Toolbar;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
+import app.revanced.extension.youtube.settings.YouTubeActivityHook;
+
+/**
+ * Preference fragment for ReVanced settings.
+ */
+@SuppressWarnings("deprecation")
+public class YouTubePreferenceFragment extends ToolbarPreferenceFragment {
+ /**
+ * The main PreferenceScreen used to display the current set of preferences.
+ */
+ private PreferenceScreen preferenceScreen;
+
+ /**
+ * Initializes the preference fragment.
+ */
+ @Override
+ protected void initialize() {
+ super.initialize();
+
+ try {
+ preferenceScreen = getPreferenceScreen();
+ Utils.sortPreferenceGroups(preferenceScreen);
+ setPreferenceScreenToolbar(preferenceScreen);
+ } catch (Exception ex) {
+ Logger.printException(() -> "initialize failure", ex);
+ }
+ }
+
+ /**
+ * Called when the fragment starts.
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+ try {
+ // Initialize search controller if needed.
+ if (YouTubeActivityHook.searchViewController != null) {
+ // Trigger search data collection after fragment is ready.
+ YouTubeActivityHook.searchViewController.initializeSearchData();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onStart failure", ex);
+ }
+ }
+
+ /**
+ * Sets toolbar for all nested preference screens.
+ */
+ @Override
+ protected void customizeToolbar(Toolbar toolbar) {
+ YouTubeActivityHook.setToolbarLayoutParams(toolbar);
+ }
+
+ /**
+ * Perform actions after toolbar setup.
+ */
+ @Override
+ protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
+ if (YouTubeActivityHook.searchViewController != null
+ && YouTubeActivityHook.searchViewController.isSearchActive()) {
+ toolbar.post(() -> YouTubeActivityHook.searchViewController.closeSearch());
+ }
+ }
+
+ /**
+ * Returns the preference screen for external access by SearchViewController.
+ */
+ public PreferenceScreen getPreferenceScreenForSearch() {
+ return preferenceScreen;
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchResultsAdapter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchResultsAdapter.java
new file mode 100644
index 000000000..e5497c60a
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchResultsAdapter.java
@@ -0,0 +1,28 @@
+package app.revanced.extension.youtube.settings.search;
+
+import android.content.Context;
+import android.preference.PreferenceScreen;
+
+import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
+import app.revanced.extension.shared.settings.search.BaseSearchViewController;
+import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
+
+import java.util.List;
+
+/**
+ * YouTube-specific search results adapter.
+ */
+@SuppressWarnings("deprecation")
+public class YouTubeSearchResultsAdapter extends BaseSearchResultsAdapter {
+
+ public YouTubeSearchResultsAdapter(Context context, List items,
+ BaseSearchViewController.BasePreferenceFragment fragment,
+ BaseSearchViewController searchViewController) {
+ super(context, items, fragment, searchViewController);
+ }
+
+ @Override
+ protected PreferenceScreen getMainPreferenceScreen() {
+ return fragment.getPreferenceScreenForSearch();
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java
new file mode 100644
index 000000000..b979994ea
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/search/YouTubeSearchViewController.java
@@ -0,0 +1,70 @@
+package app.revanced.extension.youtube.settings.search;
+
+import android.app.Activity;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.view.View;
+import android.widget.Toolbar;
+
+import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
+import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
+import app.revanced.extension.shared.settings.search.BaseSearchViewController;
+import app.revanced.extension.youtube.settings.preference.YouTubePreferenceFragment;
+import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
+
+/**
+ * YouTube-specific search view controller implementation.
+ */
+@SuppressWarnings("deprecation")
+public class YouTubeSearchViewController extends BaseSearchViewController {
+
+ public static YouTubeSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
+ YouTubePreferenceFragment fragment) {
+ return new YouTubeSearchViewController(activity, toolbar, fragment);
+ }
+
+ private YouTubeSearchViewController(Activity activity, Toolbar toolbar, YouTubePreferenceFragment fragment) {
+ super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
+ }
+
+ @Override
+ protected BaseSearchResultsAdapter createSearchResultsAdapter() {
+ return new YouTubeSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
+ }
+
+ @Override
+ protected boolean isSpecialPreferenceGroup(Preference preference) {
+ return preference instanceof SponsorBlockPreferenceGroup;
+ }
+
+ @Override
+ protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
+ }
+
+ // Static method for Activity finish.
+ public static boolean handleFinish(YouTubeSearchViewController searchViewController) {
+ if (searchViewController != null && searchViewController.isSearchActive()) {
+ searchViewController.closeSearch();
+ return true;
+ }
+ return false;
+ }
+
+ // Adapter to wrap YouTubePreferenceFragment to BasePreferenceFragment interface.
+ private record PreferenceFragmentAdapter(YouTubePreferenceFragment fragment) implements BasePreferenceFragment {
+ @Override
+ public PreferenceScreen getPreferenceScreenForSearch() {
+ return fragment.getPreferenceScreenForSearch();
+ }
+
+ @Override
+ public View getView() {
+ return fragment.getView();
+ }
+
+ @Override
+ public Activity getActivity() {
+ return fragment.getActivity();
+ }
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java
index d3df3f230..986c5d3b0 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java
@@ -4,10 +4,9 @@ import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
+import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
@@ -47,9 +46,10 @@ import kotlin.Unit;
/**
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
- *
+ *
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
+@SuppressLint("NewApi")
public class SegmentPlaybackController {
/**
@@ -121,7 +121,6 @@ public class SegmentPlaybackController {
/**
* Used to prevent re-showing a previously hidden skip button when exiting an embedded segment.
* Only used when {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
- *
* A collection of segments that have automatically hidden the skip button for, and all segments in this list
* contain the current video time. Segment are removed when playback exits the segment.
*/
@@ -866,23 +865,11 @@ public class SegmentPlaybackController {
Window window = dialog.getWindow();
if (window != null) {
- // Remove window animations and use custom fade animation.
- window.setWindowAnimations(0);
-
- WindowManager.LayoutParams params = window.getAttributes();
- params.gravity = Gravity.BOTTOM;
- params.y = dipToPixels(72);
- int portraitWidth = Utils.percentageWidthToPixels(60); // 60% of the screen width.
-
- if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
- portraitWidth = Math.min(portraitWidth, Utils.percentageHeightToPixels(60)); // 60% of the screen height.
- }
- params.width = portraitWidth;
- params.dimAmount = 0.0f;
- window.setAttributes(params);
- window.setBackgroundDrawable(null);
+ window.setWindowAnimations(0); // Remove window animations and use custom fade animation.
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
+
+ Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 72, 60, true);
}
if (dismissUndoToast()) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
index 20a5f3af5..a8c76628c 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
@@ -14,16 +14,19 @@ import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
+import java.util.Locale;
import java.util.UUID;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
-import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
+import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
+@SuppressWarnings("NewApi")
public class SponsorBlockSettings {
/**
* Minimum length a SB user id must be, as set by SB API.
@@ -50,11 +53,15 @@ public class SponsorBlockSettings {
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
- // clear existing behavior, as browser plugin exports no behavior for ignored categories
+ // Clear existing behavior, as browser plugin exports no behavior for ignored categories.
category.setBehaviour(CategoryBehaviour.IGNORE);
if (barTypesObject.has(category.keyValue)) {
JSONObject categoryObject = barTypesObject.getJSONObject(category.keyValue);
- category.setColor(categoryObject.getString("color"));
+ // Older ReVanced SB exports lack an opacity value.
+ if (categoryObject.has("color") && categoryObject.has("opacity")) {
+ category.setColorWithOpacity(categoryObject.getString("color"));
+ category.setOpacity((float) categoryObject.getDouble("opacity"));
+ }
}
}
@@ -64,7 +71,7 @@ public class SponsorBlockSettings {
String categoryKey = categorySelectionObject.getString("name");
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
if (category == null) {
- continue; // unsupported category, ignore
+ continue; // Unsupported category, ignore.
}
final int desktopValue = categorySelectionObject.getInt("option");
@@ -73,7 +80,7 @@ public class SponsorBlockSettings {
Utils.showToastLong(categoryKey + " unknown behavior key: " + categoryKey);
} else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) {
Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue);
- category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // use closest match
+ category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // Use closest match.
} else {
category.setBehaviour(behaviour);
}
@@ -93,7 +100,7 @@ public class SponsorBlockSettings {
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save(settingsJson.getBoolean("showTimeWithSkips"));
String serverAddress = settingsJson.getString("serverAddress");
- if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format
+ if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format.
Settings.SB_API_URL.save(serverAddress);
}
@@ -103,7 +110,7 @@ public class SponsorBlockSettings {
}
Settings.SB_SEGMENT_MIN_DURATION.save(minDuration);
- if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced
+ if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced.
int skipCount = settingsJson.getInt("skipCount");
if (skipCount < 0) {
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
@@ -121,7 +128,7 @@ public class SponsorBlockSettings {
Utils.showToastLong(str("revanced_sb_settings_import_successful"));
} catch (Exception ex) {
- Logger.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
+ Logger.printInfo(() -> "failed to import settings", ex); // Use info level, as we are showing our own toast.
Utils.showToastLong(str("revanced_sb_settings_import_failed", ex.getMessage()));
}
}
@@ -133,14 +140,16 @@ public class SponsorBlockSettings {
Logger.printDebug(() -> "Creating SponsorBlock export settings string");
JSONObject json = new JSONObject();
- JSONObject barTypesObject = new JSONObject(); // categories' colors
- JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
+ JSONObject barTypesObject = new JSONObject(); // Categories' colors.
+ JSONArray categorySelectionsArray = new JSONArray(); // Categories' behavior.
SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted();
for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.keyValue;
- categoryObject.put("color", category.getColorString());
+ // SB settings use separate color and opacity.
+ categoryObject.put("color", category.getColorStringWithoutOpacity());
+ categoryObject.put("opacity", category.getOpacity());
barTypesObject.put(categoryKey, categoryObject);
if (category.behaviour != CategoryBehaviour.IGNORE) {
@@ -167,7 +176,7 @@ public class SponsorBlockSettings {
return json.toString(2);
} catch (Exception ex) {
- Logger.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
+ Logger.printInfo(() -> "failed to export settings", ex); // Use info level, as we are showing our own toast.
Utils.showToastLong(str("revanced_sb_settings_export_failed", ex));
return "";
}
@@ -184,7 +193,7 @@ public class SponsorBlockSettings {
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
// Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
dialogContext,
null, // No title.
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
@@ -217,15 +226,12 @@ public class SponsorBlockSettings {
return false;
}
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
- // Could use Patterns.compile, but this is simpler
+ // Could use Patterns.compile, but this is simpler.
final int lastDotIndex = serverAddress.lastIndexOf('.');
- if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) {
- return false;
- }
+ return lastDotIndex > 0 && !serverAddress.substring(lastDotIndex).contains("/");
// Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)"
// but that should not be done on the main thread.
// Instead, assume the domain exists and the user knows what they're doing.
- return true;
}
/**
@@ -251,6 +257,22 @@ public class SponsorBlockSettings {
return uuid;
}
+ public static String migrateOldColorString(String colorString, float opacity) {
+ if (colorString.length() >= 8) {
+ return colorString;
+ }
+
+ // Change color string from #RGB to #ARGB using default alpha.
+ if (colorString.startsWith("#")) {
+ colorString = colorString.substring(1);
+ }
+
+ String alphaHex = String.format(Locale.US, "%02X", (int)(opacity * 255));
+ String argbColorString = '#' + alphaHex + colorString.substring(0, 6);
+ Logger.printDebug(() -> "Migrating old color string with default opacity: " + argbColorString);
+ return argbColorString;
+ }
+
private static boolean initialized;
public static void initialize() {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
index 59ee84544..cf74549e4 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
@@ -13,8 +13,6 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.EditText;
-import androidx.annotation.NonNull;
-
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
import java.time.Duration;
@@ -53,11 +51,11 @@ public class SponsorBlockUtils {
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
- // start
+ // Start.
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
break;
case DialogInterface.BUTTON_POSITIVE:
- // end
+ // End.
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
break;
}
@@ -98,7 +96,7 @@ public class SponsorBlockUtils {
SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights();
CharSequence[] titles = new CharSequence[categories.length];
for (int i = 0, length = categories.length; i < length; i++) {
- titles[i] = categories[i].getTitleWithColorDot();
+ titles[i] = categories[i].getTitle().toString();
}
newUserCreatedSegmentCategory = null;
@@ -163,7 +161,7 @@ public class SponsorBlockUtils {
SponsorSegment segment = segments[which];
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
- ? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
+ ? SegmentVote.voteTypesWithoutCategoryChange // Highlight segments cannot change category.
: SegmentVote.values();
final int voteOptionsLength = voteOptions.length;
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
@@ -282,7 +280,7 @@ public class SponsorBlockUtils {
}
}
- public static void onVotingClicked(@NonNull Context context) {
+ public static void onVotingClicked(Context context) {
try {
Utils.verifyOnMainThread();
SponsorSegment[] segments = SegmentPlaybackController.getSegments();
@@ -304,7 +302,7 @@ public class SponsorBlockUtils {
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
- spannableBuilder.append(segment.category.getTitleWithColorDot());
+ spannableBuilder.append(segment.category.getTitle().toString());
spannableBuilder.append('\n');
String startTime = formatSegmentTime(segment.start);
@@ -317,7 +315,7 @@ public class SponsorBlockUtils {
}
if (i + 1 != numberOfSegments) {
- // prevents trailing new line after last segment
+ // Prevents trailing new line after last segment.
spannableBuilder.append('\n');
}
@@ -333,13 +331,13 @@ public class SponsorBlockUtils {
}
}
- private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) {
+ private static void onNewCategorySelect(SponsorSegment segment, Context context) {
try {
Utils.verifyOnMainThread();
final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights();
CharSequence[] titles = new CharSequence[values.length];
- for (int i = 0; i < values.length; i++) {
- titles[i] = values[i].getTitleWithColorDot();
+ for (int i = 0, length = values.length; i < length; i++) {
+ titles[i] = values[i].getTitle().toString();
}
new AlertDialog.Builder(context)
@@ -370,7 +368,6 @@ public class SponsorBlockUtils {
}
}
-
static void sendViewRequestAsync(SponsorSegment segment) {
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
return;
@@ -424,7 +421,6 @@ public class SponsorBlockUtils {
String secondsStr = matcher.group(4);
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
-
try {
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
//noinspection ConstantConditions
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
index fadc61d64..ce7ef84ed 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
@@ -1,16 +1,11 @@
package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.StringRef.sf;
-import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.COLOR_DOT_STRING;
import static app.revanced.extension.youtube.settings.Settings.*;
import android.graphics.Color;
import android.graphics.Paint;
-import android.text.Spannable;
-import android.text.SpannableString;
import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.RelativeSizeSpan;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -26,40 +21,39 @@ import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.StringRef;
import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.youtube.settings.Settings;
public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_segments_sponsor_sum"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
- SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
+ SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_segments_selfpromo_sum"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
- SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
+ SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_segments_interaction_sum"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
- SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
+ SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
/**
* Unique category that is treated differently than the rest.
*/
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_segments_highlight_sum"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
- SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
+ SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR),
INTRO("intro", sf("revanced_sb_segments_intro"), sf("revanced_sb_segments_intro_sum"),
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
- SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
+ SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_segments_outro_sum"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
- SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
+ SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
PREVIEW("preview", sf("revanced_sb_segments_preview"), sf("revanced_sb_segments_preview_sum"),
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
- SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
+ SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
HOOK("hook", sf("revanced_sb_segments_hook"), sf("revanced_sb_segments_hook_sum"), sf("revanced_sb_skip_button_hook"), sf("revanced_sb_skipped_hook"),
- SB_CATEGORY_HOOK, SB_CATEGORY_HOOK_COLOR, SB_CATEGORY_HOOK_OPACITY),
+ SB_CATEGORY_HOOK, SB_CATEGORY_HOOK_COLOR),
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_segments_filler_sum"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
- SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
+ SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_segments_nomusic_sum"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
- SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
+ SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR),
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
- SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
+ SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR);
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
@@ -88,10 +82,13 @@ public enum SegmentCategory {
FILLER,
MUSIC_OFFTOPIC,
};
+
+ public static final float CATEGORY_DEFAULT_OPACITY = 0.7f;
+
private static final Map mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
/**
- * Categories currently enabled, formatted for an API call
+ * Categories currently enabled, formatted for an API call.
*/
public static String sponsorBlockAPIFetchCategories = "[]";
@@ -100,21 +97,30 @@ public enum SegmentCategory {
mValuesMap.put(value.keyValue, value);
}
+ /**
+ * Returns an array of categories excluding the unsubmitted category.
+ */
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
+ /**
+ * Returns an array of categories excluding the highlight category.
+ */
public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights;
}
+ /**
+ * Retrieves a category by its key.
+ */
@Nullable
public static SegmentCategory byCategoryKey(@NonNull String key) {
return mValuesMap.get(key);
}
/**
- * Must be called if behavior of any category is changed.
+ * Updates the list of enabled categories for API calls. Must be called when any category's behavior changes.
*/
public static void updateEnabledCategories() {
Utils.verifyOnMainThread();
@@ -134,6 +140,9 @@ public enum SegmentCategory {
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
}
+ /**
+ * Loads all category settings from persistent storage.
+ */
public static void loadAllCategoriesFromSettings() {
for (SegmentCategory category : values()) {
category.loadFromSettings();
@@ -141,45 +150,35 @@ public enum SegmentCategory {
updateEnabledCategories();
}
- @ColorInt
- public static int applyOpacityToColor(@ColorInt int color, float opacity) {
- if (opacity < 0 || opacity > 1.0f) {
- throw new IllegalArgumentException("Invalid opacity: " + opacity);
- }
- final int opacityInt = (int) (255 * opacity);
- return (color & 0x00FFFFFF) | (opacityInt << 24);
- }
-
public final String keyValue;
- public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
- private final StringSetting colorSetting;
- private final FloatSetting opacitySetting;
+ public final StringSetting behaviorSetting;
+ public final StringSetting colorSetting;
public final StringRef title;
public final StringRef description;
/**
- * Skip button text, if the skip occurs in the first quarter of the video
+ * Skip button text, if the skip occurs in the first quarter of the video.
*/
public final StringRef skipButtonTextBeginning;
/**
- * Skip button text, if the skip occurs in the middle half of the video
+ * Skip button text, if the skip occurs in the middle half of the video.
*/
public final StringRef skipButtonTextMiddle;
/**
- * Skip button text, if the skip occurs in the last quarter of the video
+ * Skip button text, if the skip occurs in the last quarter of the video.
*/
public final StringRef skipButtonTextEnd;
/**
- * Skipped segment toast, if the skip occurred in the first quarter of the video
+ * Skipped segment toast, if the skip occurred in the first quarter of the video.
*/
public final StringRef skippedToastBeginning;
/**
- * Skipped segment toast, if the skip occurred in the middle half of the video
+ * Skipped segment toast, if the skip occurred in the middle half of the video.
*/
public final StringRef skippedToastMiddle;
/**
- * Skipped segment toast, if the skip occurred in the last quarter of the video
+ * Skipped segment toast, if the skip occurred in the last quarter of the video.
*/
public final StringRef skippedToastEnd;
@@ -193,7 +192,7 @@ public enum SegmentCategory {
/**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
- * Caller must also {@link #updateEnabledCategories()}.
+ * Caller must also call {@link #updateEnabledCategories()}.
*/
public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE;
@@ -201,19 +200,19 @@ public enum SegmentCategory {
StringRef skipButtonText,
StringRef skippedToastText,
StringSetting behavior,
- StringSetting color, FloatSetting opacity) {
+ StringSetting color) {
this(keyValue, title, description,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
behavior,
- color, opacity);
+ color);
}
SegmentCategory(String keyValue, StringRef title, StringRef description,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
StringSetting behavior,
- StringSetting color, FloatSetting opacity) {
+ StringSetting color) {
this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title);
this.description = Objects.requireNonNull(description);
@@ -225,11 +224,13 @@ public enum SegmentCategory {
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color);
- this.opacitySetting = Objects.requireNonNull(opacity);
this.paint = new Paint();
loadFromSettings();
}
+ /**
+ * Loads the category's behavior and color from settings.
+ */
private void loadFromSettings() {
String behaviorString = behaviorSetting.get();
CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString);
@@ -242,118 +243,93 @@ public enum SegmentCategory {
this.behaviour = savedBehavior;
String colorString = colorSetting.get();
- final float opacity = opacitySetting.get();
try {
- setColor(colorString);
- setOpacity(opacity);
+ setColorWithOpacity(colorString);
} catch (Exception ex) {
- Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
+ Logger.printException(() -> "Invalid color: " + colorString, ex);
colorSetting.resetToDefault();
- opacitySetting.resetToDefault();
loadFromSettings();
}
}
+ /**
+ * Sets the behavior of the category and saves it to settings.
+ */
public void setBehaviour(CategoryBehaviour behaviour) {
this.behaviour = Objects.requireNonNull(behaviour);
this.behaviorSetting.save(behaviour.reVancedKeyValue);
}
- private void updateColor() {
- color = applyOpacityToColor(color, opacitySetting.get());
+ /**
+ * Sets the segment color with opacity from a color string in #AARRGGBB format.
+ */
+ public void setColorWithOpacity(String colorString) throws IllegalArgumentException {
+ int colorWithOpacity = Color.parseColor(colorString);
+ colorSetting.save(String.format(Locale.US, "#%08X", colorWithOpacity));
+ color = colorWithOpacity;
paint.setColor(color);
}
/**
- * @param opacity Segment color opacity between [0, 1].
+ * @param opacity [0, 1] opacity value.
*/
- public void setOpacity(float opacity) throws IllegalArgumentException {
- if (opacity < 0 || opacity > 1) {
- throw new IllegalArgumentException("Invalid opacity: " + opacity);
- }
-
- opacitySetting.save(opacity);
- updateColor();
- }
-
- public float getOpacity() {
- return opacitySetting.get();
- }
-
- public float getOpacityDefault() {
- return opacitySetting.defaultValue;
- }
-
- public void resetColorAndOpacity() {
- setColor(colorSetting.defaultValue);
- setOpacity(opacitySetting.defaultValue);
+ public void setOpacity(double opacity) {
+ color = Color.argb((int) (opacity * 255), Color.red(color), Color.green(color), Color.blue(color));
+ paint.setColor(color);
}
/**
- * @param colorString Segment color with #RRGGBB format.
- */
- public void setColor(String colorString) throws IllegalArgumentException {
- color = Color.parseColor(colorString);
- colorSetting.save(colorString);
-
- updateColor();
- }
-
- /**
- * @return Integer color of #RRGGBB format.
+ * Gets the color with opacity applied (ARGB).
*/
@ColorInt
- public int getColorNoOpacity() {
- return color & 0x00FFFFFF;
+ public int getColorWithOpacity() {
+ return color;
}
/**
- * @return Integer color of #RRGGBB format.
+ * @return The default color with opacity applied.
*/
@ColorInt
- public int getColorNoOpacityDefault() {
- return Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
+ public int getDefaultColorWithOpacity() {
+ return Color.parseColor(colorSetting.defaultValue);
}
/**
- * @return Hex color string of #RRGGBB format with no opacity level.
+ * Gets the color as a hex string with opacity (#AARRGGBB).
*/
- public String getColorString() {
- return String.format(Locale.US, "#%06X", getColorNoOpacity());
- }
-
- private static SpannableString getCategoryColorDotSpan(String text, @ColorInt int color) {
- SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING + text);
- dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return dotSpan;
- }
-
- public static SpannableString getCategoryColorDot(@ColorInt int color) {
- SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING);
- dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- dotSpan.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return dotSpan;
- }
-
- public SpannableString getCategoryColorDot() {
- return getCategoryColorDot(color);
- }
-
- public SpannableString getTitleWithColorDot(@ColorInt int categoryColor) {
- return getCategoryColorDotSpan(" " + title, categoryColor);
- }
-
- public SpannableString getTitleWithColorDot() {
- return getTitleWithColorDot(color);
+ public String getColorStringWithOpacity() {
+ return String.format(Locale.US, "#%08X", getColorWithOpacity());
}
/**
- * @param segmentStartTime video time the segment category started
- * @param videoLength length of the video
- * @return the skip button text
+ * @return The color as a hex string without opacity (#RRGGBB).
+ */
+ public String getColorStringWithoutOpacity() {
+ final int colorNoOpacity = getColorWithOpacity() & 0x00FFFFFF;
+ return String.format(Locale.US, "#%06X", colorNoOpacity);
+ }
+
+ /**
+ * @return [0, 1] opacity value.
+ */
+ public double getOpacity() {
+ double opacity = Color.alpha(color) / 255.0;
+ return Math.round(opacity * 100.0) / 100.0; // Round to 2 decimal digits.
+ }
+
+ /**
+ * Gets the title of the category.
+ */
+ public StringRef getTitle() {
+ return title;
+ }
+
+ /**
+ * Gets the skip button text based on segment position.
+ *
+ * @param segmentStartTime Video time the segment category started.
+ * @param videoLength Length of the video.
+ * @return The skip button text.
*/
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
@@ -375,9 +351,11 @@ public enum SegmentCategory {
}
/**
- * @param segmentStartTime video time the segment category started
- * @param videoLength length of the video
- * @return 'skipped segment' toast message
+ * Gets the skipped segment toast message based on segment position.
+ *
+ * @param segmentStartTime Video time the segment category started.
+ * @param videoLength Length of the video.
+ * @return The skipped segment toast message.
*/
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
deleted file mode 100644
index 3da8cd5d5..000000000
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
+++ /dev/null
@@ -1,372 +0,0 @@
-package app.revanced.extension.youtube.sponsorblock.objects;
-
-import static app.revanced.extension.shared.StringRef.str;
-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.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextWatcher;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.*;
-
-import androidx.annotation.ColorInt;
-
-import java.util.Locale;
-import java.util.Objects;
-
-import app.revanced.extension.shared.Logger;
-import app.revanced.extension.shared.ResourceType;
-import app.revanced.extension.shared.Utils;
-import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
-import app.revanced.extension.shared.settings.preference.ColorPickerView;
-
-@SuppressWarnings("deprecation")
-public class SegmentCategoryListPreference extends ListPreference {
- private final SegmentCategory category;
-
- /**
- * RGB format (no alpha).
- */
- @ColorInt
- private int categoryColor;
- /**
- * [0, 1]
- */
- private float categoryOpacity;
- private int selectedDialogEntryIndex;
-
- private TextView dialogColorDotView;
- private EditText dialogColorEditText;
- private EditText dialogOpacityEditText;
- private ColorPickerView dialogColorPickerView;
- private Dialog dialog;
-
- public SegmentCategoryListPreference(Context context, SegmentCategory category) {
- super(context);
- this.category = Objects.requireNonNull(category);
-
- // Edit: Using preferences to sync together multiple pieces
- // of code is messy and should be rethought.
- setKey(category.behaviorSetting.key);
- setDefaultValue(category.behaviorSetting.defaultValue);
-
- final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
- setEntries(isHighlightCategory
- ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
- : CategoryBehaviour.getBehaviorDescriptions());
- setEntryValues(isHighlightCategory
- ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
- : CategoryBehaviour.getBehaviorKeyValues());
- super.setSummary(category.description.toString());
-
- updateUI();
- }
-
- @Override
- protected void showDialog(Bundle state) {
- try {
- Context context = getContext();
- categoryColor = category.getColorNoOpacity();
- categoryOpacity = category.getOpacity();
- selectedDialogEntryIndex = findIndexOfValue(getValue());
-
- // Create the main layout for the dialog content.
- LinearLayout contentLayout = new LinearLayout(context);
- contentLayout.setOrientation(LinearLayout.VERTICAL);
-
- // 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(dipToPixels(10), 0, 0, 0);
- contentLayout.addView(radioGroup);
-
- // Inflate the color picker view.
- View colorPickerContainer = LayoutInflater.from(context).inflate(
- getResourceIdentifier(ResourceType.LAYOUT, "revanced_color_picker"), null);
- dialogColorPickerView = colorPickerContainer.findViewById(
- getResourceIdentifier(ResourceType.ID, "revanced_color_picker_view"));
- dialogColorPickerView.setColor(categoryColor);
- contentLayout.addView(colorPickerContainer);
-
- // Grid layout for color and opacity inputs.
- GridLayout gridLayout = new GridLayout(context);
- gridLayout.setColumnCount(3);
- gridLayout.setRowCount(2);
- gridLayout.setPadding(dipToPixels(16), 0, 0, 0);
-
- GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
- gridParams.rowSpec = GridLayout.spec(0); // First row.
- gridParams.columnSpec = GridLayout.spec(0); // First column.
- TextView colorTextLabel = new TextView(context);
- colorTextLabel.setText(str("revanced_sb_color_dot_label"));
- colorTextLabel.setLayoutParams(gridParams);
- gridLayout.addView(colorTextLabel);
-
- gridParams = new GridLayout.LayoutParams();
- gridParams.rowSpec = GridLayout.spec(0); // First row.
- gridParams.columnSpec = GridLayout.spec(1); // Second column.
- gridParams.setMargins(0, 0, dipToPixels(10), 0);
- dialogColorDotView = new TextView(context);
- dialogColorDotView.setLayoutParams(gridParams);
- gridLayout.addView(dialogColorDotView);
- updateCategoryColorDot();
-
- gridParams = new GridLayout.LayoutParams();
- gridParams.rowSpec = GridLayout.spec(0); // First row.
- gridParams.columnSpec = GridLayout.spec(2); // Third column.
- dialogColorEditText = new EditText(context);
- dialogColorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
- | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- dialogColorEditText.setAutofillHints((String) null);
- dialogColorEditText.setTypeface(Typeface.MONOSPACE);
- dialogColorEditText.setTextLocale(Locale.US);
- dialogColorEditText.setText(getColorString(categoryColor));
- dialogColorEditText.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable edit) {
- try {
- String colorString = edit.toString();
- String normalizedColorString = ColorPickerPreference.cleanupColorCodeString(colorString);
-
- if (!normalizedColorString.equals(colorString)) {
- edit.replace(0, colorString.length(), normalizedColorString);
- return;
- }
-
- if (normalizedColorString.length() != ColorPickerPreference.COLOR_STRING_LENGTH) {
- // User is still typing out the color.
- return;
- }
-
- // Remove the alpha channel.
- final int newColor = Color.parseColor(colorString) & 0x00FFFFFF;
- // Changing view color causes callback into this class.
- dialogColorPickerView.setColor(newColor);
- } catch (Exception ex) {
- // Should never be reached since input is validated before using.
- Logger.printException(() -> "colorEditText afterTextChanged failure", ex);
- }
- }
- });
- gridLayout.addView(dialogColorEditText, gridParams);
-
- gridParams = new GridLayout.LayoutParams();
- gridParams.rowSpec = GridLayout.spec(1); // Second row.
- gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
- TextView opacityLabel = new TextView(context);
- opacityLabel.setText(str("revanced_sb_color_opacity_label"));
- opacityLabel.setLayoutParams(gridParams);
- gridLayout.addView(opacityLabel);
-
- gridParams = new GridLayout.LayoutParams();
- gridParams.rowSpec = GridLayout.spec(1); // Second row.
- gridParams.columnSpec = GridLayout.spec(2); // Third column.
- dialogOpacityEditText = new EditText(context);
- dialogOpacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL
- | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
- dialogOpacityEditText.setAutofillHints((String) null);
- dialogOpacityEditText.setTypeface(Typeface.MONOSPACE);
- dialogOpacityEditText.setTextLocale(Locale.US);
- dialogOpacityEditText.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable edit) {
- try {
- String editString = edit.toString();
- final int opacityStringLength = editString.length();
-
- final int maxOpacityStringLength = 4; // [0.00, 1.00]
- if (opacityStringLength > maxOpacityStringLength) {
- edit.delete(maxOpacityStringLength, opacityStringLength);
- return;
- }
-
- final float opacity = opacityStringLength == 0
- ? 0
- : Float.parseFloat(editString);
- if (opacity < 0) {
- categoryOpacity = 0;
- edit.replace(0, opacityStringLength, "0");
- return;
- } else if (opacity > 1.0f) {
- categoryOpacity = 1;
- edit.replace(0, opacityStringLength, "1.0");
- return;
- } else if (!editString.endsWith(".")) {
- // Ignore "0." and "1." until the user finishes entering a valid number.
- categoryOpacity = opacity;
- }
-
- updateCategoryColorDot();
- } catch (Exception ex) {
- // Should never happen.
- Logger.printException(() -> "opacityEditText afterTextChanged failure", ex);
- }
- }
- });
- gridLayout.addView(dialogOpacityEditText, gridParams);
- updateOpacityText();
-
- contentLayout.addView(gridLayout);
-
- // Create ScrollView to wrap the content layout.
- ScrollView contentScrollView = new ScrollView(context);
- contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
- contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
- LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- 0,
- 1.0f
- );
- contentScrollView.setLayoutParams(scrollViewParams);
- contentScrollView.addView(contentLayout);
-
- // Create the custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
- context,
- category.title.toString(), // Title.
- null, // No message (replaced by contentLayout).
- null, // No EditText.
- null, // OK button text.
- () -> {
- // OK button action.
- if (selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
- String value = getEntryValues()[selectedDialogEntryIndex].toString();
- if (callChangeListener(value)) {
- setValue(value);
- category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
- SegmentCategory.updateEnabledCategories();
- }
-
- try {
- category.setColor(dialogColorEditText.getText().toString());
- category.setOpacity(categoryOpacity);
- } catch (IllegalArgumentException ex) {
- Utils.showToastShort(str("revanced_settings_color_invalid"));
- }
-
- 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) {
- Logger.printException(() -> "resetButton onClick failure", ex);
- }
- },
- false // Do not dismiss dialog on Neutral button click.
- );
-
- // Add the ScrollView to the dialog's main layout.
- LinearLayout dialogMainLayout = dialogPair.second;
- dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
-
- // Set up color picker listener.
- // Do last to prevent listener callbacks while setting up view.
- dialogColorPickerView.setOnColorChangedListener(color -> {
- if (categoryColor == color) {
- return;
- }
- categoryColor = color;
- String hexColor = getColorString(color);
- Logger.printDebug(() -> "onColorChanged: " + hexColor);
-
- updateCategoryColorDot();
- dialogColorEditText.setText(hexColor);
- dialogColorEditText.setSelection(hexColor.length());
- });
-
- // Show the dialog.
- dialog = dialogPair.first;
- dialog.show();
- } catch (Exception ex) {
- Logger.printException(() -> "showDialog failure", ex);
- }
- }
-
- @Override
- protected void onDialogClosed(boolean positiveResult) {
- // Nullify dialog references.
- dialogColorDotView = null;
- dialogColorEditText = null;
- dialogOpacityEditText = null;
- dialogColorPickerView = null;
-
- if (dialog != null) {
- dialog.dismiss();
- dialog = null;
- }
- }
-
- @ColorInt
- private int applyOpacityToCategoryColor() {
- return applyOpacityToColor(categoryColor, categoryOpacity);
- }
-
- public void updateUI() {
- categoryColor = category.getColorNoOpacity();
- categoryOpacity = category.getOpacity();
-
- setTitle(category.getTitleWithColorDot(applyOpacityToCategoryColor()));
- }
-
- private void updateCategoryColorDot() {
- dialogColorDotView.setText(SegmentCategory.getCategoryColorDot(applyOpacityToCategoryColor()));
- }
-
- private void updateOpacityText() {
- dialogOpacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
- }
-
- @Override
- public void setSummary(CharSequence summary) {
- // Ignore calls to set the summary.
- // Summary is always the description of the category.
- //
- // This is required otherwise the ReVanced preference fragment
- // sets all ListPreference summaries to show the current selection.
- }
-}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java
new file mode 100644
index 000000000..55099d508
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryPreference.java
@@ -0,0 +1,165 @@
+package app.revanced.extension.youtube.sponsorblock.objects;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings.migrateOldColorString;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
+import app.revanced.extension.shared.ui.ColorDot;
+
+@SuppressWarnings("deprecation")
+public class SegmentCategoryPreference extends ColorPickerPreference {
+ public final SegmentCategory category;
+
+ /**
+ * View displaying a colored dot in the widget area.
+ */
+ private View widgetColorDot;
+
+ // Fields to store dialog state for the OK button handler.
+ private int selectedDialogEntryIndex;
+ private CharSequence[] entryValues;
+
+
+ public SegmentCategoryPreference(Context context, SegmentCategory category) {
+ super(context);
+ this.category = Objects.requireNonNull(category);
+
+ // Set key to color setting for persistence.
+ // Edit: Using preferences to sync together multiple pieces of code is messy and should be rethought.
+ setKey(category.colorSetting.key);
+ setTitle(category.title.toString());
+ setSummary(category.description.toString());
+
+ // Enable opacity slider for this preference.
+ setOpacitySliderEnabled(true);
+
+ setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
+
+ // Sync initial color from category.
+ setText(category.getColorStringWithOpacity());
+ updateUI();
+ }
+
+ @Override
+ public final void setText(String colorString) {
+ try {
+ // Migrate old data imported in the settings UI.
+ // This migration is needed here because pasting into the settings
+ // immediately syncs the data with the preferences.
+ colorString = migrateOldColorString(colorString, SegmentCategory.CATEGORY_DEFAULT_OPACITY);
+ super.setText(colorString);
+
+ // Save to category.
+ category.setColorWithOpacity(colorString);
+ updateUI();
+
+ // Notify the listener about the color change.
+ if (colorChangeListener != null) {
+ colorChangeListener.onColorChanged(getKey(), category.getColorWithOpacity());
+ }
+ } catch (IllegalArgumentException ex) {
+ Utils.showToastShort(str("revanced_settings_color_invalid"));
+ setText(category.colorSetting.defaultValue);
+ } catch (Exception ex) {
+ String colorStringFinal = colorString;
+ Logger.printException(() -> "setText failure: " + colorStringFinal, ex);
+ }
+ }
+
+ @Nullable
+ @Override
+ protected View createExtraDialogContentView(Context context) {
+ final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
+ entryValues = isHighlightCategory
+ ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
+ : CategoryBehaviour.getBehaviorKeyValues();
+
+ String currentBehavior = category.behaviorSetting.get();
+ selectedDialogEntryIndex = -1;
+ for (int i = 0; i < entryValues.length; i++) {
+ if (entryValues[i].equals(currentBehavior)) {
+ selectedDialogEntryIndex = i;
+ break;
+ }
+ }
+
+ RadioGroup radioGroup = new RadioGroup(context);
+ radioGroup.setOrientation(RadioGroup.VERTICAL);
+ CharSequence[] entries = isHighlightCategory
+ ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
+ : CategoryBehaviour.getBehaviorDescriptions();
+
+ 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(dipToPixels(10), 0, dipToPixels(10), dipToPixels(10));
+ return radioGroup;
+ }
+
+ @Override
+ protected void onDialogOkClicked() {
+ if (selectedDialogEntryIndex >= 0 && entryValues != null) {
+ String value = entryValues[selectedDialogEntryIndex].toString();
+ category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
+ SegmentCategory.updateEnabledCategories();
+ }
+ }
+
+ @Override
+ protected void onDialogNeutralClicked() {
+ try {
+ final int defaultColor = category.getDefaultColorWithOpacity();
+ dialogColorPickerView.setColor(defaultColor);
+ } catch (Exception ex) {
+ Logger.printException(() -> "Reset button failure", ex);
+ }
+ }
+
+ public void updateUI() {
+ try {
+ if (category.behaviorSetting != null) {
+ setEnabled(category.behaviorSetting.isAvailable());
+ }
+
+ updateWidgetColorDot();
+ } catch (Exception ex) {
+ Logger.printException(() -> "updateUI failure for category: " + category.keyValue, ex);
+ }
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
+ updateWidgetColorDot();
+ }
+
+ private void updateWidgetColorDot() {
+ if (widgetColorDot == null) return;
+
+ ColorDot.applyColorDot(
+ widgetColorDot,
+ category.getColorWithOpacity(),
+ isEnabled()
+ );
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
index 4528cfae9..b9669ec21 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
@@ -16,7 +16,7 @@ public class SponsorSegment implements Comparable {
public enum SegmentVote {
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
- CATEGORY_CHANGE(sf("revanced_sb_vote_category"), -1, true); // apiVoteType is not used for category change
+ CATEGORY_CHANGE(sf("revanced_sb_vote_category"), -1, true); // ApiVoteType is not used for category change.
public static final SegmentVote[] voteTypesWithoutCategoryChange = {
UPVOTE,
@@ -104,7 +104,7 @@ public class SponsorSegment implements Comparable {
/**
* @return The start/end time in range form.
* Range times are adjusted since it uses inclusive and Segments use exclusive.
- *
+ *
* {@link SegmentCategory#HIGHLIGHT} is unique and
* returns a range from the start of the video until the highlight.
*/
@@ -116,7 +116,7 @@ public class SponsorSegment implements Comparable {
}
/**
- * @return the length of this segment, in milliseconds. Always a positive number.
+ * @return the length of this segment, in milliseconds. Always a positive number.
*/
public long length() {
return end - start;
@@ -148,8 +148,7 @@ public class SponsorSegment implements Comparable {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (!(o instanceof SponsorSegment)) return false;
- SponsorSegment other = (SponsorSegment) o;
+ if (!(o instanceof SponsorSegment other)) return false;
return Objects.equals(UUID, other.UUID)
&& category == other.category
&& start == other.start
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java
index 175d49b94..6a5f03231 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java
@@ -1,5 +1,9 @@
package app.revanced.extension.youtube.sponsorblock.ui;
+import static app.revanced.extension.shared.Utils.getResourceColor;
+import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
+
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.GradientDrawable;
@@ -10,15 +14,10 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
-import app.revanced.extension.shared.ResourceType;
+import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
-import app.revanced.extension.shared.Logger;
-
-import static app.revanced.extension.shared.Utils.getResourceColor;
-import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
-import static app.revanced.extension.shared.Utils.getResourceIdentifier;
public final class NewSegmentLayout extends FrameLayout {
private static final ColorStateList rippleColorStateList = new ColorStateList(
@@ -46,10 +45,7 @@ public final class NewSegmentLayout extends FrameLayout {
super(context, attributeSet, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(
- getResourceIdentifier(context,
- ResourceType.LAYOUT, "revanced_sb_new_segment"),
- this,
- true
+ getResourceIdentifierOrThrow(context, "revanced_sb_new_segment", "layout"), this, true
);
initializeButton(
@@ -108,7 +104,7 @@ public final class NewSegmentLayout extends FrameLayout {
*/
private void initializeButton(final Context context, final String resourceIdentifierName,
final ButtonOnClickHandlerFunction handler, final String debugMessage) {
- ImageButton button = findViewById(getResourceIdentifier(context, ResourceType.ID, resourceIdentifierName));
+ ImageButton button = findViewById(getResourceIdentifierOrThrow(context, resourceIdentifierName, "id"));
// Add ripple effect
RippleDrawable rippleDrawable = new RippleDrawable(
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java
index 17b388bb7..9573102f3 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java
@@ -4,6 +4,7 @@ import static app.revanced.extension.shared.Utils.getResourceColor;
import static app.revanced.extension.shared.Utils.getResourceDimension;
import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
import android.content.Context;
import android.graphics.Canvas;
@@ -57,9 +58,10 @@ public class SkipSponsorButton extends FrameLayout {
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
super(context, attributeSet, defStyleAttr, defStyleRes);
- LayoutInflater.from(context).inflate(getResourceIdentifier(context, ResourceType.LAYOUT, "revanced_sb_skip_sponsor_button"), this, true); // layout:skip_ad_button
+ LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(context, ResourceType.LAYOUT, "revanced_sb_skip_sponsor_button"), this, true); // layout:skip_ad_button
setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height
- skipSponsorBtnContainer = Objects.requireNonNull(findViewById(getResourceIdentifier(context, ResourceType.ID, "revanced_sb_skip_sponsor_button_container"))); // id:skip_ad_button_container
+ skipSponsorBtnContainer = Objects.requireNonNull(findViewById(getResourceIdentifierOrThrow(
+ context, ResourceType.ID, "revanced_sb_skip_sponsor_button_container"))); // id:skip_ad_button_container
background = new Paint();
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java
index 098f8d599..d8c8a13fe 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockAboutPreference.java
@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
import android.content.Context;
import android.util.AttributeSet;
-import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
+import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
@SuppressWarnings("unused")
public class SponsorBlockAboutPreference extends UrlLinkPreference {
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java
index e0149f2f6..2d13bec8a 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java
@@ -10,11 +10,11 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
+import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceGroup;
import android.preference.SwitchPreference;
-import android.text.Html;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Pair;
@@ -29,13 +29,16 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.BooleanSetting;
+import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
+import app.revanced.extension.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
-import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
+import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryPreference;
/**
* Lots of old code that could be converted to a half dozen custom preferences,
@@ -54,27 +57,9 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
*/
private boolean preferencesInitialized;
- private SwitchPreference sbEnabled;
- private SwitchPreference addNewSegment;
- private SwitchPreference votingEnabled;
- private SwitchPreference autoHideSkipSegmentButton;
- private SwitchPreference compactSkipButton;
- private SwitchPreference squareLayout;
- private SwitchPreference showSkipToast;
- private SwitchPreference trackSkips;
- private SwitchPreference showTimeWithoutSegments;
- private SwitchPreference toastOnConnectionError;
- private CustomDialogListPreference autoHideSkipSegmentButtonDuration;
- private CustomDialogListPreference showSkipToastDuration;
-
- private ResettableEditTextPreference newSegmentStep;
- private ResettableEditTextPreference minSegmentDuration;
- private EditTextPreference privateUserId;
private EditTextPreference importExport;
- private Preference apiUrl;
- private PreferenceCategory segmentCategory;
- private final List segmentCategories = new ArrayList<>();
+ private final List segmentCategories = new ArrayList<>();
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -99,60 +84,17 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
try {
Logger.printDebug(() -> "updateUI");
- final boolean enabled = Settings.SB_ENABLED.get();
- if (!enabled) {
+ if (!Settings.SB_ENABLED.get()) {
SponsorBlockViewController.hideAll();
SegmentPlaybackController.setCurrentVideoId(null);
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
SponsorBlockViewController.hideNewSegmentLayout();
}
- // Voting and add new segment buttons automatically show/hide themselves.
SponsorBlockViewController.updateLayout();
- sbEnabled.setChecked(enabled);
-
- addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
- addNewSegment.setEnabled(enabled);
-
- votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
- votingEnabled.setEnabled(enabled);
-
- autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
- autoHideSkipSegmentButton.setEnabled(enabled);
-
- autoHideSkipSegmentButtonDuration.setValue(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().toString());
- autoHideSkipSegmentButtonDuration.setEnabled(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.isAvailable());
-
- compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
- compactSkipButton.setEnabled(enabled);
-
- showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
- showSkipToast.setEnabled(enabled);
-
- squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
- squareLayout.setEnabled(enabled);
-
- showSkipToastDuration.setValue(Settings.SB_TOAST_ON_SKIP_DURATION.get().toString());
- showSkipToastDuration.setEnabled(Settings.SB_TOAST_ON_SKIP_DURATION.isAvailable());
-
- toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
- toastOnConnectionError.setEnabled(enabled);
-
- trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
- trackSkips.setEnabled(enabled);
-
- showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
- showTimeWithoutSegments.setEnabled(enabled);
-
- newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
- newSegmentStep.setEnabled(enabled);
-
- minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
- minSegmentDuration.setEnabled(enabled);
-
- privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
- privateUserId.setEnabled(enabled);
+ // Preferences are synced by AbstractPreferenceFragment since keys are set
+ // and a Setting exist with the same key.
// If the user has a private user id, then include a subtext that mentions not to share it.
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
@@ -160,11 +102,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
: str("revanced_sb_settings_ie_sum");
importExport.setSummary(importExportSummary);
- apiUrl.setEnabled(enabled);
- importExport.setEnabled(enabled);
- segmentCategory.setEnabled(enabled);
-
- for (SegmentCategoryListPreference category : segmentCategories) {
+ for (SegmentCategoryPreference category : segmentCategories) {
category.updateUI();
}
} catch (Exception ex) {
@@ -172,6 +110,50 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
}
}
+ public void updateUIDelayed() {
+ // Must use a delay, so AbstractPreferenceFragment can
+ // update the availability of the settings.
+ Utils.runOnMainThreadDelayed(this::updateUI, 50);
+ }
+
+ private void initializePreference(Preference preference, Setting> setting, String key) {
+ initializePreference(preference, setting, key, true);
+ }
+
+ private void initializePreference(Preference preference, Setting> setting,
+ String key, boolean setDetailedSummary) {
+ preference.setKey(setting.key);
+ preference.setTitle(str(key));
+ preference.setEnabled(setting.isAvailable());
+ boolean shouldSetSummary = true;
+
+ if (preference instanceof SwitchPreference switchPref && setting instanceof BooleanSetting boolSetting) {
+ switchPref.setChecked(boolSetting.get());
+ if (setDetailedSummary) {
+ switchPref.setSummaryOn(str(key + "_sum_on"));
+ switchPref.setSummaryOff(str(key + "_sum_off"));
+ shouldSetSummary = false;
+ }
+ } else if (preference instanceof ResettableEditTextPreference resetPref) {
+ resetPref.setText(setting.get().toString());
+ } else if (preference instanceof EditTextPreference editPref) {
+ editPref.setText(setting.get().toString());
+ } else if (preference instanceof ListPreference listPref) {
+ listPref.setEntries(Utils.getResourceStringArray(key + "_entries"));
+ listPref.setEntryValues(Utils.getResourceStringArray(key + "_entry_values"));
+ listPref.setValue(setting.get().toString());
+
+ if (preference instanceof CustomDialogListPreference dialogPref) {
+ // Sets a static summary without overwriting it.
+ dialogPref.setStaticSummary(str(key + "_sum"));
+ }
+ }
+
+ if (shouldSetSummary) {
+ preference.setSummary(str(key + "_sum"));
+ }
+ }
+
protected void onAttachedToActivity() {
try {
super.onAttachedToActivity();
@@ -183,20 +165,19 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
}
return;
}
-
preferencesInitialized = true;
Logger.printDebug(() -> "Creating settings preferences");
Context context = getContext();
SponsorBlockSettings.initialize();
- sbEnabled = new SwitchPreference(context);
- sbEnabled.setTitle(str("revanced_sb_enable_sb"));
- sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
+ SwitchPreference sbEnabled = new SwitchPreference(context);
+ initializePreference(sbEnabled, Settings.SB_ENABLED,
+ "revanced_sb_enable_sb", false);
addPreference(sbEnabled);
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_ENABLED.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
@@ -204,109 +185,98 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
addPreference(appearanceCategory);
- votingEnabled = new SwitchPreference(context);
- votingEnabled.setTitle(str("revanced_sb_enable_voting"));
- votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
- votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
+ SwitchPreference votingEnabled = new SwitchPreference(context);
+ initializePreference(votingEnabled, Settings.SB_VOTING_BUTTON,
+ "revanced_sb_enable_voting");
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(votingEnabled);
- compactSkipButton = new SwitchPreference(context);
- compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
- compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
- compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
+ SwitchPreference compactSkipButton = new SwitchPreference(context);
+ initializePreference(compactSkipButton, Settings.SB_COMPACT_SKIP_BUTTON,
+ "revanced_sb_enable_compact_skip_button");
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(compactSkipButton);
- autoHideSkipSegmentButton = new SwitchPreference(context);
- autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
- autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
- autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
+ SwitchPreference autoHideSkipSegmentButton = new SwitchPreference(context);
+ initializePreference(autoHideSkipSegmentButton, Settings.SB_AUTO_HIDE_SKIP_BUTTON,
+ "revanced_sb_enable_auto_hide_skip_segment_button");
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(autoHideSkipSegmentButton);
- String[] durationEntries = Utils.getResourceStringArray("revanced_sb_duration_entries");
- String[] durationEntryValues = Utils.getResourceStringArray("revanced_sb_duration_entry_values");
-
- autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
- autoHideSkipSegmentButtonDuration.setTitle(str("revanced_sb_auto_hide_skip_button_duration"));
- autoHideSkipSegmentButtonDuration.setSummary(str("revanced_sb_auto_hide_skip_button_duration_sum"));
- autoHideSkipSegmentButtonDuration.setEntries(durationEntries);
- autoHideSkipSegmentButtonDuration.setEntryValues(durationEntryValues);
+ CustomDialogListPreference autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
+ initializePreference(autoHideSkipSegmentButtonDuration, Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION,
+ "revanced_sb_auto_hide_skip_button_duration");
autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(
- SponsorBlockDuration.valueOf((String) newValue)
- );
- updateUI();
+ SponsorBlockDuration newDuration = SponsorBlockDuration.valueOf((String) newValue);
+ Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(newDuration);
+ ((CustomDialogListPreference) preference1).setValue(newDuration.name());
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration);
- showSkipToast = new SwitchPreference(context);
- showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
- showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
- showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
+ SwitchPreference showSkipToast = new SwitchPreference(context);
+ initializePreference(showSkipToast, Settings.SB_TOAST_ON_SKIP,
+ "revanced_sb_general_skiptoast");
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(showSkipToast);
- showSkipToastDuration = new CustomDialogListPreference(context);
- showSkipToastDuration.setTitle(str("revanced_sb_toast_on_skip_duration"));
- showSkipToastDuration.setSummary(str("revanced_sb_toast_on_skip_duration_sum"));
- showSkipToastDuration.setEntries(durationEntries);
- showSkipToastDuration.setEntryValues(durationEntryValues);
+ CustomDialogListPreference showSkipToastDuration = new CustomDialogListPreference(context);
+ initializePreference(showSkipToastDuration, Settings.SB_TOAST_ON_SKIP_DURATION,
+ "revanced_sb_toast_on_skip_duration");
+ // Sets a static summary without overwriting it.
+ showSkipToastDuration.setStaticSummary(str("revanced_sb_toast_on_skip_duration_sum"));
showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
- Settings.SB_TOAST_ON_SKIP_DURATION.save(
- SponsorBlockDuration.valueOf((String) newValue)
- );
- updateUI();
+ SponsorBlockDuration newDuration = SponsorBlockDuration.valueOf((String) newValue);
+ Settings.SB_TOAST_ON_SKIP_DURATION.save(newDuration);
+ ((CustomDialogListPreference) preference1).setValue(newDuration.name());
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(showSkipToastDuration);
- showTimeWithoutSegments = new SwitchPreference(context);
- showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
- showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
- showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
+ SwitchPreference showTimeWithoutSegments = new SwitchPreference(context);
+ initializePreference(showTimeWithoutSegments, Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS,
+ "revanced_sb_general_time_without");
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(showTimeWithoutSegments);
- squareLayout = new SwitchPreference(context);
- squareLayout.setTitle(str("revanced_sb_square_layout"));
- squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
- squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
+ SwitchPreference squareLayout = new SwitchPreference(context);
+ initializePreference(squareLayout, Settings.SB_SQUARE_LAYOUT,
+ "revanced_sb_square_layout");
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
appearanceCategory.addPreference(squareLayout);
- segmentCategory = new PreferenceCategory(context);
+ PreferenceCategory segmentCategory = new PreferenceCategory(context);
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
addPreference(segmentCategory);
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
- SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category);
+ SegmentCategoryPreference categoryPreference = new SegmentCategoryPreference(context, category);
segmentCategories.add(categoryPreference);
segmentCategory.addPreference(categoryPreference);
}
@@ -315,24 +285,23 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
addPreference(createSegmentCategory);
- addNewSegment = new SwitchPreference(context);
- addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
- addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
- addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
+ SwitchPreference addNewSegment = new SwitchPreference(context);
+ initializePreference(addNewSegment, Settings.SB_CREATE_NEW_SEGMENT,
+ "revanced_sb_enable_create_segment");
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
Boolean newValue = (Boolean) o;
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
preference1.getContext(),
- str("revanced_sb_guidelines_popup_title"), // Title.
- str("revanced_sb_guidelines_popup_content"), // Message.
- null, // No EditText.
- str("revanced_sb_guidelines_popup_open"), // OK button text.
- () -> openGuidelines(), // OK button action.
- null, // Cancel button action.
+ str("revanced_sb_guidelines_popup_title"), // Title.
+ str("revanced_sb_guidelines_popup_content"), // Message.
+ null, // No EditText.
+ str("revanced_sb_guidelines_popup_open"), // OK button text.
+ this::openGuidelines, // OK button action.
+ null, // Cancel button action.
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
- () -> {}, // Neutral button action (dismiss only).
- true // Dismiss dialog when onNeutralClick.
+ () -> {}, // Neutral button action (dismiss only).
+ true // Dismiss dialog when onNeutralClick.
);
// Set dialog as non-cancelable.
@@ -344,21 +313,21 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
dialogPair.first.show();
}
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
createSegmentCategory.addPreference(addNewSegment);
- newSegmentStep = new ResettableEditTextPreference(context);
- newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
- newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
- newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
+ ResettableEditTextPreference newSegmentStep = new ResettableEditTextPreference(context);
+ initializePreference(newSegmentStep, Settings.SB_CREATE_NEW_SEGMENT_STEP,
+ "revanced_sb_general_adjusting");
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
try {
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
if (newAdjustmentValue != 0) {
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
+ updateUIDelayed();
return true;
}
} catch (NumberFormatException ex) {
@@ -366,7 +335,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
}
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
- updateUI();
+ updateUIDelayed();
return false;
});
createSegmentCategory.addPreference(newSegmentStep);
@@ -384,49 +353,47 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
generalCategory.setTitle(str("revanced_sb_general"));
addPreference(generalCategory);
- toastOnConnectionError = new SwitchPreference(context);
- toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
- toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
- toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
+ SwitchPreference toastOnConnectionError = new SwitchPreference(context);
+ initializePreference(toastOnConnectionError, Settings.SB_TOAST_ON_CONNECTION_ERROR,
+ "revanced_sb_toast_on_connection_error");
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
generalCategory.addPreference(toastOnConnectionError);
- trackSkips = new SwitchPreference(context);
- trackSkips.setTitle(str("revanced_sb_general_skipcount"));
- trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
- trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
+ SwitchPreference trackSkips = new SwitchPreference(context);
+ initializePreference(trackSkips, Settings.SB_TRACK_SKIP_COUNT,
+ "revanced_sb_general_skipcount");
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
generalCategory.addPreference(trackSkips);
- minSegmentDuration = new ResettableEditTextPreference(context);
- minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
- minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
- minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
+ ResettableEditTextPreference minSegmentDuration = new ResettableEditTextPreference(context);
+ initializePreference(minSegmentDuration, Settings.SB_SEGMENT_MIN_DURATION,
+ "revanced_sb_general_min_duration");
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
try {
Float minTimeDuration = Float.valueOf(newValue.toString());
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
+ updateUIDelayed();
return true;
} catch (NumberFormatException ex) {
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
}
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
- updateUI();
+ updateUIDelayed();
return false;
});
generalCategory.addPreference(minSegmentDuration);
- privateUserId = new EditTextPreference(context) {
+ EditTextPreference privateUserId = new EditTextPreference(context) {
@Override
protected void showDialog(Bundle state) {
try {
@@ -439,7 +406,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
editText.setSelection(initialValue.length()); // Move cursor to end.
// Create custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
@@ -475,31 +442,32 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
}
}
};
- privateUserId.setTitle(str("revanced_sb_general_uuid"));
- privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
+ initializePreference(privateUserId, Settings.SB_PRIVATE_USER_ID,
+ "revanced_sb_general_uuid");
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
String newUUID = newValue.toString();
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
+ updateUIDelayed();
return false;
}
Settings.SB_PRIVATE_USER_ID.save(newUUID);
- updateUI();
+ updateUIDelayed();
return true;
});
generalCategory.addPreference(privateUserId);
- apiUrl = new Preference(context);
- apiUrl.setTitle(str("revanced_sb_general_api_url"));
- apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
+ Preference apiUrl = new Preference(context);
+ initializePreference(apiUrl, Settings.SB_API_URL,
+ "revanced_sb_general_api_url");
apiUrl.setOnPreferenceClickListener(preference1 -> {
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(Settings.SB_API_URL.get());
// Create a custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
str("revanced_sb_general_api_url"), // Title.
null, // No message, EditText replaces it.
@@ -538,8 +506,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
Context context = getContext();
EditText editText = getEditText();
+ editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
+
// Create a custom dialog.
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
context,
str("revanced_sb_settings_ie"), // Title.
null, // No message, EditText replaces it.
@@ -588,7 +559,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
});
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
SponsorBlockSettings.importDesktopSettings((String) newValue);
- updateUI();
+ updateUIDelayed();
return true;
});
generalCategory.addPreference(importExport);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java
index 7d7f3fbe7..6b6d7c4de 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java
@@ -1,5 +1,6 @@
package app.revanced.extension.youtube.sponsorblock.ui;
+import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static android.text.Html.fromHtml;
import static app.revanced.extension.shared.StringRef.str;
@@ -7,7 +8,6 @@ import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
@@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
+import app.revanced.extension.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
@@ -27,7 +28,6 @@ import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
/**
* User skip stats.
- *
* None of the preferences here show up in search results because
* a category cannot be added to another category for the search results.
* Additionally the stats must load remotely on a background thread which means the
@@ -48,6 +48,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
super(context, attrs);
}
+ @Override
protected void onAttachedToActivity() {
try {
super.onAttachedToActivity();
@@ -97,8 +98,8 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
if (stats.totalSegmentCountIncludingIgnored > 0) {
// If user has not created any segments, there's no reason to set a username.
String userName = stats.userName;
- EditTextPreference preference = new ResettableEditTextPreference(context);
- preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
+ ResettableEditTextPreference preference = new ResettableEditTextPreference(context);
+ preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName), FROM_HTML_MODE_COMPACT));
preference.setSummary(str("revanced_sb_stats_username_change"));
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, value) -> {
@@ -107,7 +108,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
String errorMessage = SBRequester.setUsername(newUserName);
Utils.runOnMainThread(() -> {
if (errorMessage == null) {
- preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
+ preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName), FROM_HTML_MODE_COMPACT));
preference.setText(newUserName);
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
} else {
@@ -118,6 +119,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
});
return true;
});
+ preference.setEnabled(Settings.SB_PRIVATE_USER_ID.isAvailable()); // Sync with private user ID setting.
addPreference(preference);
}
@@ -125,7 +127,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
// Number of segment submissions (does not include ignored segments).
Preference preference = new Preference(context);
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
- preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
+ preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted), FROM_HTML_MODE_COMPACT));
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
if (stats.totalSegmentCountIncludingIgnored == 0) {
preference.setSelectable(false);
@@ -137,6 +139,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
return true;
});
}
+ preference.setEnabled(Settings.SB_ENABLED.isAvailable());
addPreference(preference);
}
@@ -144,8 +147,9 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
// "user reputation". Usually not useful since it appears most users have zero reputation.
// But if there is a reputation then show it here.
Preference preference = new Preference(context);
- preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
+ preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation), FROM_HTML_MODE_COMPACT));
preference.setSelectable(false);
+ preference.setEnabled(Settings.SB_ENABLED.isAvailable());
if (stats.reputation != 0) {
addPreference(preference);
}
@@ -166,14 +170,15 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
stats_saved_sum = str("revanced_sb_stats_saved_sum",
SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
}
- preference.setTitle(fromHtml(stats_saved));
- preference.setSummary(fromHtml(stats_saved_sum));
+ preference.setTitle(fromHtml(stats_saved, FROM_HTML_MODE_COMPACT));
+ preference.setSummary(fromHtml(stats_saved_sum, FROM_HTML_MODE_COMPACT));
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
preference1.getContext().startActivity(i);
return false;
});
+ preference.setEnabled(Settings.SB_ENABLED.isAvailable());
addPreference(preference);
}
} catch (Exception ex) {
@@ -187,16 +192,16 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
Runnable updateStatsSelfSaved = () -> {
String formatted = SponsorBlockUtils.getNumberOfSkipsString(
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
- preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
+ preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted), FROM_HTML_MODE_COMPACT));
String formattedSaved = SponsorBlockUtils.getTimeSavedString(
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
- preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
+ preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved), FROM_HTML_MODE_COMPACT));
};
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
- Pair dialogPair = Utils.createCustomDialog(
+ Pair dialogPair = CustomDialog.create(
preference.getContext(),
str("revanced_sb_stats_self_saved_reset_title"), // Title.
null, // No message.
@@ -219,6 +224,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
return true;
});
+ preference.setEnabled(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.isAvailable());
addPreference(preference);
}
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java
index bcd1b8895..d6e027802 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java
@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.sponsorblock.ui;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
+import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
import android.content.Context;
import android.view.LayoutInflater;
@@ -63,10 +64,9 @@ public class SponsorBlockViewController {
Context context = Utils.getContext();
RelativeLayout layout = new RelativeLayout(context);
- layout.setLayoutParams(new RelativeLayout.LayoutParams(
- RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
- LayoutInflater.from(context).inflate(getResourceIdentifier(ResourceType.LAYOUT,
- "revanced_sb_inline_sponsor_overlay"), layout);
+ layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
+ LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(
+ "revanced_sb_inline_sponsor_overlay", "layout"), layout);
inlineSponsorOverlayRef = new WeakReference<>(layout);
viewGroup.addView(layout);
@@ -86,13 +86,13 @@ public class SponsorBlockViewController {
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
skipHighlightButtonRef = new WeakReference<>(Objects.requireNonNull(
- layout.findViewById(getResourceIdentifier(ResourceType.ID, "revanced_sb_skip_highlight_button"))));
+ layout.findViewById(getResourceIdentifier("revanced_sb_skip_highlight_button", "id"))));
skipSponsorButtonRef = new WeakReference<>(Objects.requireNonNull(
- layout.findViewById(getResourceIdentifier(ResourceType.ID, "revanced_sb_skip_sponsor_button"))));
+ layout.findViewById(getResourceIdentifier("revanced_sb_skip_sponsor_button", "id"))));
NewSegmentLayout newSegmentLayout = Objects.requireNonNull(
- layout.findViewById(getResourceIdentifier(ResourceType.ID, "revanced_sb_new_segment_view")));
+ layout.findViewById(getResourceIdentifier("revanced_sb_new_segment_view", "id")));
newSegmentLayoutRef = new WeakReference<>(newSegmentLayout);
newSegmentLayout.updateLayout();
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
index c5854fd2f..176238a08 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
@@ -92,7 +92,7 @@ class SwipeControlsConfigurationProvider {
val overlayBackgroundOpacity: Int by lazy {
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
- if (opacity < 0 || opacity > 100) {
+ if (opacity !in 0..100) {
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
}
@@ -115,14 +115,13 @@ class SwipeControlsConfigurationProvider {
* Resets to default and shows a toast if the color string is invalid or empty.
*/
val overlayVolumeProgressColor: Int by lazy {
+ // Use lazy to avoid repeat parsing. Changing color requires app restart.
getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR)
}
private fun getSettingColor(setting: StringSetting): Int {
- try {
- //noinspection UseKtx
- val color = Color.parseColor(setting.get())
- return (0xBF000000.toInt() or (color and 0x00FFFFFF))
+ return try {
+ Color.parseColor(setting.get())
} catch (ex: IllegalArgumentException) {
// This code should never be reached.
// Color picker rejects and will not save bad colors to a setting.
@@ -151,7 +150,7 @@ class SwipeControlsConfigurationProvider {
*/
val overlayTextSize: Int by lazy {
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
- if (size < 1 || size > 30) {
+ if (size !in 1..30) {
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
return@lazy Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
index c9d61a8d3..248de966d 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
@@ -24,7 +24,7 @@ public class PlayerControlButton {
boolean buttonEnabled();
}
- private static final int fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
+ public static final int fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
private static final int fadeOutDuration = Utils.getResourceInteger("fade_duration_scheduled");
private final WeakReference containerRef;
@@ -249,4 +249,13 @@ public class PlayerControlButton {
textOverlay.setText(text);
}
}
+
+ /**
+ * Returns the appropriate dialog background color depending on the current theme.
+ */
+ public static int getDialogBackgroundColor() {
+ return Utils.getResourceColor(
+ Utils.isDarkModeEnabled() ? "yt_black1" : "yt_white1"
+ );
+ }
}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
index bd558c4c8..bf133e120 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
@@ -2,38 +2,30 @@ package app.revanced.extension.youtube.videoplayer;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.shared.settings.preference.CustomDialogListPreference.*;
import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_PREMIUM_NAME;
+import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.fadeInDuration;
+import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.getDialogBackgroundColor;
-import android.app.Dialog;
import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
-import android.view.Gravity;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.animation.Animation;
-import android.view.animation.TranslateAnimation;
-import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.TextView;
+import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import app.revanced.extension.shared.ui.SheetBottomDialog;
+import app.revanced.extension.youtube.shared.PlayerType;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -44,6 +36,7 @@ import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
import app.revanced.extension.youtube.settings.Settings;
import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
@SuppressWarnings("unused")
public class VideoQualityDialogButton {
@@ -61,6 +54,11 @@ public class VideoQualityDialogButton {
});
}
+ /**
+ * Weak reference to the currently open dialog.
+ */
+ private static WeakReference currentDialog;
+
/**
* Injection point.
*/
@@ -217,41 +215,14 @@ public class VideoQualityDialogButton {
}
}
- Dialog dialog = new Dialog(context);
- dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
- dialog.setCanceledOnTouchOutside(true);
- dialog.setCancelable(true);
+ // Preset size constants.
+ final int dip8 = dipToPixels(8);
+ final int dip12 = dipToPixels(12);
+ final int dip16 = dipToPixels(16);
- final int dip4 = dipToPixels(4); // Height for handle bar.
- final int dip5 = dipToPixels(5); // Padding for mainLayout.
- final int dip6 = dipToPixels(6); // Bottom margin.
- final int dip8 = dipToPixels(8); // Side padding.
- final int dip16 = dipToPixels(16); // Left padding for ListView.
- final int dip20 = dipToPixels(20); // Margin below handle.
- final int dip40 = dipToPixels(40); // Width for handle bar.
-
- LinearLayout mainLayout = new LinearLayout(context);
- mainLayout.setOrientation(LinearLayout.VERTICAL);
- mainLayout.setPadding(dip5, dip8, dip5, dip8);
-
- ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(12), null, null));
- background.getPaint().setColor(Utils.getDialogBackgroundColor());
- mainLayout.setBackground(background);
-
- View handleBar = new View(context);
- ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
- Utils.createCornerRadii(4), null, null));
- final int baseColor = Utils.getDialogBackgroundColor();
- final int adjustedHandleBarBackgroundColor = Utils.adjustColorBrightness(
- baseColor, 0.9f, 1.25f);
- handleBackground.getPaint().setColor(adjustedHandleBarBackgroundColor);
- handleBar.setBackground(handleBackground);
- LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
- handleParams.gravity = Gravity.CENTER_HORIZONTAL;
- handleParams.setMargins(0, 0, 0, dip20);
- handleBar.setLayoutParams(handleParams);
- mainLayout.addView(handleBar);
+ // Create main layout.
+ SheetBottomDialog.DraggableLinearLayout mainLayout =
+ SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor());
// Create SpannableStringBuilder for formatted text.
SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
@@ -299,16 +270,20 @@ public class VideoQualityDialogButton {
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
- titleParams.setMargins(dip8, 0, 0, dip20);
+ titleParams.setMargins(dip12, dip16, 0, dip16);
titleView.setLayoutParams(titleParams);
mainLayout.addView(titleView);
+ // Create ListView for quality selection.
ListView listView = new ListView(context);
CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
adapter.setSelectedPosition(listViewSelectedIndex);
listView.setAdapter(adapter);
listView.setDivider(null);
- listView.setPadding(dip16, 0, 0, 0);
+
+ // Create dialog.
+ SheetBottomDialog.SlideDialog dialog = SheetBottomDialog.createSlideDialog(context, mainLayout, fadeInDuration);
+ currentDialog = new WeakReference<>(dialog);
listView.setOnItemClickListener((parent, view, which, id) -> {
try {
@@ -323,112 +298,41 @@ public class VideoQualityDialogButton {
}
});
- LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
- listViewParams.setMargins(0, 0, 0, dip5);
- listView.setLayoutParams(listViewParams);
mainLayout.addView(listView);
- LinearLayout wrapperLayout = new LinearLayout(context);
- wrapperLayout.setOrientation(LinearLayout.VERTICAL);
- wrapperLayout.setPadding(dip8, 0, dip8, 0);
- wrapperLayout.addView(mainLayout);
- dialog.setContentView(wrapperLayout);
-
- Window window = dialog.getWindow();
- if (window != null) {
- WindowManager.LayoutParams params = window.getAttributes();
- params.gravity = Gravity.BOTTOM;
- params.y = dip6;
- int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
- if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
- portraitWidth = Math.min(
- portraitWidth,
- context.getResources().getDisplayMetrics().heightPixels);
- // Limit height in landscape mode.
- params.height = Utils.percentageHeightToPixels(80);
- } else {
- params.height = WindowManager.LayoutParams.WRAP_CONTENT;
- }
- params.width = portraitWidth;
- window.setAttributes(params);
- window.setBackgroundDrawable(null);
- }
-
- final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
- Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
- slideInABottomAnimation.setDuration(fadeDurationFast);
- mainLayout.startAnimation(slideInABottomAnimation);
-
- // noinspection ClickableViewAccessibility
- mainLayout.setOnTouchListener(new View.OnTouchListener() {
- final float dismissThreshold = dipToPixels(100);
- float touchY;
- float translationY;
-
+ // Create observer for PlayerType changes.
+ Function1 playerTypeObserver = new Function1<>() {
@Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- touchY = event.getRawY();
- translationY = mainLayout.getTranslationY();
- return true;
- case MotionEvent.ACTION_MOVE:
- final float deltaY = event.getRawY() - touchY;
- if (deltaY >= 0) {
- mainLayout.setTranslationY(translationY + deltaY);
- }
- return true;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (mainLayout.getTranslationY() > dismissThreshold) {
- //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 {
- TranslateAnimation slideBack = new TranslateAnimation(
- 0, 0, mainLayout.getTranslationY(), 0);
- slideBack.setDuration(fadeDurationFast);
- mainLayout.startAnimation(slideBack);
- mainLayout.setTranslationY(0);
- }
- return true;
- default:
- return false;
+ public Unit invoke(PlayerType type) {
+ SheetBottomDialog.SlideDialog current = currentDialog.get();
+ if (current == null || !current.isShowing()) {
+ // Should never happen.
+ PlayerType.getOnChange().removeObserver(this);
+ Logger.printException(() -> "Removing player type listener as dialog is null or closed");
+ } else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
+ current.dismiss();
+ Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
}
+ return Unit.INSTANCE;
}
+ };
+
+ // Add observer to dismiss dialog when entering PiP mode.
+ PlayerType.getOnChange().addObserver(playerTypeObserver);
+
+ // Remove observer when dialog is dismissed.
+ dialog.setOnDismissListener(d -> {
+ PlayerType.getOnChange().removeObserver(playerTypeObserver);
+ Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
});
- dialog.show();
+ dialog.show(); // Show the dialog.
} catch (Exception ex) {
Logger.printException(() -> "showVideoQualityDialog failure", ex);
}
}
private static class CustomQualityAdapter extends ArrayAdapter {
- private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
- ResourceType.LAYOUT, "revanced_custom_list_item_checked");
- private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
- ResourceType.ID, "revanced_check_icon");
- private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
- ResourceType.ID, "revanced_check_icon_placeholder");
- private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
- ResourceType.ID, "revanced_item_text");
private int selectedPosition = -1;
@@ -448,14 +352,14 @@ public class VideoQualityDialogButton {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(
- CUSTOM_LIST_ITEM_CHECKED_ID,
+ LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
parent,
false
);
viewHolder = new ViewHolder();
- viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID);
- viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID);
- viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID);
+ viewHolder.checkIcon = convertView.findViewById(ID_REVANCED_CHECK_ICON);
+ viewHolder.placeholder = convertView.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
+ viewHolder.textView = convertView.findViewById(ID_REVANCED_ITEM_TEXT);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
diff --git a/gradle.properties b/gradle.properties
index 6f587b080..b8885395f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
-version = 5.40.0-dev.7
+version = 5.40.1-dev.1
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
index c0f0b4a13..13c491f32 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt
@@ -24,7 +24,8 @@ val hideVideoAdsPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
index 4d598e402..f0b1974c3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt
@@ -17,7 +17,8 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
index 7ebf68171..3c0f160dc 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt
@@ -27,7 +27,8 @@ val permanentRepeatPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/castbutton/HideCastButton.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/castbutton/HideCastButton.kt
index 4e18e6305..2c1fbdea8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/castbutton/HideCastButton.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/castbutton/HideCastButton.kt
@@ -33,7 +33,8 @@ val hideCastButton = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
index 56687a7fa..1fb5c6d6f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt
@@ -1,6 +1,5 @@
package app.revanced.patches.music.layout.compactheader
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
@@ -10,7 +9,6 @@ import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.settings.PreferenceScreen
import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
-import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@@ -29,7 +27,8 @@ val hideCategoryBar = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt
index 49ae813fe..cee300bc8 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt
@@ -40,7 +40,8 @@ val navigationBarPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
index bb043b173..159b9dc4d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt
@@ -28,7 +28,8 @@ val hideGetPremiumPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt
deleted file mode 100644
index 7012360a5..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package app.revanced.patches.music.layout.upgradebutton
-
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.AccessFlags
-import app.revanced.patcher.fingerprint
-
-internal val pivotBarConstructorFingerprint by fingerprint {
- accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
- parameters("L", "Z")
- opcodes(
- Opcode.CHECK_CAST,
- Opcode.INVOKE_INTERFACE,
- Opcode.GOTO,
- Opcode.IPUT_OBJECT,
- Opcode.RETURN_VOID
- )
-}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
index acd0cb1c2..0b0f24497 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/HideUpgradeButtonPatch.kt
@@ -1,104 +1,12 @@
package app.revanced.patches.music.layout.upgradebutton
-import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
-import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
-import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
-import app.revanced.patcher.extensions.newLabel
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patcher.util.smali.toInstructions
-import app.revanced.patches.all.misc.resources.addResources
-import app.revanced.patches.all.misc.resources.addResourcesPatch
-import app.revanced.patches.music.misc.extension.sharedExtensionPatch
-import app.revanced.patches.music.misc.settings.PreferenceScreen
-import app.revanced.patches.music.misc.settings.settingsPatch
-import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
-import app.revanced.util.getReference
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import app.revanced.patches.music.layout.navigationbar.navigationBarPatch
-private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/HideUpgradeButtonPatch;"
-
-@Deprecated("This patch will be removed in the future.")
+@Deprecated("Patch is obsolete and was replaced by navigation bar patch", ReplaceWith("navigationBarPatch"))
@Suppress("unused")
-val hideUpgradeButton = bytecodePatch(
- description = "Hides the upgrade tab from the pivot bar.",
-) {
- dependsOn(
- sharedExtensionPatch,
- settingsPatch,
- addResourcesPatch,
- )
-
- compatibleWith(
- "com.google.android.apps.youtube.music"(
- "7.29.52"
- )
- )
-
- execute {
- addResources("music", "layout.upgradebutton.hideUpgradeButtonPatch")
-
- // TODO: Add an extension patch to allow this to be enabled/disabled in app.
- if (false) {
- PreferenceScreen.ADS.addPreferences(
- SwitchPreference("revanced_music_hide_upgrade_button")
- )
- }
-
- pivotBarConstructorFingerprint.method.apply {
- val pivotBarElementFieldReference =
- getInstruction(pivotBarConstructorFingerprint.instructionMatches.last().index - 1)
- .getReference()
-
- val register = getInstruction(0).registerC
-
- // First compile all the needed instructions.
- val instructionList = """
- invoke-interface { v0 }, Ljava/util/List;->size()I
- move-result v1
- const/4 v2, 0x4
- invoke-interface {v0, v2}, Ljava/util/List;->remove(I)Ljava/lang/Object;
- iput-object v0, v$register, $pivotBarElementFieldReference
- """.toInstructions().toMutableList()
-
- val endIndex = pivotBarConstructorFingerprint.instructionMatches.last().index
-
- // Replace the instruction to retain the label at given index.
- replaceInstruction(
- endIndex - 1,
- instructionList[0], // invoke-interface.
- )
- // Do not forget to remove this instruction since we added it already.
- instructionList.removeFirst()
-
- val exitInstruction = instructionList.last() // iput-object
- addInstruction(
- endIndex,
- exitInstruction,
- )
- // Do not forget to remove this instruction since we added it already.
- instructionList.removeLast()
-
- // Add the necessary if statement to remove the upgrade tab button in case it exists.
- instructionList.add(
- 2, // if-le.
- BuilderInstruction22t(
- Opcode.IF_LE,
- 1,
- 2,
- newLabel(endIndex),
- ),
- )
-
- addInstructions(
- endIndex,
- instructionList,
- )
- }
- }
+val hideUpgradeButton = bytecodePatch{
+ dependsOn(navigationBarPatch)
}
@Deprecated("Patch was renamed", ReplaceWith("hideUpgradeButton"))
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
index 70e707fbb..245569d9e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt
@@ -17,7 +17,8 @@ val bypassCertificateChecksPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
index 3d81296a7..bf35b24d5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt
@@ -17,7 +17,8 @@ val backgroundPlaybackPatch = bytecodePatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt
index f4747b2fc..76ee0e060 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt
@@ -15,7 +15,8 @@ val enableDebuggingPatch = enableDebuggingPatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
},
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt
index f655043cf..f6eed822f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/settings/SettingsPatch.kt
@@ -1,6 +1,5 @@
package app.revanced.patches.music.misc.settings
-import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
@@ -10,20 +9,19 @@ import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.settings.preference.BasePreference
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
+import app.revanced.patches.shared.misc.settings.preference.InputType
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
+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.settingsPatch
-import app.revanced.util.ResourceGroup
-import app.revanced.util.copyResources
+import app.revanced.patches.youtube.misc.settings.modifyActivityForSettingsInjection
import app.revanced.util.copyXmlNode
import app.revanced.util.inputStreamFromBundledResource
-import com.android.tools.smali.dexlib2.util.MethodUtil
-private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
- "Lapp/revanced/extension/shared/settings/BaseActivityHook;"
private const val GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
- "Lapp/revanced/extension/music/settings/GoogleApiActivityHook;"
+ "Lapp/revanced/extension/music/settings/MusicActivityHook;"
private val preferences = mutableSetOf()
@@ -31,28 +29,19 @@ private val settingsResourcePatch = resourcePatch {
dependsOn(
resourceMappingPatch,
settingsPatch(
- listOf(
+ rootPreferences = listOf(
IntentPreference(
titleKey = "revanced_settings_title",
summaryKey = null,
intent = newIntent("revanced_settings_intent"),
) to "settings_headers"
),
- preferences
+ preferences = preferences
)
)
execute {
- // TODO: Remove this when search will be abstract.
- copyResources(
- "settings",
- ResourceGroup(
- "layout",
- "revanced_music_settings_with_toolbar.xml"
- )
- )
-
val targetResource = "values/styles.xml"
inputStreamFromBundledResource(
"settings/music",
@@ -100,24 +89,25 @@ val settingsPatch = bytecodePatch(
selectable = true,
)
- // Modify GoogleApiActivity and remove all existing layout code.
- // Must modify an existing activity and cannot add a new activity to the manifest,
- // as that fails for root installations.
-
- googleApiActivityFingerprint.method.addInstructions(
- 1,
- """
- invoke-static { }, $GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/music/settings/GoogleApiActivityHook;
- move-result-object v0
- invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
- return-void
- """
+ PreferenceScreen.GENERAL.addPreferences(
+ SwitchPreference("revanced_settings_search_history")
)
- // Remove other methods as they will break as the onCreate method is modified above.
- googleApiActivityFingerprint.classDef.apply {
- methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
- }
+ PreferenceScreen.MISC.addPreferences(
+ TextPreference(
+ key = null,
+ titleKey = "revanced_pref_import_export_title",
+ summaryKey = "revanced_pref_import_export_summary",
+ inputType = InputType.TEXT_MULTI_LINE,
+ tag = "app.revanced.extension.shared.settings.preference.ImportExportPreference",
+ )
+ )
+
+ modifyActivityForSettingsInjection(
+ googleApiActivityFingerprint.classDef,
+ googleApiActivityFingerprint.method,
+ GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR
+ )
}
finalize {
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
index 0452bc3cf..b01f74cca 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt
@@ -33,7 +33,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch(
compatibleWith(
"com.google.android.apps.youtube.music"(
- "7.29.52"
+ "7.29.52",
+ "8.10.52"
)
)
},
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt
index 8da55cfdb..7576afa57 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt
@@ -57,14 +57,15 @@ fun settingsPatch (
copyResources(
"settings",
ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"),
+ ResourceGroup("menu", "revanced_search_menu.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_search_remove.xml",
"revanced_settings_toolbar_arrow_left.xml",
),
ResourceGroup("layout",
@@ -72,6 +73,16 @@ fun settingsPatch (
// Color picker.
"revanced_color_dot_widget.xml",
"revanced_color_picker.xml",
+ // Search.
+ "revanced_preference_search_history_item.xml",
+ "revanced_preference_search_history_screen.xml",
+ "revanced_preference_search_no_result.xml",
+ "revanced_preference_search_result_color.xml",
+ "revanced_preference_search_result_group_header.xml",
+ "revanced_preference_search_result_list.xml",
+ "revanced_preference_search_result_regular.xml",
+ "revanced_preference_search_result_switch.xml",
+ "revanced_settings_with_toolbar.xml"
)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt
index 5e19646e5..3b4840a83 100644
--- a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt
@@ -12,7 +12,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR =
- "Lapp/revanced/extension/tiktok/settings/AdPersonalizationActivityHook;"
+ "Lapp/revanced/extension/tiktok/settings/TikTokActivityHook;"
val settingsPatch = bytecodePatch(
name = "Settings",
diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt
index f75912e1c..66d790718 100644
--- a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt
@@ -29,7 +29,7 @@ private const val MENU_DISMISS_EVENT_CLASS_DESCRIPTOR =
"Ltv/twitch/android/feature/settings/menu/SettingsMenuViewDelegate\$Event\$OnDismissClicked;"
private const val EXTENSION_PACKAGE = "app/revanced/extension/twitch"
-private const val ACTIVITY_HOOKS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/settings/AppCompatActivityHook;"
+private const val ACTIVITY_HOOKS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/settings/TwitchActivityHook;"
private const val UTILS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/Utils;"
private val preferences = mutableSetOf()
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt
index 38efd3d89..6b01641b1 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt
@@ -53,10 +53,10 @@ private val swipeControlsResourcePatch = resourcePatch {
ListPreference("revanced_swipe_overlay_style"),
TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER),
TextPreference("revanced_swipe_overlay_progress_brightness_color",
- tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
+ tag = "app.revanced.extension.shared.settings.preference.ColorPickerWithOpacitySliderPreference",
inputType = InputType.TEXT_CAP_CHARACTERS),
TextPreference("revanced_swipe_overlay_progress_volume_color",
- tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
+ tag = "app.revanced.extension.shared.settings.preference.ColorPickerWithOpacitySliderPreference",
inputType = InputType.TEXT_CAP_CHARACTERS),
TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER),
TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER),
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
index f49feb03d..62c4d084d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt
@@ -172,7 +172,10 @@ val hideLayoutComponentsPatch = bytecodePatch(
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
SwitchPreference("revanced_hide_keyword_content_search"),
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
- NonInteractivePreference("revanced_hide_keyword_content_about"),
+ NonInteractivePreference(
+ key = "revanced_hide_keyword_content_about",
+ tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference"
+ ),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about_whole_words",
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
@@ -206,7 +209,10 @@ val hideLayoutComponentsPatch = bytecodePatch(
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
- SwitchPreference("revanced_hide_horizontal_shelves"),
+ SwitchPreference(
+ key = "revanced_hide_horizontal_shelves",
+ tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference"
+ ),
SwitchPreference("revanced_hide_image_shelf"),
SwitchPreference("revanced_hide_latest_posts"),
SwitchPreference("revanced_hide_mix_playlists"),
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
index ec1638382..afb5f32f5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt
@@ -4,6 +4,8 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
import app.revanced.patches.all.misc.resources.addResources
@@ -29,8 +31,8 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/shared/settings/BaseActivityHook;"
-private const val LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
- "Lapp/revanced/extension/youtube/settings/LicenseActivityHook;"
+private const val YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/youtube/settings/YouTubeActivityHook;"
private val preferences = mutableSetOf()
@@ -38,7 +40,7 @@ private val settingsResourcePatch = resourcePatch {
dependsOn(
resourceMappingPatch,
settingsPatch(
- listOf(
+ rootPreferences = listOf(
IntentPreference(
titleKey = "revanced_settings_title",
summaryKey = null,
@@ -58,7 +60,7 @@ private val settingsResourcePatch = resourcePatch {
)
) to "settings_fragment_cairo",
),
- preferences
+ preferences = preferences
)
)
@@ -66,7 +68,8 @@ private val settingsResourcePatch = resourcePatch {
// Use same colors as stock YouTube.
overrideThemeColors("@color/yt_white1", "@color/yt_black3")
- arrayOf(
+ copyResources(
+ "settings",
ResourceGroup("drawable",
"revanced_settings_icon.xml",
"revanced_settings_screen_00_about.xml",
@@ -82,23 +85,15 @@ private val settingsResourcePatch = resourcePatch {
"revanced_settings_screen_10_sponsorblock.xml",
"revanced_settings_screen_11_misc.xml",
"revanced_settings_screen_12_video.xml",
- ),
- ResourceGroup("layout",
- "revanced_preference_with_icon_no_search_result.xml",
- "revanced_search_suggestion_item.xml",
- "revanced_settings_with_toolbar.xml"
- ),
- ResourceGroup("menu", "revanced_search_menu.xml")
- ).forEach { resourceGroup ->
- copyResources("settings", resourceGroup)
- }
+ )
+ )
// Copy style properties used to fix over-sized copy menu that appear in EditTextPreference.
// For a full explanation of how this fixes the issue, see the comments in this style file
// and the comments in the extension code.
val targetResource = "values/styles.xml"
inputStreamFromBundledResource(
- "settings/host",
+ "settings/youtube",
targetResource,
)!!.let { inputStream ->
"resources".copyXmlNode(
@@ -208,97 +203,6 @@ val settingsPatch = bytecodePatch(
)
)
-
- // Modify the license activity and remove all existing layout code.
- // Must modify an existing activity and cannot add a new activity to the manifest,
- // as that fails for root installations.
- licenseActivityOnCreateFingerprint.let {
- val superClass = it.classDef.superclass
-
- it.method.addInstructions(
- 0,
- """
- # Some targets have extra instructions before the call to super method.
- invoke-super { p0, p1 }, $superClass->onCreate(Landroid/os/Bundle;)V
- invoke-static {}, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/youtube/settings/LicenseActivityHook;
- move-result-object v0
- invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
- return-void
- """
- )
- }
-
- // Remove other methods as they will break as the onCreate method is modified above.
- licenseActivityOnCreateFingerprint.classDef.apply {
- methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
- }
-
- licenseActivityOnCreateFingerprint.classDef.apply {
- // Add attachBaseContext method to override the context for setting a specific language.
- ImmutableMethod(
- type,
- "attachBaseContext",
- listOf(ImmutableMethodParameter("Landroid/content/Context;", null, null)),
- "V",
- AccessFlags.PROTECTED.value,
- null,
- null,
- MutableMethodImplementation(3),
- ).toMutable().apply {
- addInstructions(
- """
- invoke-static { p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
- move-result-object p1
- invoke-super { p0, p1 }, $superclass->attachBaseContext(Landroid/content/Context;)V
- return-void
- """
- )
- }.let(methods::add)
-
- // Add onBackPressed method to handle back button presses, delegating to SearchViewController.
- ImmutableMethod(
- type,
- "onBackPressed",
- emptyList(),
- "V",
- AccessFlags.PUBLIC.value,
- null,
- null,
- MutableMethodImplementation(3),
- ).toMutable().apply {
- addInstructions(
- """
- invoke-static {}, Lapp/revanced/extension/youtube/settings/SearchViewController;->handleBackPress()Z
- move-result v0
- if-nez v0, :search_handled
- invoke-virtual { p0 }, Landroid/app/Activity;->finish()V
- :search_handled
- return-void
- """
- )
- }.let(methods::add)
-
- // Add onConfigurationChanged method to handle configuration changes (e.g., screen orientation).
- ImmutableMethod(
- type,
- "onConfigurationChanged",
- listOf(ImmutableMethodParameter("Landroid/content/res/Configuration;", null, null)),
- "V",
- AccessFlags.PUBLIC.value,
- null,
- null,
- MutableMethodImplementation(3)
- ).toMutable().apply {
- addInstructions(
- """
- invoke-super { p0, p1 }, Landroid/app/Activity;->onConfigurationChanged(Landroid/content/res/Configuration;)V
- invoke-static { p0, p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->handleConfigurationChanged(Landroid/app/Activity;Landroid/content/res/Configuration;)V
- return-void
- """
- )
- }.let(methods::add)
- }
-
// Update shared dark mode status based on YT theme.
// This is needed because YT allows forcing light/dark mode
// which then differs from the system dark mode status.
@@ -307,25 +211,108 @@ val settingsPatch = bytecodePatch(
val register = getInstruction(index).registerA
addInstructionsAtControlFlowLabel(
index,
- "invoke-static { v$register }, ${LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
+ "invoke-static { v$register }, ${YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
)
}
}
+// // Add setting to force Cairo settings fragment on/off.
+// cairoFragmentConfigFingerprint.let {
+// it.method.insertLiteralOverride(
+// it.instructionMatches.last().index,
+// "$LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
+// )
+// }
+// }
+
// Add setting to force Cairo settings fragment on/off.
- cairoFragmentConfigFingerprint.let {
- it.method.insertLiteralOverride(
- it.instructionMatches.last().index,
- "$LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
- )
- }
- }
+ cairoFragmentConfigFingerprint.method.insertLiteralOverride(
+ cairoFragmentConfigFingerprint.instructionMatches.first().index,
+ "$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
+ )
+
+ modifyActivityForSettingsInjection(
+ licenseActivityOnCreateFingerprint.classDef,
+ licenseActivityOnCreateFingerprint.method,
+ YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR
+ )
+ }
finalize {
PreferenceScreen.close()
}
}
+/**
+ * Modifies the activity to show ReVanced settings instead of it's original purpose.
+ */
+internal fun modifyActivityForSettingsInjection(
+ activityOnCreateClass: MutableClass,
+ activityOnCreateMethod: MutableMethod,
+ extensionClassType: String
+) {
+ // Modify Activity and remove all existing layout code.
+ // Must modify an existing activity and cannot add a new activity to the manifest,
+ // as that fails for root installations.
+ activityOnCreateMethod.addInstructions(
+ 1,
+ """
+ invoke-static { p0 }, $extensionClassType->initialize(Landroid/app/Activity;)V
+ return-void
+ """
+ )
+
+ // Remove other methods as they will break as the onCreate method is modified above.
+ activityOnCreateClass.apply {
+ methods.removeIf { it != activityOnCreateMethod && !MethodUtil.isConstructor(it) }
+ }
+
+ // Override base context to allow using ReVanced specific settings.
+ ImmutableMethod(
+ activityOnCreateClass.type,
+ "attachBaseContext",
+ listOf(ImmutableMethodParameter("Landroid/content/Context;", null, null)),
+ "V",
+ AccessFlags.PROTECTED.value,
+ null,
+ null,
+ MutableMethodImplementation(3),
+ ).toMutable().apply {
+ addInstructions(
+ """
+ invoke-static { p1 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
+ move-result-object p1
+ invoke-super { p0, p1 }, ${activityOnCreateClass.superclass}->attachBaseContext(Landroid/content/Context;)V
+ return-void
+ """
+ )
+ }.let(activityOnCreateClass.methods::add)
+
+ // Override finish() to intercept back gesture.
+ ImmutableMethod(
+ activityOnCreateClass.type,
+ "finish",
+ emptyList(),
+ "V",
+ AccessFlags.PUBLIC.value,
+ null,
+ null,
+ MutableMethodImplementation(3),
+ ).toMutable().apply {
+ addInstructions(
+ """
+ invoke-static {}, $extensionClassType->handleFinish()Z
+ move-result v0
+ if-nez v0, :search_handled
+ invoke-super { p0 }, Landroid/app/Activity;->finish()V
+ return-void
+ :search_handled
+ return-void
+ """
+ )
+ }.let(activityOnCreateClass.methods::add)
+}
+
/**
* Creates an intent to open ReVanced settings.
*/
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt
index 3da73226b..b042b11a0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt
@@ -17,7 +17,7 @@ internal val settingsMenuVideoSpeedGroup = mutableSetOf()
@Suppress("unused")
val playbackSpeedPatch = bytecodePatch(
name = "Playback speed",
- description = "Adds options to customize available playback speeds, set default a playback speed, " +
+ description = "Adds options to customize available playback speeds, set a default playback speed, " +
"and show a speed dialog button in the video player.",
) {
dependsOn(
diff --git a/patches/src/main/resources/addresources/values-af-rZA/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-af-rZA/strings.xml
+++ b/patches/src/main/resources/addresources/values-af-rZA/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-am-rET/strings.xml b/patches/src/main/resources/addresources/values-am-rET/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-am-rET/strings.xml
+++ b/patches/src/main/resources/addresources/values-am-rET/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ar-rSA/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/strings.xml
index 34d523cc3..996d7def4 100644
--- a/patches/src/main/resources/addresources/values-ar-rSA/strings.xml
+++ b/patches/src/main/resources/addresources/values-ar-rSA/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
تسجيل تصحيح الأخطاء
تم تمكين تسجيلات تصحيح الأخطاء
تم تعطيل تسجيلات تصحيح الأخطاء
+ سجل تتبع المكدس
+ تتضمن سجلات التصحيح سجل تتبع المكدس
+ لا تتضمن سجلات التصحيح سجل تتبع المكدس
+ عرض ملاحظة عند وجود خطأ في ReVanced
+ يتم عرض ملاحظة في حالة حدوث خطأ
+ لا يتم عرض ملاحظة في حالة حدوث خطأ
+ "يؤدي إيقاف تشغيل ملاحظات الأخطاء إلى إخفاء كافة إشعارات أخطاء ReVanced.
+
+لن يتم إعلامك بأي أخطاء غير متوقعة."
تصدير سجلات تصحيح الأخطاء
نسخ سجلات تصحيح أخطاء ReVanced إلى الحافظة
تم تعطيل تسجيلات تصحيح الأخطاء
@@ -151,15 +160,6 @@ Second \"item\" text"
يمكن أن يساعد هذا في تحديد المكونات عند إنشاء عوامل تصفية مخصصة.
ومع ذلك، سيؤدي تمكين هذا أيضًا إلى تسجيل بعض بيانات المستخدم مثل عنوان IP الخاص بك."
- سجل تتبع المكدس
- تتضمن سجلات التصحيح سجل تتبع المكدس
- لا تتضمن سجلات التصحيح سجل تتبع المكدس
- عرض ملاحظة عند وجود خطأ في ReVanced
- يتم عرض ملاحظة في حالة حدوث خطأ
- لا يتم عرض ملاحظة في حالة حدوث خطأ
- "يؤدي إيقاف تشغيل ملاحظات الأخطاء إلى إخفاء كافة إشعارات أخطاء ReVanced.
-
-لن يتم إعلامك بأي أخطاء غير متوقعة."
إخفاء بطاقات الألبوم
@@ -586,6 +586,10 @@ Second \"item\" text"
إخفاء إيقاف الإعلانات
زر إيقاف الإعلانات مخفي
زر إيقاف الإعلانات معروض
+
+ إخفاء التعليقات
+ زر التعليقات مخفي
+ زر التعليقات ظاهر
إخفاء الإبلاغ
@@ -1242,8 +1246,9 @@ Second \"item\" text"
إذا تم إيقاف تشغيله لاحقًا، من المستحسن مسح بيانات التطبيق لمنع حدوث أخطاء في واجهة المستخدم."
الهدف من تغيير إصدار التطبيق
- 19.35.36 - استعادة أيقونات مشغل Shorts القديمة
- 19.01.34 - استعادة أيقونات التنقل القديمة
+ 20.13.41 - استعادة شريط إجراءات الفيديو غير المطوي
+ 19.35.36 - استعادة أيقونات مشغل Shorts القديمة
+ 19.01.34 - استعادة أيقونات التنقل القديمة
تغيير صفحة البداية
@@ -1559,12 +1564,15 @@ Second \"item\" text"
• عميل تجريبي وقد يتوقف عن العمل في أي وقت
• لا يوجد ترميز الفيديو AV1
• قد لا يتم تشغيل الفيديوهات المخصصة للأطفال عند تسجيل الخروج أو عند استخدام وضع التصفح المتخفي
+
+ • فرض الصوت الأصلي غير متاح
عرض في إحصاءات تقنية
يتم عرض نوع العميل في إحصاءات تقنية
تم إخفاء نوع العميل في إحصاءات تقنية
لغة بث الصوت
لتحديد لغة صوتية معينة، قم بإيقاف تشغيل \"فرض لغة الصوت الأصلية\"
+ اختيار لغة البث غير متاح مع Android Studio
@@ -1599,23 +1607,23 @@ Second \"item\" text"
شريط التنقل
إخفاء أزرار شريط التنقل أو تغييرها
- إخفاء زر \"الرئيسية\"
+ إخفاء الرئيسية
زر \"الرئيسية\" مخفي
زر \"الرئيسية\" ظاهر
- إخفاء زر \"المقتطفات\"
+ إخفاء المقاطع
زر \"المقتطفات\" مخفي
زر \"المقتطفات\" ظاهر
- إخفاء زر \"استكشاف\"
+ إخفاء استكشاف
زر \"استكشاف\" مخفي
زر \"استكشاف\" ظاهر
- إخفاء زر \"المكتبة\"
+ إخفاء المكتبة
زر \"المكتبة\" مخفي
زر \"المكتبة\" ظاهر
- إخفاء زر \"الترقية\"
+ إخفاء ترقية
زر \"الترقية\" مخفي
زر \"الترقية\" ظاهر
إخفاء شريط التنقل
diff --git a/patches/src/main/resources/addresources/values-as-rIN/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/strings.xml
index 89ca72a06..a1f6341bd 100644
--- a/patches/src/main/resources/addresources/values-as-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-as-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -245,6 +246,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-az-rAZ/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/strings.xml
index 50b6a7bbf..b3ed48ac1 100644
--- a/patches/src/main/resources/addresources/values-az-rAZ/strings.xml
+++ b/patches/src/main/resources/addresources/values-az-rAZ/strings.xml
@@ -107,6 +107,15 @@ Oynatma işləməyə bilər"
Sazlama jurnalı
Sazlama jurnalı işləkdir
Sazlama jurnalı qeyri-aktivdir
+ Yığın izləri jurnalı
+ Sazlama jurnalına yığın izləri daxildir
+ Sazlama jurnalına yığın izləri daxil deyil
+ ReVanced xətasında ani bildiriş göstər
+ Xəta baş verərsə ani bildiriş görünür
+ Xəta baş verərsə ani bildiriş görünmür
+ "Xəta ani bildirişlərin qapatmaq, bütün ReVanced xəta bildirişlərin gizlədir.
+
+Gözlənilməz hallardan xəbərdar olmayacaqsınız."
Sazlama qeydlərini ixrac edin
ReVanced sazlama qeydlərini buferə köçürür
Sazlama qeydi qapalıdır
@@ -151,15 +160,6 @@ Oynatma işləməyə bilər"
Bu, xüsusi filtrlər yaradarkən quruluşları müəyyən etməyə kömək edə bilər.
Hər halda, bunu aktivləşdirmə IP ünvanınız kimi bəzi istifadəçi məlumatın da daxil edəcək."
- Yığın izləri jurnalı
- Sazlama jurnalına yığın izləri daxildir
- Sazlama jurnalına yığın izləri daxil deyil
- ReVanced xətasında ani bildiriş göstər
- Xəta baş verərsə ani bildiriş görünür
- Xəta baş verərsə ani bildiriş görünmür
- "Xəta ani bildirişlərin qapatmaq, bütün ReVanced xəta bildirişlərin gizlədir.
-
-Gözlənilməz hallardan xəbərdar olmayacaqsınız."
Albom kartlarını gizlət
@@ -586,6 +586,10 @@ Ekranın sağ tərəfində düzünə sürüşdürərək səs səviyyəsini tənz
Reklamları Dayandırın-ı gizlət
Reklamları dayandır düyməsi gizlidir
Reklamları dayandır düyməsi görünür
+
+ Şərhləri Gizlət
+ Şərhlər düyməsi gizlidir
+ Şərhlər düyməsi görünür
\"Xəbər verin\"i gizlət
@@ -1241,8 +1245,9 @@ Bu tətbiqin görünüşün və xüsusiyyətlərin dəyişdirəcək, lakin bilin
Sonradan qapadılarsa, UI səhvlərin önləmək üçün tətbiq məlumatların silmək tövsiyə olunur."
Saxta tətbiq versiyası hədəfi
- 19.35.36 - Köhnə Shorts oynadıcı işarələrin bərpa et
- 19.01.34 - Köhnə fəaliyyət simvolların bərpa et
+ 20.13.41 - Yığılmayan video fəaliyyət cizgisin bərpa et
+ 19.35.36 - Köhnə Shorts oynadıcı işarələrin bərpa et
+ 19.01.34 - Köhnə fəaliyyət simvolların bərpa et
Başlatma səhifəsini dəyişdir
@@ -1558,12 +1563,15 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
• Təcrübi qəbuledici və hər vaxt işləməyi dayandıra bilər
• AV1 video kodlayıcı yoxdur
• Giriş edilməyəndə və ya gizli rejimdə uşaq videoları oynadıla bilməz
+
+ • \"Orijinal səsi zorla\" əlçatmazdır
İstək üçün Statistikada göstər
Qəbuledici növü İstək üçün statistikada göstərilir
Qəbuledici nerd üçün Statistikada gizlidir
Səs yayım dili
Xüsusi səs dilini seçmək üçün \"Orijinal səs dilini zorlanı\" qapat
+ Yayım dili seçimi Android Studio ilə əlçatmazdır
@@ -1585,6 +1593,9 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
Kəsintisiz təkrarlama qapalıdır
+ Yayım düyməsini gizlət
+ Yayım düyməsi gizlidir
+ Yayım düyməsi göstərilir
+ Fəaliyyət cizgisi
+ Fəaliyyət cizgisi düymələrini gizlət və ya dəyiş
+ Ev-i Gizlət
+ Ev düyməsi gizlidir
+ Ev düyməsi görünür
+ Nümunələri Gizlət
+ Nümunələr düyməsi gizlidir
+ Nümunələr düyməsi görünür
+ Kəşf edin-i gizlət
+ Kəşf et düyməsi gizlidir
+ Kəşf et düyməsi görünür
+ Kitabxananı gizlət
+ Kitabxana düyməsi gizlidir
+ Kitabxana düyməsi görünür
+ Təkmilləşdirməni Gizlət
+ Təkmilləşdirmə düyməsi gizlidir
+ Təkmilləşdirmə düyməsi görünür
+ Fəaliyyət cizgisin gizlət
+ Fəaliyyət cizgisi gizlidir
+ Fəaliyyət cizgisi görünür
+ Fəaliyyət düyməsi etiketlərini gizlət
+ Etiketlər gizlidir
+ Etiketlər görünür
\'Musiqi Premiumu Əldə et\' etiketini gizlət
diff --git a/patches/src/main/resources/addresources/values-be-rBY/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/strings.xml
index 4e16211e9..2088ba77a 100644
--- a/patches/src/main/resources/addresources/values-be-rBY/strings.xml
+++ b/patches/src/main/resources/addresources/values-be-rBY/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Запіс адладкі
Журналы адладкі ўключаны
Журналы адладкі адключаны
+ Сляды стэка журнала
+ Журналы адладкі ўключаюць трасіроўку стэка
+ Журналы адладкі не ўключаюць трасіроўку стэка
+ Паказаць тост пры памылцы ReVanced
+ Паказваць toast у выпадку памылкі
+ Не паказваць toast у выпадку памылкі
+ "Адключэнне паведамленняў пра памылкі схавае ўсе апавяшчэнні ReVanced пра памылкі.
+
+Вы не будзеце атрымліваць апавяшчэнні пра нечаканыя падзеі."
Экспартаваць адладачныя лагі
Капіруе адладачныя лагі ReVanced у буфер абмену
Адладачнае лагаванне адключана
@@ -151,15 +160,6 @@ Second \"item\" text"
Гэта можа дапамагчы ідэнтыфікаваць кампаненты пры стварэнні карыстацкіх фільтраў.
Аднак уключэнне гэтага параметра таксама будзе запісваць некаторыя даныя карыстальніка, такія як ваш IP-адрас."
- Сляды стэка журнала
- Журналы адладкі ўключаюць трасіроўку стэка
- Журналы адладкі не ўключаюць трасіроўку стэка
- Паказаць тост пры памылцы ReVanced
- Паказваць toast у выпадку памылкі
- Не паказваць toast у выпадку памылкі
- "Адключэнне паведамленняў пра памылкі схавае ўсе апавяшчэнні ReVanced пра памылкі.
-
-Вы не будзеце атрымліваць апавяшчэнні пра нечаканыя падзеі."
Схаваць карты альбома
@@ -586,6 +586,10 @@ Second \"item\" text"
Схаваць Спыніць рэкламу
Кнопка \"Спыніць рэкламу\" схавана
Кнопка \"Спыніць рэкламу\" паказана
+
+ Схаваць каментарыі
+ Кнопка каментарыяў схавана
+ Кнопка каментарыяў паказана
Схаваць справаздачу
@@ -1243,8 +1247,9 @@ Second \"item\" text"
Калі пазней будзе адключана, рэкамендуецца ачысціць даныя прыкладання, каб пазбегнуць памылак у інтэрфейсе."
Падробка мэтавай версіі праграмы
- 19.35.36 — Восстановить старые значки плеера Shorts
- 19.01.34 - Аднаўленне старых значкоў навігацыі
+ 20.13.41 - Аднавіць не згорнуты радок дзеянняў відэа
+ 19.35.36 — Восстановить старые значки плеера Shorts
+ 19.01.34 - Аднаўленне старых значкоў навігацыі
Змяніць стартавую старонку
@@ -1560,12 +1565,15 @@ Second \"item\" text"
• Эксперыментальны кліент і можа спыніць працу ў любы час
• Няма відэакідавання AV1
• Дзіцячыя відэа могуць не прайгравацца ў стане выхаду з акаўнта або ў рэжыме інкогніта
+
+ • Прымусовы арыгінальны аўдыё недаступны
Паказаць у статыстыцы для спецыялістаў
Тып кліента адлюстроўваецца ў статыстыцы для спецыялістаў
Кліент схаваны ў статыстыцы для спецыялістаў
Мова аўдыяпатоку
Каб выбраць пэўную мову аўдыё, адключыце \'Прымусовая арыгінальная мова аўдыё\'
+ Выбар мовы трансляцыі недаступны ў Android Studio
@@ -1600,23 +1608,23 @@ Second \"item\" text"
Панэль навігацыі
Схаваць або змяніць кнопкі панэлі навігацыі
- Схаваць кнопку \"Галоўная\"
+ Схаваць Галоўную
Кнопка \"Галоўная\" схавана
Кнопка \"Галоўная\" паказана
- Схаваць кнопку \"Узоры\"
+ Схаваць Узоры
Кнопка \"Узоры\" схавана
Кнопка \"Узоры\" паказана
- Схаваць кнопку \"Агляд\"
+ Схаваць Агляд
Кнопка \"Агляд\" схавана
Кнопка \"Агляд\" паказана
- Схаваць кнопку \"Бібліятэка\"
+ Схаваць Бібліятэку
Кнопка \"Бібліятэка\" схавана
Кнопка \"Бібліятэка\" паказана
- Схаваць кнопку \"Абнавіць\"
+ Схаваць Прэміум
Кнопка \"Абнавіць\" схавана
Кнопка \"Абнавіць\" паказана
Схаваць панэль навігацыі
diff --git a/patches/src/main/resources/addresources/values-bg-rBG/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/strings.xml
index 420c5cbde..21234bbc7 100644
--- a/patches/src/main/resources/addresources/values-bg-rBG/strings.xml
+++ b/patches/src/main/resources/addresources/values-bg-rBG/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Дневник на отстраняването на грешки
Дневникът за остраняване на грешки е активиран
Дневникът за остраняване на грешки е деактивиран
+ Следи от стека на дневника
+ Дневникът за отстраняване на грешки съдържа следи от стека
+ Дневникът за отстраняване на грешки не съдържа следи от стека
+ Покажи системно съобщение при ReVanced грешка
+ Показва се toast, ако възникне грешка
+ Не се показва toast, ако възникне грешка
+ "Изключването на изскачащи съобщения за грешки крие всички известия за грешки на ReVanced.
+
+Няма да бъдете уведомени за неочаквани събития."
Експортиране на логове за отстраняване на грешки
Копира логовете за отстраняване на грешки на ReVanced в клипборда
Отстраняването на грешки е деактивирано
@@ -151,15 +160,6 @@ Second \"item\" text"
Това може да помогне за идентифициране на компоненти при създаване на персонализирани филтри.
Активирането на тази настройка обаче ще регистрира и някои потребителски данни, като например вашия IP адрес."
- Следи от стека на дневника
- Дневникът за отстраняване на грешки съдържа следи от стека
- Дневникът за отстраняване на грешки не съдържа следи от стека
- Покажи системно съобщение при ReVanced грешка
- Показва се toast, ако възникне грешка
- Не се показва toast, ако възникне грешка
- "Изключването на изскачащи съобщения за грешки крие всички известия за грешки на ReVanced.
-
-Няма да бъдете уведомени за неочаквани събития."
\"Карти на албумите\"
@@ -586,6 +586,10 @@ Second \"item\" text"
Скриване на „Спиране на реклами“
Бутонът за спиране на реклами е скрит
Бутонът за спиране на реклами е показан
+
+ Скрий коментари
+ Бутонът за коментари е скрит
+ Бутонът за коментари е показан
Бутон за доклади
@@ -1242,8 +1246,9 @@ Second \"item\" text"
Ако по-късно бъде изключено, препоръчително е да изчистите данните на приложението, за да предотвратите грешки в потребителския интерфейс."
Подлъгване за версията на
- 19.35.36 - Възстановете старите икони на Shorts в плейъра
- 19.01.34 - Възстановяване на стари икони за навигация
+ 20.13.41 - Възстановяване на несгъваема лента с действия за видео
+ 19.35.36 - Възстановете старите икони на Shorts в плейъра
+ 19.01.34 - Възстановяване на стари икони за навигация
Промяна на началната страница
@@ -1559,12 +1564,15 @@ Second \"item\" text"
• Експериментален клиент и може да спре да работи по всяко време
• Без AV1 видео кодек
• Детските видеоклипове може да не се възпроизвеждат, когато сте излезли от профила си или в режим \"инкогнито\"
+
+ • Принудителният оригинален звук не е наличен
Poka6i v Statistiki za nerds
Tipът na klienta se poka6va v Statistiki za nerds
Klientът e skriт v Statistiki za nerds
Език на аудио потока
За да изберете конкретен аудио език, изключете \'Принудително оригинален аудио език\'
+ Изборът на език на потока не е наличен с Android Studio
@@ -1599,23 +1607,23 @@ Second \"item\" text"
Навигационна лента
Скриване или промяна на бутоните на навигационната лента
- Скриване на бутона Начало
+ Скриване на Начало
Бутонът Начало е скрит
Бутонът Начало е показан
- Скриване на бутона Мостри
+ Скриване на Мостри
Бутонът Мостри е скрит
Бутонът Мостри е показан
- Скриване на бутона Проучване
+ Скриване на Разглеждане
Бутонът Проучване е скрит
Бутонът Проучване е показан
- Скриване на бутона Библиотека
+ Скриване на Библиотека
Бутонът Библиотека е скрит
Бутонът Библиотека е показан
- Скриване на бутона Надграждане
+ Скриване на Надстройка
Бутонът Надграждане е скрит
Бутонът Надграждане е показан
Скриване на навигационната лента
diff --git a/patches/src/main/resources/addresources/values-bn-rBD/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/strings.xml
index 1baa688da..8876b2690 100644
--- a/patches/src/main/resources/addresources/values-bn-rBD/strings.xml
+++ b/patches/src/main/resources/addresources/values-bn-rBD/strings.xml
@@ -107,6 +107,15 @@ MicroG-এর জন্য ব্যাটারি অপ্টিমাইজ
ডিবাগ লগিং
ডিবাগ লগ সক্রিয় হয়েছে
ডিবাগ লগ নিষ্ক্রিয় হয়েছে
+ স্টেক ট্রেস লগ
+ ডিবাগ লগ স্টেক ট্রেস সংযুক্ত করবে
+ ডিবাগ লগ স্টেক ট্রেস সংযুক্ত করবে না
+ ReVanced এর ত্রুটির ক্ষেত্রে টোস্ট দেখান
+ যদি ত্রুটি ঘটে তবে toast দেখানো হবে
+ যদি ত্রুটি ঘটে তবে toast দেখানো হবে না
+ "ত্রুটি \"toast\" বন্ধ করে ReVanced ত্রুটি বিজ্ঞপ্তিগুলি লুকানো হয়।
+
+আপনি কোনও অপ্রত্যাশিত ঘটনার বিষয়ে অবহিত হবেন না।"
ডিবাগ লগগুলি রফতানি করুন
ক্লিপবোর্ডে ReVanced ডিবাগ লগগুলি অনুলিপি করে
ডিবাগ লগিং নিষ্ক্রিয় করা হয়েছে
@@ -147,15 +156,6 @@ MicroG-এর জন্য ব্যাটারি অপ্টিমাইজ
ডিবাগ লগ প্রটোকল বাফার সংযুক্ত করবে
ডিবাগ লগ প্রটোকল বাফার সংযুক্ত করবে না
"এই সেটিংস সক্ষম করলে কিছু UI উপাদানের জন্য অন-স্ক্রীন পাঠ্য সহ অতিরিক্ত লেআউট ডেটা লগ করা হবে।\n\nএটি কাস্টম ফিল্টার তৈরি করার সময় উপাদান সনাক্ত করতে সাহায্য করতে পারে।\n\nতবে, এটি সক্রিয় করলে আপনার আইপি ঠিকানার মতো কিছু ব্যবহারকারীর ডেটাও লগ করা হবে।"
- স্টেক ট্রেস লগ
- ডিবাগ লগ স্টেক ট্রেস সংযুক্ত করবে
- ডিবাগ লগ স্টেক ট্রেস সংযুক্ত করবে না
- ReVanced এর ত্রুটির ক্ষেত্রে টোস্ট দেখান
- যদি ত্রুটি ঘটে তবে toast দেখানো হবে
- যদি ত্রুটি ঘটে তবে toast দেখানো হবে না
- "ত্রুটি \"toast\" বন্ধ করে ReVanced ত্রুটি বিজ্ঞপ্তিগুলি লুকানো হয়।
-
-আপনি কোনও অপ্রত্যাশিত ঘটনার বিষয়ে অবহিত হবেন না।"
অ্যালবাম কার্ড লুকান
@@ -582,6 +582,10 @@ MicroG-এর জন্য ব্যাটারি অপ্টিমাইজ
বিজ্ঞাপন বন্ধ করুন লুকান
বিজ্ঞাপন বন্ধ করুন বোতামটি লুকানো আছে
বিজ্ঞাপন বন্ধ করুন বোতামটি দেখানো হচ্ছে
+
+ মন্তব্য লুকান
+ মন্তব্য বোতাম লুকানো আছে
+ মন্তব্য বোতাম দেখানো আছে
Report লুকান
@@ -1238,8 +1242,9 @@ YouTube সেটিংসে অটো প্লে পরিবর্তন
পরে যদি বন্ধ করা হয়, UI বাগ এড়াতে অ্যাপ্লিকেশন ডেটা পরিষ্কার করার পরামর্শ দেওয়া হয়।"
স্পুফ অ্যাপ সংস্করণ লক্ষ্য
- 19.35.36 - পুরনো Shorts প্লেয়ার আইকন পুনরুদ্ধার করুন
- 19.01.34 - পুরনো নেভিগেশন আইকন পুনরুদ্ধার করুন
+ 20.13.41 - প্রসারিত নয় এমন ভিডিও অ্যাকশন বার পুনরুদ্ধার করুন
+ 19.35.36 - পুরনো Shorts প্লেয়ার আইকন পুনরুদ্ধার করুন
+ 19.01.34 - পুরনো নেভিগেশন আইকন পুনরুদ্ধার করুন
শুরুর পৃষ্ঠা পরিবর্তন করুন
@@ -1555,12 +1560,15 @@ DeArrow সম্পর্কে আরও জানতে এখানে ট
• পরীক্ষামূলক ক্লায়েন্ট এবং যেকোনো সময় কাজ করা বন্ধ করতে পারে
• কোনো AV1 ভিডিও কোডেক নেই
• লগআউট করা হলে বা ছদ্মবেশী মোডে বাচ্চাদের ভিডিও চলতে নাও পারে
+
+ • মূল অডিও জোরপূর্বক উপলব্ধ নেই
স্ট্যাটস ফর নার্ডসে দেখান
স্ট্যাটস ফর নার্ডসে ক্লায়েন্ট প্রকার দেখানো হবে
স্ট্যাটস ফর নার্ডসে ক্লায়েন্ট লুকানো হবে
অডিও স্ট্রিম ভাষা
একটি নির্দিষ্ট অডিও ভাষা নির্বাচন করতে, \'মূল অডিও ভাষা জোর করে চালু রাখুন\' বন্ধ করুন
+ অ্যান্ড্রয়েড স্টুডিও সহ স্ট্রিম ভাষার নির্বাচন উপলব্ধ নেই
@@ -1595,23 +1603,23 @@ DeArrow সম্পর্কে আরও জানতে এখানে ট
নেভিগেশন বার
নেভিগেশন বারের বোতামগুলি লুকান বা পরিবর্তন করুন
- হোম বোতাম লুকান
+ হোম লুকান
হোম বোতাম লুকানো আছে
হোম বোতাম দেখানো আছে
- স্যাম্পল বোতাম লুকান
+ স্যাম্পল লুকান
স্যাম্পল বোতাম লুকানো আছে
স্যাম্পল বোতাম দেখানো আছে
- অনুসন্ধান বোতাম লুকান
+ এক্সপ্লোর লুকান
অনুসন্ধান বোতাম লুকানো আছে
অনুসন্ধান বোতাম দেখানো আছে
- লাইব্রেরি বোতাম লুকান
+ লাইব্রেরি লুকান
লাইব্রেরি বোতাম লুকানো আছে
লাইব্রেরি বোতাম দেখানো আছে
- আপগ্রেড বোতাম লুকান
+ আপগ্রেড লুকান
আপগ্রেড বোতাম লুকানো আছে
আপগ্রেড বোতাম দেখানো আছে
নেভিগেশন বার লুকান
diff --git a/patches/src/main/resources/addresources/values-bs-rBA/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-bs-rBA/strings.xml
+++ b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ca-rES/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-ca-rES/strings.xml
+++ b/patches/src/main/resources/addresources/values-ca-rES/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml
index 8c092a5c9..7001dccee 100644
--- a/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml
+++ b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml
@@ -107,6 +107,15 @@ Přehrávání nemusí fungovat"
Debugovací záznamy
Debugovací záznamy jsou zapnuty
Debugovací záznamy jsou vypnuty
+ Protokolovat trasování zásobníku
+ Protokoly ladění obsahují trasování zásobníku
+ Ladící protokoly nezahrnují sledování zásobníku
+ Zobrazit oznámení v případě chyby ReVanced
+ Zobrazí se toast, pokud dojde k chybě
+ Toast se nezobrazí, pokud dojde k chybě
+ "Vypnutí chybových toastů skryje všechna chybová oznámení ReVanced.
+
+Nebudete informováni o žádné neočekávané události."
Exportovat ladicí protokoly
Zkopíruje ladicí protokoly ReVanced do schránky
Ladění je vypnuto
@@ -151,15 +160,6 @@ Přehrávání nemusí fungovat"
To může pomoci identifikovat komponenty při vytváření vlastních filtrů.
Povolením této možnosti se však budou zaznamenávat i některá uživatelská data, jako je vaše IP adresa."
- Protokolovat trasování zásobníku
- Protokoly ladění obsahují trasování zásobníku
- Ladící protokoly nezahrnují sledování zásobníku
- Zobrazit oznámení v případě chyby ReVanced
- Zobrazí se toast, pokud dojde k chybě
- Toast se nezobrazí, pokud dojde k chybě
- "Vypnutí chybových toastů skryje všechna chybová oznámení ReVanced.
-
-Nebudete informováni o žádné neočekávané události."
Skrýt karty alb
@@ -586,6 +586,10 @@ Hlasitost se upravuje svislým přejetím po pravé straně obrazovky"
Skrýt Zastavit reklamy
Tlačítko pro zastavení reklam je skryté
Tlačítko pro zastavení reklam je zobrazeno
+
+ Skrýt komentáře
+ Tlačítko komentářů je skryto
+ Tlačítko komentářů je zobrazeno
Skrýt Nahlásit
@@ -1242,8 +1246,9 @@ To změní vzhled a funkce aplikace, ale mohou se objevit neznámé vedlejší e
Pokud bude později vypnuta, doporučujeme vymazat data aplikace, aby se zabránilo chybám uživatelského rozhraní."
Cíl napodobení verze aplikace
- 19.35.36 - Obnovuje staré ikony Shorts přehrávače
- 19.01.34 – Obnovit staré ikony navigace
+ 20.13.41 - Obnovit nerozbalený panel akcí videa
+ 19.35.36 - Obnovuje staré ikony Shorts přehrávače
+ 19.01.34 – Obnovit staré ikony navigace
Změnit úvodní stránku
@@ -1559,12 +1564,15 @@ Povolením této funkce lze odemknout vyšší kvality videa"
• Experimentální klient a může kdykoli přestat fungovat
• Žádný video kodek AV1
• Dětská videa se nemusí přehrávat, když jste odhlášení nebo v anonymním režimu
+
+ • Vynutit původní zvuk není k dispozici
Zobrazit ve statistikách pro nadšence
Typ klienta se zobrazuje ve statistikách pro nadšence
Klient je skrytý ve statistikách pro nadšence
Jazyk zvukového streamu
Chcete-li vybrat konkrétní zvukový jazyk, vypněte „Vynutit původní zvukový jazyk“
+ Volba jazyka streamu není k dispozici s Android Studiem
@@ -1599,23 +1607,23 @@ Povolením této funkce lze odemknout vyšší kvality videa"
Navigační panel
Skrýt nebo změnit tlačítka navigačního panelu
- Skrýt tlačítko Domů
+ Skrýt Domů
Tlačítko Domů je skryté
Tlačítko Domů je zobrazeno
- Skrýt tlačítko Ukázky
+ Skrýt Ukázky
Tlačítko Ukázky je skryté
Tlačítko Ukázky je zobrazeno
- Skrýt tlačítko Prozkoumat
+ Skrýt Prozkoumat
Tlačítko Prozkoumat je skryté
Tlačítko Prozkoumat je zobrazeno
- Skrýt tlačítko Knihovna
+ Skrýt Knihovnu
Tlačítko Knihovna je skryté
Tlačítko Knihovna je zobrazeno
- Skrýt tlačítko Vylepšit
+ Skrýt Upgradovat
Tlačítko Vylepšit je skryté
Tlačítko Vylepšit je zobrazeno
Skrýt navigační panel
diff --git a/patches/src/main/resources/addresources/values-da-rDK/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/strings.xml
index 15af0db71..282a112d2 100644
--- a/patches/src/main/resources/addresources/values-da-rDK/strings.xml
+++ b/patches/src/main/resources/addresources/values-da-rDK/strings.xml
@@ -107,6 +107,15 @@ Afspilning fungerer muligvis ikke"
Fejlfindingslogning
Fejlfindingslogge er aktiveret
Fejlfindingslogge er deaktiveret
+ Logstakspor
+ Fejlfindingslogge inkluderer stakspor
+ Fejlfindingslogge inkluderer ikke stakspor
+ Vis toastbesked ved ReVanced-fejl
+ Toast vises, hvis der opstår en fejl
+ Toast vises ikke, hvis der opstår en fejl
+ "Hvis du deaktiverer fejl-toasts, skjules alle ReVanced-fejlmeddelelser.
+
+Du modtager ikke notifikationer om uventede hændelser."
Eksportér fejlsøgningslogfiler
Kopierer ReVanced-fejlsøgningslogfiler til udklipsholderen
Fejlsøgningslogning er deaktiveret
@@ -151,15 +160,6 @@ Afspilning fungerer muligvis ikke"
Dette kan hjælpe med at identificere komponenter, når der oprettes brugerdefinerede filtre.
Aktivering af dette vil dog også logge nogle brugerdata, såsom din IP-adresse."
- Logstakspor
- Fejlfindingslogge inkluderer stakspor
- Fejlfindingslogge inkluderer ikke stakspor
- Vis toastbesked ved ReVanced-fejl
- Toast vises, hvis der opstår en fejl
- Toast vises ikke, hvis der opstår en fejl
- "Hvis du deaktiverer fejl-toasts, skjules alle ReVanced-fejlmeddelelser.
-
-Du modtager ikke notifikationer om uventede hændelser."
Skjul albumkort
@@ -586,6 +586,10 @@ Juster lydstyrken ved at swipe lodret i højre side af skærmen"
Skjul Stop reklamer
Knappen \"Stop annoncer\" er skjult
Knappen \"Stop annoncer\" vises
+
+ Skjul kommentarer
+ Kommentarknappen er skjult
+ Kommentarknappen vises
Skjul Rapport
@@ -1244,8 +1248,9 @@ Dette ændrer appens udseende og funktioner, men ukendte bivirkninger kan foreko
Hvis det senere slås fra, anbefales det at rydde app-dataene for at forhindre UI-fejl."
Spoof app version mål
- 19.35.36 - Gendan gamle Shorts player ikoner
- 19.01.34 - Gendan gamle navigationsikoner
+ 20.13.41 - Gendan ikke-kollapset videohandlingslinje
+ 19.35.36 - Gendan gamle Shorts player ikoner
+ 19.01.34 - Gendan gamle navigationsikoner
Skift startside
@@ -1561,12 +1566,15 @@ Aktivering af dette kan låse op for højere videokvalitet"
• Eksperimentel klient og kan stoppe med at fungere når som helst
• Intet AV1-videokodek
• Videoer til børn afspilles muligvis ikke, når du er logget ud eller i inkognitotilstand
+
+ • Tving original lyd er ikke tilgængelig
Vis i Statistik for nørder
Klienttypen vises i Statistik for nørder
Klienten er skjult i Statistik for nørder
Lydstreamsprog
For at vælge et specifikt lydsprog, slå \'Gennemtving originalt lydsprog\' fra
+ Valg af streaming-sprog er ikke tilgængeligt med Android Studio
@@ -1601,23 +1609,23 @@ Aktivering af dette kan låse op for højere videokvalitet"
Navigationslinje
Skjul eller skift navigationslinjeknapper
- Skjul Startside-knap
+ Skjul Startside
Startside-knappen er skjult
Startside-knappen vises
- Skjul Kortklip-knap
+ Skjul Uddrag
Kortklip-knappen er skjult
Kortklip-knappen vises
- Skjul Udforsk-knap
+ Skjul Udforsk
Udforsk-knappen er skjult
Udforsk-knappen vises
- Skjul Bibliotek-knap
+ Skjul Bibliotek
Bibliotek-knappen er skjult
Bibliotek-knappen vises
- Skjul Opgrader-knap
+ Skjul Opgrader
Opgrader-knappen er skjult
Opgrader-knappen vises
Skjul navigationslinje
diff --git a/patches/src/main/resources/addresources/values-de-rDE/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/strings.xml
index 0efed29ce..d7fe3ac5c 100644
--- a/patches/src/main/resources/addresources/values-de-rDE/strings.xml
+++ b/patches/src/main/resources/addresources/values-de-rDE/strings.xml
@@ -107,6 +107,15 @@ Die Wiedergabe funktioniert möglicherweise nicht"
Debug-Protokollierung
Debug-Protokolle sind aktiviert
Debug-Protokolle sind deaktiviert
+ Stacktraces protokollieren
+ Debug-Protokolle enthalten Stacktrace
+ Debug-Logs enthalten keine Stack-Traces
+ Toast bei ReVanced Fehler anzeigen
+ Ein Toast wird angezeigt, wenn ein Fehler auftritt
+ Es wird keine Toast-Nachricht angezeigt, wenn ein Fehler auftritt
+ "Das Ausschalten von Fehler-Toasts blendet alle Benachrichtigungen über Fehler in ReVanced aus.
+
+Sie werden nicht über unerwartete Ereignisse informiert."
Debug-Protokolle exportieren
Kopiert ReVanced-Debug-Protokolle in die Zwischenablage
Debug-Protokollierung ist deaktiviert
@@ -151,15 +160,6 @@ Die Wiedergabe funktioniert möglicherweise nicht"
Dies kann helfen, Komponenten bei der Erstellung benutzerdefinierter Filter zu identifizieren.
Wenn Sie dies aktivieren, werden jedoch auch einige Benutzerdaten wie Ihre IP-Adresse protokolliert."
- Stacktraces protokollieren
- Debug-Protokolle enthalten Stacktrace
- Debug-Logs enthalten keine Stack-Traces
- Toast bei ReVanced Fehler anzeigen
- Ein Toast wird angezeigt, wenn ein Fehler auftritt
- Es wird keine Toast-Nachricht angezeigt, wenn ein Fehler auftritt
- "Das Ausschalten von Fehler-Toasts blendet alle Benachrichtigungen über Fehler in ReVanced aus.
-
-Sie werden nicht über unerwartete Ereignisse informiert."
Albumkarten ausblenden
@@ -583,6 +583,10 @@ Passen Sie die Helligkeit an, indem Sie auf der linken Seite des Bildschirms ver
Werbung stoppen ausblenden
Schaltfläche \"Werbung stoppen\" ist ausgeblendet
Schaltfläche \"Werbung stoppen\" ist sichtbar
+
+ Kommentare ausblenden
+ Kommentare-Schaltfläche ist ausgeblendet
+ Kommentare-Schaltfläche wird angezeigt
Bericht ausblenden
@@ -1239,8 +1243,9 @@ Dadurch ändert sich das Erscheinungsbild und die Funktionen der App, es können
Wenn Sie die Funktion später deaktivieren, wird empfohlen, die App-Daten zu löschen, um UI-Fehler zu vermeiden."
Spoof-App-Versionsziel
- 19.35.36 - Alte Shorts Spielersymbole wiederherstellen
- 19.01.34 - Alte Navigations-Symbole wiederherstellen
+ 20.13.41 - Nicht eingeklappte Video-Aktionsleiste wiederherstellen
+ 19.35.36 - Alte Shorts Spielersymbole wiederherstellen
+ 19.01.34 - Alte Navigations-Symbole wiederherstellen
Startseite ändern
@@ -1556,12 +1561,15 @@ Durch Aktivieren dieser Option können höhere Videoqualitäten freigeschaltet w
• Experimenteller Client und kann jederzeit aufhören zu funktionieren
• Kein AV1-Videocodec
• Kinder-Videos werden möglicherweise nicht abgespielt, wenn du abgemeldet bist oder den Inkognito-Modus verwendest.
+
+ • Originalton erzwingen ist nicht verfügbar
In Statistiken für Nerds anzeigen
Der Client-Typ wird in den Statistiken für Nerds angezeigt
Der Client wird in den Statistiken für Nerds ausgeblendet
Audiodatenstromsprache
Um eine bestimmte Audiosprache auszuwählen, deaktivieren Sie „Original-Audiosprache erzwingen“
+ Die Auswahl der Stream-Sprache ist mit Android Studio nicht verfügbar.
@@ -1596,23 +1604,23 @@ Durch Aktivieren dieser Option können höhere Videoqualitäten freigeschaltet w
Navigationsleiste
Navigationsleisten-Schaltflächen ausblenden oder ändern
- Start-Schaltfläche ausblenden
+ Startseite ausblenden
Start-Schaltfläche ist ausgeblendet
Start-Schaltfläche wird angezeigt
- Samples-Schaltfläche ausblenden
+ Kurzvideos ausblenden
Samples-Schaltfläche ist ausgeblendet
Samples-Schaltfläche wird angezeigt
- Entdecken-Schaltfläche ausblenden
+ Erkunden ausblenden
Entdecken-Schaltfläche ist ausgeblendet
Entdecken-Schaltfläche wird angezeigt
- Mediathek-Schaltfläche ausblenden
+ Mediathek ausblenden
Mediathek-Schaltfläche ist ausgeblendet
Mediathek-Schaltfläche wird angezeigt
- Upgrade-Schaltfläche ausblenden
+ Upgrade ausblenden
Upgrade-Schaltfläche ist ausgeblendet
Upgrade-Schaltfläche wird angezeigt
Navigationsleiste ausblenden
diff --git a/patches/src/main/resources/addresources/values-el-rGR/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/strings.xml
index 478bc23c8..cd9670794 100644
--- a/patches/src/main/resources/addresources/values-el-rGR/strings.xml
+++ b/patches/src/main/resources/addresources/values-el-rGR/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Καταγραφή εντοπισμού σφαλμάτων
Η καταγραφή εντοπισμού σφαλμάτων είναι ενεργοποιημένη
Η καταγραφή εντοπισμού σφαλμάτων είναι απενεργοποιημένη
+ Καταγραφή ιχνών στοίβας
+ Τα αρχεία καταγραφής σφαλμάτων περιλαμβάνουν ίχνη στοίβας
+ Τα αρχεία καταγραφής σφαλμάτων δεν περιλαμβάνουν ίχνη στοίβας
+ Εμφάνιση μηνυμάτων σφαλμάτων ReVanced
+ Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν προκύψει σφάλμα
+ Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν προκύψει σφάλμα
+ "Η απενεργοποίηση των μηνυμάτων σφαλμάτων κρύβει όλες τις ειδοποιήσεις σφαλμάτων που αφορούν το ReVanced.
+
+Δεν θα ειδοποιηθείτε για τυχόν απρόβλεπτα γεγονότα."
Εξαγωγή αρχείων καταγραφής εντοπισμού σφαλμάτων
Αντιγραφή των αρχείων καταγραφής εντοπισμού σφαλμάτων ReVanced στο πρόχειρο
Η καταγραφή εντοπισμού σφαλμάτων είναι απενεργοποιημένη
@@ -151,15 +160,6 @@ Second \"item\" text"
Αυτό μπορεί να βοηθήσει στον εντοπισμό στοιχείων κατά τη δημιουργία προσαρμοσμένων φίλτρων.
Ωστόσο, η ενεργοποίηση αυτής της ρύθμισης θα καταγράψει επίσης ορισμένα δεδομένα χρήστη, όπως τη διεύθυνση IP σας."
- Καταγραφή ιχνών στοίβας
- Τα αρχεία καταγραφής σφαλμάτων περιλαμβάνουν ίχνη στοίβας
- Τα αρχεία καταγραφής σφαλμάτων δεν περιλαμβάνουν ίχνη στοίβας
- Εμφάνιση μηνυμάτων σφαλμάτων ReVanced
- Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν προκύψει σφάλμα
- Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν προκύψει σφάλμα
- "Η απενεργοποίηση των μηνυμάτων σφαλμάτων κρύβει όλες τις ειδοποιήσεις σφαλμάτων που αφορούν το ReVanced.
-
-Δεν θα ειδοποιηθείτε για τυχόν απρόβλεπτα γεγονότα."
Κάρτες άλμπουμ
@@ -588,6 +588,10 @@ Second \"item\" text"
Κουμπί «Διακοπή διαφημίσεων»
Κρυμμένο
Εμφανίζεται
+
+ Απόκρυψη σχολίων
+ Κρυμμένο
+ Εμφανίζεται
Μενού «Αναφορά»
@@ -1243,8 +1247,9 @@ Second \"item\" text"
Αν αργότερα απενεργοποιηθεί, συνιστάται η εκκαθάριση δεδομένων της εφαρμογής για την αποφυγή σφαλμάτων UI."
Έκδοση παραποίησης της εφαρμογής
- 19.35.36 - Επαναφορά των παλιών εικονιδίων της οθόνης αναπαραγωγής Shorts
- 19.01.34 - Επαναφορά παλιών εικονιδίων γραμμής πλοήγησης
+ 20.13.41 - Επαναφορά της μη συμπτυγμένης γραμμής ενεργειών βίντεο
+ 19.35.36 - Επαναφορά των παλιών εικονιδίων της οθόνης αναπαραγωγής Shorts
+ 19.01.34 - Επαναφορά παλιών εικονιδίων γραμμής πλοήγησης
Αλλαγή αρχικής σελίδας
@@ -1558,12 +1563,15 @@ Second \"item\" text"
• Πειραματικός πελάτης και μπορεί να σταματήσει να λειτουργεί ανά πάσα στιγμή
• Δεν υπάρχει ο κωδικοποιητής βίντεο AV1
• Τα βίντεο για παιδιά ενδέχεται να μην αναπαράγονται αν είστε αποσυνδεδεμένοι ή σε λειτουργία ανώνυμης περιήγησης
+
+ • Ο εξαναγκασμός αρχικής γλώσσας ήχου δεν είναι διαθέσιμος
Εμφάνιση στο μενού «Στατιστικά για σπασίκλες»
Το πρόγραμμα πελάτη εμφανίζεται στο μενού «Στατιστικά για σπασίκλες»
Το πρόγραμμα πελάτη δεν εμφανίζεται στο μενού «Στατιστικά για σπασίκλες»
Γλώσσα ροής ήχου
Για να επιλέξετε μια συγκεκριμένη γλώσσα ήχου, απενεργοποιήστε το «Εξαναγκασμός αρχικής γλώσσας ήχου»
+ Η επιλογή γλώσσας ροής δεν είναι διαθέσιμη με το Android Studio
@@ -1585,7 +1593,7 @@ Second \"item\" text"
Η μόνιμη επανάληψη είναι απενεργοποιημένη
- Απόκρυψη κουμπιού μετάδοσης
+ Κουμπί μετάδοσης
Κρυμμένο
Εμφανίζεται
@@ -1596,30 +1604,30 @@ Second \"item\" text"
Γραμμή πλοήγησης
- Απόκρυψη ή αλλαγή κουμπιών γραμμής πλοήγησης
+ Απόκρυψη ή αλλαγή κουμπιών της γραμμής πλοήγησης
- Απόκρυψη κουμπιού Αρχικής οθόνης
+ Κουμπί «Αρχική»
Κρυμμένο
Εμφανίζεται
- Απόκρυψη κουμπιού Δειγμάτων
- Το κουμπί «Δείγματα» είναι κρυμμένο
- Το κουμπί «Δείγματα» εμφανίζεται
+ Απόκρυψη δειγμάτων
+ Κρυμμένο
+ Εμφανίζεται
- Απόκρυψη κουμπιού Εξερεύνησης
- Το κουμπί «Εξερεύνηση» είναι κρυμμένο
- Το κουμπί «Εξερεύνηση» εμφανίζεται
+ Απόκρυψη «Εξερεύνηση»
+ Κρυμμένο
+ Εμφανίζεται
- Απόκρυψη κουμπιού Βιβλιοθήκης
- Το κουμπί «Βιβλιοθήκη» είναι κρυμμένο
- Το κουμπί «Βιβλιοθήκη» εμφανίζεται
+ Απόκρυψη «Βιβλιοθήκη»
+ Κρυμμένο
+ Εμφανίζεται
- Απόκρυψη κουμπιού «Αναβάθμιση»
- Το κουμπί «Αναβάθμιση» είναι κρυμμένο
- Το κουμπί «Αναβάθμιση» εμφανίζεται
+ Απόκρυψη «Αναβάθμιση»
+ Κρυμμένο
+ Εμφανίζεται
Γραμμή πλοήγησης
- Η γραμμή πλοήγησης θα είναι κρυμμένη κατά την αναπαραγωγή Shorts
- Η γραμμή πλοήγησης εμφανίζεται κατά την αναπαραγωγή Shorts
+ Κρυμμένη
+ Εμφανίζεται
Ονομασίες κουμπιών γραμμής πλοήγησης
Κρυμμένες
Εμφανίζονται
diff --git a/patches/src/main/resources/addresources/values-es-rES/strings.xml b/patches/src/main/resources/addresources/values-es-rES/strings.xml
index fa88a6fa4..35390a52d 100644
--- a/patches/src/main/resources/addresources/values-es-rES/strings.xml
+++ b/patches/src/main/resources/addresources/values-es-rES/strings.xml
@@ -107,6 +107,15 @@ La reproducción podría no funcionar"
Registro de depuración
Los registros de depuración están habilitados
Los registros de depuración están desactivados
+ Registrar stack traces
+ Los registros de depuración incluyen stack trace
+ Los registros de depuración no incluyen stack trace
+ Mostrar aviso de error en ReVanced
+ Se mostrará un aviso si se produce un error
+ No se mostrará un aviso si se produce un error
+ "Desactivar los avisos de error oculta todas las notificaciones de error de ReVanced.
+
+No se le notificará de ningún evento inesperado."
Exportar registros de depuración
Copia los registros de depuración de ReVanced al portapapeles
El registro de depuración está desactivado
@@ -151,15 +160,6 @@ La reproducción podría no funcionar"
Esto puede ayudar a identificar componentes al crear filtros personalizados.
Sin embargo, si activas esto, también se registrarán algunos datos del usuario, como tu dirección IP."
- Registrar stack traces
- Los registros de depuración incluyen stack trace
- Los registros de depuración no incluyen stack trace
- Mostrar aviso de error en ReVanced
- Se mostrará un aviso si se produce un error
- No se mostrará un aviso si se produce un error
- "Desactivar los avisos de error oculta todas las notificaciones de error de ReVanced.
-
-No se le notificará de ningún evento inesperado."
Ocultar tarjetas de álbum
@@ -586,6 +586,10 @@ Ajusta el volumen deslizando verticalmente en el lado derecho de la pantalla"Ocultar Detener anuncios
El botón de detener anuncios está oculto
El botón de detener anuncios está mostrado
+
+ Ocultar comentarios
+ El botón de comentarios está oculto
+ El botón de comentarios es visible
Ocultar informe
@@ -1233,8 +1237,9 @@ Esto cambiará la apariencia y las características de la aplicación, pero pued
Si se desactiva posteriormente, se recomienda borrar los datos de la aplicación para evitar errores en la interfaz de usuario."
Versión objetiva de aplicación falsificada
- 19.35.36 - Restaurar iconos antiguos del reproductor de Shorts
- 19.01.34 - Restaurar iconos de navegación antiguos
+ 20.13.41 - Restaurar barra de acciones de vídeo no colapsada
+ 19.35.36 - Restaurar iconos antiguos del reproductor de Shorts
+ 19.01.34 - Restaurar iconos de navegación antiguos
Cambiar página de inicio
@@ -1550,12 +1555,15 @@ Habilitar esto puede desbloquear calidades de vídeo más altas"
• El cliente es experimental y puede dejar de funcionar en cualquier momento
• Sin códec de vídeo AV1
• Es posible que los vídeos infantiles no se reproduzcan cuando se cierra la sesión o se está en modo incógnito
+
+ • Forzar audio original no está disponible
Mostrar en Estadísticas para nerds
El tipo de cliente se muestra en Estadísticas para nerds
El cliente está oculto en Estadísticas para nerds
Idioma de la transmisión de audio
Para seleccionar un idioma de audio específico, desactiva \"Forzar idioma de audio original\"
+ La selección de idioma de transmisión no está disponible con Android Studio
@@ -1590,23 +1598,23 @@ Habilitar esto puede desbloquear calidades de vídeo más altas"
Barra de navegación
Ocultar o cambiar los botones de la barra de navegación
- Ocultar el botón Inicio
+ Ocultar pestaña \'Inicio\'
El botón Inicio está oculto
El botón Inicio es visible
- Ocultar el botón Muestras
+ Ocultar muestras
El botón Muestras está oculto
El botón Muestras es visible
- Ocultar el botón Explorar
+ Ocultar Explorar
El botón Explorar está oculto
El botón Explorar es visible
- Ocultar el botón Biblioteca
+ Ocultar Biblioteca
El botón Biblioteca está oculto
El botón Biblioteca es visible
- Ocultar el botón Actualizar
+ Ocultar Premium
El botón Actualizar está oculto
El botón Actualizar es visible
Ocultar la barra de navegación
diff --git a/patches/src/main/resources/addresources/values-et-rEE/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/strings.xml
index e01a31817..82c512c94 100644
--- a/patches/src/main/resources/addresources/values-et-rEE/strings.xml
+++ b/patches/src/main/resources/addresources/values-et-rEE/strings.xml
@@ -107,6 +107,15 @@ Taasesitus ei pruugi töötada"
Veaotsingulogimine
Veaotsingulogimine on lubatud
Vea logid on keelatud
+ Logi stekira jäljed
+ Silumispäevikud sisaldavad virna jäljendamise
+ Silumispäevikud ei sisalda virna jäljendamise
+ ReVanced veaga ilmumisel kuvag
+ Toast kuvatakse, kui ilmneb tõrge
+ Toast ei kuvata, kui ilmneb tõrge
+ "Vea teadete väljalülitamine peidab kõik ReVanced i veateadete märguanded.
+
+Teid ei teavitata ühestki ootamatust sündmusest."
Ekspordi silumislogid
Kopeerib ReVanced silumislogid lõikelauale
Silumislogimine on keelatud
@@ -151,15 +160,6 @@ Taasesitus ei pruugi töötada"
See võib aidata komponente tuvastada kohandatud filtrite loomisel.
Selle lubamine logib aga ka mõningaid kasutajaandmeid, näiteks teie IP-aadressi."
- Logi stekira jäljed
- Silumispäevikud sisaldavad virna jäljendamise
- Silumispäevikud ei sisalda virna jäljendamise
- ReVanced veaga ilmumisel kuvag
- Toast kuvatakse, kui ilmneb tõrge
- Toast ei kuvata, kui ilmneb tõrge
- "Vea teadete väljalülitamine peidab kõik ReVanced i veateadete märguanded.
-
-Teid ei teavitata ühestki ootamatust sündmusest."
Peida albumikaardid
@@ -586,6 +586,10 @@ Helitugevuse reguleerimiseks pühkige ekraani paremal küljel vertikaalselt"Peida reklaamide peatamine
Peata reklaamide nupp on peidetud
Peata reklaamide nupp on nähtav
+
+ Peida kommentaarid
+ Kommentaaride nupp on peidetud
+ Kommentaaride nupp on nähtav
Peida Teata
@@ -1242,8 +1246,9 @@ See muudab rakenduse välimust ja funktsioone, kuid võivad esineda tundmatud k
Kui see hiljem välja lülitatakse, on soovitatav rakenduse andmed kustutada, et vältida kasutajaliidese vigu."
Võltsitud rakenduse versiooni siht
- 19.35.36 - Taastage vanad Shortsi esitajaikoonid
- 19.01.34 – Taasta vanad navigeerimisikoonid
+ 20.13.41 - Taasta mittekokkuvarisenud video tegevusriba
+ 19.35.36 - Taastage vanad Shortsi esitajaikoonid
+ 19.01.34 – Taasta vanad navigeerimisikoonid
Muuda alguslehte
@@ -1559,12 +1564,15 @@ Selle lubamine võib avada kõrgema video kvaliteedi"
• Eksperimentaalne klient ja võib igal ajal töötamast lakata
• Puuduv AV1 videokoodek
• Video lastele ei pruugi taasesitada, kui olete välja logitud või inkognito režiimis
+
+ • Sunni algne heli pole saadaval
Kuva statistikas \"Nerdide jaoks\"
Klienditüüp on statistikas \"Nerdide jaoks\" nähtav
Klient on statistikas \"Nerdide jaoks\" peidetud
Helivoo keel
Konkreetse helikeele valimiseks lülita välja \"Sunni algne helikeel\"
+ Voogedastuse keelevalik ei ole Android Studioga saadaval
@@ -1599,23 +1607,23 @@ Selle lubamine võib avada kõrgema video kvaliteedi"
Navigeerimisriba
Peida või muuda navigeerimisriba nuppe
- Peida Avalehe nupp
+ Peida Avaleht
Avalehe nupp on peidetud
Avalehe nupp on nähtav
- Peida Näidiste nupp
+ Peida Näidised
Näidiste nupp on peidetud
Näidiste nupp on nähtav
- Peida Avasta nupp
+ Peida Avasta
Avasta nupp on peidetud
Avasta nupp on nähtav
- Peida Kogu nupp
+ Peida Kogu
Kogu nupp on peidetud
Kogu nupp on nähtav
- Peida Uuenda nupp
+ Peida Uuenda
Uuenda nupp on peidetud
Uuenda nupp on nähtav
Peida navigeerimisriba
diff --git a/patches/src/main/resources/addresources/values-eu-rES/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-eu-rES/strings.xml
+++ b/patches/src/main/resources/addresources/values-eu-rES/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-fa-rIR/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/strings.xml
index b3632d5ff..8967c9990 100644
--- a/patches/src/main/resources/addresources/values-fa-rIR/strings.xml
+++ b/patches/src/main/resources/addresources/values-fa-rIR/strings.xml
@@ -136,6 +136,7 @@ Second \"item\" text"
+
@@ -293,6 +294,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-fi-rFI/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
index 037cb4823..6b0d2a917 100644
--- a/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
+++ b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml
@@ -107,6 +107,15 @@ Toisto ei ehkä toimi"
Virheenkorjauksen kirjaaminen
Virheenkorjauslokit ovat käytössä
Virheenkorjauslokit eivät ole käytössä
+ Loki pinojäljet
+ Vianetsintälokit sisältävät pinojäljet
+ Vianetsintälokit eivät sisällä pinojälkiä
+ Näytä ponnahdusilmoitus ReVanced-virheen tapahtuessa
+ Ponnahdusilmoitus näytetään virheen tapahtuessa
+ Ponnahdusilmoitusta ei näytetä virheen tapahtuessa
+ "Virheilmoitusten poistaminen käytöstä piilottaa kaikki ReVancedin virheilmoitukset.
+
+Et saa ilmoituksia odottamattomista tapahtumista."
Vie virheenkorjauslokit
Kopioi ReVancedin virheenkorjauslokit leikepöydälle
Virheenkorjausloki ei ole käytössä
@@ -151,15 +160,6 @@ Toisto ei ehkä toimi"
Tämä voi auttaa komponenttien tunnistamisessa, kun luot mukautettuja suodattimia.
Tämän käyttöönotto kirjaa myös joitakin käyttäjätietoja, kuten IP-osoitteesi."
- Loki pinojäljet
- Vianetsintälokit sisältävät pinojäljet
- Vianetsintälokit eivät sisällä pinojälkiä
- Näytä ponnahdusilmoitus ReVanced-virheen tapahtuessa
- Ponnahdusilmoitus näytetään virheen tapahtuessa
- Ponnahdusilmoitusta ei näytetä virheen tapahtuessa
- "Virheilmoitusten poistaminen käytöstä piilottaa kaikki ReVancedin virheilmoitukset.
-
-Et saa ilmoituksia odottamattomista tapahtumista."
Piilota albumikortit
@@ -586,6 +586,10 @@ Säädä äänenvoimakkuutta pyyhkäisemällä pystysuoraan näytön oikealta pu
Piilota \"Estä mainokset\"
Estä mainokset -painike on piilotettu
Estä mainokset -painike näytetään
+
+ Piilota kommentit
+ Kommenttipainike on piilotettu
+ Kommenttipainike näytetään
Piilota Tee ilmoitus
@@ -1242,8 +1246,9 @@ Tämä muuttaa sovelluksen ulkoasua ja ominaisuuksia, mutta tuntemattomia sivuva
Jos tämä poistetaan myöhemmin käytöstä, on suositeltavaa tyhjentää sovelluksen tiedot käyttöliittymävirheiden välttämiseksi."
Naamioitava kohdeversio
- 19.35.36 - Palauta vanhat Shorts-soittimen kuvakkeet
- 19.01.34 - Palauta vanhat navigointikuvakkeet
+ 20.13.41 - Palauta laajennettu videon toimintopalkki
+ 19.35.36 - Palauta vanhat Shorts-soittimen kuvakkeet
+ 19.01.34 - Palauta vanhat navigointikuvakkeet
Vaihda aloitussivua
@@ -1559,12 +1564,15 @@ Tämä voi avata korkealaatuisemmat videot"
• Kokeellinen asiakasohjelma, joka saattaa lakata toimimasta milloin tahansa
• Ei AV1-videokoodekkia
• Lasten videot eivät ehkä toistu, kun olet kirjautunut ulos tai incognito-tilassa
+
+ • Pakota alkuperäinen ääni ei ole käytettävissä
Näytä teknisissä tiedoissa
Asiakastyyppi näytetään teknisissä tiedoissa
Asiakastyyppi on piilotettu teknisissä tiedoissa
Äänivirran kieli
Valitaksesi tietyn äänikielen, poista käytöstä \'Pakota alkuperäinen äänikieli\'
+ Striimin kielivalinta ei ole käytettävissä Android Studion kanssa
@@ -1599,23 +1607,23 @@ Tämä voi avata korkealaatuisemmat videot"
Navigointipalkki
Piilota tai muuta navigointipalkin painikkeita
- Piilota Etusivu-painike
+ Piilota Koti
Etusivu-painike on piilotettu
Etusivu-painike näytetään
- Piilota Näytteet-painike
+ Piilota Näytteet
Näytteet-painike on piilotettu
Näytteet-painike näytetään
- Piilota Tutustu-painike
+ Piilota Tutki
Tutustu-painike on piilotettu
Tutustu-painike näytetään
- Piilota Kirjasto-painike
+ Piilota kirjasto
Kirjasto-painike on piilotettu
Kirjasto-painike näytetään
- Piilota Päivitä-painike
+ Piilota Päivitä
Päivitä-painike on piilotettu
Päivitä-painike näytetään
Piilota navigointipalkki
diff --git a/patches/src/main/resources/addresources/values-fil-rPH/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/strings.xml
index 735bc7e49..d38b503dd 100644
--- a/patches/src/main/resources/addresources/values-fil-rPH/strings.xml
+++ b/patches/src/main/resources/addresources/values-fil-rPH/strings.xml
@@ -107,6 +107,15 @@ Maaaring hindi gumana ang pag-playback"
Pag-log sa pag-debug
Ang mga debug log ay pinagana
Ang mga debug log ay hindi pinagana
+ Mga bakas ng stack ng log
+ Kasama sa mga debug log ang stack trace
+ Hindi kasama sa mga debug log ang stack trace
+ Ipakita ang toast sa ReVanced error
+ Ipinapakita ang Toast kung may naganap na error
+ Hindi ipinapakita ang Toast kung may naganap na error
+ "Ang pag-off ng mga toast ng error ay nagtatago ng lahat ng mga abiso ng error ng ReVanced.
+
+Hindi ka aabisuhan ng anumang hindi inaasahang mga kaganapan."
I-export ang mga debug log
Kinokopya ang mga ReVanced debug log sa clipboard
Hindi pinagana ang pag-log ng debug
@@ -151,15 +160,6 @@ Maaaring hindi gumana ang pag-playback"
Maaaring makatulong ito na matukoy ang mga bahagi kapag lumilikha ng mga custom na filter.
Gayunpaman, ang pagpapagana nito ay magtatala rin ng ilang data ng user gaya ng iyong IP address."
- Mga bakas ng stack ng log
- Kasama sa mga debug log ang stack trace
- Hindi kasama sa mga debug log ang stack trace
- Ipakita ang toast sa ReVanced error
- Ipinapakita ang Toast kung may naganap na error
- Hindi ipinapakita ang Toast kung may naganap na error
- "Ang pag-off ng mga toast ng error ay nagtatago ng lahat ng mga abiso ng error ng ReVanced.
-
-Hindi ka aabisuhan ng anumang hindi inaasahang mga kaganapan."
Itago ang mga album card
@@ -586,6 +586,10 @@ Ayusin ang volume sa pamamagitan ng pag-swipe nang patayo sa kanang bahagi ng sc
Itago ang Ihinto ang mga ad
Nakatago ang button ng pagtigil sa mga ad
Napakita ang button ng pagtigil sa mga ad
+
+ Itago ang mga Komento
+ Nakatago ang pindutan ng Mga Komento
+ Ipinapakita ang pindutan ng Mga Komento
Itago ang Ulat
@@ -1240,8 +1244,9 @@ Ito ay magbabago sa hitsura at mga tampok ng app, ngunit maaaring mangyari ang m
Kung mamaya ay patayin, inirerekumenda na i-clear ang data ng app upang maiwasan ang mga bug ng UI."
Target na bersyon ng Spoof app
- 19.35.36 - Ibalik ang mga lumang icon ng Shorts player
- 19.01.34 - Ibalik ang mga lumang icon ng navigation
+ 20.13.41 - Ibalik ang hindi nakatiklop na video action bar
+ 19.35.36 - Ibalik ang mga lumang icon ng Shorts player
+ 19.01.34 - Ibalik ang mga lumang icon ng navigation
Baguhin ang panimulang pahina
@@ -1557,12 +1562,15 @@ Ang pagpapagana nito ay maaaring magbukas ng mas mataas na kalidad ng video"• Pang-eksperimentong kliyente at maaaring huminto sa paggana anumang oras
• Walang AV1 video codec
• Mga video ng mga bata ay maaaring hindi ma-play kapag naka-log out o nasa incognito mode
+
+ • Ang Pilitin ang orihinal na audio ay hindi magagamit
Ipakita sa Mga Istatistika para sa mga nerds
Ipinapakita ang uri ng kliyente sa Mga Istatistika para sa mga nerds
Nakatago ang kliyente sa Mga Istatistika para sa mga nerds
Wika ng audio stream
Upang pumili ng isang partikular na wika ng audio, i-off ang \'Puwersahin ang orihinal na wika ng audio\'
+ Hindi available ang pagpili ng wika ng stream sa Android Studio
@@ -1597,23 +1605,23 @@ Ang pagpapagana nito ay maaaring magbukas ng mas mataas na kalidad ng video"Navigation bar
Itago o baguhin ang mga pindutan ng navigation bar
- Itago ang pindutan ng Home
+ Itago ang Tahanan
Nakatago ang pindutan ng Home
Nakalabas ang pindutan ng Home
- Itago ang pindutan ng Samples
+ Itago ang Mga Sample
Nakatago ang pindutan ng Samples
Nakalabas ang pindutan ng Samples
- Itago ang pindutan ng Explore
+ Itago ang Galugarin
Nakatago ang pindutan ng Explore
Nakalabas ang pindutan ng Explore
- Itago ang pindutan ng Library
+ Itago ang Aklatan
Nakatago ang pindutan ng Library
Nakalabas ang pindutan ng Library
- Itago ang pindutan ng Upgrade
+ Itago ang I-upgrade
Nakatago ang pindutan ng Upgrade
Nakalabas ang pindutan ng Upgrade
Itago ang navigation bar
diff --git a/patches/src/main/resources/addresources/values-fr-rFR/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/strings.xml
index 0678f2e4c..6cc1a414f 100644
--- a/patches/src/main/resources/addresources/values-fr-rFR/strings.xml
+++ b/patches/src/main/resources/addresources/values-fr-rFR/strings.xml
@@ -107,6 +107,15 @@ Il est possible que la lecture ne fonctionne pas"
Journalisation de débogage
Les journaux de débogage sont activés
Les journaux de débogage sont désactivés
+ Journaliser les stack traces
+ Les journaux de débogage incluent les stack traces
+ Les journaux de débogage n\'incluent pas les stack traces
+ Afficher un message toast en cas d\'erreur ReVanced
+ Un message toast est affiché en cas d\'erreur
+ Aucun message toast n\'est affiché en cas d\'erreur
+ "La désactivation des messages toasts d'erreur masque toutes les notifications d'erreur ReVanced.
+
+Vous ne serez pas informé des événements inattendus."
Exporter les journaux de débogage
Copie les journaux de débogage ReVanced dans le presse-papiers
La journalisation de débogage est désactivée
@@ -151,15 +160,6 @@ Il est possible que la lecture ne fonctionne pas"
Cela peut aider à identifier les composants lors de la création de filtres personnalisés.
Toutefois, l'activation de cette option entraînera également l'enregistrement de certaines données utilisateur, telles que votre adresse IP."
- Journaliser les stack traces
- Les journaux de débogage incluent les stack traces
- Les journaux de débogage n\'incluent pas les stack traces
- Afficher un message toast en cas d\'erreur ReVanced
- Un message toast est affiché en cas d\'erreur
- Aucun message toast n\'est affiché en cas d\'erreur
- "La désactivation des messages toasts d'erreur masque toutes les notifications d'erreur ReVanced.
-
-Vous ne serez pas informé des événements inattendus."
Masquer les fiches d\'album
@@ -586,6 +586,10 @@ Réglez le volume en balayant verticalement sur le côté droit de l'écran"Masquer \"Zéro annonce\"
Le bouton Zéro annonce est masqué
Le bouton Zéro annonce est affiché
+
+ Masquer les commentaires
+ Le bouton Commentaires est masqué
+ Le bouton Commentaires est affiché
Masquer \"Signaler\"
@@ -1243,8 +1247,9 @@ Cela modifiera l'apparence et les fonctionnalités de l'application, mais il peu
Si désactivé ultérieurement, il est recommandé d'effacer les données de l'application pour éviter des bugs d'interface."
Version cible
- 19.35.36 - Restaurer les anciennes icônes du lecteur Shorts
- 19.01.34 - Restaurer les anciennes icônes de navigation
+ 20.13.41 - Restaurer la barre d\'action vidéo non réduite
+ 19.35.36 - Restaurer les anciennes icônes du lecteur Shorts
+ 19.01.34 - Restaurer les anciennes icônes de navigation
Modifier la page de démarrage
@@ -1560,12 +1565,15 @@ Activer cette option peut déverrouiller des qualités vidéo supérieures"• Client expérimental, peut cesser de fonctionner à tout moment
• Codec vidéo AV1 indisponible
• La lecture des vidéos pour enfants peut ne pas fonctionner lorsque vous êtes déconnecté ou en mode navigation privée
+
+ • Forcer l\'audio original n\'est pas disponible
Afficher dans les Statistiques avancées
Le type de client est affiché dans les Statistiques avancées
Le client est caché dans les Statistiques avancées
Langue du flux audio
Pour sélectionner une langue audio spécifique, désactivez \"Forcer la langue audio d\'origine\"
+ La sélection de la langue du flux n\'est pas disponible avec Android Studio
@@ -1600,23 +1608,23 @@ Activer cette option peut déverrouiller des qualités vidéo supérieures"Barre de navigation
Masquer ou modifier les boutons de la barre de navigation
- Masquer le bouton Accueil
+ Masquer Accueil
Le bouton Accueil est masqué
Le bouton Accueil est affiché
- Masquer le bouton Extraits
+ Masquer Extraits
Le bouton Extraits est masqué
Le bouton Extraits est affiché
- Masquer le bouton Explorer
+ Masquer Explorer
Le bouton Explorer est masqué
Le bouton Explorer est affiché
- Masquer le bouton Bibliothèque
+ Masquer Bibliothèque
Le bouton Bibliothèque est masqué
Le bouton Bibliothèque est affiché
- Masquer le bouton S\'abonner
+ Masquer Premium
Le bouton S\'abonner est masqué
Le bouton S\'abonner est affiché
Masquer la barre de navigation
diff --git a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
index 739a8164a..3aaa3b244 100644
--- a/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
+++ b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml
@@ -107,6 +107,15 @@ Seans nach n-oibreoidh an t-athsheinm"
Dífhabhtú logáil
Tá logaí dífhabhtaithe cumasaithe
Tá logaí dífhabhtaithe díchumasaithe
+ Rianta cruach logála
+ Cuimsíonn logaí dífhabhtaithe rian cruach
+ Ní chuimsíonn logaí dífhabhtaithe rian cruach
+ Taispeáin tósta ar earráid ReVanced
+ Taispeántar tósta má tharlaíonn earráid
+ Ní thaispeántar tósta má tharlaíonn earráid
+ "Má mhúchtar tóstaí earráide, folaítear gach fógra earráide ReVanced.
+
+Ní chuirfear ar an eolas thú faoi aon imeachtaí gan choinne."
Easpórtáil logaí dífhabhtaithe
Cóipeálann sé logaí dífhabhtaithe ReVanced chuig an gearrthaisce
Tá logáil dífhabhtaithe díchumasaithe
@@ -151,15 +160,6 @@ Seans nach n-oibreoidh an t-athsheinm"
Is féidir leis seo cabhrú le comhpháirteanna a aithint agus scagairí saincheaptha á gcruthú.
Mar sin féin, logálfaidh sé seo roinnt sonraí úsáideora freisin, mar shampla do sheoladh IP."
- Rianta cruach logála
- Cuimsíonn logaí dífhabhtaithe rian cruach
- Ní chuimsíonn logaí dífhabhtaithe rian cruach
- Taispeáin tósta ar earráid ReVanced
- Taispeántar tósta má tharlaíonn earráid
- Ní thaispeántar tósta má tharlaíonn earráid
- "Má mhúchtar tóstaí earráide, folaítear gach fógra earráide ReVanced.
-
-Ní chuirfear ar an eolas thú faoi aon imeachtaí gan choinne."
Folaigh cártaí albam
@@ -586,6 +586,10 @@ Coigeartaigh an toirt trí haisceartán go hingearach ar thaobh deas an scáile
Folaigh Stad fógraí
Tá cnaipe stad fógraí i bhfolach
Tá cnaipe stad fógraí taispeánta
+
+ Folaigh Tuairimí
+ Tá cnaipe na dTuairimí folaithe
+ Tá cnaipe na dTuairimí taispeánta
Folaigh Tuairisc
@@ -1242,8 +1246,9 @@ Athróidh sé seo cuma agus gnéithe an aip, ach d'fhéadfadh fo-iarsmaí anaith
Má dhiúltaítear é níos déanaí, moltar sonraí an aip a ghlanadh chun buganna UI a chosc."
Sprioc leagan aip spoof
- 19.35.36 - Athchóirigh sean-deilbhíní imreoir Shorts
- 19.01.34 - Athchóiriú Sean Icóin Treorach
+ 20.13.41 - Cuir an barra gníomhaíochta físe neamhchomhbhrúite ar ais
+ 19.35.36 - Athchóirigh sean-deilbhíní imreoir Shorts
+ 19.01.34 - Athchóiriú Sean Icóin Treorach
Athraigh an leathanach tosaigh
@@ -1559,12 +1564,15 @@ Is féidir le seo caighdeáin físeáin níos airde a dhíghlasáil"
• Cliant turgnamhach é seo agus féadfaidh sé stop a chur ag obair ag am ar bith
• Níl aon chóidéir físe AV1
• Videos faoi phaistí uaireanta nach imreoidh nuair a bhíonn tú logáilte amach nó i mod incognito
+
+ • Níl Éigean fuaime bunaidh ar fáil
Taispeáin i Staitisticí do nerds
Taispeántar cineál an chliaint i Staitisticí do nerds
Tá an cliant curtha i bhfolach i Staitisticí do nerds
Teanga an tsrutha fuaime
Chun teanga fuaime shonrach a roghnú, múch \'Cuir iallach ar bhunteanga fuaime\'
+ Níl roghnú theanga an tsrutha ar fáil le Android Studio
@@ -1599,23 +1607,23 @@ Is féidir le seo caighdeáin físeáin níos airde a dhíghlasáil"
Barra nascleanúna
Folaigh nó athraigh cnaipí an bharra nascleanúna
- Folaigh cnaipe Baile
+ Folaigh Baile
Tá cnaipe Baile folaithe
Tá cnaipe Baile taispeánta
- Folaigh cnaipe Samplaí
+ Folaigh Samplaí
Tá cnaipe Samplaí folaithe
Tá cnaipe Samplaí taispeánta
- Folaigh cnaipe Fionnachtana
+ Folaigh Scrúdaigh
Tá cnaipe Fionnachtana folaithe
Tá cnaipe Fionnachtana taispeánta
- Folaigh cnaipe Leabharlann
+ Folaigh Leabharlann
Tá cnaipe Leabharlann folaithe
Tá cnaipe Leabharlann taispeánta
- Folaigh cnaipe Uasghrádú
+ Folaigh Uasghrádaigh
Tá cnaipe Uasghrádú folaithe
Tá cnaipe Uasghrádú taispeánta
Folaigh barra nascleanúna
diff --git a/patches/src/main/resources/addresources/values-gl-rES/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-gl-rES/strings.xml
+++ b/patches/src/main/resources/addresources/values-gl-rES/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-gu-rIN/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-gu-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-gu-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-hi-rIN/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/strings.xml
index bad4f0c2d..905532c0b 100644
--- a/patches/src/main/resources/addresources/values-hi-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-hi-rIN/strings.xml
@@ -31,6 +31,8 @@ Second \"item\" text"
+ प्लेबैक समस्याओं को रोकने के लिए क्लाइंट वीडियो स्ट्रीम को स्पूफ करें
+ प्लेबैक समस्याओं को रोकने के लिए क्लाइंट वीडियो स्ट्रीम को स्पूफ करें
@@ -89,6 +91,7 @@ Second \"item\" text"
+
@@ -243,6 +246,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-hr-rHR/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml
index bad4f0c2d..4f74c1712 100644
--- a/patches/src/main/resources/addresources/values-hr-rHR/strings.xml
+++ b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml
@@ -21,6 +21,7 @@ Second \"item\" text"
+ Provjere nisu uspjele
@@ -89,6 +90,7 @@ Second \"item\" text"
+
@@ -243,6 +245,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-hu-rHU/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/strings.xml
index b9809e8b6..a90935daa 100644
--- a/patches/src/main/resources/addresources/values-hu-rHU/strings.xml
+++ b/patches/src/main/resources/addresources/values-hu-rHU/strings.xml
@@ -107,6 +107,15 @@ A lejátszás nem működhet"
Hibakeresési naplózás
A hibakeresési naplók engedélyezve vannak
A hibakeresési naplók le vannak tiltva
+ Naplóverem nyomai
+ A hibakeresési naplók tartalmazzák a verem nyomkövetését
+ A hibakeresési naplók nem tartalmazzák a verem nyomkövetését
+ Üzenet megjelenítése ReVanced hiba esetén
+ A \"toast\" üzenet jelenik meg, ha hiba történik
+ Nem jelenik meg \"toast\" üzenet, ha hiba történik
+ "A hibaüzenetek kikapcsolása elrejti az összes ReVanced hibaértesítést.
+
+Nem fog értesülni semmilyen váratlan eseményről."
Hibakeresési naplók exportálása
A ReVanced hibakeresési naplóit a vágólapra másolja
A hibakeresési naplózás ki van kapcsolva
@@ -151,15 +160,6 @@ A lejátszás nem működhet"
Ez segíthet azonosítani az összetevőket egyedi szűrők létrehozásakor.
Ez a funkció azonban néhány felhasználói adatot is naplóz, például az IP-címét."
- Naplóverem nyomai
- A hibakeresési naplók tartalmazzák a verem nyomkövetését
- A hibakeresési naplók nem tartalmazzák a verem nyomkövetését
- Üzenet megjelenítése ReVanced hiba esetén
- A \"toast\" üzenet jelenik meg, ha hiba történik
- Nem jelenik meg \"toast\" üzenet, ha hiba történik
- "A hibaüzenetek kikapcsolása elrejti az összes ReVanced hibaértesítést.
-
-Nem fog értesülni semmilyen váratlan eseményről."
Album kártyák elrejtése
@@ -586,6 +586,10 @@ A hangerő a képernyő jobb oldalán függőlegesen húzva állítható be"Hirdetések leállítása elrejtése
A hirdetésleállítás gomb rejtett
A hirdetésleállítás gomb látható
+
+ Hozzászólások elrejtése
+ A hozzászólások gomb rejtve van
+ A hozzászólások gomb látható
Bejelentés elrejtése
@@ -1241,8 +1245,9 @@ Ez megváltoztatja az alkalmazás megjelenését és funkcióit, de előfordulha
Ha később kikapcsolja, akkor ajánlott az alkalmazás adatait törölni, hogy megakadályozza a felhasználói felület hibáit."
Hamisított alkalmazásverzió célja
- 19.35.36 - A régi Shorts lejátszó ikonok visszaállítása
- 19.01.34 - Állítsa vissza a régi navigációs ikonokat
+ 20.13.41 - Nem összecsukott videó műveletsáv visszaállítása
+ 19.35.36 - A régi Shorts lejátszó ikonok visszaállítása
+ 19.01.34 - Állítsa vissza a régi navigációs ikonokat
Kezdőlap módosítása
@@ -1556,12 +1561,15 @@ Ez a beállítás lehetővé teszi a magasabb videóminőségek feloldását"• Kísérleti kliens, és bármikor leállhat
• Nincs AV1 videokodek
• A gyermekeknek szóló videók nem játszódnak le, ha a felhasználó kijelentkezett, vagy inkognitómódban van.
+
+ • Eredeti hang kényszerítése nem érhető el
Megjelenítés a Statisztikákban
A kliens típusa a Statisztikákban látható
A kliens el van rejtve a Statisztikákban
Hangfolyam nyelve
Egy adott hangsáv nyelv kiválasztásához kapcsold ki az \"Eredeti hangsáv nyelv kikényszerítése\" opciót
+ Az Android Studio-val nem érhető el a stream nyelvének kiválasztása
@@ -1596,23 +1604,23 @@ Ez a beállítás lehetővé teszi a magasabb videóminőségek feloldását"Navigációs sáv
Navigációs sáv gombjainak elrejtése vagy módosítása
- Kezdőlap gomb elrejtése
+ Főoldal elrejtése
A Kezdőlap gomb elrejtve
A Kezdőlap gomb látható
- Minták gomb elrejtése
+ Minták elrejtése
A Minták gomb elrejtve
A Minták gomb látható
- Felfedezés gomb elrejtése
+ Felfedezés elrejtése
A Felfedezés gomb elrejtve
A Felfedezés gomb látható
- Könyvtár gomb elrejtése
+ Könyvtár elrejtése
A Könyvtár gomb elrejtve
A Könyvtár gomb látható
- Frissítés gomb elrejtése
+ Frissítés elrejtése
A Frissítés gomb elrejtve
A Frissítés gomb látható
Navigációs sáv elrejtése
diff --git a/patches/src/main/resources/addresources/values-hy-rAM/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/strings.xml
index b626c3ef9..69bfed040 100644
--- a/patches/src/main/resources/addresources/values-hy-rAM/strings.xml
+++ b/patches/src/main/resources/addresources/values-hy-rAM/strings.xml
@@ -107,6 +107,15 @@ MicroG-ի համար մարտկոցի օպտիմալացումը անջատել
Առաջնորդման մատնանշում
Մարմնացման առաջնորդման մատնանշումները ակտիվացված են
Մարմնացման առաջնորդման մատնանշումները անջատված են
+ Մուտքագրել կուտակային կետերի հետքեր
+ Մարմնացման առաջնորդման մատնանշումները պարունակում են կուտակային կետի հետք
+ Մարմնացման առաջնորդման մատնանշումները չեն պարունակում կուտակային կետի հետք
+ Ցույց տալ toast ReVanced սխալի դեպքում
+ \"Toast\"-ը ցուցադրվում է, եթե սխալ է առաջանում
+ \"Toast\"-ը չի ցուցադրվում, եթե սխալ է առաջանում
+ "Սխալների տոաստի անջատումը թաքցնում է ReVanced-ի բոլոր սխալների ծանուցումները։
+
+Դուք չեք ստանա ան予期した出来事ի մասին ծանուցում։"
Արտահանել վրեժխնդրության կարգաբերման մատյանները
Պատճենում է ReVanced-ի կարգաբերման մատյանները սեղմատախտակին
Վրեժխնդրության մատյանների գրանցումն անջատված է
@@ -151,15 +160,6 @@ MicroG-ի համար մարտկոցի օպտիմալացումը անջատել
Սա կարող է օգնել բացահայտել բաղադրիչները՝ հատուկ զտիչներ ստեղծելիս։
Այնուամենայնիվ, սա միացնելը կգրանցի նաև օգտատիրոջ որոշ տվյալներ, ինչպիսիք են ձեր IP հասցեն:"
- Մուտքագրել կուտակային կետերի հետքեր
- Մարմնացման առաջնորդման մատնանշումները պարունակում են կուտակային կետի հետք
- Մարմնացման առաջնորդման մատնանշումները չեն պարունակում կուտակային կետի հետք
- Ցույց տալ toast ReVanced սխալի դեպքում
- \"Toast\"-ը ցուցադրվում է, եթե սխալ է առաջանում
- \"Toast\"-ը չի ցուցադրվում, եթե սխալ է առաջանում
- "Սխալների տոաստի անջատումը թաքցնում է ReVanced-ի բոլոր սխալների ծանուցումները։
-
-Դուք չեք ստանա ան予期した出来事ի մասին ծանուցում։"
Թաքցնել ալբոմի քարտերը
@@ -586,6 +586,10 @@ MicroG-ի համար մարտկոցի օպտիմալացումը անջատել
Թաքցնել \"Դադարեցնել գովազդները\"
Գովազդները դադարեցնելու կոճակը թաքցված է
Գովազդները դադարեցնելու կոճակը ցուցադրված է
+
+ Թաքցնել մեկնաբանությունները
+ Մեկնաբանությունների կոճակը թաքնված է
+ Մեկնաբանությունների կոճակը ցուցադրված է
Թաքցնել Report
@@ -1243,8 +1247,9 @@ Seekbar thumbnails-ները կօգտագործեն նույն որակը, ինչ
Եթե հետագայում անջատվի, խորհուրդ է տրվում մաքրել հավելվածի տվյալները՝ UI սխալներից խուսափելու համար։"
Spoof-ի կիրառության տարբերակի նպատակ
- 19.35.36 - Վերականգնել հին Shorts պլեյերի պատկերակները
- 19.01.34 - Վերականգնել հին նավիգացիոն պատկերակները
+ 20.13.41 - Վերականգնել չծալված տեսանյութի գործողությունների տողը
+ 19.35.36 - Վերականգնել հին Shorts պլեյերի պատկերակները
+ 19.01.34 - Վերականգնել հին նավիգացիոն պատկերակները
Փոխել մեկնարկային էջը
@@ -1560,12 +1565,15 @@ Mini-player-ը կարող է գրավվել էկրանից դուրս՝ դեպի
• Փորձնական հաճախորդ է և կարող է ցանկացած պահի դադարել աշխատել
• Հեռացված բոլոր AV1 վիդեո կոդեկները
Երեխաների տեսանյութերը կարող են չհամապատասխանել հետևյալ պահանջներին՝ եթե արտոնագրման խախտումներ կան։
+
+ • Բնօրինակ ձայնի պարտադրումը հասանելի չէ
Ցուցադրել վիճակագրության ակնոցներում
Հաճախորդի տեսակը ցուցադրվում է վիճակագրության ակնոցներում
Հաճախորդը թաքնված է վիճակագրության ակնոցներում
Ձայնային հոսքի լեզուն
Որոշակի ձայնային լեզու ընտրելու համար անջատեք \'Պարտադրել բնօրինակ ձայնային լեզուն\'
+ Հոսքի լեզվի ընտրությունը հասանելի չէ Android Studio-ի հետ
@@ -1600,23 +1608,23 @@ Mini-player-ը կարող է գրավվել էկրանից դուրս՝ դեպի
Նավիգացիոն գիծ
Թաքցնել կամ փոխել նավիգացիոն գծի կոճակները
- Թաքցնել «Գլխավոր» կոճակը
+ Թաքցնել Գլխավորը
«Գլխավոր» կոճակը թաքնված է
«Գլխավոր» կոճակը ցուցադրված է
- Թաքցնել «Նմուշներ» կոճակը
+ Թաքցնել Նմուշները
«Նմուշներ» կոճակը թաքնված է
«Նմուշներ» կոճակը ցուցադրված է
- Թաքցնել «Բացահայտել» կոճակը
+ Թաքցնել Ուսումնասիրելը
«Բացահայտել» կոճակը թաքնված է
«Բացահայտել» կոճակը ցուցադրված է
- Թաքցնել «Գրադարան» կոճակը
+ Թաքցնել Գրադարանը
«Գրադարան» կոճակը թաքնված է
«Գրադարան» կոճակը ցուցադրված է
- Թաքցնել «Արդիականացնել» կոճակը
+ Թաքցնել Թարմացումը
«Արդիականացնել» կոճակը թաքնված է
«Արդիականացնել» կոճակը ցուցադրված է
Թաքցնել նավիգացիոն գիծը
diff --git a/patches/src/main/resources/addresources/values-in-rID/strings.xml b/patches/src/main/resources/addresources/values-in-rID/strings.xml
index e9df0e74b..28b4467e5 100644
--- a/patches/src/main/resources/addresources/values-in-rID/strings.xml
+++ b/patches/src/main/resources/addresources/values-in-rID/strings.xml
@@ -98,7 +98,7 @@ Jika Anda adalah pengguna YouTube Premium, setelan ini mungkin tidak diperlukan"
"Aliran video tidak dipalsukan
Pemutaran mungkin tidak berfungsi"
- Menonaktifkan pengaturan ini mungkin menyebabkan masalah pemutaran.
+ Mematikan pengaturan ini dapat menyebabkan masalah pemutaran.
Klien bawaan
@@ -107,6 +107,15 @@ Pemutaran mungkin tidak berfungsi"
Pencatatan debug
Pencatatan debug diaktifkan
Pencatatan debug dinonaktifkan
+ Jejak catatan stack
+ Pencatatan debug menyertakan jejak stack
+ Pencatatan debug tidak menyertakan jejak stack
+ Tampilkan pesan timbul pada kesalahan ReVanced
+ Pesan timbul ditampilkan jika terjadi kesalahan
+ Pesan timbul tidak ditampilkan jika terjadi kesalahan
+ "Mematikan notifikasi kesalahan menyembunyikan semua notifikasi kesalahan ReVanced.
+
+Anda tidak akan diberi tahu tentang kejadian yang tidak terduga."
Ekspor catatan debug
Salin catatan debug ReVanced ke papan klip
Pencatatan debug dinonaktifkan
@@ -151,15 +160,6 @@ Pemutaran mungkin tidak berfungsi"
Ini dapat membantu mengidentifikasi komponen saat membuat filter khusus.
Namun, mengaktifkan ini juga akan mencatat beberapa data pengguna seperti alamat IP Anda."
- Jejak catatan stack
- Pencatatan debug menyertakan jejak stack
- Pencatatan debug tidak menyertakan jejak stack
- Tampilkan pesan timbul pada kesalahan ReVanced
- Pesan timbul ditampilkan jika terjadi kesalahan
- Pesan timbul tidak ditampilkan jika terjadi kesalahan
- "Mematikan notifikasi kesalahan menyembunyikan semua notifikasi kesalahan ReVanced.
-
-Anda tidak akan diberi tahu tentang kejadian yang tidak terduga."
Sembunyikan kartu album
@@ -586,6 +586,10 @@ Menyesuaikan volume dengan mengusap secara vertikal di sisi kanan layar"Sembunyikan Hentikan iklan
Tombol hentikan iklan disembunyikan
Tombol hentikan iklan ditampilkan
+
+ Sembunyikan Komentar
+ Tombol Komentar disembunyikan
+ Tombol Komentar ditampilkan
Sembunyikan Laporkan
@@ -1241,8 +1245,9 @@ Ini akan mengubah tampilan dan fitur aplikasi, tetapi efek samping yang tidak di
Jika kemudian dimatikan, disarankan untuk menghapus data aplikasi untuk mencegah bug UI."
Target versi app yang dipalsukan
- 19.35.36 - Pulihkan ikon pemutar Shorts lama
- 19.01.34 - Pulihkan ikon navigasi lama
+ 20.13.41 - Pulihkan bilah tindakan video yang tidak diciutkan
+ 19.35.36 - Pulihkan ikon pemutar Shorts lama
+ 19.01.34 - Pulihkan ikon navigasi lama
Ubah halaman awal
@@ -1476,7 +1481,7 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"
Menggunakan bahasa audio asli
Menggunakan audio bawaan
- Untuk menggunakan fitur ini, ubah \'Spoof aliran video\' ke klien apa pun kecuali Android Studio
+ Untuk menggunakan fitur ini, ubah \'Palsukan aliran video\' ke klien apa pun kecuali Android Studio
@@ -1554,16 +1559,19 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"
Efek samping pemalsuan Android
"• Menu trek audio hilang
• Volume stabil tidak tersedia"
- • Video mungkin berhenti pada menit 1:00, atau mungkin tidak tersedia di beberapa wilayah
+ • Video mungkin berhenti pada 1:00, atau mungkin tidak tersedia di beberapa wilayah
• Klien eksperimental dan dapat berhenti berfungsi kapan saja
• Tidak ada codec video AV1
• Video anak-anak mungkin tidak dapat diputar saat keluar atau dalam mode penyamaran
+
+ • Paksa audio asli tidak tersedia
Tampilkan di Statistik untuk nerds
Jenis klien ditampilkan di Statistik untuk nerds
Klien disembunyikan di Statistik untuk nerds
Bahasa aliran audio
Untuk memilih bahasa audio tertentu, matikan \'Paksa bahasa audio asli\'
+ Pilihan bahasa streaming tidak tersedia dengan Android Studio
@@ -1585,9 +1593,9 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"
Pengulangan permanen dinonaktifkan
- Sembunyikan tombol Transmisi
- Tombol Transmisi disembunyikan
- Tombol Transmisi ditampilkan
+ Sembunyikan tombol transmisi
+ Tombol transmisi disembunyikan
+ Tombol transmisi ditampilkan
- Sembunyikan tombol upgrade
+ Sembunyikan tombol Upgrade
Tombol disembunyikan
Tombol ditampilkan
diff --git a/patches/src/main/resources/addresources/values-is-rIS/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-is-rIS/strings.xml
+++ b/patches/src/main/resources/addresources/values-is-rIS/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-it-rIT/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/strings.xml
index 11a5d47c3..ac465053a 100644
--- a/patches/src/main/resources/addresources/values-it-rIT/strings.xml
+++ b/patches/src/main/resources/addresources/values-it-rIT/strings.xml
@@ -107,6 +107,15 @@ La riproduzione potrebbe non funzionare"
Log di debug
I log di debug sono abilitati
I log di debug sono disabilitati
+ Registra stack trace
+ I log di debug includono lo stack trace
+ I log di debug non includono lo stack trace
+ Mostra notifica per errore di ReVanced
+ Il toast viene mostrato se si verifica un errore
+ Il toast non viene mostrato se si verifica un errore
+ "Disabilitare i toast di errore nasconde tutte le notifiche di errore di ReVanced.
+
+Non sarai notificato di eventi imprevisti."
Esporta i log di debug
Copia i log di debug di ReVanced negli Appunti
La registrazione di debug è disabilitata
@@ -151,15 +160,6 @@ La riproduzione potrebbe non funzionare"
Questo può aiutare a identificare i componenti durante la creazione di filtri personalizzati.
Tuttavia, l'abilitazione di questa opzione registrerà anche alcuni dati dell'utente come il tuo indirizzo IP."
- Registra stack trace
- I log di debug includono lo stack trace
- I log di debug non includono lo stack trace
- Mostra notifica per errore di ReVanced
- Il toast viene mostrato se si verifica un errore
- Il toast non viene mostrato se si verifica un errore
- "Disabilitare i toast di errore nasconde tutte le notifiche di errore di ReVanced.
-
-Non sarai notificato di eventi imprevisti."
Nascondi le schede degli album
@@ -586,6 +586,10 @@ Regola il volume scorrendo verticalmente sul lato destro dello schermo"
Nascondi Interrompi annunci
Il pulsante per interrompere gli annunci è nascosto
Il pulsante per interrompere gli annunci è mostrato
+
+ Nascondi commenti
+ Il pulsante Commenti è nascosto
+ Il pulsante Commenti è mostrato
Nascondi Segnala
@@ -1241,8 +1245,9 @@ Questo cambierà l'aspetto e le funzionalità dell'app, ma potrebbero verificars
Se in seguito verrà disattivato, si consiglia di cancellare i dati dell'app per evitare bug dell'interfaccia."
Target della versione dell\'app desiderata
- 19.35.36 - Ripristinare le vecchie icone del player Shorts
- 19.01.34 - Ripristina le vecchie icone di navigazione
+ 20.13.41 - Ripristina la barra delle azioni video non compressa
+ 19.35.36 - Ripristinare le vecchie icone del player Shorts
+ 19.01.34 - Ripristina le vecchie icone di navigazione
Cambia pagina iniziale
@@ -1558,12 +1563,15 @@ Abilitare questa opzione può sbloccare qualità video più elevate"
• Client sperimentale e potrebbe smettere di funzionare in qualsiasi momento
• Nessun codec video AV1
• I video dei bambini potrebbero non essere riprodotti quando si è disconnessi o in modalità di navigazione in incognito
+
+ • L\'audio originale forzato non è disponibile
Mostra nelle statistiche per nerd
Il tipo di client è mostrato nelle statistiche per nerd
Il client è nascosto nelle statistiche per nerd
Lingua del flusso audio
Per selezionare una lingua audio specifica, disattiva \'Forza lingua audio originale\'
+ La selezione della lingua dello stream non è disponibile con Android Studio
@@ -1598,23 +1606,23 @@ Abilitare questa opzione può sbloccare qualità video più elevate"
Barra di navigazione
Nascondi o modifica i pulsanti della barra di navigazione
- Nascondi il pulsante Home
+ Nascondi Home
Il pulsante Home è nascosto
Il pulsante Home è mostrato
- Nascondi il pulsante Campioni
+ Nascondi Esempi
Il pulsante Campioni è nascosto
Il pulsante Campioni è mostrato
- Nascondi il pulsante Esplora
+ Nascondi Esplora
Il pulsante Esplora è nascosto
Il pulsante Esplora è mostrato
- Nascondi il pulsante Libreria
+ Nascondi Libreria
Il pulsante Libreria è nascosto
Il pulsante Libreria è mostrato
- Nascondi il pulsante Aggiorna
+ Nascondi Aggiorna
Il pulsante Aggiorna è nascosto
Il pulsante Aggiorna è mostrato
Nascondi la barra di navigazione
diff --git a/patches/src/main/resources/addresources/values-iw-rIL/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/strings.xml
index 056e1b5a2..910332f9a 100644
--- a/patches/src/main/resources/addresources/values-iw-rIL/strings.xml
+++ b/patches/src/main/resources/addresources/values-iw-rIL/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
רישום איתור באגים
יומני איתור באגים מופעלים
יומני איתור באגים מושבתים
+ רשום עקבות מחסנית
+ יומני איתור באגים כוללים עקבות מחסנית
+ יומני איתור באגים אינם כוללים עקבות מחסנית
+ הצג הודעה קופצת בעת שגיאת ReVanced
+ הודעה קופצת מוצגת אם מתרחשת שגיאה
+ הודעה קופצת אינה מוצגת אם מתרחשת שגיאה
+ "השבתת הודעות שגיאה קופצות מסתירה את כל הודעות השגיאה של ReVanced.
+
+לא תקבל הודעה על אירועים בלתי צפויים."
ייצוא יומני איתור באגים
מעתיק יומנים לאיתור באגים של Revanced ללוח ההעתקה
תיעוד איתור באגים מושבת
@@ -151,15 +160,6 @@ Second \"item\" text"
זה יכול לעזור לזהות רכיבים בעת יצירת מסננים מותאמים אישית.
עם זאת, הפעלת הגדרה זו תתעד גם כמה נתוני משתמש כמו כתובת ה-IP שלך."
- רשום עקבות מחסנית
- יומני איתור באגים כוללים עקבות מחסנית
- יומני איתור באגים אינם כוללים עקבות מחסנית
- הצג הודעה קופצת בעת שגיאת ReVanced
- הודעה קופצת מוצגת אם מתרחשת שגיאה
- הודעה קופצת אינה מוצגת אם מתרחשת שגיאה
- "השבתת הודעות שגיאה קופצות מסתירה את כל הודעות השגיאה של ReVanced.
-
-לא תקבל הודעה על אירועים בלתי צפויים."
הסתר כרטיסי אלבום
@@ -586,6 +586,10 @@ Second \"item\" text"
הסתר עצירת מודעות
לחצן עצירת מודעות מוסתר
לחצן עצירת מודעות מוצג
+
+ הסתר תגובות
+ הלחצן \'תגובות\' מוסתר
+ הלחצן \'תגובות\' מוצג
הסתר דיווח
@@ -1244,8 +1248,9 @@ Second \"item\" text"
אם נכבה מאוחר יותר, מומלץ לנקות את נתוני היישום כדי למנוע באגים של ממשק המשתמש."
יעד גרסת יישום מזויפת
- 19.35.36 - שחזר סמלי נגן Shorts ישנים
- 19.01.34 - שחזר סמלי ניווט ישנים
+ 20.13.41 - שחזור סרגל פעולות וידאו לא מכווץ
+ 19.35.36 - שחזר סמלי נגן Shorts ישנים
+ 19.01.34 - שחזר סמלי ניווט ישנים
שנה את דף ההתחלה
@@ -1561,12 +1566,15 @@ Second \"item\" text"
• לקוח ניסיוני ועשוי להפסיק לפעול בכל עת
• אין קודק וידאו מסוג AV1
• ייתכן שסרטוני Kids לא יופעלו כשאתה מנותק או במצב פרטי
+
+ • כפיית שמע מקורי אינה זמינה
הצג בנתונים לגיקים
סוג הלקוח מוצג בנתונים לגיקים
הלקוח מוסתר בנתונים לגיקים
שפת זרם השמע
כדי לבחור שפת שמע ספציפית, כבה את \'אכוף שפת שמע מקורית\'
+ בחירת שפת הזרם אינה זמינה עם אנדרואיד סטודיו
@@ -1601,23 +1609,23 @@ Second \"item\" text"
סרגל ניווט
הסתר או שנה לחצני סרגל ניווט
- הסתר לחצן \'דף הבית\'
+ הסתר דף הבית
לחצן \'דף הבית\' מוסתר
לחצן \'דף הבית\' מוצג
- הסתר לחצן \'דוגמיות\'
+ הסתר דוגמאות
לחצן \'דוגמיות\' מוסתר
לחצן \'דוגמיות\' מוצג
- הסתר לחצן \'גלו\'
+ הסתר גלו
לחצן \'גלו\' מוסתר
לחצן \'גלו\' מוצג
- הסתר לחצן \'ספרייה\'
+ הסתר ספרייה
לחצן \'ספרייה\' מוסתר
לחצן \'ספרייה\' מוצג
- הסתר לחצן \'שדרוג\'
+ הסתר שדרוג
לחצן \'שדרוג\' מוסתר
לחצן \'שדרוג\' מוצג
הסתר סרגל ניווט
diff --git a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
index 1a479c21f..c416cb942 100644
--- a/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
+++ b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml
@@ -107,6 +107,15 @@ YouTube Premium ユーザーの場合、この設定は必要ない可能性が
デバッグログを有効化
デバッグログは有効です
デバッグログは無効です
+ スタック トレースをログに記録
+ デバッグログにスタック トレースが含まれます
+ デバッグログにスタック トレースは含まれません
+ ReVanced のエラー時にトーストを表示
+ エラーが発生した場合にトーストが表示されます
+ エラーが発生してもトーストは表示されません
+ "エラートーストをオフにすると、ReVanced のすべてのエラー通知が非表示になります。
+
+予期しないイベントが発生した場合でも通知されなくなります。"
デバッグログのエクスポート
ReVanced のデバッグログをクリップボードにコピーします
デバッグログは無効です
@@ -152,15 +161,6 @@ YouTube Premium ユーザーの場合、この設定は必要ない可能性が
これは、カスタムフィルターを作成する際にコンポーネントを識別するのに役立ちます。
ただし、これを有効にすると、IP アドレスなどの一部のユーザーデータもログに記録されます。"
- スタック トレースをログに記録
- デバッグログにスタック トレースが含まれます
- デバッグログにスタック トレースは含まれません
- ReVanced のエラー時にトーストを表示
- エラーが発生した場合にトーストが表示されます
- エラーが発生してもトーストは表示されません
- "エラートーストをオフにすると、ReVanced のすべてのエラー通知が非表示になります。
-
-予期しないイベントが発生した場合でも通知されなくなります。"
アルバムカードを非表示
@@ -588,6 +588,10 @@ YouTube Premium ユーザーの場合、この設定は必要ない可能性が
「広告を停止」を非表示
「広告を停止」ボタンは表示されません
「広告を停止」ボタンは表示されます
+
+ コメントボタンを非表示
+ コメントボタンは表示されません
+ コメントボタンは表示されます
報告ボタンを非表示
@@ -741,15 +745,15 @@ YouTube Premium ユーザーの場合、この設定は必要ない可能性が
自動再生ボタンを非表示
- プレーヤー オーバーレイの自動再生ボタンは表示されません
- プレーヤー オーバーレイの自動再生ボタンは表示されます
+ 自動再生ボタンはプレーヤー オーバーレイに表示されません
+ 自動再生ボタンはプレーヤー オーバーレイに表示されます
字幕ボタンを非表示
- プレーヤー オーバーレイの字幕ボタンは表示されません
- プレーヤー オーバーレイの字幕ボタンは表示されます
+ 字幕ボタンはプレーヤー オーバーレイに表示されません
+ 字幕ボタンはプレーヤー オーバーレイに表示されます
キャストボタンを非表示
- プレーヤー オーバーレイのキャストボタンは表示されません
- プレーヤー オーバーレイのキャストボタンは表示されます
+ キャストボタンはプレーヤー オーバーレイに表示されません
+ キャストボタンはプレーヤー オーバーレイに表示されます
プレーヤー コントロールの背景を非表示
プレーヤー コントロールの背景は表示されません
プレーヤー コントロールの背景は表示されます
@@ -1245,8 +1249,9 @@ Automotive レイアウト
再び偽装を無効にする場合は、UI のバグを防ぐためにアプリデータを消去することをお勧めします。"
アプリバージョンの偽装先
- 19.35.36 - 古いショート プレーヤーのアイコンを復元
- 19.01.34 - 古いナビゲーション アイコンを復元
+ 20.13.41 - アクション ボタンの文字表示を復元
+ 19.35.36 - 古いショート プレーヤーのアイコンを復元
+ 19.01.34 - 古いナビゲーション アイコンを復元
スタート画面を変更
@@ -1561,12 +1566,15 @@ Automotive レイアウト
• 実験的なクライアントであり、いつでも動作しなくなる可能性がある
• AV1 コーデックが利用できない
• ログアウト時またはシークレット モード時に、子ども向け動画が再生されない可能性がある
+
+ •「オリジナルの音声を強制的に使用」が利用できない
統計情報に表示する
統計情報に現在のクライアントが表示されます
統計情報に現在のクライアントは表示されません
音声ストリームの言語
特定の音声言語を選択するには、「オリジナルの音声を強制的に使用」を無効にしてください
+ Android Studio では、ストリームの言語を選択できません
@@ -1589,8 +1597,8 @@ Automotive レイアウト
キャストボタンを非表示
- プレーヤー オーバーレイのキャストボタンは表示されません
- プレーヤー オーバーレイのキャストボタンは表示されます
+ キャストボタンはプレーヤー オーバーレイに表示されません
+ キャストボタンはプレーヤー オーバーレイに表示されます
- ナビゲーションバー
- ナビゲーションバーのボタンを非表示または変更
+ ナビゲーション バー
+ ナビゲーション バーのボタンを変更または非表示にします
- ホームボタンを非表示
+ 「ホーム」を非表示
ホームボタンは表示されません
ホームボタンは表示されます
- サンプルボタンを非表示
+ 「サンプル」を非表示
サンプルボタンは表示されません
サンプルボタンは表示されます
- 探索ボタンを非表示
+ 「探索」を非表示
探索ボタンは表示されません
探索ボタンは表示されます
- ライブラリボタンを非表示
- ライブラリボタンは表示されません
- ライブラリボタンは表示されます
+ 「ライブラリ」を非表示
+ ライブラリ ボタンは表示されません
+ ライブラリ ボタンは表示されます
- アップグレード ボタンを非表示
+ 「アップグレード」を非表示
アップグレード ボタンは表示されません
アップグレード ボタンは表示されます
- ナビゲーションバーを非表示
- ナビゲーションバーは表示されません
- ナビゲーションバーは表示されます
+ ナビゲーション バーを非表示
+ ナビゲーション バーは表示されません
+ ナビゲーション バーは表示されます
ボタンをアイコンのみで表示
ナビゲーション ボタンはアイコンのみで表示されます
ナビゲーション ボタンはアイコンと文字で表示されます
diff --git a/patches/src/main/resources/addresources/values-ka-rGE/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-ka-rGE/strings.xml
+++ b/patches/src/main/resources/addresources/values-ka-rGE/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml
+++ b/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-km-rKH/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-km-rKH/strings.xml
+++ b/patches/src/main/resources/addresources/values-km-rKH/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-kn-rIN/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/strings.xml
index ebdb14402..949a2d95b 100644
--- a/patches/src/main/resources/addresources/values-kn-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-kn-rIN/strings.xml
@@ -104,6 +104,7 @@ Second \"item\" text"
+
@@ -258,6 +259,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
index a2e1b0025..e9ee33505 100644
--- a/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
+++ b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml
@@ -107,6 +107,15 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
디버그 로깅
디버그 로깅을 활성화합니다
디버그 로깅을 비활성화합니다
+ 로그 스택 트레이스
+ 디버그 로그에 로그 스택 트레이스를 포함합니다
+ 디버그 로그에 로그 스택 트레이스를 포함하지 않습니다
+ ReVanced 오류 메시지 표시하기
+ 오류가 발생하면 팝업 메시지를 표시합니다
+ 오류가 발생하면 팝업 메시지를 표시하지 않습니다
+ "오류 메시지를 비활성화하면 모든 ReVanced 오류 알림이 숨겨집니다.
+
+예상되지 않은 이벤트에 대한 알림을 받지 못할 수 있습니다."
디버그 로그 내보내기
ReVanced 디버그 로그를 클립보드에 복사할 수 있습니다
디버그 로깅이 비활성화되어 있습니다
@@ -151,15 +160,6 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
이 설정은 사용자 정의 필터를 만들 경우에 구성 요소를 식별하는 데 도움이 될 수 있습니다.
그러나 이 설정을 활성화하면 IP 주소와 같은 일부 사용자 데이터도 기록됩니다."
- 로그 스택 트레이스
- 디버그 로그에 로그 스택 트레이스를 포함합니다
- 디버그 로그에 로그 스택 트레이스를 포함하지 않습니다
- ReVanced 오류 메시지 표시하기
- 오류가 발생하면 팝업 메시지를 표시합니다
- 오류가 발생하면 팝업 메시지를 표시하지 않습니다
- "오류 메시지를 비활성화하면 모든 ReVanced 오류 알림이 숨겨집니다.
-
-예상되지 않은 이벤트에 대한 알림을 받지 못할 수 있습니다."
음악 앨범 카드 숨기기
@@ -584,6 +584,10 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
광고 중지 버튼 숨기기
광고 중지 버튼이 숨겨집니다
광고 중지 버튼이 표시됩니다
+
+ 댓글 버튼 숨기기
+ 댓글 버튼이 숨겨집니다
+ 댓글 버튼이 표시됩니다
신고 버튼 숨기기
@@ -656,7 +660,7 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
알려진 문제점:
• 동영상 광고가 강제로 숨겨집니다"
- 만들기 버튼과 알림 버튼의 위치를 교환하지 않습니다\n\n알려진 문제점:\n• 서버에서 더 많은 광고가 로드될 수 있습니다\n• Shorts 광고가 더 이상 숨겨지지 않습니다
+ 만들기 버튼과 알림 버튼의 위치를 교환하지 않습니다
"이 설정을 비활성화하면 Shorts 광고가 차단되지 않습니다.
이 설정을 변경해도 적용되지 않는다면 시크릿 모드로 전환해 보세요."
@@ -944,7 +948,7 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
싫어요 수를 표시할 수 없습니다 (클라이언트 API 제한 도달)
싫어요 수를 표시할 수 없습니다 (%s)
- ReturnYouTubeDislike를 사용하여 투표하려면 동영상을 다시 로드하세요
+ ReturnYouTubeDislike를 사용하여 투표하려면 동영상을 다시 불러오세요
소유자에 의해 숨겨짐
싫어요 수를 표시합니다
@@ -1243,8 +1247,9 @@ YouTube Premium 사용자라면 이 설정은 필요하지 않을 수 있습니
나중에 이 기능을 비활성화하면 앱 레이아웃 버그를 방지하기 위해 앱 데이터를 지우는 것이 좋습니다."
변경할 앱 버전
- 19.35.36 - 이전 Shorts 플레이어 아이콘을 복원합니다
- 19.01.34 - 이전 하단바 아이콘을 복원합니다
+ 20.13.41 - 접히지 않은 동영상 동작바를 복원합니다
+ 19.35.36 - 이전 Shorts 플레이어 아이콘을 복원합니다
+ 19.01.34 - 이전 하단바 아이콘을 복원합니다
앱 시작 페이지 변경하기
@@ -1480,9 +1485,9 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"
링크를 공유할 경우에 URL에서 추적 쿼리 매개변수를 삭제하지 않습니다
- 원본 오디오 스트림 언어 강제로 활성화하기
- 원본 오디오 스트림 언어를 사용 중입니다
- 기본 오디오 스트림 언어를 사용 중입니다
+ 원본 오디오 언어 강제로 활성화하기
+ 원본 오디오 언어를 사용 중입니다
+ 기본 오디오 언어를 사용 중입니다
이 기능을 사용하려면, \'동영상 스트림 변경하기\'에서 기본 클라이언트를 Android Studio를 제외한 다른 클라이언트로 변경하세요
@@ -1566,12 +1571,15 @@ DeArrow에 대해 자세히 알아보려면 여기를 탭하세요"
• 실험용 클라이언트이며 언제든지 작동이 중단될 수 있습니다
• AV1 코덱이 지원되지 않습니다
• Kids 동영상은 로그인을 하지 않았거나 시크릿 모드에서는 재생되지 않을 수 있습니다
+
+ • 원본 오디오를 강제로 활성화할 수 없습니다
전문 통계에서 표시하기
동영상 스트림을 가져오는 데 사용되는 클라이언트가 전문 통계에서 표시됩니다
동영상 스트림을 가져오는 데 사용되는 클라이언트가 전문 통계에서 표시되지 않습니다
오디오 스트림 언어
- 특정 오디오 스트림 언어를 선택하려면, \'원본 오디오 스트림 언어 강제로 활성화하기\'를 끄세요
+ 특정 오디오 언어를 선택하려면, \'원본 오디오 언어 강제로 활성화하기\'를 끄세요
+ Android Studio에서는 스트림 언어를 선택할 수 없습니다
diff --git a/patches/src/main/resources/addresources/values-ky-rKG/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-ky-rKG/strings.xml
+++ b/patches/src/main/resources/addresources/values-ky-rKG/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-lo-rLA/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-lo-rLA/strings.xml
+++ b/patches/src/main/resources/addresources/values-lo-rLA/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-lt-rLT/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/strings.xml
index fbf726ab5..1add49b0d 100644
--- a/patches/src/main/resources/addresources/values-lt-rLT/strings.xml
+++ b/patches/src/main/resources/addresources/values-lt-rLT/strings.xml
@@ -107,6 +107,15 @@ Grojimas gali neveikti"
Debugavimo žurnalas
Debugavimo žurnalai yra įgalinti
Debugavimo žurnalai yra išjungti
+ Žurnalų steko pėdsakai
+ Debugavimo žurnalai apima steko pėdsaką
+ Debugavimo žurnalai neapima steko pėdsako
+ Rodyti \"toast\" apie ReVanced klaidą
+ Jei įvyksta klaida, rodomas pranešimas „toast“
+ Jei įvyksta klaida, pranešimas „toast“ nerodomas
+ "Klaidos isjungimas paslepia visus „ReVanced“ klaidu pranesimus.
+
+Apie netikėtus įvykius nebus pranešta."
Eksportuoti derinimo žurnalus
Nukopijuoja „ReVanced“ derinimo žurnalus į mainų sritį
Derinimo žurnalų registravimas išjungtas
@@ -151,15 +160,6 @@ Grojimas gali neveikti"
Tai gali padėti identifikuoti komponentus kuriant pasirinktinius filtrus.
Tačiau įjungus šį nustatymą taip pat bus registruojami kai kurie vartotojo duomenys, pvz., jūsų IP adresas."
- Žurnalų steko pėdsakai
- Debugavimo žurnalai apima steko pėdsaką
- Debugavimo žurnalai neapima steko pėdsako
- Rodyti \"toast\" apie ReVanced klaidą
- Jei įvyksta klaida, rodomas pranešimas „toast“
- Jei įvyksta klaida, pranešimas „toast“ nerodomas
- "Klaidos isjungimas paslepia visus „ReVanced“ klaidu pranesimus.
-
-Apie netikėtus įvykius nebus pranešta."
Slėpti albumo korteles
@@ -586,6 +586,10 @@ Reguliuokite garsumą braukdami vertikaliai dešinėje ekrano pusėje"
Slėpti \"Stabdyti reklamą\"
Skelbimų stabdymo mygtukas paslėptas
Skelbimų stabdymo mygtukas rodomas
+
+ Slėpti komentarus
+ Komentarų mygtukas paslėptas
+ Komentarų mygtukas rodomas
Slėpti Pranešti
@@ -1243,8 +1247,9 @@ Tai pakeis programos išvaizdą ir funkcijas, bet gali atsirasti nežinomų šal
Jei vėliau išjungta, rekomenduojama išvalyti programos duomenis, kad būtų išvengta vartotojo sąsajos klaidų."
Programėlės versijos apsimetinėjimo tikslas
- 19.35.36 - Atkurti senus \"Shorts\" grotuvo piktogramas
- 19.01.34 – Atkurti senas naršymo piktogramas
+ 20.13.41 – Atkurti neišskleistą vaizdo veiksmų juostą
+ 19.35.36 - Atkurti senus \"Shorts\" grotuvo piktogramas
+ 19.01.34 – Atkurti senas naršymo piktogramas
Keisti pradžios puslapį
@@ -1560,12 +1565,15 @@ Gali būti atrakinta aukštesnės vaizdo įrašų kokybės, bet galite patirti v
• Eksperimentinis klientas ir bet kada gali nustoti veikti
• Nėra AV1 vaizdo kodeko
• Vaikų vaizdo įrašai gali būti neatkuriami, kai atsijungiama arba naudojamas inkognito režimas
+
+ • Priverstinai naudoti originalų garsą nepasiekiama
Rodyti statistinėje informacijoje \"tik profesionalams\"
Kliento tipas rodomas statistinėje informacijoje \"tik profesionalams\"
Klientas paslėptas statistinėje informacijoje \"tik profesionalams\"
Garso srauto kalba
Norėdami pasirinkti konkrečią garso kalbą, išjunkite „Priverstinė originali garso kalba“.
+ Transliacijos kalbos pasirinkimas nepasiekiamas naudojant „Android Studio“
@@ -1600,23 +1608,23 @@ Gali būti atrakinta aukštesnės vaizdo įrašų kokybės, bet galite patirti v
Naršymo juosta
Slėpti arba keisti naršymo juostos mygtukus
- Slėpti mygtuką „Pagrindinis“
+ Slėpti Pagrindinį
Mygtukas „Pagrindinis“ yra paslėptas
Mygtukas „Pagrindinis“ yra rodomas
- Slėpti mygtuką „Pavyzdžiai“
+ Slėpti Pavyzdžius
Mygtukas „Pavyzdžiai“ yra paslėptas
Mygtukas „Pavyzdžiai“ yra rodomas
- Slėpti mygtuką „Naršyti“
+ Slėpti Naršyti
Mygtukas „Naršyti“ yra paslėptas
Mygtukas „Naršyti“ yra rodomas
- Slėpti mygtuką „Biblioteka“
+ Slėpti Biblioteką
Mygtukas „Biblioteka“ yra paslėptas
Mygtukas „Biblioteka“ yra rodomas
- Slėpti mygtuką „Naujinti“
+ Slėpti Atnaujinti
Mygtukas „Naujinti“ yra paslėptas
Mygtukas „Naujinti“ yra rodomas
Slėpti naršymo juostą
diff --git a/patches/src/main/resources/addresources/values-lv-rLV/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml
index 7640b8328..6cdd5e4b1 100644
--- a/patches/src/main/resources/addresources/values-lv-rLV/strings.xml
+++ b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml
@@ -107,6 +107,15 @@ Atskaņošana var nedarboties"
Atkļūdošanas žurnāls
Atkļūdošanas žurnāli ir iespējoti
Atkļūdošanas žurnāli ir atspējoti
+ Reģistrēt steka izsekošanu
+ Atkļūdošanas žurnāli ietver steka izsekošanu
+ Atkļūdošanas žurnāli neietver steka izsekošanu
+ Rādīt paziņojumu par ReVanced kļūdu
+ Ja rodas kļūda, tiek rādīts toast
+ Ja rodas kļūda, toast netiek rādīts
+ "Kļūdu paziņojumu izslēgšana paslēpj visus ReVanced kļūdu paziņojumus.
+
+Jūs netiksit informēts par neparedzētiem notikumiem."
Eksportēt atkļūdošanas žurnālus
Kopē ReVanced atkļūdošanas žurnālus starpliktuvē
Atkļūdošanas žurnālu reģistrēšana ir atspējota
@@ -151,15 +160,6 @@ Atskaņošana var nedarboties"
Tas var palīdzēt identificēt komponentus, veidojot pielāgotus filtrus.
Tomēr, iespējojot šo iestatījumu, tiks reģistrēti arī daži lietotāja dati, piemēram, jūsu IP adrese."
- Reģistrēt steka izsekošanu
- Atkļūdošanas žurnāli ietver steka izsekošanu
- Atkļūdošanas žurnāli neietver steka izsekošanu
- Rādīt paziņojumu par ReVanced kļūdu
- Ja rodas kļūda, tiek rādīts toast
- Ja rodas kļūda, toast netiek rādīts
- "Kļūdu paziņojumu izslēgšana paslēpj visus ReVanced kļūdu paziņojumus.
-
-Jūs netiksit informēts par neparedzētiem notikumiem."
Paslēpt albumu kartītes
@@ -586,6 +586,10 @@ Regulējiet skaļumu, velkot vertikāli ekrāna labajā pusē"
Slēpt Pārtraukt reklāmas
Apturēt reklāmas poga ir paslēpta
Apturēt reklāmas poga ir redzama
+
+ Slēpt komentārus
+ Komentāru poga ir paslēpta
+ Komentāru poga ir redzama
Paslēpt Ziņot
@@ -1243,8 +1247,9 @@ Tas mainīs lietotnes izskatu un funkcijas, taču var rasties nezināmas blakusp
Ja vēlāk tiks izslēgts, ieteicams notīrīt lietotnes datus, lai novērstu lietotāja saskarnes kļūdas."
Viltot lietotnes versijas mērķis
- 19.35.36 - Atjaunot vecās Shorts spēlētāja ikonas
- 19.01.34 - Atjaunot vecās navigācijas ikonas
+ 20.13.41 - Atjaunot nesaspiestu video darbību joslu
+ 19.35.36 - Atjaunot vecās Shorts spēlētāja ikonas
+ 19.01.34 - Atjaunot vecās navigācijas ikonas
Mainīt sākuma lapu
@@ -1560,12 +1565,15 @@ Var tikt atbloķētas augstākas video kvalitātes, taču var rasties video atsk
• Eksperimentāls klients un jebkurā brīdī var pārtraukt darbu
• Nav pieejams AV1 video kodeks
• Bērnu videoklipi var netikt atskaņoti, kad esat izrakstījies vai inkognito režīmā.
+
+ • Piespiest oriģinālo skaņu nav pieejams
Rādīt statistiskos datos entuziastiem
Klienta tips tiek rādīts statistiskos datos entuziastiem
Klients ir paslēpts statistiskos datos entuziastiem
Audio straumes valoda
Lai atlasītu konkrētu audio valodu, izslēdziet “Piespiest oriģinālo audio valodu”
+ Straumju valodas izvēle nav pieejama ar Android Studio
@@ -1600,23 +1608,23 @@ Var tikt atbloķētas augstākas video kvalitātes, taču var rasties video atsk
Navigācijas josla
Slēpt vai mainīt navigācijas joslas pogas
- Slēpt pogu Sākums
+ Paslēpt Sākumlapu
Poga Sākums ir paslēpta
Poga Sākums ir redzama
- Slēpt pogu Paraugi
+ Paslēpt Īsos klipus
Poga Paraugi ir paslēpta
Poga Paraugi ir redzama
- Slēpt pogu Izpētīt
+ Slēpt Izpētīt
Poga Izpētīt ir paslēpta
Poga Izpētīt ir redzama
- Slēpt pogu Bibliotēka
+ Slēpt bibliotēku
Poga Bibliotēka ir paslēpta
Poga Bibliotēka ir redzama
- Slēpt pogu Jaunināt
+ Slēpt jaunināšanu
Poga Jaunināt ir paslēpta
Poga Jaunināt ir redzama
Slēpt navigācijas joslu
diff --git a/patches/src/main/resources/addresources/values-mk-rMK/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-mk-rMK/strings.xml
+++ b/patches/src/main/resources/addresources/values-mk-rMK/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ml-rIN/strings.xml b/patches/src/main/resources/addresources/values-ml-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-ml-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-ml-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-mn-rMN/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-mn-rMN/strings.xml
+++ b/patches/src/main/resources/addresources/values-mn-rMN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-mr-rIN/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-mr-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-mr-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ms-rMY/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/strings.xml
index e7a089136..aea3c57e7 100644
--- a/patches/src/main/resources/addresources/values-ms-rMY/strings.xml
+++ b/patches/src/main/resources/addresources/values-ms-rMY/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -244,6 +245,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-my-rMM/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-my-rMM/strings.xml
+++ b/patches/src/main/resources/addresources/values-my-rMM/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-nb-rNO/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-nb-rNO/strings.xml
+++ b/patches/src/main/resources/addresources/values-nb-rNO/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ne-rIN/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/strings.xml
index 54e3c3092..31a443ec9 100644
--- a/patches/src/main/resources/addresources/values-ne-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-ne-rIN/strings.xml
@@ -91,6 +91,7 @@ Second \"item\" text"
+
@@ -245,6 +246,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-nl-rNL/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml
index c3bd399d8..89a289c2d 100644
--- a/patches/src/main/resources/addresources/values-nl-rNL/strings.xml
+++ b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml
@@ -107,6 +107,15 @@ Afspeelproblemen kunnen optreden"
Debuglogboek
Debuglogboeken zijn ingeschakeld
Debuglogboeken zijn uitgeschakeld
+ Stacktraces loggen
+ Debuglogboeken bevatten stacktrace
+ Debuglogs bevatten geen stacktrace
+ Toon toastmelding bij ReVanced error
+ Toastmelding wordt weergegeven als er een fout optreedt
+ Toastmelding wordt niet weergegeven als er een fout optreedt
+ "Het uitschakelen van foutmeldingen verbergt alle ReVanced-foutmeldingen.
+
+U wordt niet op de hoogte gesteld van onverwachte gebeurtenissen."
Foutopsporingslogboeken exporteren
Kopieert ReVanced-foutopsporingslogboeken naar het klembord
Foutopsporing is uitgeschakeld
@@ -151,15 +160,6 @@ Afspeelproblemen kunnen optreden"
Dit kan helpen bij het identificeren van componenten bij het maken van aangepaste filters.
Als u dit inschakelt, worden echter ook bepaalde gebruikersgegevens, zoals uw IP-adres, vastgelegd."
- Stacktraces loggen
- Debuglogboeken bevatten stacktrace
- Debuglogs bevatten geen stacktrace
- Toon toastmelding bij ReVanced error
- Toastmelding wordt weergegeven als er een fout optreedt
- Toastmelding wordt niet weergegeven als er een fout optreedt
- "Het uitschakelen van foutmeldingen verbergt alle ReVanced-foutmeldingen.
-
-U wordt niet op de hoogte gesteld van onverwachte gebeurtenissen."
Verberg albumkaarten
@@ -586,6 +586,10 @@ Pas het volume aan door verticaal over de rechterkant van het scherm te vegen"
Advertenties stoppen verbergen
Knop \"Advertenties stoppen\" is verborgen
Knop \"Advertenties stoppen\" is zichtbaar
+
+ Reacties verbergen
+ De reactieknop is verborgen
+ De reactieknop wordt weergegeven
Melden verbergen
@@ -1242,8 +1246,9 @@ Dit zal het uiterlijk en de functies van de app veranderen, maar er kunnen onbek
Als het later wordt uitgeschakeld, wordt aanbevolen om de app-gegevens te wissen om UI-fouten te voorkomen."
Doel voor vervalsen app-versie
- 19.35.36 - Herstel oude pictogrammen voor Shorts-speler
- 19.01.34 - Herstel oude navigatie-iconen
+ 20.13.41 - Herstel niet-ingeklapte videobalk
+ 19.35.36 - Herstel oude pictogrammen voor Shorts-speler
+ 19.01.34 - Herstel oude navigatie-iconen
Startpagina wijzigen
@@ -1557,12 +1562,15 @@ Het inschakelen hiervan kan hogere videokwaliteiten ontgrendelen"
• Experimentele client en kan elk moment stoppen met werken
• Geen AV1-videocodec
• \"Kinder\"-Video’s worden mogelijk niet afgespeeld wanneer u bent uitgelogd of de incognitomodus gebruikt
+
+ • Oorspronkelijke audio forceren is niet beschikbaar
Weergeven in Stats for nerds
Het clienttype wordt getoond in Stats for nerds
Client is verborgen in Stats for nerds
Audiostreamtaal
Om een specifieke audiotaal te selecteren, schakel \"Oorspronkelijke audiotaal forceren\" uit
+ Streamtaalselectie is niet beschikbaar met Android Studio
@@ -1597,23 +1605,23 @@ Het inschakelen hiervan kan hogere videokwaliteiten ontgrendelen"
Navigatiebalk
Navigatiebalkknoppen verbergen of wijzigen
- Startknop verbergen
+ Startpagina verbergen
Startknop is verborgen
Startknop wordt weergegeven
- Knop Voorbeelden verbergen
+ Fragmenten verbergen
Knop Voorbeelden is verborgen
Knop Voorbeelden wordt weergegeven
- Knop Ontdekken verbergen
+ Verberg Ontdekken
Knop Ontdekken is verborgen
Knop Ontdekken wordt weergegeven
- Knop Bibliotheek verbergen
+ Verberg Bibliotheek
Knop Bibliotheek is verborgen
Knop Bibliotheek wordt weergegeven
- Upgrade-knop verbergen
+ Verberg Upgraden
Upgrade-knop is verborgen
Upgrade-knop wordt weergegeven
Navigatiebalk verbergen
diff --git a/patches/src/main/resources/addresources/values-or-rIN/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-or-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-or-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-pa-rIN/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-pa-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-pa-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-pl-rPL/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/strings.xml
index 93b597706..ca5a642a6 100644
--- a/patches/src/main/resources/addresources/values-pl-rPL/strings.xml
+++ b/patches/src/main/resources/addresources/values-pl-rPL/strings.xml
@@ -107,6 +107,15 @@ Odtwarzanie może nie działać"
Logi do debugowania
Logi debugowania są włączone
Logi debugowania są wyłączone
+ Logi śladów stosów
+ Logi do debugowania zawierają ślady stosów
+ Logi do debugowania nie zawierają śladów stosów
+ Komunikaty o błędach ReVanced
+ Wyświetlany jest toast, jeśli wystąpi błąd
+ Toast nie jest wyświetlany, jeśli wystąpi błąd
+ "Wyłączenie notyfikacji błędów ukrywa wszystkie powiadomienia o błędach ReVanced.
+
+Nie będziesz informowany o żadnych nieoczekiwanych zdarzeniach."
Eksportuj dzienniki debugowania
Kopiuje dzienniki debugowania ReVanced do schowka
Rejestrowanie debugowania jest wyłączone
@@ -147,15 +156,6 @@ Odtwarzanie może nie działać"
Logi do debugowania zawierają bufory
Logi do debugowania nie zawierają buforów
"Włączenie tego ustawienia spowoduje rejestrowanie dodatkowych danych układu, w tym tekstu na ekranie dla niektórych komponentów interfejsu użytkownika.\n\nMoże to pomóc w identyfikacji komponentów podczas tworzenia filtrów niestandardowych.\n\nWłączenie tej opcji spowoduje jednak również rejestrowanie niektórych danych użytkownika, takich jak adres IP."
- Logi śladów stosów
- Logi do debugowania zawierają ślady stosów
- Logi do debugowania nie zawierają śladów stosów
- Komunikaty o błędach ReVanced
- Wyświetlany jest toast, jeśli wystąpi błąd
- Toast nie jest wyświetlany, jeśli wystąpi błąd
- "Wyłączenie notyfikacji błędów ukrywa wszystkie powiadomienia o błędach ReVanced.
-
-Nie będziesz informowany o żadnych nieoczekiwanych zdarzeniach."
Ukryj karty albumów
@@ -582,6 +582,10 @@ Dostosuj głośność, przesuwając pionowo po prawej stronie ekranu"
Ukryj \"Zatrzymaj reklamy\"
Przycisk zatrzymania reklam jest ukryty
Przycisk zatrzymania reklam jest widoczny
+
+ Ukryj komentarze
+ Przycisk komentarzy jest ukryty
+ Przycisk komentarzy jest widoczny
Przycisk od zgłaszania
@@ -1238,8 +1242,9 @@ Spowoduje to zmianę wyglądu i funkcjonalności aplikacji, ale mogą wystąpić
Jeśli później zostanie wyłączony, zaleca się wyczyszczenie danych aplikacji, aby zapobiec błędom interfejsu użytkownika."
Docelowa oszukiwana wersja aplikacji
- 19.35.36 - Przywraca stare ikony odtwarzacza Shortsów
- 19.01.34 – Przywróć stare ikony nawigacji
+ 20.13.41 - Przywróć niezwinięty pasek akcji wideo
+ 19.35.36 - Przywraca stare ikony odtwarzacza Shortsów
+ 19.01.34 – Przywróć stare ikony nawigacji
Zmień stronę startową
@@ -1555,12 +1560,15 @@ Włączenie tego może odblokować wyższe jakości wideo"
• Eksperymentalny klient i może przestać działać w każdej chwili
• Žádný video kodek AV1
• Filmy dla dzieci mogą nie być odtwarzane po wylogowaniu lub w trybie incognito
+
+ • Wymuś oryginalny dźwięk jest niedostępny
Pokaż w statystykach dla nerdów
Typ klienta jest wyświetlany w Statystykach dla nerdów
Klient jest ukryty w statystykach dla nerdów
Język strumienia audio
Aby wybrać konkretny język audio, wyłącz \"Wymuś oryginalny język audio\"
+ Wybór języka strumienia nie jest dostępny w przypadku Android Studio
@@ -1595,23 +1603,23 @@ Włączenie tego może odblokować wyższe jakości wideo"
Pasek nawigacji
Ukryj lub zmień przyciski paska nawigacji
- Ukryj przycisk Główna
+ Ukryj Główną
Przycisk Główna jest ukryty
Przycisk Główna jest pokazany
- Ukryj przycisk Wycinki
+ Ukryj Fragmenty
Przycisk Wycinki jest ukryty
Przycisk Wycinki jest pokazany
- Ukryj przycisk Przeglądaj
+ Ukryj Odkrywaj
Przycisk Przeglądaj jest ukryty
Przycisk Przeglądaj jest pokazany
- Ukryj przycisk Biblioteka
+ Ukryj Bibliotekę
Przycisk Biblioteka jest ukryty
Przycisk Biblioteka jest pokazany
- Ukryj przycisk Ulepsz
+ Ukryj Ulepsz
Przycisk Ulepsz jest ukryty
Przycisk Ulepsz jest pokazany
Ukryj pasek nawigacji
diff --git a/patches/src/main/resources/addresources/values-pt-rBR/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/strings.xml
index a81842d78..ccd4ea1c0 100644
--- a/patches/src/main/resources/addresources/values-pt-rBR/strings.xml
+++ b/patches/src/main/resources/addresources/values-pt-rBR/strings.xml
@@ -107,6 +107,15 @@ A reprodução pode não funcionar"
Registro de depuração
Registro de depuração está ativado
Registro de depuração está desativado
+ Registro de rastreamento em pilha
+ Registro de depuração incluem rastreamento em pilha
+ Registro de depuração não incluem rastreamento em pilha
+ Mostrar notificação flutuante de erro do ReVanced
+ O toast é exibido se ocorrer um erro
+ O toast não é exibido se ocorrer um erro
+ "Desligar as notificações de erro oculta todas as notificações de erro do ReVanced.
+
+Você não será notificado sobre nenhum evento inesperado."
Exportar registros de depuração
Copia os registros de depuração do ReVanced para a área de transferência
O registro de depuração está desativado
@@ -151,15 +160,6 @@ A reprodução pode não funcionar"
Isso pode ajudar a identificar componentes ao criar filtros personalizados.
No entanto, ativar isso também registrará alguns dados do usuário, como seu endereço IP."
- Registro de rastreamento em pilha
- Registro de depuração incluem rastreamento em pilha
- Registro de depuração não incluem rastreamento em pilha
- Mostrar notificação flutuante de erro do ReVanced
- O toast é exibido se ocorrer um erro
- O toast não é exibido se ocorrer um erro
- "Desligar as notificações de erro oculta todas as notificações de erro do ReVanced.
-
-Você não será notificado sobre nenhum evento inesperado."
Ocultar cartões de álbum
@@ -586,6 +586,10 @@ Ajuste o volume deslizando verticalmente no lado direito da tela"
Ocultar \"Parar anúncios\"
O botão Parar anúncios está oculto
O botão Parar anúncios é exibido
+
+ Ocultar Comentários
+ Botão de comentários está oculto
+ Botão de comentários está visível
Ocultar Denúncia
@@ -1239,8 +1243,9 @@ Isso mudará a aparência e os recursos do aplicativo, mas podem ocorrer efeitos
Se posteriormente desativado, é recomendável limpar os dados do aplicativo para evitar bugs na IU."
Versão de spoofing alvo
- 19.35.36 - Restaurar ícones antigos do player dos Shorts
- 19.01.34 - Restaurar ícones de navegação antigos
+ 20.13.41 - Restaurar barra de ações de vídeo não recolhida
+ 19.35.36 - Restaurar ícones antigos do player dos Shorts
+ 19.01.34 - Restaurar ícones de navegação antigos
Alterar página inicial
@@ -1556,12 +1561,15 @@ Habilitar isso pode desbloquear qualidades de vídeo mais altas"
• Cliente experimental e pode parar de funcionar a qualquer momento
• Sem codec de vídeo AV1
• Vídeos infantis podem não ser reproduzidos quando estiver desconectado ou no modo de navegação anônima
+
+ • Forçar áudio original não está disponível
Mostrar em Estatísticas para nerds
O tipo de cliente é mostrado em Estatísticas para nerds
O cliente está oculto em Estatísticas para nerds
Idioma do fluxo de áudio
Para selecionar um idioma de áudio específico, desative \"Forçar idioma de áudio original\"
+ A seleção de idioma do stream não está disponível com o Android Studio
@@ -1596,23 +1604,23 @@ Habilitar isso pode desbloquear qualidades de vídeo mais altas"
Barra de navegação
Ocultar ou alterar botões da barra de navegação
- Ocultar botão Início
+ Ocultar Início
Botão Início oculto
Botão Início visível
- Ocultar botão Amostras
+ Ocultar Amostras
Botão Amostras oculto
Botão Amostras visível
- Ocultar botão Explorar
+ Ocultar Explorar
Botão Explorar oculto
Botão Explorar visível
- Ocultar botão Biblioteca
+ Ocultar Biblioteca
Botão Biblioteca oculto
Botão Biblioteca visível
- Ocultar botão Fazer upgrade
+ Ocultar Atualizar
Botão Fazer upgrade oculto
Botão Fazer upgrade visível
Ocultar barra de navegação
diff --git a/patches/src/main/resources/addresources/values-pt-rPT/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/strings.xml
index f840c6ccb..2de0a67b4 100644
--- a/patches/src/main/resources/addresources/values-pt-rPT/strings.xml
+++ b/patches/src/main/resources/addresources/values-pt-rPT/strings.xml
@@ -107,6 +107,15 @@ A reprodução pode não funcionar"
Registo da depuração
Os registos da depuração estão ativados
Os registos da depuração estão desativados
+ Pilha de registos
+ Os registos da depuração incluem stack trace
+ Os registos da depuração não incluem stack trace
+ Mostrar toast com erro de ReVanced
+ O toast é exibido se ocorrer um erro
+ O toast não é exibido se ocorrer um erro
+ "A desativação dos erros toasts oculta todas as notificações de erro do ReVanced.
+
+Não será notificado de quaisquer eventos inesperados."
Exportar registos de depuração
Copia os registos de depuração do ReVanced para a área de transferência
O registo de depuração está desativado
@@ -151,15 +160,6 @@ A reprodução pode não funcionar"
Isto pode ajudar a identificar componentes ao criar filtros personalizados.
No entanto, ativar isto também irá registar alguns dados do utilizador, como o seu endereço IP."
- Pilha de registos
- Os registos da depuração incluem stack trace
- Os registos da depuração não incluem stack trace
- Mostrar toast com erro de ReVanced
- O toast é exibido se ocorrer um erro
- O toast não é exibido se ocorrer um erro
- "A desativação dos erros toasts oculta todas as notificações de erro do ReVanced.
-
-Não será notificado de quaisquer eventos inesperados."
Esconder cartões de álbuns
@@ -586,6 +586,10 @@ Ajuste o volume deslizando verticalmente no lado direito da tela"
Ocultar Parar anúncios
O botão Parar anúncios está oculto
O botão Parar anúncios está exibido
+
+ Ocultar Comentários
+ O botão de comentários está oculto
+ O botão de comentários está exibido
Esconder relatório
@@ -1242,8 +1246,9 @@ Layout automotivo
Nếu sau này tắt, bạn nên xóa dữ liệu ứng dụng để tránh lỗi UI."
Destaque de versão do app
- 19.35.36 - Restaurar os icones antigos do reprodutor dos Shorts
- 19.01.34 - Restaurar ícones antigos de navegação
+ 20.13.41 - Restaurar barra de ação de vídeo não recolhida
+ 19.35.36 - Restaurar os icones antigos do reprodutor dos Shorts
+ 19.01.34 - Restaurar ícones antigos de navegação
Alterar página inicial
@@ -1559,12 +1564,15 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"• Cliente experimental e pode parar de funcionar a qualquer momento
• Nenhum codec de vídeo AV1
• Vídeos infantis podem não ser reproduzidos quando desconectado ou no modo anônimo
+
+ • Forçar áudio original não está disponível
Mostrar em Estatísticas para nerds
O tipo de cliente é mostrado em Estatísticas para nerds
O cliente está oculto em Estatísticas para nerds
Idioma do fluxo de áudio
Para selecionar um idioma de áudio específico, desative \'Forçar idioma de áudio original\'
+ A seleção do idioma do stream não está disponível com o Android Studio
@@ -1599,23 +1607,23 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"Barra de navegação
Ocultar ou alterar botões da barra de navegação
- Ocultar botão Início
+ Ocultar Início
Botão Início está oculto
Botão Início está exibido
- Ocultar botão Amostras
+ Ocultar Curtos
Botão Amostras está oculto
Botão Amostras está exibido
- Ocultar botão Explorar
+ Ocultar Explorar
Botão Explorar está oculto
Botão Explorar está exibido
- Ocultar botão Biblioteca
+ Ocultar Biblioteca
Botão Biblioteca está oculto
Botão Biblioteca está exibido
- Ocultar botão Fazer upgrade
+ Ocultar Premium
Botão Fazer upgrade está oculto
Botão Fazer upgrade está exibido
Ocultar barra de navegação
diff --git a/patches/src/main/resources/addresources/values-ro-rRO/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/strings.xml
index ff7331b28..04025e4b5 100644
--- a/patches/src/main/resources/addresources/values-ro-rRO/strings.xml
+++ b/patches/src/main/resources/addresources/values-ro-rRO/strings.xml
@@ -107,6 +107,15 @@ Redarea poate să nu funcționeze"
Jurnal depanare
Jurnalele de depanare sunt activate
Jurnalele de depanare sunt dezactivate
+ Urme stive de jurnal
+ Jurnalele depanării includ urmărirea stivelor
+ Jurnalele de depanare nu includ urmărirea stivelor
+ Arată toast la eroare ReVanced
+ Se afișează un toast dacă apare o eroare
+ Nu se afișează un toast dacă apare o eroare
+ "Oprirea toasturilor de eroare ascunde toate notificările de eroare ReVanced.
+
+Nu veți fi notificat de niciun eveniment neașteptat."
Exportare jurnale de depanare
Copiază jurnalele de depanare ReVanced în clipboard
Înregistrarea în jurnal de depanare este dezactivată
@@ -151,15 +160,6 @@ Redarea poate să nu funcționeze"
Acest lucru poate ajuta la identificarea componentelor atunci când creați filtre personalizate.
Cu toate acestea, activarea acestei opțiuni va înregistra și unele date ale utilizatorului, cum ar fi adresa dvs. IP."
- Urme stive de jurnal
- Jurnalele depanării includ urmărirea stivelor
- Jurnalele de depanare nu includ urmărirea stivelor
- Arată toast la eroare ReVanced
- Se afișează un toast dacă apare o eroare
- Nu se afișează un toast dacă apare o eroare
- "Oprirea toasturilor de eroare ascunde toate notificările de eroare ReVanced.
-
-Nu veți fi notificat de niciun eveniment neașteptat."
Ascundeți cardurile de album
@@ -586,6 +586,10 @@ Reglați volumul glisând vertical pe partea dreaptă a ecranului"
Ascunde \'Oprește anunțurile\'
Butonul de oprire a reclamelor este ascuns
Butonul de oprire a reclamelor este afișat
+
+ Ascunde comentariile
+ Butonul de comentarii este ascuns
+ Butonul de comentarii este afișat
Ascunde raportul
@@ -1241,8 +1245,9 @@ Acest lucru va schimba aspectul și caracteristicile aplicației, dar pot apăre
Dacă este dezactivat ulterior, se recomandă să ștergeți datele aplicației pentru a preveni erorile UI."
Țintă versiune falsificată a aplicației
- 19.35.36 - Restaurează pictogramele vechi ale playerului Shorts
- 19.01.34 - Restaurați pictogramele de navigare vechi
+ 20.13.41 - Restabiliți bara de acțiune video necolapsată
+ 19.35.36 - Restaurează pictogramele vechi ale playerului Shorts
+ 19.01.34 - Restaurați pictogramele de navigare vechi
Schimbă pagina de start
@@ -1558,12 +1563,15 @@ Activarea acestei opțiuni poate debloca calități video mai mari"
• Client experimental și se poate opri din funcționare oricând
• Fara codec video AV1
• Copiii nu pot urmări videoclipuri atunci când sunt deconectați sau în modul incognito
+
+ • Forțarea sunetului original nu este disponibilă
Afișează în Statistici pentru pasionați
Tipul clientului este afișat în Statistici pentru pasionați
Clientul este ascuns în Statistici pentru pasionați
Limba fluxului audio
Pentru a selecta o limbă audio specifică, dezactivați „Forțează limba audio originală”
+ Selecția limbii fluxului nu este disponibilă cu Android Studio
@@ -1598,23 +1606,23 @@ Activarea acestei opțiuni poate debloca calități video mai mari"
Bară de navigare
Ascundeți sau modificați butoanele barei de navigare
- Ascundeți butonul Acasă
+ Ascunde Acasă
Butonul Acasă este ascuns
Butonul Acasă este afișat
- Ascundeți butonul Mostre
+ Ascunde Mostre
Butonul Mostre este ascuns
Butonul Mostre este afișat
- Ascundeți butonul Explorare
+ Ascundeți Explorare
Butonul Explorare este ascuns
Butonul Explorare este afișat
- Ascundeți butonul Bibliotecă
+ Ascundeți Bibliotecă
Butonul Bibliotecă este ascuns
Butonul Bibliotecă este afișat
- Ascundeți butonul Upgrade
+ Ascundeți Upgrade
Butonul Upgrade este ascuns
Butonul Upgrade este afișat
Ascundeți bara de navigare
diff --git a/patches/src/main/resources/addresources/values-ru-rRU/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/strings.xml
index 2f7d3ef57..b1feb2931 100644
--- a/patches/src/main/resources/addresources/values-ru-rRU/strings.xml
+++ b/patches/src/main/resources/addresources/values-ru-rRU/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Включить журналы отладки
Журналы отладки включены
Журналы отладки отключены
+ Журнал трассировки стека
+ В журналы отладки включена трассировка стека
+ В журналы отладки не включена трассировка стека
+ Показать всплывающее уведомление при ошибке Revanced
+ Всплывающее уведомление при возникновении ошибки Revanced показано
+ Всплывающее уведомление при возникновении ошибки Revanced скрыто
+ "Отключение всплывающих уведомлений об ошибках скроет все сообщения об ошибках ReVanced.
+
+Вы не будете уведомлены о каких-либо непредвиденных событиях."
Экспортировать журналы отладки
Копирует журналы отладки ReVanced в буфер обмена
Журналы отладки отключены
@@ -151,15 +160,6 @@ Second \"item\" text"
Это может помочь определить компоненты при создании пользовательских фильтров.
Однако включение этой функции также приведет к регистрации некоторых пользовательских данных, таких как ваш IP-адрес."
- Журнал трассировки стека
- В журналы отладки включена трассировка стека
- В журналы отладки не включена трассировка стека
- Показать всплывающее уведомление при ошибке Revanced
- Всплывающее уведомление при возникновении ошибки Revanced показано
- Всплывающее уведомление при возникновении ошибки Revanced скрыто
- "Отключение всплывающих уведомлений об ошибках скроет все сообщения об ошибках ReVanced.
-
-Вы не будете уведомлены о каких-либо непредвиденных событиях."
Скрыть карточки альбомов
@@ -586,6 +586,10 @@ Second \"item\" text"
Скрыть кнопку \"Не показывать рекламу\"
Кнопка \"Не показывать рекламу\" скрыта
Кнопка \"Не показывать рекламу\" показана
+
+ Скрыть комментарии
+ Кнопка \"Комментарии\" в Shorts скрыта
+ Кнопка \"Комментарии\" в Shorts показана
Скрыть кнопку \"Пожаловаться\"
@@ -1244,8 +1248,9 @@ Second \"item\" text"
Если позже данный параметр будет отключен, рекомендуется очистить данные приложения для предотвращения проблем с пользовательским интерфейсом."
Подменить версию приложения на
- 19.35.36 - Восстановление старых иконок плеера Shorts
- 19.01.34 - Восстановление старых иконок панели навигации
+ 20.13.41 - Восстановить не свернутую панель действий видео
+ 19.35.36 - Восстановление старых иконок плеера Shorts
+ 19.01.34 - Восстановление старых иконок панели навигации
Изменить начальную страницу
@@ -1566,12 +1571,15 @@ Second \"item\" text"
• Экспериментальный клиент и может перестать работать в любое время
• Отсутствует видеокодек AV1
• Видео для детей могут не воспроизводиться при выходе из системы или в режиме инкогнито
+
+ • Принудительная оригинальная звуковая дорожка недоступна
Показать в \"Статистике для сисадминов\"
Тип клиента в \"Статистике для сисадминов\" показан
Тип клиента в \"Статистике для сисадминов\" скрыт
Язык аудиопотока
Чтобы выбрать определенный язык аудио, отключите \"Принудительный язык оригинального аудио\"
+ Выбор языка трансляции недоступен в Android Studio
@@ -1610,19 +1618,19 @@ Second \"item\" text"
Кнопка \"Главная\" в панели навигации скрыта
Кнопка \"Главная\" в панели навигации показана
- Скрыть кнопку \"Семплы\"
+ Скрыть Сэмплы
Кнопка \"Семплы\" скрыта
Кнопка \"Семплы\" показана
- Скрыть кнопку \"Обзор\"
+ Скрыть Обзор
Кнопка \"Обзор\" скрыта
Кнопка \"Обзор\" показана
- Скрыть кнопку \"Библиотека\"
+ Скрыть Библиотеку
Кнопка \"Библиотека\" скрыта
Кнопка \"Библиотека\" показана
- Скрыть кнопку \"Премиум\"
+ Скрыть Премиум
Кнопка \"Премиум\" скрыта
Кнопка \"Премиум\" показана
Скрыть панель навигации
diff --git a/patches/src/main/resources/addresources/values-si-rLK/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-si-rLK/strings.xml
+++ b/patches/src/main/resources/addresources/values-si-rLK/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-sk-rSK/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/strings.xml
index c8a22c223..4fe16b2ee 100644
--- a/patches/src/main/resources/addresources/values-sk-rSK/strings.xml
+++ b/patches/src/main/resources/addresources/values-sk-rSK/strings.xml
@@ -105,6 +105,15 @@ Prehrávanie nemusí fungovať"
Debug logovanie
Denníky ladenia sú povolené
Denníky ladenia sú zakázané
+ Zaznamenať stopy zásobníka
+ Denníky ladenia obsahujú sledovanie zásobníka
+ Denníky ladenia neobsahujú sledovanie zásobníka
+ Zobraziť toast pri chybe ReVanced
+ Ak sa vyskytne chyba, zobrazí sa upozornenie toast
+ Ak sa vyskytne chyba, nezobrazí sa upozornenie toast
+ "Vypnutie upozornení o chybách skryje všetky upozornenia o chybách v ReVanced.
+
+Nebudete informovaní o žiadnych nepredvídaných udalostiach."
Exportovať protokoly ladenia
Skopíruje protokoly ladenia ReVanced do schránky
Protokolovanie ladenia je vypnuté
@@ -149,15 +158,6 @@ Prehrávanie nemusí fungovať"
Toto môže pomôcť identifikovať komponenty pri vytváraní vlastných filtrov.
Povolením tejto funkcie sa však budú zaznamenávať aj niektoré používateľské údaje, ako napríklad vaša IP adresa."
- Zaznamenať stopy zásobníka
- Denníky ladenia obsahujú sledovanie zásobníka
- Denníky ladenia neobsahujú sledovanie zásobníka
- Zobraziť toast pri chybe ReVanced
- Ak sa vyskytne chyba, zobrazí sa upozornenie toast
- Ak sa vyskytne chyba, nezobrazí sa upozornenie toast
- "Vypnutie upozornení o chybách skryje všetky upozornenia o chybách v ReVanced.
-
-Nebudete informovaní o žiadnych nepredvídaných udalostiach."
Skryť karty albumov
@@ -584,6 +584,10 @@ Upravte hlasitosť posúvaním vertikálne na pravej strane obrazovky"
Skryť Zastaviť reklamy
Tlačidlo Zastaviť reklamy je skryté
Tlačidlo Zastaviť reklamy je zobrazené
+
+ Skryť komentáre
+ Tlačidlo komentárov je skryté
+ Tlačidlo komentárov je zobrazené
Skryť prehľad
@@ -1237,8 +1241,9 @@ Zmení sa vzhľad a funkcie aplikácie, ale môžu sa vyskytnúť neznáme vedľ
Ak sa neskôr vypne, odporúča sa vymazať údaje aplikácie, aby sa zabránilo chybám používateľského rozhrania."
Falošná cieľová verzia aplikácie
- 19.35.36 - Obnoviť staré ikony Shorts prehrávača
- 19.01.34 – Obnoviť staré ikony navigácie
+ 20.13.41 - Obnoviť nerozbalený panel akcií videa
+ 19.35.36 - Obnoviť staré ikony Shorts prehrávača
+ 19.01.34 – Obnoviť staré ikony navigácie
Zmeniť úvodnú stránku
@@ -1552,12 +1557,15 @@ Povolením tejto možnosti môžete odomknúť vyššie kvality videa"
• Experimentálny klient a môže kedykoľvek prestať fungovať
• Žiadny video kodek AV1
• Videá pre deti sa nemusia prehrávať, keď ste odhlásení alebo v režime inkognito
+
+ • Vynútenie pôvodného zvuku nie je k dispozícii
Zobraziť v štatistike pre expertov
Typ klienta sa zobrazuje v štatistikách pre expertov
Klient je skrytý v štatistikách pre expertov
Jazyk zvukového streamu
Ak chcete vybrať konkrétny jazyk zvuku, vypnite „Vynútiť pôvodný jazyk zvuku“
+ Výber jazyka streamu nie je k dispozícii v aplikácii Android Studio
@@ -1592,23 +1600,23 @@ Povolením tejto možnosti môžete odomknúť vyššie kvality videa"
Navigačný panel
Skryť alebo zmeniť tlačidlá navigačného panela
- Skryť tlačidlo Domov
+ Skryť Domov
Tlačidlo Domov je skryté
Tlačidlo Domov je zobrazené
- Skryť tlačidlo Ukážky
+ Skryť Ukážky
Tlačidlo Ukážky je skryté
Tlačidlo Ukážky je zobrazené
- Skryť tlačidlo Preskúmať
+ Skryť Preskúmať
Tlačidlo Preskúmať je skryté
Tlačidlo Preskúmať je zobrazené
- Skryť tlačidlo Knižnica
+ Skryť Knižnicu
Tlačidlo Knižnica je skryté
Tlačidlo Knižnica je zobrazené
- Skryť tlačidlo Inovovať
+ Skryť Vylepšiť
Tlačidlo Inovovať je skryté
Tlačidlo Inovovať je zobrazené
Skryť navigačný panel
diff --git a/patches/src/main/resources/addresources/values-sl-rSI/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/strings.xml
index 7c423d2e7..94c12a856 100644
--- a/patches/src/main/resources/addresources/values-sl-rSI/strings.xml
+++ b/patches/src/main/resources/addresources/values-sl-rSI/strings.xml
@@ -107,6 +107,15 @@ Predvajanje morda ne bo delovalo"
Dnevnik napak
Dnevniki napak so omogočeni
Dnevniki napak so onemogočeni
+ Dnevnik sledov steka
+ Dnevniki napak vključujejo sled sledov
+ Dnevniki napak ne vključujejo sledi sledov
+ Prikaži obvestilo pri napaki ReVanced
+ Če pride do napake, se prikaže toast
+ Če pride do napake, se toast ne prikaže
+ "Onemogočanje obvestil o napakah skrije vsa obvestila o napakah v ReVancedu.
+
+Ne boste obveščeni o nobenih nepričakovanih dogodkih."
Izvozi dnevnike napak
Kopira dnevnike napak ReVanced v odložišče
Beleženje napak je onemogočeno
@@ -151,15 +160,6 @@ Predvajanje morda ne bo delovalo"
To lahko pomaga prepoznati komponente pri ustvarjanju filtrov po meri.
Vendar pa bo omogočitev tega beležila tudi nekatere uporabniške podatke, kot je vaš naslov IP."
- Dnevnik sledov steka
- Dnevniki napak vključujejo sled sledov
- Dnevniki napak ne vključujejo sledi sledov
- Prikaži obvestilo pri napaki ReVanced
- Če pride do napake, se prikaže toast
- Če pride do napake, se toast ne prikaže
- "Onemogočanje obvestil o napakah skrije vsa obvestila o napakah v ReVancedu.
-
-Ne boste obveščeni o nobenih nepričakovanih dogodkih."
Skrij kartice albuma
@@ -586,6 +586,10 @@ Prilagodite glasnost s potegom navpično na desni strani zaslona"
Skrij Ustavi oglase
Gumb za zaustavitev oglasov je skrit
Gumb za zaustavitev oglasov je prikazan
+
+ Skrij komentarje
+ Gumb za komentarje je skrit
+ Gumb za komentarje je prikazan
Skrij Prijavi
@@ -1241,8 +1245,9 @@ To bo spremenilo videz in funkcije aplikacije, vendar se lahko pojavijo neznani
Če ga kasneje izklopite, je priporočljivo počistiti podatke aplikacije, da preprečite napake uporabniškega vmesnika."
Cilj spoofa različice aplikacije
- 19.35.36 - Obnovi stare ikone predvajalnika Shorts
- 19.01.34 – Obnovi stare ikone za krmarjenje
+ 20.13.41 - Obnovi nezloženo video akcijsko vrstico
+ 19.35.36 - Obnovi stare ikone predvajalnika Shorts
+ 19.01.34 – Obnovi stare ikone za krmarjenje
Spremeni začetno stran
@@ -1559,12 +1564,15 @@ Omogočanje tega lahko odklene višje kakovosti videa"
• Eksperimentalni odjemalec in lahko kadar koli preneha delovati
• Brez kodeka videa AV1
• Posnetki za otroke se mogoče ne bodo predvajali, ko ste odjavljeni ali v načinu brez beleženja zgodovine
+
+ • Vsili izvirni zvok ni na voljo
Pokaži v statistiki za piflarje
Vrsta odjemalca je prikazana v statistiki za piflarje
Odjemalec je skrit v statistiki za piflarje
Jezik zvočnega toka
Za izbiro določenega zvočnega jezika izklopite \'Vsili izvirni zvočni jezik\'
+ Izbira jezika pretakanja ni na voljo z Android Studio
@@ -1599,23 +1607,23 @@ Omogočanje tega lahko odklene višje kakovosti videa"
Navigacijska vrstica
Skrij ali spremeni gumbe navigacijske vrstice
- Skrij gumb Domov
+ Skrij Domov
Gumb Domov je skrit
Gumb Domov je prikazan
- Skrij gumb Vzorci
+ Skrij Vzorci
Gumb Vzorci je skrit
Gumb Vzorci je prikazan
- Skrij gumb Raziskovanje
+ Skrij Raziskovanje
Gumb Raziskovanje je skrit
Gumb Raziskovanje je prikazan
- Skrij gumb Knjižnica
+ Skrij Knjižnico
Gumb Knjižnica je skrit
Gumb Knjižnica je prikazan
- Skrij gumb Nadgradi
+ Skrij Nadgradnjo
Gumb Nadgradi je skrit
Gumb Nadgradi je prikazan
Skrij navigacijsko vrstico
diff --git a/patches/src/main/resources/addresources/values-sq-rAL/strings.xml b/patches/src/main/resources/addresources/values-sq-rAL/strings.xml
index 20ff26be3..316c17bbc 100644
--- a/patches/src/main/resources/addresources/values-sq-rAL/strings.xml
+++ b/patches/src/main/resources/addresources/values-sq-rAL/strings.xml
@@ -107,6 +107,15 @@ Riprodhimi mund të mos funksionojë"
Regjistrimi i depurimit
Regjistrimet e depurimit janë të aktivizuara
Regjistrimet e depurimit janë të çaktivizuara
+ Regjistro gjurmët e grumbullimit
+ Regjistrimet e depurimit përfshijnë gjurmë grumbullimi
+ Regjistrimet e depurimit nuk përfshijnë gjurmë grumbullimi
+ Shfaqni toast për gabimet e ReVanced
+ Shfaqet një toast nëse ndodh një gabim
+ Nuk shfaqet toast nëse ndodh një gabim
+ "Fshirja e toast-eve të gabimit fsheh të gjitha njoftimet e gabimeve të ReVanced.
+
+Ju nuk do të njoftoheni për ndonjë ngjarje të papritur."
Eksporto regjistrat e korrigjimit
Kopjon regjistrat e korrigjimit të ReVanced në kujtesën e shkurtër
Regjistrimi i korrigjimeve është çaktivizuar
@@ -151,15 +160,6 @@ Riprodhimi mund të mos funksionojë"
Kjo mund të ndihmojë në identifikimin e komponentëve kur krijoni filtra të personalizuar.
Gjithsesi, aktivizimi i këtij opsioni do të regjistrojë edhe disa të dhëna të përdoruesit, siç është adresa juaj IP."
- Regjistro gjurmët e grumbullimit
- Regjistrimet e depurimit përfshijnë gjurmë grumbullimi
- Regjistrimet e depurimit nuk përfshijnë gjurmë grumbullimi
- Shfaqni toast për gabimet e ReVanced
- Shfaqet një toast nëse ndodh një gabim
- Nuk shfaqet toast nëse ndodh një gabim
- "Fshirja e toast-eve të gabimit fsheh të gjitha njoftimet e gabimeve të ReVanced.
-
-Ju nuk do të njoftoheni për ndonjë ngjarje të papritur."
Fshih kartat e albumit
@@ -586,6 +586,10 @@ Përshtate shkëlqimin duke rrëshqitur vertikalisht në anën e majtë të ekra
Fshih \"Ndalo reklamat\"
Butoni Ndalo reklamat është i fshehur
Butoni Ndalo reklamat është i shfaqur
+
+ Fshih Komentet
+ Butoni i komenteve është i fshehur
+ Butoni i komenteve është shfaqur
Fsheh \"Raporton\"
@@ -1240,8 +1244,9 @@ Kjo do të ndryshojë pamjen dhe veçoritë e aplikacionit, por mund të ndodhin
Nëse më vonë është çaktivizuar, është e rekomanduar të fshiheni të dhënat e aplikacionit për të parandaluar gabimet e UI."
Shënjestër e versionit të aplikacionit të mashtuar
- 19.35.36 - Rikthe ikonat e vjetra të lojtarit Shorts
- 19.01.34 - Ristauro ikonave te vjetra te navigimit
+ 20.13.41 - Rivendos shiritin e veprimeve të videos jo të palosur
+ 19.35.36 - Rikthe ikonat e vjetra të lojtarit Shorts
+ 19.01.34 - Ristauro ikonave te vjetra te navigimit
Ndrysho faqen e fillimit
@@ -1557,12 +1562,15 @@ Aktivizimi i kësaj mund të zhbllokojë cilësi më të larta video"
• Klient eksperimental dhe mund të ndalojë së funksionuari në çdo kohë
• Nuk ka codec video AV1
• Video për fëmijë mund të mos luajnë kur jeni jashtë llogarisë ose në modalitetin incognito
+
+ • Detyro audio origjinale nuk është i disponueshëm
Shfaqni në Statistikat për nerdës
Lloji i klientit shfaqet në Statistikat për nerds
Klienti është fshehur në statistikat për nerds
Gjuha e transmetimit audio
Për të zgjedhur një gjuhë specifike audio, çaktivizoni \"Detyro gjuhën origjinale audio\"
+ Zgjedhja e gjuhës së transmetimit nuk është e disponueshme me Android Studio
@@ -1597,23 +1605,23 @@ Aktivizimi i kësaj mund të zhbllokojë cilësi më të larta video"
Shiriti i navigimit
Fshih ose ndrysho butonat e shiritit të navigimit
- Fshih butonin Kryesore
+ Fshih Kreun
Butoni Kryesore është fshehur
Butoni Kryesore është shfaqur
- Fshih butonin Mostra
+ Fshih Mostrat
Butoni Mostra është fshehur
Butoni Mostra është shfaqur
- Fshih butonin Eksploro
+ Fshih Eksplorimin
Butoni Eksploro është fshehur
Butoni Eksploro është shfaqur
- Fshih butonin Biblioteka
+ Fshih Bibliotekën
Butoni Biblioteka është fshehur
Butoni Biblioteka është shfaqur
- Fshih butonin Azhorno
+ Fshih përmirësimin
Butoni Azhorno është fshehur
Butoni Azhorno është shfaqur
Fshih shiritin e navigimit
diff --git a/patches/src/main/resources/addresources/values-sr-rCS/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
index fb81b0167..1dc952d8f 100644
--- a/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
+++ b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml
@@ -107,6 +107,15 @@ Reprodukcija možda neće raditi"
Evidentiranje otklanjanja grešaka
Evidencije otklanjanja grešaka su omogućene
Evidencije otklanjanja grešaka su onemogućene
+ Evidentiranje praćenja steka
+ Evidencije otklanjanja grešaka sadrže praćenje steka
+ Evidencije otklanjanja grešaka ne sadrže praćenje steka
+ Prikaži iskačuće obaveštenje pri grešci s ReVancedom
+ Iskačuće obaveštenje je prikazano, ako dođe do greške
+ Iskačuće obaveštenje nije prikazano, ako dođe do greške
+ "Isključivanje iskačućih obaveštenja o greškama sakriva sve obaveštenja o greškama u ReVancedu.
+
+Nećete biti obavešteni ni o kakvim neočekivanim događajima."
Izvezi evidencije otklanjanja grešaka
Kopira evidencije otklanjanja grešaka ReVanceda u privremenu memoriju
Evidentiranje otklanjanja grešaka je onemogućeno
@@ -151,15 +160,6 @@ Reprodukcija možda neće raditi"
Ovo može pomoći u identifikaciji komponenti prilikom pravljenja prilagođenih filtera.
Međutim, omogućavanje ovoga će takođe evidentirati neke korisničke podatke, kao što je vaša IP adresa."
- Evidentiranje praćenja steka
- Evidencije otklanjanja grešaka sadrže praćenje steka
- Evidencije otklanjanja grešaka ne sadrže praćenje steka
- Prikaži iskačuće obaveštenje pri grešci s ReVancedom
- Iskačuće obaveštenje je prikazano, ako dođe do greške
- Iskačuće obaveštenje nije prikazano, ako dođe do greške
- "Isključivanje iskačućih obaveštenja o greškama sakriva sve obaveštenja o greškama u ReVancedu.
-
-Nećete biti obavešteni ni o kakvim neočekivanim događajima."
Sakrij kartice albuma
@@ -586,6 +586,10 @@ Podesite jačinu zvuka prevlačenjem vertikalno na desnoj strani ekrana"Sakrij dugme „Zaustavi oglase”
Dugme „Zaustavi oglase” je skriveno
Dugme „Zaustavi oglase” je prikazano
+
+ Sakrij komentare
+ Dugme za komentare je skriveno
+ Dugme za komentare je prikazano
Sakrij dugme „Prijavi”
@@ -1241,8 +1245,9 @@ Ovo će promeniti izgled i funkcije aplikacije, ali se mogu pojaviti i nepoznati
Ako se kasnije isključi, preporučuje se da izbrišete podatke aplikacije da biste sprečili greške u korisničkom interfejsu."
Ciljna verzija aplikacije za lažiranje
- 19.35.36 - Vraća stare ikonice Shorts plejera
- 19.01.34 - Vraća stare ikonice navigacije
+ 20.13.41 - Vrati nesakupljenu akcionu traku za video
+ 19.35.36 - Vraća stare ikonice Shorts plejera
+ 19.01.34 - Vraća stare ikonice navigacije
Promena polazne stranice
@@ -1558,12 +1563,15 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"
• Eksperimentalni klijent i može prestati da radi bilo kada
• Nema video kodeka AV1
• Videi za decu se možda neće puštati kada ste odjavljeni ili u režimu bez arhiviranja
+
+ • Forsiranje originalnog zvuka nije dostupno
Prikaži u „Statistici za znalce”
Tip klijenta je prikazan u „Statistici za znalce”
Tip klijenta je skriven u „Statistici za znalce”
Jezik audio strima
Da biste izabrali određeni jezik zvuka, isključite opciju „Prisili originalni jezik zvuka”
+ Izbor jezika strima nije dostupan sa Android Studijem
@@ -1598,23 +1606,23 @@ Ako ovo omogućite, mogu biti otključani viši kvaliteti videa"
Traka za navigaciju
Sakrijte ili promenite dugmad trake za navigaciju
- Sakrij dugme „Početna”
+ Sakrij Početnu
Dugme „Početna” je skriveno
Dugme „Početna” je prikazano
- Sakrij dugme „Uzorci”
+ Sakrij Uzorke
Dugme „Uzorci” je skriveno
Dugme „Uzorci” je prikazano
- Sakrij dugme „Istražite”
+ Sakrij Istražite
Dugme „Istražite” je skriveno
Dugme „Istražite” je prikazano
- Sakrij dugme „Biblioteka”
+ Sakrij Biblioteku
Dugme „Biblioteka” je skriveno
Dugme „Biblioteka” je prikazano
- Sakrij dugme „Nadogradite”
+ Sakrij Nadogradi
Dugme „Nadogradite” je skriveno
Dugme „Nadogradite” je prikazano
Sakrij traku za navigaciju
diff --git a/patches/src/main/resources/addresources/values-sr-rSP/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
index fd0da35ea..2a04d4762 100644
--- a/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
+++ b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Евидентирање отклањања грешака
Евиденције отклањања грешака су омогућене
Евиденције отклањања грешака су онемогућене
+ Евидентирање праћења стека
+ Евиденције отклањања грешака садрже праћење стека
+ Евиденције отклањања грешака не садрже праћење стека
+ Прикажи искачуће обавештење при грешци с ReVanced-ом
+ Искачуће обавештење је приказано, ако дође до грешке
+ Искачуће обавештење није приказано, ако дође до грешке
+ "Искључивање искачућих обавештења о грешкама сакрива сва обавештења о грешкама у ReVanced-у.
+
+Нећете бити обавештени ни о каквим неочекиваним догађајима."
Извези евиденције отклањања грешака
Копира евиденције отклањања грешака ReVanced-а у привремену меморију
Евидентирање отклањања грешака је онемогућено
@@ -151,15 +160,6 @@ Second \"item\" text"
Ово може помоћи у идентификацији компоненти приликом прављења прилагођених филтера.
Међутим, омогућавање овога ће такође евидентирати неке корисничке податке, као што је ваша IP адреса."
- Евидентирање праћења стека
- Евиденције отклањања грешака садрже праћење стека
- Евиденције отклањања грешака не садрже праћење стека
- Прикажи искачуће обавештење при грешци с ReVanced-ом
- Искачуће обавештење је приказано, ако дође до грешке
- Искачуће обавештење није приказано, ако дође до грешке
- "Искључивање искачућих обавештења о грешкама сакрива сва обавештења о грешкама у ReVanced-у.
-
-Нећете бити обавештени ни о каквим неочекиваним догађајима."
Сакриј картице албума
@@ -586,6 +586,10 @@ Second \"item\" text"
Сакриј дугме „Заустави огласе”
Дугме „Заустави огласе” је скривено
Дугме „Заустави огласе” је приказано
+
+ Сакриј коментаре
+ Дугме „Коментари” је скривено
+ Дугме „Коментари” је приказано
Сакриј дугме „Пријави”
@@ -1241,8 +1245,9 @@ Second \"item\" text"
Ако се касније искључи, препоручује се да избришете податке апликације да бисте спречили грешке у корисничком интерфејсу."
Циљна верзија апликације за лажирање
- 19.35.36 - Враћа старе иконице Shorts плејера
- 19.01.34 - Враћа старе иконице навигације
+ 20.13.41 - Врати нескупљену траку радњи видеа
+ 19.35.36 - Враћа старе иконице Shorts плејера
+ 19.01.34 - Враћа старе иконице навигације
Промена полазне странице
@@ -1561,12 +1566,15 @@ Second \"item\" text"
• Експериментални клијент и може престати да ради било када
• Нема видео кодека AV1
• Видеи за децу се можда неће пуштати када сте одјављени или у режиму без архивирања
+
+ • Форсирање оригиналног звука није доступно
Прикажи у „Статистици за зналце”
Тип клијента је приказан у „Статистици за зналце”
Тип клијента је скривен у „Статистици за зналце”
Језик аудио стрима
Да бисте изабрали одређени језик звука, искључите опцију „Присили оригинални језик звука”
+ Избор језика стрима није доступан са Android Studio
@@ -1601,23 +1609,23 @@ Second \"item\" text"
Трака за навигацију
Сакријте или промените дугмад траке за навигацију
- Сакриј дугме „Почетна”
+ Сакриј Почетну
Дугме „Почетна” је скривено
Дугме „Почетна” је приказано
- Сакриј дугме „Узорци”
+ Сакриј Узорке
Дугме „Узорци” је скривено
Дугме „Узорци” је приказано
- Сакриј дугме „Истражи”
+ Сакриј Истражи
Дугме „Истражи” је скривено
Дугме „Истражи” је приказано
- Сакриј дугме „Библиотека”
+ Сакриј Библиотеку
Дугме „Библиотека” је скривено
Дугме „Библиотека” је приказано
- Сакриј дугме „Надогради”
+ Сакриј Надогради
Дугме „Надогради” је скривено
Дугме „Надогради” је приказано
Сакриј траку за навигацију
diff --git a/patches/src/main/resources/addresources/values-sv-rSE/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/strings.xml
index 62901c1e7..e91a7a77c 100644
--- a/patches/src/main/resources/addresources/values-sv-rSE/strings.xml
+++ b/patches/src/main/resources/addresources/values-sv-rSE/strings.xml
@@ -107,6 +107,15 @@ Uppspelning kanske inte fungerar"
Felsökningsloggning
Felsökningsloggar är aktiverade
Felsökningsloggar är inaktiverade
+ Logga stackspårning
+ Felsökningsloggar inkluderar stackspårning
+ Felsökningsloggar inkluderar inte stackspårning
+ Visa ett popup-meddelande om det uppstår fel med ReVanced
+ Popup-meddelande visas om det uppstår fel
+ Popup-meddelande visas inte om det uppstår fel
+ "Om du stänger av popup-felmeddelanden döljs alla ReVanced-felmeddelanden.
+
+Du kommer inte att bli meddelad om oväntade händelser."
Exportera felsökningsloggar
Kopierar ReVanced-felsökningsloggar till urklipp
Felsökningsloggar är inaktiverade
@@ -151,15 +160,6 @@ Uppspelning kanske inte fungerar"
Detta kan hjälpa till att identifiera komponenter när du skapar anpassade filter.
Men om du aktiverar detta kommer även vissa användardata, t.ex. din IP-adress, att loggas."
- Logga stackspårning
- Felsökningsloggar inkluderar stackspårning
- Felsökningsloggar inkluderar inte stackspårning
- Visa ett popup-meddelande om det uppstår fel med ReVanced
- Popup-meddelande visas om det uppstår fel
- Popup-meddelande visas inte om det uppstår fel
- "Om du stänger av popup-felmeddelanden döljs alla ReVanced-felmeddelanden.
-
-Du kommer inte att bli meddelad om oväntade händelser."
Dölj albumkort
@@ -586,6 +586,10 @@ Justera volymen genom att svepa vertikalt till höger på skärmen"
Dölj Stoppa annonser
Knappen Stoppa annonser är dold
Knappen Stoppa annonser visas
+
+ Dölj kommentarer
+ Knappen Kommentarer är dold
+ Knappen Kommentarer visas
Dölj Rapportera
@@ -1241,8 +1245,9 @@ Detta kommer att ändra utseendet och funktionerna i appen, men okända bieffekt
Om du senare inaktiverar det rekommenderar vi att rensa appens data för att förhindra fel i användargränssnittet."
Mål för Förfalska appversionen
- 19.35.36 – Återställ gamla ikoner i Shorts-spelaren
- 19.01.34 – Återställ gamla navigeringsikoner
+ 20.13.41 - Återställ icke-komprimerad handlingsfält för video
+ 19.35.36 – Återställ gamla ikoner i Shorts-spelaren
+ 19.01.34 – Återställ gamla navigeringsikoner
Byt startsida
@@ -1558,12 +1563,15 @@ Om du aktiverar detta kan högre videokvaliteter låsas upp"
• Experimentell klient och kan sluta fungera när som helst
• Ingen AV1-videokodek
• Videor för barn kanske inte spelas upp när du är utloggad eller i inkognitoläge
+
+ • Tvinga ursprungligt ljud är inte tillgängligt
Visa i Statistik för nördar
Klienttypen visas i Statistik för nördar
Klienten är dold i Statistik för nördar
Ljudströmmens språk
För att välja ett specifikt ljudspråk, inaktivera \"Tvinga originalspråk för ljud\"
+ Val av strömspråk är inte tillgängligt med Android Studio
@@ -1598,23 +1606,23 @@ Om du aktiverar detta kan högre videokvaliteter låsas upp"
Navigationsfältet
Dölj eller ändra knappar i navigeringsfältet
- Dölj Hem-knappen
+ Dölj Hem
Knappen Hem är dold
Knappen Hem visas
- Dölj Klipp-knappen
+ Dölj Kortfilmer
Klipp-knappen är dold
Klipp-knappen visas
- Dölj Utforska-knappen
+ Dölj Utforska
Utforska-knappen är dold
Utforska-knappen visas
- Dölj Bibliotek-knappen
+ Dölj Bibliotek
Bibliotek-knappen är dold
Bibliotek-knappen visas
- Dölj Uppgradera-knappen
+ Dölj Uppgradera
Uppgradera-knappen är dold
Uppgradera-knappen visas
Dölj navigeringsfältet
diff --git a/patches/src/main/resources/addresources/values-sw-rKE/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-sw-rKE/strings.xml
+++ b/patches/src/main/resources/addresources/values-sw-rKE/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-ta-rIN/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-ta-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-ta-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-te-rIN/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-te-rIN/strings.xml
+++ b/patches/src/main/resources/addresources/values-te-rIN/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-th-rTH/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/strings.xml
index c8783aed1..38d38fb1f 100644
--- a/patches/src/main/resources/addresources/values-th-rTH/strings.xml
+++ b/patches/src/main/resources/addresources/values-th-rTH/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
การบันทึกการแก้ไขข้อบกพร่อง
การบันทึกการแก้ไขข้อบกพร่องถูกเปิดใช้งาน
การบันทึกการแก้ไขข้อบกพร่องถูกปิดใช้งาน
+ บันทึกการติดตามสแต็ก
+ การบันทึกการแก้ไขข้อบกพร่องรวมถึงการติดตามสแต็ก
+ การบันทึกการแก้ไขข้อบกพร่องไม่รวมการติดตามสแต็ก
+ แสดง toast เมื่อเกิดข้อผิดพลาดของ ReVanced
+ Toast จะปรากฏขึ้นหากเกิดข้อผิดพลาด
+ Toast จะไม่ปรากฏขึ้นหากเกิดข้อผิดพลาด
+ "การปิดการแจ้งเตือนข้อผิดพลาดจะซ่อนการแจ้งเตือนข้อผิดพลาดของ ReVanced ทั้งหมด
+
+คุณจะไม่ได้รับแจ้งเกี่ยวกับเหตุการณ์ที่ไม่คาดคิดใดๆ"
ส่งออกบันทึกการแก้ไขข้อบกพร่อง
คัดลอกบันทึกการแก้ไขข้อบกพร่องของ ReVanced ไปยังคลิปบอร์ด
ปิดใช้งานการบันทึกการแก้ไขข้อบกพร่อง
@@ -151,15 +160,6 @@ Second \"item\" text"
ซึ่งจะช่วยระบุส่วนประกอบเมื่อสร้างตัวกรองที่กำหนดเองได้
อย่างไรก็ตาม การเปิดใช้งานนี้จะบันทึกข้อมูลผู้ใช้บางอย่าง เช่น ที่อยู่ IP ของคุณด้วย"
- บันทึกการติดตามสแต็ก
- การบันทึกการแก้ไขข้อบกพร่องรวมถึงการติดตามสแต็ก
- การบันทึกการแก้ไขข้อบกพร่องไม่รวมการติดตามสแต็ก
- แสดง toast เมื่อเกิดข้อผิดพลาดของ ReVanced
- Toast จะปรากฏขึ้นหากเกิดข้อผิดพลาด
- Toast จะไม่ปรากฏขึ้นหากเกิดข้อผิดพลาด
- "การปิดการแจ้งเตือนข้อผิดพลาดจะซ่อนการแจ้งเตือนข้อผิดพลาดของ ReVanced ทั้งหมด
-
-คุณจะไม่ได้รับแจ้งเกี่ยวกับเหตุการณ์ที่ไม่คาดคิดใดๆ"
ซ่อนการ์ดอัลบั้ม
@@ -584,6 +584,10 @@ Second \"item\" text"
ซ่อนหยุดโฆษณา
ปุ่มหยุดโฆษณาถูกซ่อน
ปุ่มหยุดโฆษณาถูกแสดง
+
+ ซ่อนความคิดเห็น
+ ปุ่มความคิดเห็นถูกซ่อน
+ ปุ่มความคิดเห็นแสดงอยู่
ซ่อนรายงาน
@@ -1241,8 +1245,9 @@ User id ของคุณเหมือนกับรหัสผ่าน
ถ้าปิดในภายหลัง ขอแนะนําให้ล้างข้อมูลแอปเพื่อป้องกันข้อผิดพลาดของ UI"
เป้าหมายการปลอมแปลงเวอร์ชันแอป
- 19.35.36 - คืนค่าไอคอนเครื่องเล่น Shorts เก่า
- 19.01.34 - กู้คืนไอคอนการนำทางแบบเก่า
+ 20.13.41 - กู้คืนแถบการทำงานของวิดีโอที่ไม่ได้ยุบ
+ 19.35.36 - คืนค่าไอคอนเครื่องเล่น Shorts เก่า
+ 19.01.34 - กู้คืนไอคอนการนำทางแบบเก่า
เปลี่ยนหน้าเริ่มต้น
@@ -1561,12 +1566,15 @@ User id ของคุณเหมือนกับรหัสผ่าน
• ไคลเอนต์ทดลองและอาจหยุดทำงานได้ตลอดเวลา
• ไม่มีตัวแปลงสัญญาณวิดีโอ AV1
• วิดีโอเด็กอาจไม่เล่นเมื่อลงชื่อออกหรืออยู่ในโหมดไม่ระบุตัวตน
+
+ • บังคับใช้เสียงต้นฉบับไม่พร้อมใช้งาน
แสดงในสถิติสำหรับพวกเนิร์ด
แสดงชนิดไคลเอ็นต์ในสถิติสำหรับพวกเนิร์ด
ซ่อนไคลเอ็นต์ในสถิติสำหรับพวกเนิร์ด
ภาษาของสตรีมเสียง
หากต้องการเลือกภาษาเสียงที่ต้องการ ให้ปิด \'บังคับใช้ภาษาเสียงต้นฉบับ\'
+ การเลือกภาษาของสตรีมไม่พร้อมใช้งานกับ Android Studio
@@ -1601,23 +1609,23 @@ User id ของคุณเหมือนกับรหัสผ่าน
แถบนำทาง
ซ่อนหรือเปลี่ยนปุ่มแถบนำทาง
- ซ่อนปุ่มหน้าแรก
+ ซ่อนหน้าแรก
ซ่อนปุ่มหน้าแรกแล้ว
แสดงปุ่มหน้าแรกแล้ว
- ซ่อนปุ่มตัวอย่าง
+ ซ่อนตัวอย่าง
ซ่อนปุ่มตัวอย่างแล้ว
แสดงปุ่มตัวอย่างแล้ว
- ซ่อนปุ่มสำรวจ
+ ซ่อนสำรวจ
ซ่อนปุ่มสำรวจแล้ว
แสดงปุ่มสำรวจแล้ว
- ซ่อนปุ่มคลัง
+ ซ่อนคลัง
ซ่อนปุ่มคลังแล้ว
แสดงปุ่มคลังแล้ว
- ซ่อนปุ่มอัปเกรด
+ ซ่อนอัปเกรด
ซ่อนปุ่มอัปเกรดแล้ว
แสดงปุ่มอัปเกรดแล้ว
ซ่อนแถบนำทาง
diff --git a/patches/src/main/resources/addresources/values-tr-rTR/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/strings.xml
index e38a3a8d8..893799845 100644
--- a/patches/src/main/resources/addresources/values-tr-rTR/strings.xml
+++ b/patches/src/main/resources/addresources/values-tr-rTR/strings.xml
@@ -107,6 +107,15 @@ Oynatma çalışmayabilir"
Hata ayıklama kayıtları
Hata ayıklama kayıtları etkin
Hata ayıklama kayıtları devre dışı
+ Stack traces\'in kaydını tut
+ Hata ayıklama kayıtları stack traces\'i içerir
+ Hata ayıklama kayıtları stack traces\'i içermez
+ ReVanced hatası durumunda uyarı göster
+ Hata oluşursa uyarı gösterilir
+ Hata oluşursa uyarı gösterilmez
+ "Hata bildirimlerini kapatmak, tüm ReVanced hata bildirimlerini gizler.
+
+Beklenmedik olaylar hakkında bilgilendirilmeyeceksiniz."
Hata ayıklama kayıtlarını dışa aktar
ReVanced hata ayıklama kayıtlarını panoya kopyalar
Hata ayıklama kayıtları devre dışı
@@ -151,15 +160,6 @@ Oynatma çalışmayabilir"
Bu, özel filtreler oluştururken bileşenlerin belirlenmesine yardımcı olabilir.
Ancak, bunun etkinleştirilmesi IP adresiniz gibi bazı kullanıcı verilerini de günlüğe kaydeder."
- Stack traces\'in kaydını tut
- Hata ayıklama kayıtları stack traces\'i içerir
- Hata ayıklama kayıtları stack traces\'i içermez
- ReVanced hatası durumunda uyarı göster
- Hata oluşursa uyarı gösterilir
- Hata oluşursa uyarı gösterilmez
- "Hata bildirimlerini kapatmak, tüm ReVanced hata bildirimlerini gizler.
-
-Beklenmedik olaylar hakkında bilgilendirilmeyeceksiniz."
Albüm kartlarını gizle
@@ -586,6 +586,10 @@ Ekranın sağ tarafında dikey olarak kaydırarak sesi ayarlayın"
Reklamları durdur\'u Gizle
Reklamları durdur düğmesi gizli
Reklamları durdur düğmesi görünür
+
+ Yorumları gizle
+ Yorumlar düğmesi gizli
+ Yorumlar düğmesi görünür
Bildir\'i gizle
@@ -1244,8 +1248,9 @@ Bu, uygulamanın görünümünü ve özelliklerini değiştirecektir, ancak bili
Daha sonra kapatılırsa, arayüz hatalarını önlemek için uygulama verilerinin temizlenmesi önerilir."
Uygulama sürümü taklidi hedefi
- 19.35.36 - Eski Shorts oynatıcı simgelerini geri getir
- 19.01.34 - Eski gezinti simgelerini geri getir
+ 20.13.41 - Daraltılmamış video eylem çubuğunu geri getir
+ 19.35.36 - Eski Shorts oynatıcı simgelerini geri getir
+ 19.01.34 - Eski gezinti simgelerini geri getir
Başlangıç sayfasını değiştir
@@ -1566,12 +1571,15 @@ Bunu etkinleştirmek daha yüksek video kalitelerini açabilir"
• Deneysel istemci ve her an çalışmayı durdurabilir
• AV1 video kodeği yok
• Çocuk videoları oturum açılmadığında veya gizli modda oynatılamayabilir
+
+ • Orijinal sesi zorlama mevcut değil
Meraklısı için istatistiklerde göster
İstemci tipi meraklısı için istatistiklerde gösterilir
İstemci, meraklısı için istatistiklerde gizli
Ses akışı dili
Belirli bir ses dilini seçmek için \'Orijinal ses dilini zorla\' seçeneğini kapatın
+ Yayın dili seçimi Android Studio ile kullanılamıyor
@@ -1606,23 +1614,23 @@ Bunu etkinleştirmek daha yüksek video kalitelerini açabilir"
Gezinme çubuğu
Gezinme çubuğundaki düğmeleri gizle veya değiştir
- Ana Sayfa düğmesini gizle
+ Ana Sayfa\'yı gizle
Ana Sayfa düğmesi gizli
Ana Sayfa düğmesi görünür
- Sana Özel düğmesini gizle
+ Örnekleri Gizle
Sana Özel düğmesi gizli
Sana Özel düğmesi görünür
- Keşfet düğmesini gizle
+ Keşfet\'i Gizle
Keşfet düğmesi gizli
Keşfet düğmesi görünür
- Kitaplık düğmesini gizle
+ Kitaplık\'ı Gizle
Kitaplık düğmesi gizli
Kitaplık düğmesi görünür
- Yükselt düğmesini gizle
+ Yükselt\'i Gizle
Yükselt düğmesi gizli
Yükselt düğmesi görünür
Gezinme çubuğunu gizle
diff --git a/patches/src/main/resources/addresources/values-uk-rUA/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
index 3267b6222..ab6f7a0de 100644
--- a/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
+++ b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
Журнал налагодження
Журнали налагодження ввімкнено
Журнали налагодження вимкнено
+ Реєструвати трасування стека
+ Журнали налагодження містять трасування стека
+ Журнали налагодження не містять трасування стека
+ Показувати тост при помилці ReVanced
+ Тост показується, якщо сталася помилка
+ Тост не показується, якщо сталася помилка
+ "Вимкнення сповіщень про помилки приховує всі сповіщення про помилки ReVanced.
+
+Ви не будете отримувати сповіщення про будь-які непередбачувані події."
Експортувати журнали налагодження
Копіює журнали налагодження ReVanced до буфера обміну
Журнали налагодження вимкнено
@@ -151,15 +160,6 @@ Second \"item\" text"
Це може допомогти визначити компоненти під час створення користувацьких фільтрів.
Однак, увімкнувши це, також буде записано деякі дані користувача, наприклад Вашу IP-адресу."
- Реєструвати трасування стека
- Журнали налагодження містять трасування стека
- Журнали налагодження не містять трасування стека
- Показувати тост при помилці ReVanced
- Тост показується, якщо сталася помилка
- Тост не показується, якщо сталася помилка
- "Вимкнення сповіщень про помилки приховує всі сповіщення про помилки ReVanced.
-
-Ви не будете отримувати сповіщення про будь-які непередбачувані події."
Приховати картки альбому
@@ -586,6 +586,10 @@ Second \"item\" text"
Приховати \"Зупинити показ оголошень\"
Кнопку \"Зупинити показ оголошень\" приховано
Кнопка \"Зупинити показ оголошень\" показується
+
+ Приховати \"Коментарі\"
+ Кнопку \"Коментарі\" приховано
+ Кнопка \"Коментарі\" показується
Приховати \"Поскаржитися\"
@@ -1241,8 +1245,9 @@ Second \"item\" text"
Якщо пізніше вимкнути, рекомендується очистити дані застосунку, щоб запобігти помилкам інтерфейсу."
Підробити версію програми на
- 19.35.36 - Відновлення старих іконок плеєра Shorts
- 19.01.34 - Відновлення старих іконок панелі навігації
+ 20.13.41 - Відновити незгорнуту панель дій відео
+ 19.35.36 - Відновлення старих іконок плеєра Shorts
+ 19.01.34 - Відновлення старих іконок панелі навігації
Змінити початкову сторінку
@@ -1558,12 +1563,15 @@ Second \"item\" text"
• Експериментальний клієнт, який може припинити працювати будь-якої миті
• Відсутній відеокодек AV1
• Відео для дітей можуть не відтворюватися, якщо вийти з облікового запису або перейти в анонімний режим
+
+ • \"Примусово оригінальна мова звукової доріжки\" недоступна
Показувати у \"Статистиці для сисадмінів\"
Тип клієнта відображається у вікні \"Статистика для сисадмінів\"
Тип клієнта приховано у вікні \"Статистика для сисадмінів\"
Мова звукової доріжки
Щоб вибрати певну звукову доріжку, вимкніть \"Примусово оригінальна мова звукової доріжки\"
+ Вибір мови потоку недоступний з Android Studio
@@ -1586,8 +1594,8 @@ Second \"item\" text"
Приховати кнопку трансляції
- Кнопку \"Трансляція\" у відеоплеєрі приховано
- Кнопка \"Трансляція\" у відеоплеєрі показується
+ Кнопку трансляції приховано
+ Кнопка трансляції показується
diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml
index bad4f0c2d..f697cddf7 100644
--- a/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml
+++ b/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml
@@ -89,6 +89,7 @@ Second \"item\" text"
+
@@ -243,6 +244,7 @@ Second \"item\" text"
+
diff --git a/patches/src/main/resources/addresources/values-vi-rVN/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
index 4bf3abc1a..f92ef7d9e 100644
--- a/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
+++ b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml
@@ -37,7 +37,7 @@ Second \"item\" text"
Đặt lại
Đặt lại màu
Màu không hợp lệ
- Cần khởi động lại
+ Yêu cầu khởi động lại
Khởi động lại ứng dụng để thay đổi này có hiệu lực.
Khởi động lại
Nhập
@@ -68,29 +68,29 @@ Second \"item\" text"
and changes made here must also be made there. -->
- Cài đặt GmsCore
- Các cài đặt cho GmsCore
+ GmsCore
+ Chuyển hướng tới GmsCore và thiết lập
MicroG GmsCore chưa được cài đặt. Hãy cài đặt ngay.
- Cần thực hiện
+ Yêu cầu thực hiện
"MicroG GmsCore không có quyền chạy nền.
Hãy làm theo hướng dẫn \"Don't kill my app\" dành cho điện thoại của bạn và áp dụng các bước đó để cài đặt MicroG.
Cấp quyền chạy nền là bắt buộc để ứng dụng hoạt động."
Mở trang web
- "Cần phải tắt tối ưu hóa pin cho MicroG GmsCore để tránh sự cố.
+ "Để microG GmsCore hoạt động ổn định, bạn cần phải tắt tối ưu hoá pin cho ứng dụng này.
-Tắt tối ưu hóa pin cho MicroG sẽ không ảnh hưởng đáng kể đến hiệu suất sử dụng pin.
+Đừng lo, tắt tối ưu hóa pin cho microG sẽ không làm hao pin hơn.
-Nhấn nút tiếp tục và cho phép thay đổi lựa chọn tối ưu hóa."
+Nhấn tiếp tục để thay đổi lựa chọn tối ưu hóa."
Tiếp tục
Giả mạo luồng video
- Giả mạo luồng video của thiết bị nhằm ngăn chặn lỗi khi phát
+ Giả mạo luồng video của ứng dụng khách nhằm ngăn chặn lỗi khi phát
Giả mạo luồng video
- Giả mạo luồng video của thiết bị nhằm ngăn chặn lỗi khi phát
+ Giả mạo luồng video của ứng dụng khách nhằm ngăn chặn lỗi khi phát
Giả mạo luồng video
"Luồng video đã được giả mạo
@@ -99,7 +99,7 @@ Nếu bạn là người dùng YouTube Premium, thì cài đặt này có thể
Có thể gặp lỗi khi phát."
Tắt cài đặt này có thể gây ra lỗi khi phát.
- Loại thiết bị mặc định
+ Ứng dụng khách mặc định
Gỡ lỗi
@@ -107,6 +107,15 @@ Có thể gặp lỗi khi phát."
Nhật ký gỡ lỗi
Nhật ký gỡ lỗi đã bật
Nhật ký gỡ lỗi đã tắt
+ Ghi nhật ký truy vết ngăn xếp
+ Nhật ký gỡ lỗi bao gồm truy vết ngăn xếp
+ Nhật ký gỡ lỗi không bao gồm truy vết ngăn xếp
+ Hiện thông báo ngắn về lỗi ReVanced
+ Thông báo ngắn sẽ hiện nếu xảy ra lỗi
+ Thông báo ngắn không hiện nếu xảy ra lỗi
+ "Tắt thông báo lỗi sẽ ẩn mọi thông báo lỗi từ ReVanced.
+
+Bạn sẽ không được thông báo khi xẩy ra lỗi bất ngờ."
Xuất nhật ký gỡ lỗi
Sao chép nhật ký gỡ lỗi ReVanced vào bảng nhớ tạm
Tính năng ghi nhật ký gỡ lỗi đã tắt
@@ -151,15 +160,6 @@ Có thể gặp lỗi khi phát."
Điều này có thể giúp xác định các thành phần khi tạo bộ lọc tùy chỉnh.
Tuy nhiên, bật tính năng này cũng sẽ ghi lại một số dữ liệu người dùng, chẳng hạn như địa chỉ IP của bạn."
- Ghi nhật ký truy vết ngăn xếp
- Nhật ký gỡ lỗi bao gồm truy vết ngăn xếp
- Nhật ký gỡ lỗi không bao gồm truy vết ngăn xếp
- Hiện thông báo ngắn về lỗi ReVanced
- Thông báo ngắn sẽ hiện nếu xảy ra lỗi
- Thông báo ngắn không hiện nếu xảy ra lỗi
- "Tắt thông báo lỗi sẽ ẩn mọi thông báo lỗi từ ReVanced.
-
-Bạn sẽ không được thông báo khi xẩy ra lỗi bất ngờ."
Ẩn thẻ đĩa nhạc
@@ -186,15 +186,15 @@ Bạn sẽ không được thông báo khi xẩy ra lỗi bất ngờ."
Ẩn nút micrô nổi
Nút micro nổi trong tìm kiếm bị ẩn
Nút micro nổi trong tìm kiếm đang hiển thị
- Ẩn kệ ngang
- "Các kệ ngang đã bị ẩn, chẳng hạn như:
+ Ẩn kệ cá nhân hoá
+ "Các kệ cá nhân hoá đã bị ẩn, chẳng hạn như:
• Tin nổi bật
• Tiếp tục xem
• Khám phá các chủ đề khác
• Phù hợp nhất
• Mua sắm
• Xem lại"
- Kệ ngang được hiển thị
+ Kệ cá nhân hoá được hiển thị
Ẩn kệ hình ảnh
Kệ hình ảnh trong kết quả tìm kiếm đã bị ẩn
Kệ hình ảnh trong kết quả tìm kiếm được hiển thị
@@ -586,6 +586,10 @@ Vui lòng kiểm tra lại tên gói và đảm bảo ứng dụng đã được
Ẩn Ngừng quảng cáo
Nút ngừng quảng cáo đã bị ẩn
Nút ngừng quảng cáo được hiển thị
+
+ Ẩn bình luận
+ Nút bình luận đã bị ẩn
+ Nút bình luận được hiển thị
Ẩn Báo cáo
@@ -622,9 +626,9 @@ Vui lòng kiểm tra lại tên gói và đảm bảo ứng dụng đã được
Nút tạo đoạn video đã bị ẩn
Nút tạo đoạn video được hiển thị
- Ẩn Cửa hàng
- Nút Cửa hàng đã bị ẩn
- Nút Cửa hàng được hiển thị
+ Ẩn Mua sắm
+ Nút Mua sắm đã bị ẩn
+ Nút Mua sắm được hiển thị
Ẩn Lưu
Nút Lưu đã bị ẩn
@@ -835,7 +839,7 @@ Nếu thay đổi cài đặt này không có hiệu lực, hãy thử chuyển
Ẩn đề xuất tìm kiếm
Đề xuất tìm kiếm đã bị ẩn
Đề xuất tìm kiếm được hiển thị
- Ẩn nút Cửa hàng
+ Ẩn nút Mua sắm
Nút mua sắm đã bị ẩn
Nút mua sắm được hiển thị
Ẩn nhãn dán
@@ -1243,8 +1247,9 @@ Bố cục màn hình ô tô
Nếu tắt đi sau đó, bạn nên xóa dữ liệu ứng dụng để tránh phát sinh lỗi giao diện."
Mục tiêu phiên bản giả mạo
- 19.35.36 - Khôi phục biểu tượng trình phát Shorts cũ
- 19.01.34 - Khôi phục biểu tượng điều hướng cũ
+ 20.13.41 - Khôi phục thanh thao tác video mở rộng
+ 19.35.36 - Khôi phục biểu tượng trình phát Shorts cũ
+ 19.01.34 - Khôi phục biểu tượng điều hướng cũ
Thay đổi trang bắt đầu
@@ -1562,15 +1567,18 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn""• Trình đơn bản âm thanh bị thiếu
• Âm lượng ổn định không khả dụng"
• Video có thể dừng ở 1:00, hoặc có thể không khả dụng ở một số khu vực
- • Ứng dụng thử nghiệm và có thể ngừng hoạt động bất cứ lúc nào
+ • Ứng dụng khách đang trong giai đoạn thử nghiệm và có thể ngừng hoạt động bất cứ lúc nào
• Không có codec video AV1
• Video dành cho trẻ em có thể không phát được khi đăng xuất hoặc ở chế độ ẩn danh
+
+ • Buộc âm thanh gốc không khả dụng
Hiện trong Thống kê chi tiết
- Loại máy khách đã được hiển thị trong Thống kê chi tiết
- Loại thiết bị đã ẩn trong Thống kê chi tiết
+ Ứng dụng khách đã được hiển thị trong Thống kê chi tiết
+ Ứng dụng khách đã ẩn trong Thống kê chi tiết
Ngôn ngữ luồng âm thanh
Để chọn ngôn ngữ âm thanh cụ thể, hãy tắt \'Buộc ngôn ngữ âm thanh gốc\'
+ Chọn ngôn ngữ luồng không khả dụng với Android Studio
@@ -1589,7 +1597,7 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"
Bật lặp lại vĩnh viễn
Lặp lại vĩnh viễn đã được bật
- Lặp lại vĩnh viễn đã bị tắt
+ Lặp lại vĩnh viễn đã tắt
Ẩn nút truyền
@@ -1605,36 +1613,36 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"Thanh điều hướng
Ẩn hoặc thay đổi các nút trên thanh điều hướng
- Ẩn nút Trang chủ
+ Ẩn Trang chủ
Nút Trang chủ đã bị ẩn
Nút Trang chủ được hiển thị
- Ẩn nút Mẫu
- Nút Mẫu đã bị ẩn
- Nút Mẫu được hiển thị
+ Ẩn Đoạn nhạc
+ Nút Đoạn nhạc đã bị ẩn
+ Nút Đoạn nhạc được hiển thị
- Ẩn nút Khám phá
+ Ẩn Khám phá
Nút Khám phá đã bị ẩn
Nút Khám phá được hiển thị
- Ẩn nút Thư viện
+ Ẩn Thư viện
Nút Thư viện đã bị ẩn
Nút Thư viện được hiển thị
- Ẩn nút Nâng cấp
+ Ẩn Nâng cấp
Nút Nâng cấp đã bị ẩn
Nút Nâng cấp được hiển thị
Ẩn thanh điều hướng
Thanh điều hướng đã bị ẩn
Thanh điều hướng được hiển thị
- Ẩn nhãn nút điều hướng
- Nhãn đã bị ẩn
- Nhãn được hiển thị
+ Ẩn tên nút điều hướng
+ Tên nút đã bị ẩn
+ Tên nút được hiển thị
- Ẩn nhãn \'Nhận Music Premium\'
- Nhãn đã bị ẩn
- Nhãn được hiển thị
+ Ẩn trình đơn \'Mua Music Premium\'
+ Trình đơn này đã bị ẩn
+ Trình đơn này được hiển thị
Ẩn nút nâng cấp
@@ -1691,7 +1699,7 @@ Bật tính năng này có thể mở khóa chất lượng video cao hơn"Cài đặt linh tinh
Cài đặt chung
Cài đặt khác
- Quảng cáo phía máy khách
+ Quảng cáo phía ứng dụng
Quảng cáo luồng đảm bảo phía máy khách
Nhật ký gỡ lỗi
Nhật ký gỡ lỗi đã bật
diff --git a/patches/src/main/resources/addresources/values-zh-rCN/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/strings.xml
index 3acec7405..46ae6549c 100644
--- a/patches/src/main/resources/addresources/values-zh-rCN/strings.xml
+++ b/patches/src/main/resources/addresources/values-zh-rCN/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
调试日志
已启用调试日志记录
已禁用调试日志记录
+ 记录堆栈跟踪
+ 调试日志包括堆栈跟踪
+ 调试日志不包括堆栈跟踪
+ 在发生 ReVanced 错误时显示 Toast
+ 发生错误时显示Toast
+ 发生错误时不显示Toast
+ "关闭错误 Toast 会隐藏所有 ReVanced 错误通知。
+
+您不会收到任何意外事件的通知。"
导出调试日志
将 ReVanced 调试日志复制到剪贴板
调试日志记录已禁用
@@ -151,15 +160,6 @@ Second \"item\" text"
这有助于在创建自定义过滤器时识别组件。
但是,启用此设置也会记录一些用户数据,例如您的 IP 地址。"
- 记录堆栈跟踪
- 调试日志包括堆栈跟踪
- 调试日志不包括堆栈跟踪
- 在发生 ReVanced 错误时显示 Toast
- 发生错误时显示Toast
- 发生错误时不显示Toast
- "关闭错误 Toast 会隐藏所有 ReVanced 错误通知。
-
-您不会收到任何意外事件的通知。"
隐藏专辑卡片
@@ -586,6 +586,10 @@ Second \"item\" text"
隐藏停止广告
停止广告按钮已隐藏
停止广告按钮已显示
+
+ 隐藏评论
+ 评论按钮已隐藏
+ 评论按钮已显示
隐藏「举报」
@@ -1247,8 +1251,9 @@ Second \"item\" text"
如果稍后关闭,建议清除应用程序数据以防止 UI 错误。"
伪装应用程序版本为
- 19.35.36 - 恢复旧的 Shorts 播放器图标
- 19.01.34 - 还原旧的导航图标
+ 20.13.41 - 恢复未折叠的视频操作栏
+ 19.35.36 - 恢复旧的 Shorts 播放器图标
+ 19.01.34 - 还原旧的导航图标
更改起始页
@@ -1564,12 +1569,15 @@ Second \"item\" text"
• 实验性客户端,可能随时停止工作
• 不支持 AV1 视频编解码器
• 注销或使用无痕模式时,儿童视频可能无法播放
+
+ • 强制原始音频不可用
显示在 Stats for nerds 中
客户端类型显示在 Stats for nerds 中
客户端隐藏在 Stats for nerds 中
音频流语言
要选择特定的音频语言,请关闭“强制原始音频语言”
+ Android Studio 不支持选择流语言
@@ -1604,23 +1612,23 @@ Second \"item\" text"
导航栏
隐藏或更改导航栏按钮
- 隐藏主页按钮
+ 隐藏主页
主页按钮已隐藏
主页按钮已显示
- 隐藏精选按钮
+ 隐藏短片
精选按钮已隐藏
精选按钮已显示
- 隐藏探索按钮
+ 隐藏探索
探索按钮已隐藏
探索按钮已显示
- 隐藏媒体库按钮
+ 隐藏媒体库
媒体库按钮已隐藏
媒体库按钮已显示
- 隐藏升级按钮
+ 隐藏升级
升级按钮已隐藏
升级按钮已显示
隐藏导航栏
diff --git a/patches/src/main/resources/addresources/values-zh-rTW/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml
index b1430178b..a62c677f4 100644
--- a/patches/src/main/resources/addresources/values-zh-rTW/strings.xml
+++ b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml
@@ -107,6 +107,15 @@ Second \"item\" text"
偵錯記錄
已啟用偵錯記錄檔
已停用偵錯記錄檔
+ 記錄檔堆疊追蹤
+ 除錯記錄檔會包含堆疊追蹤
+ 除錯記錄檔不會包含堆疊追蹤
+ 在 ReVanced 發生錯誤時顯示提示訊息
+ 如果發生錯誤,則顯示提示訊息
+ 如果發生錯誤,則不會顯示提示訊息
+ "關閉錯誤提示訊息會隱藏所有 ReVanced 的錯誤通知。
+
+你將不會收到任何非預期的事件通知。"
匯出偵錯記錄
將 ReVanced 偵錯記錄複製到剪貼簿
偵錯記錄已停用
@@ -151,15 +160,6 @@ Second \"item\" text"
這有助於在建立自訂過濾器時識別元件。
不過,啟用此功能也會記錄部分使用者資料,例如您的 IP 位址。"
- 記錄檔堆疊追蹤
- 除錯記錄檔會包含堆疊追蹤
- 除錯記錄檔不會包含堆疊追蹤
- 在 ReVanced 發生錯誤時顯示提示訊息
- 如果發生錯誤,則顯示提示訊息
- 如果發生錯誤,則不會顯示提示訊息
- "關閉錯誤提示訊息會隱藏所有 ReVanced 的錯誤通知。
-
-你將不會收到任何非預期的事件通知。"
隱藏專輯資訊卡
@@ -376,13 +376,13 @@ Second \"item\" text"
隱藏關鍵字內容
透過關鍵字篩選,隱藏搜尋結果與推薦內容中的影片
依關鍵字隱藏首頁影片
- 首頁影片將會被關鍵字篩選
+ 首頁分頁的影片會被關鍵字篩選
首頁影片將不會被關鍵字篩選
依關鍵字隱藏搜尋結果
搜尋結果將會被關鍵字篩選
搜尋結果將不會被關鍵字篩選
依關鍵字隱藏訂閱內容影片
- 訂閱內容分頁的影片將會被關鍵字篩選
+ 訂閱內容分頁的影片會被關鍵字篩選
訂閱內容分頁的影片將不會被關鍵字篩選
要隱藏的關鍵字
+ 隱藏留言
+ 留言按鈕已隱藏
+ 留言按鈕已顯示
隱藏檢舉
@@ -658,7 +662,7 @@ Second \"item\" text"
注意:啟用此功能也會強制隱藏影片廣告"
「建立」按鈕不與「通知」按鈕對調
- "停用此設定也會停用Shorts 的廣告攔截功能。
+ "停用此設定也會停用 Shorts 的廣告攔截功能。
如果變更此設定沒有生效,請試試看切換到無痕模式。"
隱藏導覽列按鈕標籤
@@ -684,23 +688,23 @@ Second \"item\" text"
已顯示「字幕」選單
隱藏「其他設定」
- 已隱藏「其他設定」
- 已顯示「其他設定」
+ 已隱藏「其他設定」選單
+ 已顯示「其他設定」選單
隱藏「睡眠計時器」
- 已隱藏「睡眠計時器」
- 已顯示「睡眠計時器」
+ 已隱藏「睡眠計時器」選單
+ 已顯示「睡眠計時器」選單
隱藏「循環播放影片」
- 已隱藏「循環播放」選單
- 已顯示「循環播放」選單
+ 已隱藏「循環播放影片」選單
+ 已顯示「循環播放影片」選單
隱藏「微光效果」
- 已隱藏「微光模式」選單
+ 已隱藏「微光效果」選單
已顯示「微光效果」選單
隱藏「平衡音量」
- 已顯示「平衡音量」
- 已隱藏「平衡音量」
+ 已顯示「平衡音量」選單
+ 已隱藏「平衡音量」選單
隱藏「說明和意見回饋」
已隱藏「說明和意見回饋」選單
@@ -761,7 +765,7 @@ Second \"item\" text"
已顯示片尾資訊卡
- 在全螢幕狀態下停用微光模式
+ 在全螢幕狀態下停用微光效果
已停用微光效果
已啟用微光效果
@@ -1028,7 +1032,7 @@ Second \"item\" text"
顯示「取消跳過」提示
當片段自動跳過時會顯示提示。點擊提示通知可取消跳過
不顯示提示
- 「取消跳過」提示顯示時間
+ 跳過提示時長
「取消跳過」提示顯示多久
1 秒
2 秒
@@ -1243,8 +1247,9 @@ Second \"item\" text"
如果之後關閉此設定,建議清除應用程式資料以避免 UI 錯誤。"
應用程式偽裝目標版本
- 19.35.36 - 還原舊版 Shorts 播放圖示
- 19.01.34 - 還原舊版導覽圖示
+ 20.13.41 - 還原未收合的影片操作列
+ 19.35.36 - 還原舊版 Shorts 播放圖示
+ 19.01.34 - 還原舊版導覽圖示
變更起始頁面
@@ -1273,9 +1278,9 @@ Second \"item\" text"
稍後觀看
你的剪輯
一律變更起始頁面
- "開始頁面總是變更
+ "開始頁面一律變更
-限制: 使用工具列上的返回按鈕可能無法運作"
+限制:工具列上的返回按鈕可能無法運作"
起始頁面僅在應用程式啟動時變更
@@ -1569,12 +1574,15 @@ Second \"item\" text"
• 實驗性用戶端,隨時可能停止運作
• 沒有 AV1 影片解碼器
• 在登出或無痕模式下,兒童影片可能無法播放
+
+ • 強制原始音訊不可用
顯示統計資料
已在統計資料中顯示用戶端類型
已在統計資料中隱藏用戶端類型
音訊串流語言
若要選擇特定的音訊語言,請關閉「強制使用原始音訊語言」
+ 無法使用 Android Studio 選擇串流語言
@@ -1596,9 +1604,9 @@ Second \"item\" text"
永久重複播放已停用
- 隱藏投放按鈕
- 投放按鈕已隱藏
- 投放按鈕已顯示
+ 隱藏「投放」按鈕
+ 「投放」按鈕已隱藏
+ 「投放」按鈕已顯示
diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml
index 7ffef6e7a..2eca7af0c 100644
--- a/patches/src/main/resources/addresources/values/arrays.xml
+++ b/patches/src/main/resources/addresources/values/arrays.xml
@@ -401,7 +401,7 @@
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index d0f3edac0..c21a27391 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -48,14 +48,28 @@ Second \"item\" text"
Search settings
No results found for \'%s\'
Try another keyword
+ Recent searches
Remove from search history?
+ Clear search history
+ Are you sure you want to clear all search history?
+ Search Tips
+ "• Tap a path to navigate to it
+• Long-press a setting to navigate to it
+• Press Enter to save a search query to history
+• Search ignores casing and punctuation
+• Parent settings appear above disabled child settings"
+ Search history is empty
+ To save search history, type a search query and press Enter
+ Show settings search history
+ Settings search history is shown
+ Settings search history is not shown
Show ReVanced setting icons
Setting icons are shown
Setting icons are not shown
ReVanced language
"Translations for some languages may be missing or incomplete.
-To translate new languages visit translate.revanced.app"
+To translate new languages or improve the existing translations, visit translate.revanced.app"
App language
አማርኛ
العربية
@@ -200,9 +214,6 @@ You will not be notified of any unexpected events."
Restore old settings menus
Old settings menus are shown
Old settings menus are not shown
- Show settings search history
- Settings search history is shown
- Settings search history is not shown
Disable Shorts background play
@@ -246,6 +257,7 @@ However, enabling this will also log some user data such as your IP address."Floating microphone button in search is shown
Hide horizontal shelves
"Horizontal shelves are hidden, such as:
+
• Breaking news
• Continue watching
• Explore more channels
@@ -1134,9 +1146,9 @@ This feature works best with a video quality of 720p or lower and when using a v
Already read
Show me
General
- Show a toast if API is not available
- Toast is shown if SponsorBlock is not available
- Toast is not shown if SponsorBlock is not available
+ Show a toast if API is not available
+ Toast is shown if SponsorBlock is not available
+ Toast is not shown if SponsorBlock is not available
Enable skip count tracking
Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped
Skip count tracking is not enabled
@@ -1648,12 +1660,15 @@ Enabling this can unlock higher video qualities"
• Experimental client and may stop working anytime
• No AV1 video codec
• Kids videos may not play when logged out or in incognito mode
+
+ • Force original audio is not available
Show in Stats for nerds
Client type is shown in Stats for nerds
Client is hidden in Stats for nerds
Audio stream language
To select a specific audio language, turn off \'Force original audio language\'
+ Stream language selection is not available with Android Studio
@@ -1688,23 +1703,23 @@ Enabling this can unlock higher video qualities"
Navigation bar
Hide or change navigation bar buttons
- Hide Home button
+ Hide Home
Home button is hidden
Home button is shown
- Hide Samples button
+ Hide Samples
Samples button is hidden
Samples button is shown
- Hide Explore button
+ Hide Explore
Explore button is hidden
Explore button is shown
- Hide Library button
+ Hide Library
Library button is hidden
Library button is shown
- Hide Upgrade button
+ Hide Upgrade
Upgrade button is hidden
Upgrade button is shown
Hide navigation bar
diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml b/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml
deleted file mode 100644
index d0c8a7ae4..000000000
--- a/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_custom_checkmark.xml b/patches/src/main/resources/settings/drawable/revanced_settings_custom_checkmark.xml
index 73a68f102..0623e325f 100644
--- a/patches/src/main/resources/settings/drawable/revanced_settings_custom_checkmark.xml
+++ b/patches/src/main/resources/settings/drawable/revanced_settings_custom_checkmark.xml
@@ -5,5 +5,5 @@
android:viewportHeight="24">
+ android:pathData="M9.29446,19 L3.4,12.708 L4.24339,11.8029 L9.29446,17.1896 L20.1565,5.6 L21,6.50026 Z M9.29446,19" />
diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_search_remove.xml b/patches/src/main/resources/settings/drawable/revanced_settings_search_remove.xml
new file mode 100644
index 000000000..721d37885
--- /dev/null
+++ b/patches/src/main/resources/settings/drawable/revanced_settings_search_remove.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml b/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml
index bf02245a1..b2cf7f270 100644
--- a/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml
+++ b/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml
@@ -8,13 +8,9 @@
android:clipToPadding="false">
+ android:layout_gravity="center" />
diff --git a/patches/src/main/resources/settings/layout/revanced_custom_list_item_checked.xml b/patches/src/main/resources/settings/layout/revanced_custom_list_item_checked.xml
index f0500f61a..2f2b83c14 100644
--- a/patches/src/main/resources/settings/layout/revanced_custom_list_item_checked.xml
+++ b/patches/src/main/resources/settings/layout/revanced_custom_list_item_checked.xml
@@ -14,7 +14,7 @@
android:id="@+id/revanced_check_icon"
android:layout_width="24dp"
android:layout_height="24dp"
- android:layout_marginEnd="16dp"
+ android:layout_marginEnd="24dp"
android:src="@drawable/revanced_settings_custom_checkmark"
android:visibility="gone"
android:contentDescription="@null" />
@@ -23,7 +23,7 @@
android:id="@+id/revanced_check_icon_placeholder"
android:layout_width="24dp"
android:layout_height="24dp"
- android:layout_marginEnd="16dp"
+ android:layout_marginEnd="24dp"
android:visibility="invisible" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_history_item.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_history_item.xml
new file mode 100644
index 000000000..4dabb6de4
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_history_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_history_screen.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_history_screen.xml
new file mode 100644
index 000000000..daf925cfc
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_history_screen.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_no_result.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_no_result.xml
new file mode 100644
index 000000000..4e1b0cb81
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_no_result.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_result_color.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_result_color.xml
new file mode 100644
index 000000000..51d363049
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_result_color.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_result_group_header.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_result_group_header.xml
new file mode 100644
index 000000000..3790e436f
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_result_group_header.xml
@@ -0,0 +1,19 @@
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_result_list.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_result_list.xml
new file mode 100644
index 000000000..3d4ab6163
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_result_list.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_search_result_regular.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_result_regular.xml
new file mode 100644
index 000000000..3d4ab6163
--- /dev/null
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_result_regular.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml b/patches/src/main/resources/settings/layout/revanced_preference_search_result_switch.xml
similarity index 56%
rename from patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml
rename to patches/src/main/resources/settings/layout/revanced_preference_search_result_switch.xml
index b8a180f0e..9bab731e6 100644
--- a/patches/src/main/resources/settings/layout/revanced_preference_with_icon_no_search_result.xml
+++ b/patches/src/main/resources/settings/layout/revanced_preference_search_result_switch.xml
@@ -1,44 +1,48 @@
-
-
+ android:orientation="horizontal"
+ android:background="?android:attr/selectableItemBackground"
+ android:padding="16dp"
+ android:minHeight="48dp"
+ android:gravity="center_vertical">
+ android:orientation="vertical">
+ android:textSize="16sp"
+ android:ellipsize="end"
+ android:maxLines="2" />
-
+ android:textSize="14sp"
+ android:ellipsize="end"
+ android:maxLines="10"
+ android:visibility="gone" />
+
+
+
diff --git a/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml b/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml
deleted file mode 100644
index 8699e56c8..000000000
--- a/patches/src/main/resources/settings/layout/revanced_search_suggestion_item.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/patches/src/main/resources/settings/music/values/styles.xml b/patches/src/main/resources/settings/music/values/styles.xml
index 4a692559a..663f9c373 100644
--- a/patches/src/main/resources/settings/music/values/styles.xml
+++ b/patches/src/main/resources/settings/music/values/styles.xml
@@ -1,7 +1,12 @@
+
diff --git a/patches/src/main/resources/settings/host/values/styles.xml b/patches/src/main/resources/settings/youtube/values/styles.xml
similarity index 100%
rename from patches/src/main/resources/settings/host/values/styles.xml
rename to patches/src/main/resources/settings/youtube/values/styles.xml