From cfd77800d6641b46949d4e33eb24c394ccfc7453 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:28:16 +0300 Subject: [PATCH] feat(YouTube - External downloads): Improve the selection of the external downloader package (#5504) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../app/revanced/extension/shared/Utils.java | 22 + .../CustomDialogListPreference.java | 4 +- .../ResettableEditTextPreference.java | 13 - .../youtube/patches/DownloadsPatch.java | 26 +- .../extension/youtube/settings/Settings.java | 2 +- .../CustomVideoSpeedListPreference.java | 10 +- .../ExternalDownloaderPreference.java | 444 ++++++++++++++++++ .../SegmentPlaybackController.java | 5 +- .../interaction/downloads/DownloadsPatch.kt | 6 +- .../resources/addresources/values/strings.xml | 9 +- 10 files changed, 493 insertions(+), 48 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java 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 609d99b0b..e84ac36a2 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 @@ -1438,6 +1438,28 @@ public class Utils { ); } + /** + * Converts a percentage of the screen height to actual device pixels. + * + * @param percentage The percentage of the screen height (e.g., 30 for 30%). + * @return The device pixel value corresponding to the percentage of screen height. + */ + public static int percentageHeightToPixels(int percentage) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return (int) (metrics.heightPixels * (percentage / 100.0f)); + } + + /** + * Converts a percentage of the screen width to actual device pixels. + * + * @param percentage The percentage of the screen width (e.g., 30 for 30%). + * @return The device pixel value corresponding to the percentage of screen width. + */ + public static int percentageWidthToPixels(int percentage) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return (int) (metrics.widthPixels * (percentage / 100.0f)); + } + /** * Adjusts the brightness of a color by lightening or darkening it based on the given factor. *

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 46ed1815b..4d0c1d5c1 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,7 +1,5 @@ package app.revanced.extension.shared.settings.preference; -import static app.revanced.extension.shared.Utils.dipToPixels; - import android.app.Dialog; import android.content.Context; import android.os.Bundle; @@ -26,7 +24,7 @@ public class CustomDialogListPreference extends ListPreference { /** * Custom ArrayAdapter to handle checkmark visibility. */ - private static class ListPreferenceArrayAdapter extends ArrayAdapter { + public static class ListPreferenceArrayAdapter extends ArrayAdapter { private static class SubViewDataContainer { ImageView checkIcon; View placeholder; 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 9338029d4..13de26bfa 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 @@ -1,28 +1,15 @@ 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; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.shapes.RectShape; -import android.graphics.drawable.shapes.RoundRectShape; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.Paint.Style; import android.os.Bundle; import android.preference.EditTextPreference; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.Pair; -import android.view.ViewGroup; -import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.Nullable; 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 6da31b6a4..7950c8b21 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 @@ -1,17 +1,15 @@ package app.revanced.extension.youtube.patches; +import static app.revanced.extension.youtube.settings.preference.ExternalDownloaderPreference.showDialogIfAppIsNotInstalled; + import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; - -import androidx.annotation.NonNull; import java.lang.ref.WeakReference; 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.youtube.settings.Settings; @@ -36,7 +34,7 @@ public final class DownloadsPatch { * * Appears to always be called from the main thread. */ - public static boolean inAppDownloadButtonOnClick(@NonNull String videoId) { + public static boolean inAppDownloadButtonOnClick(String videoId) { try { if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) { return false; @@ -48,6 +46,9 @@ public final class DownloadsPatch { boolean isActivityContext = true; if (context == null) { // Utils context is the application context, and not an activity context. + // + // Edit: This check may no longer be needed since YT can now + // only be launched from the main Activity (embedded usage in other apps no longer works). context = Utils.getContext(); isActivityContext = false; } @@ -64,8 +65,7 @@ public final class DownloadsPatch { * @param isActivityContext If the context parameter is for an Activity. If this is false, then * the downloader is opened as a new task (which forces YT to minimize). */ - public static void launchExternalDownloader(@NonNull String videoId, - @NonNull Context context, boolean isActivityContext) { + public static void launchExternalDownloader(String videoId, Context context, boolean isActivityContext) { try { Objects.requireNonNull(videoId); Logger.printDebug(() -> "Launching external downloader with context: " + context); @@ -73,16 +73,8 @@ public final class DownloadsPatch { // Trim string to avoid any accidental whitespace. var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim(); - boolean packageEnabled = false; - try { - packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled; - } catch (PackageManager.NameNotFoundException error) { - Logger.printDebug(() -> "External downloader could not be found: " + error); - } - - // If the package is not installed, show the toast - if (!packageEnabled) { - Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName)); + // If the package is not installed, show a dialog. + if (showDialogIfAppIsNotInstalled(context, downloaderPackageName)) { return; } 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 b57a54523..edeb7ca0b 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 @@ -191,7 +191,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE); public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action_button", FALSE); public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_name", - "org.schabi.newpipe" /* NewPipe */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON)); + "com.deniscerri.ytdl" /* YTDLnis */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON)); // Comments public static final BooleanSetting HIDE_COMMENTS_AI_CHAT_SUMMARY = new BooleanSetting("revanced_hide_comments_ai_chat_summary", FALSE); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java index c6e98fd4d..cefa7774d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java @@ -16,10 +16,8 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings({"unused", "deprecation"}) public final class CustomVideoSpeedListPreference extends CustomDialogListPreference { - /** - * Initialize a settings preference list with the available playback speeds. - */ - private void initializeEntryValues() { + { + // Initialize a settings preference list with the available playback speeds. float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds; final int numberOfEntries = customPlaybackSpeeds.length + 1; String[] preferenceListEntries = new String[numberOfEntries]; @@ -41,10 +39,6 @@ public final class CustomVideoSpeedListPreference extends CustomDialogListPrefer setEntryValues(preferenceListEntryValues); } - { - initializeEntryValues(); - } - public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } 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 new file mode 100644 index 000000000..0165e1376 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPreference.java @@ -0,0 +1,444 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.StringRef.sf; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.dipToPixels; + +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.util.TypedValue; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.preference.CustomDialogListPreference; +import app.revanced.extension.youtube.settings.Settings; + +/** + * A custom ListPreference for selecting an external downloader package with checkmarks and EditText for custom package names. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ExternalDownloaderPreference extends CustomDialogListPreference { + + /** + * Enum representing supported external downloaders with their display names, package names, and download URLs. + */ + private enum Downloader { + YTDLNIS("YTDLnis", + "com.deniscerri.ytdl", + "https://ytdlnis.org", + true), + SEAL("Seal", + "com.junkfood.seal", + "https://github.com/JunkFood02/Seal/releases/latest", + true), + GRAYJAY("Grayjay", + "com.futo.platformplayer", + "https://grayjay.app"), + LIBRETUBE("LibreTube", + "com.github.libretube", + "https://libretube.dev"), + NEWPIPE("NewPipe", + "org.schabi.newpipe", + "https://newpipe.net"), + PIPEPIPE("PipePipe", + "InfinityLoop1309.NewPipeEnhanced", + "https://pipepipe.dev"), + TUBULAR("Tubular", + "org.polymorphicshade.tubular", + "https://github.com/polymorphicshade/Tubular/releases/latest"), + OTHER(sf("revanced_external_downloader_other_item").toString(), + null, + null, + true); + + private static final Map PACKAGE_TO_ENUM = new HashMap<>(); + + static { + for (Downloader downloader : values()) { + String packageName = downloader.packageName; + if (packageName != null) { + PACKAGE_TO_ENUM.put(packageName, downloader); + } + } + } + + /** + * Finds a Downloader by its package name. This method can never return {@link #OTHER}. + * @return The Downloader enum or null if not found. + */ + @Nullable + public static Downloader findByPackageName(String packageName) { + return PACKAGE_TO_ENUM.get(Objects.requireNonNull(packageName)); + } + + public final String name; + @Nullable + public final String packageName; + @Nullable + public final String downloadUrl; + /** + * If a downloader app should be shown in the preference settings + * if the app is not currently installed. + */ + public final boolean isPreferred; + + Downloader(String name, String packageName, String downloadUrl) { + this(name, packageName, downloadUrl, false); + } + + Downloader(String name, @Nullable String packageName, @Nullable String downloadUrl, boolean isPreferred) { + this.name = name; + this.packageName = packageName; + this.downloadUrl = downloadUrl; + this.isPreferred = isPreferred; + } + + public boolean isInstalled() { + return packageName != null && isAppInstalledAndEnabled(packageName); + } + } + + private static boolean isAppInstalledAndEnabled(String packageName) { + try { + if (Utils.getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled) { + Logger.printDebug(() -> "App installed: " + packageName); + return true; + } + } catch (PackageManager.NameNotFoundException error) { + Logger.printDebug(() -> "App not installed: " + packageName); + } + return false; + } + + private EditText editText; + private CustomDialogListPreference.ListPreferenceArrayAdapter adapter; + + public ExternalDownloaderPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public ExternalDownloaderPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ExternalDownloaderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ExternalDownloaderPreference(Context context) { + super(context); + } + + private void updateEntries() { + List entries = new ArrayList<>(); + List entryValues = new ArrayList<>(); + + for (Downloader downloader : Downloader.values()) { + if (downloader.isPreferred || downloader.isInstalled()) { + String packageName = downloader.packageName; + + entries.add(downloader.name); + entryValues.add(packageName != null + ? packageName + : Downloader.OTHER.name); + } + } + + setEntries(entries.toArray(new CharSequence[0])); + setEntryValues(entryValues.toArray(new CharSequence[0])); + } + + /** + * Sets the summary for this ListPreference. + */ + @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. + } + + /** + * Shows a custom dialog with a ListView for predefined downloader packages and EditText for custom package input. + */ + @Override + protected void showDialog(@Nullable Bundle state) { + // Must set entries before showing the dialog, to handle if + // an app is installed while the settings are open in the background. + updateEntries(); + + Context context = getContext(); + String packageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get(); + + // Create the main layout for the dialog content. + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + + // Create ListView for predefined downloader apps. + ListView listView = new ListView(context); + listView.setId(android.R.id.list); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + // Create custom adapter for the ListView. + final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null; + adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter( + context, + Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"), + getEntries(), + getEntryValues(), + usingCustomDownloader + ? Downloader.OTHER.name + : packageName + ); + listView.setAdapter(adapter); + + Function updateListViewSelection = (updatedPackageName) -> { + String entryValueName = Downloader.findByPackageName(updatedPackageName) == null + ? Downloader.OTHER.name + : updatedPackageName; + CharSequence[] entryValues = getEntryValues(); + + for (int i = 0, length = entryValues.length; i < length; i++) { + String entryString = entryValues[i].toString(); + if (entryString.equals(entryValueName)) { + listView.setItemChecked(i, true); + listView.setSelection(i); + adapter.setSelectedValue(entryString); + adapter.notifyDataSetChanged(); + break; + } + } + return null; + }; + updateListViewSelection.apply(packageName); + + // Handle item click to select value. + listView.setOnItemClickListener((parent, view, position, id) -> { + String selectedValue = getEntryValues()[position].toString(); + Downloader selectedApp = Downloader.findByPackageName(selectedValue); + + if (selectedApp != null) { + editText.setText(selectedApp.packageName); + editText.setEnabled(false); // Disable editing for predefined options. + } else { + String savedPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get(); + editText.setText(Downloader.findByPackageName(savedPackageName) == null + ? savedPackageName // If the user is clicking thru options then retain existing other app. + : "" + ); + editText.setEnabled(true); // Enable editing for Custom. + editText.requestFocus(); + } + editText.setSelection(editText.getText().length()); + adapter.setSelectedValue(selectedValue); + adapter.notifyDataSetChanged(); + }); + + // Add ListView to content layout with initial height. + LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 0 // Initial height, will be updated. + ); + listViewParams.bottomMargin = dipToPixels(16); + contentLayout.addView(listView, listViewParams); + + // Add EditText for custom package name. + editText = new EditText(context); + editText.setText(packageName); + editText.setSelection(packageName.length()); + editText.setHint(str("revanced_external_downloader_other_item_hint")); + editText.setSingleLine(true); // Restrict EditText to a single line. + editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + // Set initial EditText state based on selected downloader. + editText.setEnabled(usingCustomDownloader); + editText.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) { + String updatedPackageName = edit.toString().trim(); + updateListViewSelection.apply(updatedPackageName); + } + }); + + ShapeDrawable editTextBackground = new ShapeDrawable(new RoundRectShape( + Utils.createCornerRadii(10), null, null)); + editTextBackground.getPaint().setColor(Utils.getEditTextBackground()); + final int dip8 = dipToPixels(8); + editText.setPadding(dip8, dip8, dip8, dip8); + editText.setBackground(editTextBackground); + editText.setClipToOutline(true); + contentLayout.addView(editText); + + // Create the custom dialog. + Pair dialogPair = Utils.createCustomDialog( + context, + getTitle() != null ? getTitle().toString() : "", + null, + null, + null, + () -> { + String newValue = editText.getText().toString().trim(); + if (newValue.isEmpty()) { + // Show dialog if EditText is empty. + Utils.createCustomDialog( + context, + str("revanced_external_downloader_name_title"), + str("revanced_external_downloader_empty_warning"), + null, + null, + () -> {}, // OK button does nothing (dismiss only). + null, + null, + null, + false + ).first.show(); + return; + } + + if (showDialogIfAppIsNotInstalled(getContext(), newValue)) { + return; // Invalid package. Do not save. + } + + // Save custom package name. + if (callChangeListener(newValue)) { + setValue(newValue); + } + }, + () -> {}, // Cancel button action (dismiss only). + str("revanced_settings_reset"), + () -> { // Reset action. + String defaultValue = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.defaultValue; + editText.setText(defaultValue); + editText.setSelection(defaultValue.length()); + editText.setEnabled(false); // Disable editing on reset. + updateListViewSelection.apply(defaultValue); + }, + false + ); + + // Add the content layout directly to the dialog's main layout. + LinearLayout dialogMainLayout = dialogPair.second; + dialogMainLayout.addView(contentLayout, dialogMainLayout.getChildCount() - 1); + + // Update ListView height dynamically based on orientation. + //noinspection ExtractMethodRecommender + Runnable updateListViewHeight = () -> { + int totalHeight = 0; + ListAdapter listAdapter = listView.getAdapter(); + if (listAdapter != null) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int listAdapterCount = listAdapter.getCount(); + for (int i = 0; i < listAdapterCount; i++) { + View item = listAdapter.getView(i, null, listView); + item.measure( + View.MeasureSpec.makeMeasureSpec(metrics.widthPixels, View.MeasureSpec.AT_MOST), + View.MeasureSpec.UNSPECIFIED + ); + totalHeight += item.getMeasuredHeight(); + } + totalHeight += listView.getDividerHeight() * (listAdapterCount - 1); + } + + final int orientation = context.getResources().getConfiguration().orientation; + if (orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) { + // In portrait orientation, use WRAP_CONTENT for ListView height. + listViewParams.height = LinearLayout.LayoutParams.WRAP_CONTENT; + } else { + // In landscape orientation, limit ListView height to 30% of screen height. + final int maxHeight = Utils.percentageHeightToPixels(30); + listViewParams.height = Math.min(totalHeight, maxHeight); + } + listView.setLayoutParams(listViewParams); + }; + + // Initial height calculation. + updateListViewHeight.run(); + + // Listen for configuration changes (e.g., orientation). + View dialogView = dialogPair.second; + // Recalculate height when layout changes (e.g., orientation change). + dialogView.getViewTreeObserver().addOnGlobalLayoutListener(updateListViewHeight::run); + + // Show the dialog. + dialogPair.first.show(); + } + + /** + * @return If the app is not installed and a dialog was shown. + */ + public static boolean showDialogIfAppIsNotInstalled(Context context, String packageName) { + if (isAppInstalledAndEnabled(packageName)) { + return false; + } + + Downloader downloader = Downloader.findByPackageName(packageName); + String downloadUrl = downloader != null + ? downloader.downloadUrl + : null; + String okButtonText = downloadUrl != null + ? str("gms_core_dialog_open_website_text") // Open website. + : null; // Ok. + // Show a dialog if the recommended app is not installed or if the custom package cannot be found. + String message = downloader != null + ? str("revanced_external_downloader_not_installed_warning", downloader.name) + : str("revanced_external_downloader_package_not_found_warning", packageName); + + Utils.createCustomDialog( + context, + str("revanced_external_downloader_not_found_title"), + message, + null, + okButtonText, + () -> { + try { + // OK button action: open the downloader's URL if available. + if (downloadUrl != null) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to open downloader URL: " + downloader, ex); + } + }, + () -> {}, // Cancel button action (dismiss only). + null, + null, + false + ).first.show(); + + return true; + } +} 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 86a7d200e..300488390 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 @@ -830,11 +830,10 @@ public class SegmentPlaybackController { WindowManager.LayoutParams params = window.getAttributes(); params.gravity = Gravity.BOTTOM; params.y = dipToPixels(72); - DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); - int portraitWidth = (int) (displayMetrics.widthPixels * 0.6); + int portraitWidth = Utils.percentageWidthToPixels(60); // 60% of the screen width. if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.6); + portraitWidth = Math.min(portraitWidth, Utils.percentageHeightToPixels(60)); // 60% of the screen height. } params.width = portraitWidth; params.dimAmount = 0.0f; diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt index 7076ca978..82f19f81a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -6,7 +6,6 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.shared.misc.settings.preference.InputType import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.patches.shared.misc.settings.preference.SwitchPreference @@ -40,7 +39,10 @@ private val downloadsResourcePatch = resourcePatch { preferences = setOf( SwitchPreference("revanced_external_downloader"), SwitchPreference("revanced_external_downloader_action_button"), - TextPreference("revanced_external_downloader_name", inputType = InputType.TEXT), + TextPreference( + "revanced_external_downloader_name", + tag = "app.revanced.extension.youtube.settings.preference.ExternalDownloaderPreference", + ), ), ), ) diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 19d442dac..79e04133b 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -531,8 +531,15 @@ This feature is only available for older devices" Download button opens your external downloader Download button opens the native in-app downloader Downloader package name - Package name of your installed external downloader app, such as NewPipe or Seal + Package name of your installed external downloader app + Enter the package name + Other + App not installed %s is not installed. Please install it. + "Could not find installed app with package name: %s + +Verify the package name is correct and the app is installed" + The package name cannot be empty Disable precise seeking gesture