mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-20 16:57:32 +01:00
feat(YouTube - Return YouTube Dislike): Support version 18.43.45
and 18.44.41
(#514)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
parent
9a6ec6be8c
commit
a5245b85a8
@ -2,28 +2,27 @@ package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class HideBreakingNewsPatch {
|
||||
|
||||
/**
|
||||
* When spoofing to app versions older than 17.30.35, the watch history preview bar uses
|
||||
* When spoofing to app versions 17.31.00 and older, the watch history preview bar uses
|
||||
* the same layout components as the breaking news shelf.
|
||||
*
|
||||
* Breaking news does not appear to be present in these older versions anyways.
|
||||
*/
|
||||
private static boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory() {
|
||||
return SettingsEnum.SPOOF_APP_VERSION.getBoolean()
|
||||
&& SettingsEnum.SPOOF_APP_VERSION_TARGET.getString().compareTo("17.30.35") < 0;
|
||||
}
|
||||
private static final boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("17.31.00");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideBreakingNews(View view) {
|
||||
if (!SettingsEnum.HIDE_BREAKING_NEWS.getBoolean()
|
||||
|| isSpoofingOldVersionWithHorizontalCardListWatchHistory()) return;
|
||||
|| isSpoofingOldVersionWithHorizontalCardListWatchHistory) return;
|
||||
ReVancedUtils.hideViewByLayoutParams(view);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.*;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
@ -21,12 +22,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
/**
|
||||
* Handles all interaction of UI patch components.
|
||||
@ -35,13 +31,14 @@ import app.revanced.integrations.utils.ReVancedUtils;
|
||||
* Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long.
|
||||
*
|
||||
* Temporary work around:
|
||||
* Enable app spoofing to version 18.20.39 or older, as that uses a non litho Shorts player.
|
||||
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player.
|
||||
*
|
||||
* Permanent fix (yet to be implemented), either of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree
|
||||
* (and use that hook to force the shorts dislikes to update after the fetch is completed).
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
/**
|
||||
@ -75,12 +72,18 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (!rydEnabled) {
|
||||
// Must remove all values to protect against using stale data
|
||||
// if the user enables RYD while a video is on screen.
|
||||
currentVideoData = null;
|
||||
lastLithoShortsVideoData = null;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
clearData();
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearData() {
|
||||
currentVideoData = null;
|
||||
lastLithoShortsVideoData = null;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
// Rolling number text should not be cleared,
|
||||
// as it's used if incognito Short is opened/closed
|
||||
// while a regular video is on screen.
|
||||
}
|
||||
|
||||
//
|
||||
// 17.x non litho regular video player.
|
||||
@ -137,7 +140,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (oldUITextView == null) {
|
||||
return;
|
||||
}
|
||||
oldUIReplacementSpan = videoData.getDislikesSpanForRegularVideo(oldUIOriginalSpan, false);
|
||||
oldUIReplacementSpan = videoData.getDislikesSpanForRegularVideo(oldUIOriginalSpan, false, false);
|
||||
if (!oldUIReplacementSpan.equals(oldUITextView.getText())) {
|
||||
oldUITextView.setText(oldUIReplacementSpan);
|
||||
}
|
||||
@ -188,55 +191,70 @@ public class ReturnYouTubeDislikePatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* For Litho segmented buttons and Litho Shorts player.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@Nullable AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original) {
|
||||
return onLithoTextLoaded(conversionContext, textRef, original, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param textRef Cache reference to the like/dislike char sequence,
|
||||
* @param textRef Optional cache reference to the like/dislike char sequence,
|
||||
* which may or may not be the same as the original span parameter.
|
||||
* If dislikes are added, the atomic reference must be set to the replacement span.
|
||||
* @param original Original span that was created or reused by Litho.
|
||||
* @return The original span (if nothing should change), or a replacement span that contains dislikes.
|
||||
* @param original Original char sequence was created or reused by Litho.
|
||||
* @param isRollingNumber If the span is for a Rolling Number.
|
||||
* @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original) {
|
||||
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@Nullable AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original,
|
||||
boolean isRollingNumber) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
// Remove this log statement after the a/b new litho dislikes is fixed.
|
||||
LogHelper.printDebug(() -> "conversionContext: " + conversionContextString);
|
||||
|
||||
final Spanned replacement;
|
||||
final CharSequence replacement;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
// Regular video
|
||||
// Regular video.
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
return original; // User enabled RYD while a video was on screen.
|
||||
}
|
||||
replacement = videoData.getDislikesSpanForRegularVideo((Spannable) original, true);
|
||||
// When spoofing between 17.09.xx and 17.30.xx the UI is the old layout but uses litho
|
||||
// and the dislikes is "|dislike_button.eml|"
|
||||
// but spoofing to that range gives a broken UI layout so no point checking for that.
|
||||
} else if (conversionContextString.contains("|shorts_dislike_button.eml|")) {
|
||||
if (!(original instanceof Spanned)) {
|
||||
original = new SpannableString(original);
|
||||
}
|
||||
replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original,
|
||||
true, isRollingNumber);
|
||||
|
||||
// When spoofing between 17.09.xx and 17.30.xx the UI is the old layout
|
||||
// but uses litho and the dislikes is "|dislike_button.eml|".
|
||||
// But spoofing to that range gives a broken UI layout so no point checking for that.
|
||||
} else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) {
|
||||
// Litho Shorts player.
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
// Must clear the current video here, otherwise if the user opens a regular video
|
||||
// then opens a litho short (while keeping the regular video on screen), then closes the short,
|
||||
// the original video may show the incorrect dislike value.
|
||||
currentVideoData = null;
|
||||
clearData();
|
||||
return original;
|
||||
}
|
||||
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
|
||||
if (videoData == null) {
|
||||
// The Shorts litho video id filter did not detect the video id.
|
||||
// This is normal if in incognito mode, but otherwise is not normal.
|
||||
// This is normal in incognito mode, but otherwise is abnormal.
|
||||
LogHelper.printDebug(() -> "Cannot modify Shorts litho span, data is null");
|
||||
return original;
|
||||
}
|
||||
@ -250,12 +268,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
}
|
||||
LogHelper.printDebug(() -> "Using current video data for litho span");
|
||||
}
|
||||
replacement = videoData.getDislikeSpanForShort((Spannable) original);
|
||||
replacement = videoData.getDislikeSpanForShort((Spanned) original);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
|
||||
textRef.set(replacement);
|
||||
if (textRef != null) textRef.set(replacement);
|
||||
return replacement;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onLithoTextLoaded failure", ex);
|
||||
@ -263,6 +281,123 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
//
|
||||
// Rolling Number
|
||||
//
|
||||
|
||||
/**
|
||||
* Current regular video rolling number text, if rolling number is in use.
|
||||
* This is saved to a field as it's used in every draw() call.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile CharSequence rollingNumberSpan;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String onRollingNumberLoaded(@NonNull Object conversionContext,
|
||||
@NonNull String original) {
|
||||
try {
|
||||
CharSequence replacement = onLithoTextLoaded(conversionContext, null, original, true);
|
||||
if (!replacement.toString().equals(original)) {
|
||||
rollingNumberSpan = replacement;
|
||||
return replacement.toString();
|
||||
} // Else, the text was not a likes count but instead the view count or something else.
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onRollingNumberLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Rolling Number text view modifications made by this patch.
|
||||
* Required as it appears text views can be reused for other rolling numbers (view count, upload time, etc).
|
||||
*/
|
||||
private static void removeRollingNumberPatchChanges(TextView view) {
|
||||
if (view.getCompoundDrawablePadding() != 0) {
|
||||
LogHelper.printDebug(() -> "Removing rolling number styling from TextView");
|
||||
view.setCompoundDrawablePadding(0);
|
||||
view.setCompoundDrawables(null, null, null, null);
|
||||
view.setGravity(Gravity.NO_GRAVITY);
|
||||
view.setTextAlignment(View.TEXT_ALIGNMENT_INHERIT);
|
||||
view.setSingleLine(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Rolling Number text view modifications.
|
||||
*/
|
||||
private static void addRollingNumberPatchChanges(TextView view) {
|
||||
if (view.getCompoundDrawablePadding() == 0) {
|
||||
LogHelper.printDebug(() -> "Adding rolling number styling to TextView");
|
||||
// YouTube Rolling Numbers do not use compound drawables or drawable padding.
|
||||
//
|
||||
// Single line mode prevents entire words from being entirely clipped,
|
||||
// and instead only clips the portion of text that runs off.
|
||||
// The text should not clip due to the empty end padding,
|
||||
// but use the feature just in case.
|
||||
view.setSingleLine(true);
|
||||
// Center align to distribute the horizontal padding.
|
||||
view.setGravity(Gravity.CENTER);
|
||||
view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
|
||||
ShapeDrawable shapeDrawable = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||
view.setCompoundDrawables(shapeDrawable, null, null, null);
|
||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static CharSequence updateRollingNumber(TextView view, CharSequence original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
removeRollingNumberPatchChanges(view);
|
||||
return original;
|
||||
}
|
||||
// Called for all instances of RollingNumber, so must check if text is for a dislikes.
|
||||
// Text will already have the correct content but it's missing the drawable separators.
|
||||
if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString())) {
|
||||
// The text is the video view count, upload time, or some other text.
|
||||
removeRollingNumberPatchChanges(view);
|
||||
return original;
|
||||
}
|
||||
|
||||
CharSequence replacement = rollingNumberSpan;
|
||||
if (replacement == null) {
|
||||
// User enabled RYD while a video was open,
|
||||
// or user opened/closed a Short while a regular video was opened.
|
||||
LogHelper.printDebug(() -> "Cannot update rolling number (field is null");
|
||||
removeRollingNumberPatchChanges(view);
|
||||
return original;
|
||||
}
|
||||
|
||||
// TextView does not display the tall left separator correctly,
|
||||
// as it goes outside the height bounds and messes up the layout.
|
||||
// Fix this by applying the left separator as a text view compound drawable.
|
||||
// This creates a new issue as the compound drawable is not taken into the
|
||||
// layout width sizing, but that is fixed in the span itself where it uses a blank
|
||||
// padding string that adds to the layout width but is later ignored during UI drawing.
|
||||
if (SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) {
|
||||
// Do not apply any TextView changes, and text should always fit without clipping.
|
||||
removeRollingNumberPatchChanges(view);
|
||||
} else {
|
||||
addRollingNumberPatchChanges(view);
|
||||
}
|
||||
|
||||
// Remove any padding set by Rolling Number.
|
||||
view.setPadding(0, 0, 0, 0);
|
||||
|
||||
// When displaying dislikes, the rolling animation is not visually correct
|
||||
// and the dislikes always animate (even though the dislike count has not changed).
|
||||
// The animation is caused by an image span attached to the span,
|
||||
// and using only the modified segmented span prevents the animation from showing.
|
||||
return replacement;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "updateRollingNumber failure", ex);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Non litho Shorts player.
|
||||
@ -301,7 +436,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
// Must clear the data here, in case a new video was loaded while PlayerType
|
||||
// suggested the video was not a short (can happen when spoofing to an old app version).
|
||||
currentVideoData = null;
|
||||
clearData();
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "setShortsDislikes");
|
||||
@ -405,90 +540,59 @@ public class ReturnYouTubeDislikePatch {
|
||||
* Injection point. Uses 'playback response' video id hook to preload RYD.
|
||||
*/
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
try {
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
lastPrefetchedVideoId = videoId;
|
||||
LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId);
|
||||
ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "preloadVideoId failure", ex);
|
||||
}
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
lastPrefetchedVideoId = videoId;
|
||||
LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId);
|
||||
ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Uses 'current playing' video id hook. Always called on main thread.
|
||||
*/
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
newVideoLoaded(videoId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called both on and off main thread.
|
||||
*
|
||||
* @param isShortsLithoVideoId If the video id is from {@link ReturnYouTubeDislikeFilterPatch}.
|
||||
* if true, then the video id can be null indicating the filter did
|
||||
* not find any video id.
|
||||
*/
|
||||
public static void newVideoLoaded(@Nullable String videoId, boolean isShortsLithoVideoId) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
PlayerType currentPlayerType = PlayerType.getCurrent();
|
||||
final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized();
|
||||
if (isNoneHiddenOrSlidingMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
// Must clear here, otherwise the wrong data can be used for a minimized regular video.
|
||||
currentVideoData = null;
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isShortsLithoVideoId) {
|
||||
// Litho Shorts video.
|
||||
if (videoIdIsSame(lastLithoShortsVideoData, videoId)) {
|
||||
return;
|
||||
}
|
||||
if (videoId == null) {
|
||||
// Litho filter did not detect the video id. App is in incognito mode,
|
||||
// or the proto buffer structure was changed and the video id is no longer present.
|
||||
// Must clear both currently playing and last litho data otherwise the
|
||||
// next regular video may use the wrong data.
|
||||
LogHelper.printDebug(() -> "Litho filter did not find any video ids");
|
||||
currentVideoData = null;
|
||||
lastLithoShortsVideoData = null;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
return;
|
||||
}
|
||||
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
videoData.setVideoIdIsShort(true);
|
||||
lastLithoShortsVideoData = videoData;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
} else {
|
||||
Objects.requireNonNull(videoId);
|
||||
// All other playback (including non-litho Shorts).
|
||||
if (videoIdIsSame(currentVideoData, videoId)) {
|
||||
return;
|
||||
}
|
||||
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
// Pre-emptively set the data to short status.
|
||||
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
||||
if (isNoneHiddenOrSlidingMinimized) {
|
||||
data.setVideoIdIsShort(true);
|
||||
}
|
||||
currentVideoData = data;
|
||||
if (videoIdIsSame(currentVideoData, videoId)) {
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
||||
|
||||
LogHelper.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType
|
||||
+ " isShortsLithoHook: " + isShortsLithoVideoId);
|
||||
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
// Pre-emptively set the data to short status.
|
||||
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
||||
if (isNoneHiddenOrSlidingMinimized) {
|
||||
data.setVideoIdIsShort(true);
|
||||
}
|
||||
currentVideoData = data;
|
||||
|
||||
// Current video id hook can be called out of order with the non litho Shorts text view hook.
|
||||
// Must manually update again here.
|
||||
if (!isShortsLithoVideoId && isNoneHiddenOrSlidingMinimized) {
|
||||
if (isNoneHiddenOrSlidingMinimized) {
|
||||
updateOnScreenShortsTextViews(true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@ -496,6 +600,26 @@ public class ReturnYouTubeDislikePatch {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setLastLithoShortsVideoId(@Nullable String videoId) {
|
||||
if (videoIdIsSame(lastLithoShortsVideoData, videoId)) {
|
||||
return;
|
||||
}
|
||||
if (videoId == null) {
|
||||
// Litho filter did not detect the video id. App is in incognito mode,
|
||||
// or the proto buffer structure was changed and the video id is no longer present.
|
||||
// Must clear both currently playing and last litho data otherwise the
|
||||
// next regular video may use the wrong data.
|
||||
LogHelper.printDebug(() -> "Litho filter did not find any video ids");
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "New litho Shorts video id: " + videoId);
|
||||
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
videoData.setVideoIdIsShort(true);
|
||||
lastLithoShortsVideoData = videoData;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
}
|
||||
|
||||
private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) {
|
||||
return (fetch == null && videoId == null)
|
||||
|| (fetch != null && fetch.getVideoId().equals(videoId));
|
||||
|
@ -93,7 +93,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
// Must pass a null id to correctly clear out the current video data.
|
||||
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,
|
||||
// the new incognito Short will show the old prior data.
|
||||
ReturnYouTubeDislikePatch.newVideoLoaded(matchedVideoId, true);
|
||||
ReturnYouTubeDislikePatch.setLastLithoShortsVideoId(matchedVideoId);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -4,10 +4,19 @@ import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class SpoofAppVersionPatch {
|
||||
|
||||
private static final boolean SPOOF_APP_VERSION_ENABLED = SettingsEnum.SPOOF_APP_VERSION.getBoolean();
|
||||
private static final String SPOOF_APP_VERSION_TARGET = SettingsEnum.SPOOF_APP_VERSION_TARGET.getString();
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static String getYouTubeVersionOverride(String version) {
|
||||
if (SettingsEnum.SPOOF_APP_VERSION.getBoolean()) {
|
||||
return SettingsEnum.SPOOF_APP_VERSION_TARGET.getString();
|
||||
}
|
||||
if (SPOOF_APP_VERSION_ENABLED) return SPOOF_APP_VERSION_TARGET;
|
||||
return version;
|
||||
}
|
||||
|
||||
public static boolean isSpoofingToEqualOrLessThan(String version) {
|
||||
return SPOOF_APP_VERSION_ENABLED && SPOOF_APP_VERSION_TARGET.compareTo(version) <= 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
|
||||
@ -69,7 +70,7 @@ public class ReturnYouTubeDislike {
|
||||
* Must be less than 5 seconds, as per:
|
||||
* https://developer.android.com/topic/performance/vitals/anr
|
||||
*/
|
||||
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4500;
|
||||
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
|
||||
|
||||
/**
|
||||
* How long to retain successful RYD fetches.
|
||||
@ -84,9 +85,9 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
|
||||
* Can be any almost any non-visible character.
|
||||
* Must be something YouTube is unlikely to use, as it's searched for in all usage of Rolling Number.
|
||||
*/
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye'
|
||||
|
||||
/**
|
||||
* Cached lookup of all video ids.
|
||||
@ -115,6 +116,12 @@ public class ReturnYouTubeDislike {
|
||||
private static final Rect leftSeparatorBounds;
|
||||
private static final Rect middleSeparatorBounds;
|
||||
|
||||
/**
|
||||
* Left separator horizontal padding for Rolling Number layout.
|
||||
*/
|
||||
public static final int leftSeparatorShapePaddingPixels;
|
||||
private static final ShapeDrawable leftSeparatorShape;
|
||||
|
||||
static {
|
||||
DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics();
|
||||
|
||||
@ -124,6 +131,11 @@ public class ReturnYouTubeDislike {
|
||||
final int middleSeparatorSize =
|
||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
|
||||
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
|
||||
|
||||
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp);
|
||||
|
||||
leftSeparatorShape = new ShapeDrawable(new RectShape());
|
||||
leftSeparatorShape.setBounds(leftSeparatorBounds);
|
||||
}
|
||||
|
||||
private final String videoId;
|
||||
@ -167,19 +179,31 @@ public class ReturnYouTubeDislike {
|
||||
@GuardedBy("this")
|
||||
private SpannableString replacementLikeDislikeSpan;
|
||||
|
||||
private static int getSeparatorColor() {
|
||||
return ThemeHelper.isDarkTheme()
|
||||
? 0x33FFFFFF // transparent dark gray
|
||||
: 0xFFD9D9D9; // light gray
|
||||
}
|
||||
|
||||
public static ShapeDrawable getLeftSeparatorDrawable() {
|
||||
leftSeparatorShape.getPaint().setColor(getSeparatorColor());
|
||||
return leftSeparatorShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike.
|
||||
*/
|
||||
@NonNull
|
||||
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
|
||||
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber,
|
||||
@NonNull RYDVoteData voteData) {
|
||||
if (!isSegmentedButton) {
|
||||
// Simple replacement of 'dislike' with a number/percentage.
|
||||
return newSpannableWithDislikes(oldSpannable, voteData);
|
||||
}
|
||||
|
||||
// Note: Some locales use right to left layout (arabic, hebrew, etc),
|
||||
// and care must be taken to retain the existing RTL encoding character on the likes string,
|
||||
// otherwise text will incorrectly show as left to right.
|
||||
// Note: Some locales use right to left layout (Arabic, Hebrew, etc).
|
||||
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
|
||||
String oldLikesString = oldSpannable.toString();
|
||||
|
||||
@ -202,21 +226,25 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
final boolean compactLayout = SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean();
|
||||
final int separatorColor = ThemeHelper.isDarkTheme()
|
||||
? 0x29AAAAAA // transparent dark gray
|
||||
: 0xFFD9D9D9; // light gray
|
||||
|
||||
if (!compactLayout) {
|
||||
// left separator
|
||||
String leftSeparatorString = ReVancedUtils.isRightToLeftTextLayout()
|
||||
? "\u200F " // u200F = right to left character
|
||||
: "\u200E "; // u200E = left to right character
|
||||
Spannable leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||
ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape());
|
||||
shapeDrawable.getPaint().setColor(separatorColor);
|
||||
shapeDrawable.setBounds(leftSeparatorBounds);
|
||||
leftSeparatorSpan.setSpan(new VerticallyCenteredImageSpan(shapeDrawable), 1, 2,
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE); // drawable cannot overwrite RTL or LTR character
|
||||
? "\u200F" // u200F = right to left character
|
||||
: "\u200E"; // u200E = left to right character
|
||||
final Spannable leftSeparatorSpan;
|
||||
if (isRollingNumber) {
|
||||
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||
} else {
|
||||
leftSeparatorString += " ";
|
||||
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||
// Styling spans cannot overwrite RTL or LTR character.
|
||||
leftSeparatorSpan.setSpan(
|
||||
new VerticallyCenteredImageSpan(getLeftSeparatorDrawable(), false),
|
||||
1, 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
leftSeparatorSpan.setSpan(
|
||||
new FixedWidthEmptySpan(leftSeparatorShapePaddingPixels),
|
||||
2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
builder.append(leftSeparatorSpan);
|
||||
}
|
||||
|
||||
@ -230,21 +258,41 @@ public class ReturnYouTubeDislike {
|
||||
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
|
||||
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
|
||||
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
|
||||
shapeDrawable.getPaint().setColor(separatorColor);
|
||||
shapeDrawable.getPaint().setColor(getSeparatorColor());
|
||||
shapeDrawable.setBounds(middleSeparatorBounds);
|
||||
middleSeparatorSpan.setSpan(new VerticallyCenteredImageSpan(shapeDrawable), shapeInsertionIndex, shapeInsertionIndex + 1,
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
// Use original text width if using compact layout with Rolling Number,
|
||||
// as there is no empty padding to allow any layout width differences.
|
||||
middleSeparatorSpan.setSpan(
|
||||
new VerticallyCenteredImageSpan(shapeDrawable, isRollingNumber && compactLayout),
|
||||
shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
builder.append(middleSeparatorSpan);
|
||||
|
||||
// dislikes
|
||||
builder.append(newSpannableWithDislikes(oldSpannable, voteData));
|
||||
|
||||
// Add some padding for Rolling Number segmented span.
|
||||
// Use an empty width span, as the layout uses the measured text width and not the
|
||||
// actual span width. So adding padding and then removing it while drawing gives some
|
||||
// extra wiggle room for the left separator drawable (which is not included in layout width).
|
||||
if (isRollingNumber && !compactLayout) {
|
||||
// To test this, set the device system font to the smallest available.
|
||||
// If text clipping still occurs, then increase the number of padding spaces below.
|
||||
// Any extra width will be padded around the like/dislike string
|
||||
// as it's set to center text alignment.
|
||||
Spannable rightPaddingString = new SpannableString(" ");
|
||||
rightPaddingString.setSpan(new FixedWidthEmptySpan(0), 0,
|
||||
rightPaddingString.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
builder.append(rightPaddingString);
|
||||
}
|
||||
|
||||
return new SpannableString(builder);
|
||||
}
|
||||
|
||||
// Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick.
|
||||
private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) {
|
||||
return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1;
|
||||
/**
|
||||
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
||||
*/
|
||||
public static boolean isPreviouslyCreatedSegmentedSpan(@NonNull String text) {
|
||||
return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -429,8 +477,10 @@ public class ReturnYouTubeDislike {
|
||||
* @return the replacement span containing dislikes, or the original span if RYD is not available.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original, boolean isSegmentedButton) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, false);
|
||||
public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, isRollingNumber,false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -438,12 +488,13 @@ public class ReturnYouTubeDislike {
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false, true);
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false, false, true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber,
|
||||
boolean spanIsForShort) {
|
||||
try {
|
||||
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
|
||||
@ -481,7 +532,7 @@ public class ReturnYouTubeDislike {
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
}
|
||||
if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(original)) {
|
||||
if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(original.toString())) {
|
||||
// need to recreate using original, as original has prior outdated dislike values
|
||||
if (originalDislikeSpan == null) {
|
||||
// Should never happen.
|
||||
@ -497,7 +548,7 @@ public class ReturnYouTubeDislike {
|
||||
votingData.updateUsingVote(userVote);
|
||||
}
|
||||
originalDislikeSpan = original;
|
||||
replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, votingData);
|
||||
replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, isRollingNumber, votingData);
|
||||
LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '"
|
||||
+ replacementLikeDislikeSpan + "'" + " using video: " + videoId);
|
||||
|
||||
@ -567,9 +618,44 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles a Spannable with an empty fixed width.
|
||||
*/
|
||||
class FixedWidthEmptySpan extends ReplacementSpan {
|
||||
final int fixedWidth;
|
||||
/**
|
||||
* @param fixedWith Fixed width in screen pixels.
|
||||
*/
|
||||
FixedWidthEmptySpan(int fixedWith) {
|
||||
this.fixedWidth = fixedWith;
|
||||
if (fixedWith < 0) throw new IllegalArgumentException();
|
||||
}
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
|
||||
int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) {
|
||||
return fixedWidth;
|
||||
}
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
|
||||
float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
// Nothing to draw.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertically centers a Spanned Drawable.
|
||||
*/
|
||||
class VerticallyCenteredImageSpan extends ImageSpan {
|
||||
public VerticallyCenteredImageSpan(Drawable drawable) {
|
||||
final boolean useOriginalWidth;
|
||||
|
||||
/**
|
||||
* @param useOriginalWidth Use the original layout width of the text this span is applied to,
|
||||
* and not the bounds of the Drawable. Drawable is always displayed using it's own bounds,
|
||||
* and this setting only affects the layout width of the entire span.
|
||||
*/
|
||||
public VerticallyCenteredImageSpan(Drawable drawable, boolean useOriginalWidth) {
|
||||
super(drawable);
|
||||
this.useOriginalWidth = useOriginalWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -581,13 +667,17 @@ class VerticallyCenteredImageSpan extends ImageSpan {
|
||||
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
|
||||
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
|
||||
final int drawHeight = bounds.bottom - bounds.top;
|
||||
final int halfDrawHeight = drawHeight / 2;
|
||||
final int yCenter = paintMetrics.ascent + fontHeight / 2;
|
||||
|
||||
fontMetrics.ascent = yCenter - drawHeight / 2;
|
||||
fontMetrics.ascent = yCenter - halfDrawHeight;
|
||||
fontMetrics.top = fontMetrics.ascent;
|
||||
fontMetrics.bottom = yCenter + drawHeight / 2;
|
||||
fontMetrics.bottom = yCenter + halfDrawHeight;
|
||||
fontMetrics.descent = fontMetrics.bottom;
|
||||
}
|
||||
if (useOriginalWidth) {
|
||||
return (int) paint.measureText(text, start, end);
|
||||
}
|
||||
return bounds.right;
|
||||
}
|
||||
|
||||
@ -600,8 +690,13 @@ class VerticallyCenteredImageSpan extends ImageSpan {
|
||||
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
|
||||
final int yCenter = y + paintMetrics.descent - fontHeight / 2;
|
||||
final Rect drawBounds = drawable.getBounds();
|
||||
float translateX = x;
|
||||
if (useOriginalWidth) {
|
||||
// Horizontally center the drawable in the same space as the original text.
|
||||
translateX += (paint.measureText(text, start, end) - (drawBounds.right - drawBounds.left)) / 2;
|
||||
}
|
||||
final int translateY = yCenter - (drawBounds.bottom - drawBounds.top) / 2;
|
||||
canvas.translate(x, translateY);
|
||||
canvas.translate(translateX, translateY);
|
||||
drawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
* {@link #fetchVotes(String)} HTTP read timeout.
|
||||
* To locally debug and force timeouts, change this to a very small number (ie: 100)
|
||||
*/
|
||||
private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 5 * 1000; // 5 Seconds.
|
||||
private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds.
|
||||
|
||||
/**
|
||||
* Default connection and response timeout for voting and registration.
|
||||
|
@ -13,6 +13,7 @@ import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
@ -21,8 +22,7 @@ import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
SettingsEnum.SPOOF_APP_VERSION.getBoolean()
|
||||
&& SettingsEnum.SPOOF_APP_VERSION_TARGET.getString().compareTo("18.33.40") <= 0;
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
|
||||
|
||||
/**
|
||||
* If dislikes are shown on Shorts.
|
||||
|
@ -92,7 +92,7 @@ public class ReVancedUtils {
|
||||
* All tasks run at max thread priority.
|
||||
*/
|
||||
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
||||
2, // 2 threads always ready to go
|
||||
3, // 3 threads always ready to go
|
||||
Integer.MAX_VALUE,
|
||||
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
||||
TimeUnit.SECONDS,
|
||||
|
Loading…
x
Reference in New Issue
Block a user