fix(YouTube - Settings): Use an overlay to show search results (#5806)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-09-21 16:19:29 +03:00
committed by GitHub
parent ebb446b22a
commit ece8076f7c
104 changed files with 6066 additions and 3453 deletions

View File

@@ -19,6 +19,7 @@ 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;
@@ -65,6 +66,17 @@ public final class AlternativeThumbnailsPatch {
public boolean isAvailable() {
return usingDeArrowAnywhere();
}
@Override
public List<Setting<?>> 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 {
@@ -80,6 +92,17 @@ public final class AlternativeThumbnailsPatch {
public boolean isAvailable() {
return usingStillImagesAnywhere();
}
@Override
public List<Setting<?>> getParentSettings() {
return List.of(
ALT_THUMBNAIL_HOME,
ALT_THUMBNAIL_SUBSCRIPTIONS,
ALT_THUMBNAIL_LIBRARY,
ALT_THUMBNAIL_PLAYER,
ALT_THUMBNAIL_SEARCH
);
}
}
public enum ThumbnailOption {
@@ -451,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;

View File

@@ -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<Setting<?>> getParentSettings() {
return List.of(Settings.CHANGE_START_PAGE);
}
}
/**

View File

@@ -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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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).

View File

@@ -27,11 +27,11 @@ public final class DownloadsPatch {
/**
* Injection point.
*
* <p>
* 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.
*
* <p>
* Appears to always be called from the main thread.
*/
public static boolean inAppDownloadButtonOnClick(String videoId) {

View File

@@ -1,5 +1,7 @@
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;
@@ -37,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("player_control_previous_button_touch_area", "id");
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("player_control_next_button_touch_area", "id");
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
"player_control_next_button_touch_area", "id");
/**
* Injection point.

View File

@@ -12,12 +12,14 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "SpellCheckingInspection"})
@SuppressWarnings("SpellCheckingInspection")
public final class MiniplayerPatch {
/**
@@ -173,6 +175,14 @@ public final class MiniplayerPatch {
public boolean isAvailable() {
return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
}
@Override
public List<Setting<?>> getParentSettings() {
return List.of(
Settings.MINIPLAYER_TYPE,
Settings.MINIPLAYER_DRAG_AND_DROP
);
}
}
public static final class MiniplayerHideOverlayButtonsAvailability implements Setting.Availability {
@@ -185,11 +195,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<Setting<?>> 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<Setting<?>> 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<Setting<?>> 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<Setting<?>> getParentSettings() {
return List.of(Settings.MINIPLAYER_TYPE);
}
}
/**
* Injection point.
*
* <p>
* Enables a handler that immediately closes the miniplayer when the video is minimized,
* effectively disabling the miniplayer.
*/
@@ -378,4 +436,4 @@ public final class MiniplayerPatch {
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
}
}
}
}

View File

@@ -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<Setting<?>> getParentSettings() {
return List.of(Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS);
}
}
private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED

View File

@@ -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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
context,
finalTitle, // Title.
finalMessage, // Message.

View File

@@ -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<Setting<?>> getParentSettings() {
return List.of(BaseSettings.SPOOF_VIDEO_STREAMS);
}
}
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();

View File

@@ -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<Dialog> currentDialog = new WeakReference<>(null);
private static WeakReference<SheetBottomDialog.SlideDialog> 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<Float, Void> 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<Float, Void> 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<PlayerType, Unit> 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<PlayerType, Unit> 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);
}
}

View File

@@ -151,13 +151,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(
seekbarStyle,
"style"
);
if (styleIdentifierDefault == 0) {
throw new RuntimeException("Seekbar style not found: " + seekbarStyle);
}
Resources.Theme theme = Utils.getContext().getResources().newTheme();
theme.applyStyle(styleIdentifierDefault, true);

View File

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

View File

@@ -1,414 +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.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<String> 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(Context context) {
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(
"revanced_search_view", "id"));
searchContainer = activity.findViewById(getResourceIdentifier(
"revanced_search_view_container", "id"));
// Initialize AutoCompleteTextView.
autoCompleteTextView = searchView.findViewById(
searchView.getContext().getResources().getIdentifier(
"android:id/search_src_text", null, null));
// 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("action_search", "id");
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "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(
"action_search", "id")).setVisible(false);
toolbar.setTitle("");
searchContainer.setVisibility(View.VISIBLE);
searchView.requestFocus();
// Show keyboard.
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
// Show suggestions with a slight delay.
if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
searchView.postDelayed(() -> {
if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
autoCompleteTextView.showDropDown();
}
}, 100); // 100ms delay to ensure focus is stable.
}
}
/**
* Closes the search view and hides the keyboard.
*/
public void closeSearch() {
isSearchActive = false;
toolbar.getMenu().findItem(getResourceIdentifier(
"action_search", "id")).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<String> {
public SearchHistoryAdapter(Context context, List<String> history) {
super(context, 0, history);
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
if (convertView == null) {
convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
"revanced_search_suggestion_item", "layout"), null);
}
// Apply rounded corners programmatically.
convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
String query = getItem(position);
// Set query text.
TextView textView = convertView.findViewById(getResourceIdentifier(
"suggestion_text", "id"));
if (textView != null) {
textView.setText(query);
}
// Set click listener for inserting query into SearchView.
convertView.setOnClickListener(v -> {
searchView.setQuery(query, true); // Insert selected query and submit.
});
// Set long click listener for deletion confirmation.
convertView.setOnLongClickListener(v -> {
Pair<Dialog, LinearLayout> 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;
}
}
}

View File

@@ -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;
@@ -15,10 +14,6 @@ import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.Fullscr
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.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
@@ -179,16 +174,15 @@ public class Settings extends BaseSettings {
// Miniplayer
public static final EnumSetting<MiniplayerType> 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 MiniplayerPatch.MiniplayerAnyModernAvailability());
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, new MiniplayerPatch.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 MiniplayerPatch.MiniplayerHideOverlayButtonsAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
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 MiniplayerPatch.MiniplayerHideSubtextsAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, new MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability());
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, new MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability());
// External downloader
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
@@ -254,8 +248,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<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
@@ -381,9 +373,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));
@@ -405,7 +397,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));
@@ -421,46 +413,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);
@@ -471,6 +453,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
@@ -535,6 +528,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
@@ -543,4 +548,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()));
}
}

View File

@@ -2,26 +2,23 @@ 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;
import android.widget.Toolbar;
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.
@@ -29,16 +26,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
* <p>
* 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);
}
/**
@@ -49,7 +44,7 @@ public class LicenseActivityHook extends BaseActivityHook {
final var theme = Utils.isDarkModeEnabled()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
activity.setTheme(Utils.getResourceIdentifierOrThrow(theme, "style"));
}
/**
@@ -57,7 +52,7 @@ public class LicenseActivityHook extends BaseActivityHook {
*/
@Override
protected int getContentViewResourceId() {
return Utils.getResourceIdentifier("revanced_settings_with_toolbar", "layout");
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**
@@ -76,7 +71,7 @@ public class LicenseActivityHook extends BaseActivityHook {
*/
@Override
protected Drawable getNavigationIcon() {
return ReVancedPreferenceFragment.getBackButtonDrawable();
return YouTubePreferenceFragment.getBackButtonDrawable();
}
/**
@@ -88,7 +83,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.
@@ -96,32 +91,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();
}
/**
@@ -164,12 +145,14 @@ public class LicenseActivityHook extends BaseActivityHook {
}
/**
* Handles configuration changes, such as orientation, to update the search view.
* Injection point.
* <p>
* 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);
}
}

View File

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

View File

@@ -36,6 +36,7 @@ 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.shared.ui.CustomDialog;
import app.revanced.extension.youtube.settings.Settings;
/**
@@ -210,7 +211,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null;
adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter(
context,
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
getEntries(),
getEntryValues(),
usingCustomDownloader
@@ -302,7 +303,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
contentLayout.addView(editText);
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
context,
getTitle() != null ? getTitle().toString() : "",
null,
@@ -312,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"),
@@ -415,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,

View File

@@ -1,483 +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.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<AbstractPreferenceSearchData<?>> 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<String, PreferenceCategory> categoryMap = new HashMap<>();
String queryLower = Utils.removePunctuationToLowercase(query);
Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
Pattern.CASE_INSENSITIVE);
for (AbstractPreferenceSearchData<?> data : allPreferences) {
if (data.matchesSearchQuery(queryLower)) {
data.applyHighlighting(queryLower, queryPattern);
String navigationPath = data.navigationPath;
PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
newGroup.setTitle(navigationPath);
preferenceScreen.addPreference(newGroup);
return newGroup;
});
group.addPreference(data.preference);
}
}
// Show 'No results found' if search results are empty.
if (categoryMap.isEmpty()) {
Preference noResultsPreference = new Preference(preferenceScreen.getContext());
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
noResultsPreference.setSelectable(false);
// Set icon for the placeholder preference.
noResultsPreference.setLayoutResource(getResourceIdentifier(
"revanced_preference_with_icon_no_search_result", "layout"));
noResultsPreference.setIcon(getResourceIdentifier("revanced_settings_search_icon", "drawable"));
preferenceScreen.addPreference(noResultsPreference);
}
}
}
@SuppressWarnings("deprecation")
class AbstractPreferenceSearchData<T extends Preference> {
/**
* @return The navigation path for the given preference, such as "Player > Action buttons".
*/
private static String getPreferenceNavigationString(Preference preference) {
Deque<CharSequence> pathElements = new ArrayDeque<>();
while (true) {
preference = preference.getParent();
if (preference == null) {
if (pathElements.isEmpty()) {
return "";
}
Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
}
if (!(preference instanceof NoTitlePreferenceCategory)
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
CharSequence title = preference.getTitle();
if (title != null && title.length() > 0) {
pathElements.addFirst(title);
}
}
}
}
/**
* Highlights the search query in the given text by applying color span.
* @param text The original text to process.
* @param queryPattern The search query to highlight.
* @return The text with highlighted query matches as a SpannableStringBuilder.
*/
static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
if (TextUtils.isEmpty(text)) {
return text;
}
final int 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<Preference> {
@Nullable
CharSequence originalSummary;
@Nullable
String searchSummary;
PreferenceSearchData(Preference pref) {
super(pref);
}
void updateSearchDataIfNeeded() {
super.updateSearchDataIfNeeded();
CharSequence summary = preference.getSummary();
if (originalSummary != summary) {
originalSummary = summary;
searchSummary = Utils.removePunctuationToLowercase(summary);
}
}
boolean matchesSearchQuery(String query) {
return super.matchesSearchQuery(query)
|| searchSummary != null && searchSummary.contains(query);
}
@Override
void applyHighlighting(String query, Pattern queryPattern) {
super.applyHighlighting(query, queryPattern);
preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
}
@CallSuper
void clearHighlighting() {
if (highlightingApplied) {
preference.setSummary(originalSummary);
}
super.clearHighlighting();
}
}
/**
* Switch preference type that uses summaryOn and summaryOff.
*/
@SuppressWarnings("deprecation")
class SwitchPreferenceSearchData extends AbstractPreferenceSearchData<SwitchPreference> {
@Nullable
CharSequence originalSummaryOn, originalSummaryOff;
@Nullable
String searchSummaryOn, searchSummaryOff;
SwitchPreferenceSearchData(SwitchPreference pref) {
super(pref);
}
void updateSearchDataIfNeeded() {
super.updateSearchDataIfNeeded();
CharSequence summaryOn = preference.getSummaryOn();
if (originalSummaryOn != summaryOn) {
originalSummaryOn = summaryOn;
searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
}
CharSequence summaryOff = preference.getSummaryOff();
if (originalSummaryOff != summaryOff) {
originalSummaryOff = summaryOff;
searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
}
}
boolean matchesSearchQuery(String query) {
return super.matchesSearchQuery(query)
|| searchSummaryOn != null && searchSummaryOn.contains(query)
|| searchSummaryOff != null && searchSummaryOff.contains(query);
}
@Override
void applyHighlighting(String query, Pattern queryPattern) {
super.applyHighlighting(query, queryPattern);
preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
}
@CallSuper
void clearHighlighting() {
if (highlightingApplied) {
preference.setSummaryOn(originalSummaryOn);
preference.setSummaryOff(originalSummaryOff);
}
super.clearHighlighting();
}
}
/**
* List preference type that uses entries.
*/
@SuppressWarnings("deprecation")
class ListPreferenceSearchData extends AbstractPreferenceSearchData<ListPreference> {
@Nullable
CharSequence[] originalEntries;
@Nullable
String searchEntries;
ListPreferenceSearchData(ListPreference pref) {
super(pref);
}
void updateSearchDataIfNeeded() {
super.updateSearchDataIfNeeded();
CharSequence[] entries = preference.getEntries();
if (originalEntries != entries) {
originalEntries = entries;
searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
}
}
boolean matchesSearchQuery(String query) {
return super.matchesSearchQuery(query)
|| searchEntries != null && searchEntries.contains(query);
}
@Override
void applyHighlighting(String query, Pattern queryPattern) {
super.applyHighlighting(query, queryPattern);
if (originalEntries != null) {
final int length = originalEntries.length;
CharSequence[] highlightedEntries = new CharSequence[length];
for (int i = 0; i < length; i++) {
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
// Cannot highlight the summary text, because ListPreference uses
// the toString() of the summary CharSequence which strips away all formatting.
}
preference.setEntries(highlightedEntries);
}
}
@CallSuper
void clearHighlighting() {
if (highlightingApplied) {
preference.setEntries(originalEntries);
}
super.clearHighlighting();
}
}

View File

@@ -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;
@@ -99,6 +100,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
+ '\n' + str("revanced_spoof_video_streams_about_kids_videos");
}
setSummary(summary);
// Use better formatting for bullet points.
setSummary(BulletPointPreference.formatIntoBulletPoints(summary));
}
}

View File

@@ -1,44 +0,0 @@
package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.util.AttributeSet;
import app.revanced.extension.shared.Logger;
/**
* Simple preference that opens a url when clicked.
*/
@SuppressWarnings("deprecation")
public class UrlLinkPreference extends Preference {
protected String externalUrl;
{
setOnPreferenceClickListener(pref -> {
if (externalUrl == null) {
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
return false;
}
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(externalUrl));
pref.getContext().startActivity(i);
return true;
});
}
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public UrlLinkPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UrlLinkPreference(Context context) {
super(context);
}
}

View File

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

View File

@@ -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<BaseSearchResultItem> items,
BaseSearchViewController.BasePreferenceFragment fragment,
BaseSearchViewController searchViewController) {
super(context, items, fragment, searchViewController);
}
@Override
protected PreferenceScreen getMainPreferenceScreen() {
return fragment.getPreferenceScreenForSearch();
}
}

View File

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

View File

@@ -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;
@@ -48,9 +47,10 @@ import kotlin.Unit;
/**
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
*
* <p>
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
@SuppressLint("NewApi")
public class SegmentPlaybackController {
/**
@@ -122,7 +122,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.
*/
@@ -867,23 +866,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()) {

View File

@@ -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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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() {

View File

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

View File

@@ -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<String, SegmentCategory> 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) {

View File

@@ -1,371 +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.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("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPickerContainer.findViewById(
getResourceIdentifier("revanced_color_picker_view", "id"));
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<Dialog, LinearLayout> 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.
}
}

View File

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

View File

@@ -16,7 +16,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
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<SponsorSegment> {
/**
* @return The start/end time in range form.
* Range times are adjusted since it uses inclusive and Segments use exclusive.
*
* <p>
* {@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<SponsorSegment> {
}
/**
* @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<SponsorSegment> {
@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

View File

@@ -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,14 +14,10 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
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(
@@ -45,7 +45,7 @@ public final class NewSegmentLayout extends FrameLayout {
super(context, attributeSet, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(
getResourceIdentifier(context, "revanced_sb_new_segment", "layout"), this, true
getResourceIdentifierOrThrow(context, "revanced_sb_new_segment", "layout"), this, true
);
initializeButton(
@@ -104,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, resourceIdentifierName, "id"));
ImageButton button = findViewById(getResourceIdentifierOrThrow(context, resourceIdentifierName, "id"));
// Add ripple effect
RippleDrawable rippleDrawable = new RippleDrawable(

View File

@@ -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;
@@ -56,9 +57,11 @@ 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, "revanced_sb_skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(context,
"revanced_sb_skip_sponsor_button", "layout"), 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, "revanced_sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
skipSponsorBtnContainer = Objects.requireNonNull(findViewById(getResourceIdentifierOrThrow(
context, "revanced_sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
background = new Paint();
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);

View File

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

View File

@@ -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<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
private final List<SegmentCategoryPreference> 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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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);

View File

@@ -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<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
Pair<Dialog, LinearLayout> 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);
}
}

View File

@@ -1,6 +1,6 @@
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,7 +63,8 @@ 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("revanced_sb_inline_sponsor_overlay", "layout"), layout);
LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(
"revanced_sb_inline_sponsor_overlay", "layout"), layout);
inlineSponsorOverlayRef = new WeakReference<>(layout);
viewGroup.addView(layout);
@@ -82,14 +83,14 @@ public class SponsorBlockViewController {
});
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
skipHighlightButtonRef = new WeakReference<>(Objects.requireNonNull(
layout.findViewById(getResourceIdentifier("revanced_sb_skip_highlight_button", "id"))));
skipHighlightButtonRef = new WeakReference<>(layout.findViewById(getResourceIdentifierOrThrow(
"revanced_sb_skip_highlight_button", "id")));
skipSponsorButtonRef = new WeakReference<>(Objects.requireNonNull(
layout.findViewById(getResourceIdentifier("revanced_sb_skip_sponsor_button", "id"))));
skipSponsorButtonRef = new WeakReference<>(layout.findViewById(getResourceIdentifierOrThrow(
"revanced_sb_skip_sponsor_button", "id")));
NewSegmentLayout newSegmentLayout = Objects.requireNonNull(
layout.findViewById(getResourceIdentifier("revanced_sb_new_segment_view", "id")));
NewSegmentLayout newSegmentLayout = layout.findViewById(getResourceIdentifierOrThrow(
"revanced_sb_new_segment_view", "id"));
newSegmentLayoutRef = new WeakReference<>(newSegmentLayout);
newSegmentLayout.updateLayout();

View File

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

View File

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

View File

@@ -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;
@@ -43,6 +35,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 {
@@ -60,6 +53,11 @@ public class VideoQualityDialogButton {
});
}
/**
* Weak reference to the currently open dialog.
*/
private static WeakReference<SheetBottomDialog.SlideDialog> currentDialog;
/**
* Injection point.
*/
@@ -216,41 +214,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();
@@ -298,16 +269,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 {
@@ -322,112 +297,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<PlayerType, Unit> 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<String> {
private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
"revanced_custom_list_item_checked", "layout");
private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
"revanced_check_icon", "id");
private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
"revanced_check_icon_placeholder", "id");
private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
"revanced_item_text", "id");
private int selectedPosition = -1;
@@ -447,14 +351,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();