mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-12-30 22:55:49 +01:00
fix(youtube/return-youtube-dislikes): fix temporarily frozen video after opening a shorts (#396)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
7ed0f46a7c
commit
6a94bd2237
@ -2,26 +2,42 @@ package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.graphics.Rect;
|
||||
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.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/**
|
||||
* Handles all interaction of UI patch components.
|
||||
*
|
||||
* Does not handle creating dislike spans or anything to do with {@link ReturnYouTubeDislikeApi}.
|
||||
*/
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
|
||||
|
||||
/**
|
||||
* Resource identifier of old UI dislike button.
|
||||
*/
|
||||
@ -60,7 +76,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (oldUIReplacementSpan == null || oldUIReplacementSpan.toString().equals(s.toString())) {
|
||||
return;
|
||||
}
|
||||
s.replace(0, s.length(), oldUIReplacementSpan);
|
||||
s.replace(0, s.length(), oldUIReplacementSpan); // Causes a recursive call back into this listener
|
||||
}
|
||||
};
|
||||
|
||||
@ -87,6 +103,8 @@ public class ReturnYouTubeDislikePatch {
|
||||
|| textView == null) {
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "setOldUILayoutDislikes");
|
||||
|
||||
if (oldUIOriginalSpan == null) {
|
||||
// Use value of the first instance, as it appears TextViews can be recycled
|
||||
// and might contain dislikes previously added by the patch.
|
||||
@ -97,23 +115,19 @@ public class ReturnYouTubeDislikePatch {
|
||||
textView.removeTextChangedListener(oldUiTextWatcher);
|
||||
textView.addTextChangedListener(oldUiTextWatcher);
|
||||
|
||||
/**
|
||||
* If the patch is changed to include the dislikes button as a parameter to this method,
|
||||
* then if the button is already selected the dislikes could be adjusted using
|
||||
* {@link ReturnYouTubeDislike#setUserVote(Vote)}
|
||||
*/
|
||||
|
||||
updateOldUIDislikesTextView();
|
||||
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setOldUILayoutDislikes failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
@ -158,21 +172,153 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replacement text to use for "Dislikes" while RYD is fetching.
|
||||
*/
|
||||
private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-");
|
||||
|
||||
/**
|
||||
* Dislikes TextViews used by Shorts.
|
||||
*
|
||||
* Multiple TextViews are loaded at once (for the prior and next videos to swipe to).
|
||||
* Keep track of all of them, and later pick out the correct one based on their on screen position.
|
||||
*/
|
||||
private static final List<WeakReference<TextView>> shortsTextViewRefs = new ArrayList<>();
|
||||
|
||||
private static void clearRemovedShortsTextViews() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
shortsTextViewRefs.removeIf(ref -> ref.get() == null);
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException(); // YouTube requires Android N or greater
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called when a Shorts dislike is updated.
|
||||
* Handles update asynchronously, otherwise Shorts video will be frozen while the UI thread is blocked.
|
||||
*
|
||||
* @return if RYD is enabled and the TextView was updated
|
||||
*/
|
||||
public static boolean setShortsDislikes(@NonNull View likeDislikeView) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean() || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "setShortsDislikes");
|
||||
|
||||
TextView textView = (TextView) likeDislikeView;
|
||||
textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text
|
||||
shortsTextViewRefs.add(new WeakReference<>(textView));
|
||||
|
||||
if (likeDislikeView.isSelected() && isShortTextViewOnScreen(textView)) {
|
||||
LogHelper.printDebug(() -> "Shorts dislike is already selected");
|
||||
ReturnYouTubeDislike.setUserVote(Vote.DISLIKE);
|
||||
}
|
||||
|
||||
// For the first short played, the shorts dislike hook is called after the video id hook.
|
||||
// But for most other times this hook is called before the video id (which is not ideal).
|
||||
// Must update the TextViews here, and also after the videoId changes.
|
||||
updateOnScreenShortsTextViews(false);
|
||||
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setShortsDislikes failure", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param forceUpdate if false, then only update the 'loading text views.
|
||||
* If true, update all on screen text views.
|
||||
*/
|
||||
private static void updateOnScreenShortsTextViews(boolean forceUpdate) {
|
||||
try {
|
||||
clearRemovedShortsTextViews();
|
||||
if (shortsTextViewRefs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "updateShortsTextViews");
|
||||
String videoId = VideoInformation.getVideoId();
|
||||
|
||||
Runnable update = () -> {
|
||||
Spanned shortsDislikesSpan = ReturnYouTubeDislike.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
|
||||
ReVancedUtils.runOnMainThreadNowOrLater(() -> {
|
||||
if (!videoId.equals(VideoInformation.getVideoId())) {
|
||||
// User swiped to new video before fetch completed
|
||||
LogHelper.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update text views that appear to be visible on screen.
|
||||
// Only 1 will be the actual textview for the current Short,
|
||||
// but discarded and not yet garbage collected views can remain.
|
||||
// So must set the dislike span on all views that match.
|
||||
for (WeakReference<TextView> textViewRef : shortsTextViewRefs) {
|
||||
TextView textView = textViewRef.get();
|
||||
if (textView == null) {
|
||||
continue;
|
||||
}
|
||||
if (isShortTextViewOnScreen(textView)
|
||||
&& (forceUpdate || textView.getText().toString().equals(SHORTS_LOADING_SPAN.toString()))) {
|
||||
LogHelper.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan);
|
||||
textView.setText(shortsDislikesSpan);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
if (ReturnYouTubeDislike.fetchCompleted()) {
|
||||
update.run(); // Network call is completed, no need to wait on background thread.
|
||||
} else {
|
||||
ReVancedUtils.runOnBackgroundThread(update);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "updateVisibleShortsTextViews failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a view is within the screen bounds.
|
||||
*/
|
||||
private static boolean isShortTextViewOnScreen(@NonNull View view) {
|
||||
final int[] location = new int[2];
|
||||
view.getLocationInWindow(location);
|
||||
if (location[0] <= 0 && location[1] <= 0) { // Lower bound
|
||||
return false;
|
||||
}
|
||||
Rect windowRect = new Rect();
|
||||
view.getWindowVisibleDisplayFrame(windowRect); // Upper bound
|
||||
return location[0] < windowRect.width() && location[1] < windowRect.height();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a Shorts dislike Spanned is created.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned original) {
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
|
||||
if (!videoId.equals(currentVideoId)) {
|
||||
currentVideoId = videoId;
|
||||
|
||||
final boolean noneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
if (noneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
ReturnYouTubeDislike.setCurrentVideoId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
|
||||
if (noneHiddenOrMinimized) {
|
||||
// Shorts TextView hook can be called out of order with the video id hook.
|
||||
// Must manually update again here.
|
||||
updateOnScreenShortsTextViews(true);
|
||||
}
|
||||
}
|
||||
return ReturnYouTubeDislike.getDislikeSpanForShort(original);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
LogHelper.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,10 +333,14 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrMinimized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
ReturnYouTubeDislike.sendVote(v);
|
||||
|
||||
updateOldUIDislikesTextView();
|
||||
return;
|
||||
}
|
||||
|
@ -25,8 +25,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
@ -45,13 +48,47 @@ import app.revanced.integrations.utils.ThemeHelper;
|
||||
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
|
||||
*/
|
||||
public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Simple wrapper to cache a Future.
|
||||
*/
|
||||
private static class RYDCachedFetch {
|
||||
/**
|
||||
* How long to retain cached RYD fetches.
|
||||
*/
|
||||
static final long CACHE_TIMEOUT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes
|
||||
|
||||
@NonNull
|
||||
final Future<RYDVoteData> future;
|
||||
final String videoId;
|
||||
final long timeFetched;
|
||||
RYDCachedFetch(@NonNull Future<RYDVoteData> future, @NonNull String videoId) {
|
||||
this.future = Objects.requireNonNull(future);
|
||||
this.videoId = Objects.requireNonNull(videoId);
|
||||
this.timeFetched = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean isExpired(long now) {
|
||||
return (now - timeFetched) > CACHE_TIMEOUT_MILLISECONDS;
|
||||
}
|
||||
|
||||
boolean futureInProgressOrFinishedSuccessfully() {
|
||||
try {
|
||||
return !future.isDone() || future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS) != null;
|
||||
} catch (ExecutionException | InterruptedException | TimeoutException ex) {
|
||||
LogHelper.printInfo(() -> "failed to lookup cache", ex); // will never happen
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
|
||||
*
|
||||
* 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_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE = 4000;
|
||||
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
|
||||
|
||||
/**
|
||||
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
|
||||
@ -59,6 +96,12 @@ public class ReturnYouTubeDislike {
|
||||
*/
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character
|
||||
|
||||
/**
|
||||
* Cached lookup of RYD fetches.
|
||||
*/
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static final Map<String, RYDCachedFetch> futureCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Used to send votes, one by one, in the same order the user created them.
|
||||
*/
|
||||
@ -85,6 +128,13 @@ public class ReturnYouTubeDislike {
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Future<RYDVoteData> voteFetchFuture;
|
||||
|
||||
/**
|
||||
* Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Vote userVote;
|
||||
|
||||
/**
|
||||
* Original dislike span, before modifications.
|
||||
*/
|
||||
@ -135,13 +185,25 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setCurrentVideoId(@Nullable String videoId) {
|
||||
public static void setCurrentVideoId(@Nullable String videoId) {
|
||||
synchronized (videoIdLockObject) {
|
||||
if (videoId == null && currentVideoId != null) {
|
||||
LogHelper.printDebug(() -> "Clearing data");
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
final long now = System.currentTimeMillis();
|
||||
futureCache.values().removeIf(value -> {
|
||||
final boolean expired = value.isExpired(now);
|
||||
if (expired) LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId);
|
||||
return expired;
|
||||
});
|
||||
} else {
|
||||
throw new IllegalStateException(); // YouTube requires Android N or greater
|
||||
}
|
||||
currentVideoId = videoId;
|
||||
dislikeDataIsShort = false;
|
||||
userVote = null;
|
||||
voteFetchFuture = null;
|
||||
originalDislikeSpan = null;
|
||||
replacementLikeDislikeSpan = null;
|
||||
@ -154,7 +216,7 @@ public class ReturnYouTubeDislike {
|
||||
public static void clearCache() {
|
||||
synchronized (videoIdLockObject) {
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
LogHelper.printDebug(() -> "Clearing cache");
|
||||
LogHelper.printDebug(() -> "Clearing replacement spans");
|
||||
}
|
||||
replacementLikeDislikeSpan = null;
|
||||
}
|
||||
@ -198,11 +260,16 @@ public class ReturnYouTubeDislike {
|
||||
// If a Short is opened while a regular video is on screen, this will incorrectly set this as false.
|
||||
// But this check is needed to fix unusual situations of opening/closing the app
|
||||
// while both a regular video and a short are on screen.
|
||||
dislikeDataIsShort = PlayerType.getCurrent().isNoneOrHidden();
|
||||
dislikeDataIsShort = PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
|
||||
// No need to wrap the call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call.
|
||||
RYDCachedFetch entry = futureCache.get(videoId);
|
||||
if (entry != null && entry.futureInProgressOrFinishedSuccessfully()) {
|
||||
LogHelper.printDebug(() -> "Using cached RYD fetch: "+ entry.videoId);
|
||||
voteFetchFuture = entry.future;
|
||||
return;
|
||||
}
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
futureCache.put(videoId, new RYDCachedFetch(voteFetchFuture, videoId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,13 +307,28 @@ public class ReturnYouTubeDislike {
|
||||
@NonNull
|
||||
private static Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) {
|
||||
try {
|
||||
Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
|
||||
if (fetchFuture == null) {
|
||||
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
|
||||
return oldSpannable;
|
||||
}
|
||||
// Absolutely cannot be holding any lock during get().
|
||||
RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
if (votingData == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
|
||||
return oldSpannable;
|
||||
}
|
||||
|
||||
// Must check against existing replacements, after the fetch,
|
||||
// otherwise concurrent threads can create the same replacement same multiple times.
|
||||
// Also do the replacement comparison and creation in a single synchronized block.
|
||||
synchronized (videoIdLockObject) {
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
if (spansHaveEqualTextAndColor(replacementLikeDislikeSpan, oldSpannable)) {
|
||||
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) {
|
||||
if (spansHaveEqualTextAndColor(oldSpannable, replacementLikeDislikeSpan)) {
|
||||
LogHelper.printDebug(() -> "Ignoring previously created dislikes span");
|
||||
return oldSpannable;
|
||||
}
|
||||
if (spansHaveEqualTextAndColor(Objects.requireNonNull(originalDislikeSpan), oldSpannable)) {
|
||||
if (spansHaveEqualTextAndColor(oldSpannable, originalDislikeSpan)) {
|
||||
LogHelper.printDebug(() -> "Replacing span with previously created dislike span");
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
@ -258,31 +340,19 @@ public class ReturnYouTubeDislike {
|
||||
return oldSpannable;
|
||||
}
|
||||
oldSpannable = originalDislikeSpan;
|
||||
} else {
|
||||
originalDislikeSpan = oldSpannable; // most up to date original
|
||||
}
|
||||
}
|
||||
|
||||
// Must block the current thread until fetching is done.
|
||||
// There's no known way to edit the text after creation yet.
|
||||
Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
|
||||
if (fetchFuture == null) {
|
||||
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
|
||||
return oldSpannable;
|
||||
}
|
||||
RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||
if (votingData == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
|
||||
return oldSpannable;
|
||||
}
|
||||
// No replacement span exist, create it now.
|
||||
|
||||
SpannableString replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
|
||||
synchronized (videoIdLockObject) {
|
||||
replacementLikeDislikeSpan = replacement;
|
||||
if (userVote != null) {
|
||||
votingData.updateUsingVote(userVote);
|
||||
}
|
||||
originalDislikeSpan = oldSpannable;
|
||||
replacementLikeDislikeSpan = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
|
||||
LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + replacementLikeDislikeSpan + "'");
|
||||
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
final Spanned oldSpannableLogging = oldSpannable;
|
||||
LogHelper.printDebug(() -> "Replaced: '" + oldSpannableLogging + "' with: '" + replacement + "'");
|
||||
return replacement;
|
||||
} catch (TimeoutException e) {
|
||||
LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast
|
||||
} catch (Exception e) {
|
||||
@ -291,13 +361,22 @@ public class ReturnYouTubeDislike {
|
||||
return oldSpannable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the RYD fetch call has completed.
|
||||
*/
|
||||
public static boolean fetchCompleted() {
|
||||
Future<RYDVoteData> future = getVoteFetchFuture();
|
||||
return future != null && future.isDone();
|
||||
}
|
||||
|
||||
public static void sendVote(@NonNull Vote vote) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
Objects.requireNonNull(vote);
|
||||
try {
|
||||
// Must make a local copy of videoId, since it may change between now and when the vote thread runs.
|
||||
String videoIdToVoteFor = getCurrentVideoId();
|
||||
if (videoIdToVoteFor == null || dislikeDataIsShort != PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
if (videoIdToVoteFor == null ||
|
||||
(SettingsEnum.RYD_SHORTS.getBoolean() && dislikeDataIsShort != PlayerType.getCurrent().isNoneHiddenOrMinimized())) {
|
||||
// User enabled RYD after starting playback of a video.
|
||||
// Or shorts was loaded with regular video present, then shorts was closed,
|
||||
// and then user voted on the now visible original video.
|
||||
@ -317,27 +396,48 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
});
|
||||
|
||||
clearCache(); // UI needs updating
|
||||
|
||||
// Update the downloaded vote data.
|
||||
Future<RYDVoteData> future = getVoteFetchFuture();
|
||||
if (future == null) {
|
||||
LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null");
|
||||
return;
|
||||
}
|
||||
// The future should always be completed before user can like/dislike, but use a timeout just in case.
|
||||
RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||
if (voteData == null) {
|
||||
// RYD fetch failed
|
||||
LogHelper.printDebug(() -> "Cannot update UI (vote data not available)");
|
||||
return;
|
||||
}
|
||||
voteData.updateUsingVote(vote);
|
||||
setUserVote(vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Error trying to send vote", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setUserVote(@NonNull Vote vote) {
|
||||
Objects.requireNonNull(vote);
|
||||
try {
|
||||
LogHelper.printDebug(() -> "setUserVote: " + vote);
|
||||
|
||||
// Update the downloaded vote data.
|
||||
Future<RYDVoteData> future = getVoteFetchFuture();
|
||||
if (future != null && future.isDone()) {
|
||||
RYDVoteData voteData;
|
||||
try {
|
||||
voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} catch (ExecutionException | InterruptedException | TimeoutException ex) {
|
||||
// Should never happen
|
||||
LogHelper.printInfo(() -> "Could not update vote data", ex);
|
||||
return;
|
||||
}
|
||||
if (voteData == null) {
|
||||
// RYD fetch failed
|
||||
LogHelper.printDebug(() -> "Cannot update UI (vote data not available)");
|
||||
return;
|
||||
}
|
||||
|
||||
voteData.updateUsingVote(vote);
|
||||
} // Else, vote will be applied after vote data is received
|
||||
|
||||
synchronized (videoIdLockObject) {
|
||||
if (userVote != vote) {
|
||||
userVote = vote;
|
||||
clearCache(); // UI needs updating
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setUserVote failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered.
|
||||
*
|
||||
@ -363,6 +463,7 @@ public class ReturnYouTubeDislike {
|
||||
/**
|
||||
* @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) {
|
||||
if (!isSegmentedButton) {
|
||||
// Simple replacement of 'dislike' with a number/percentage.
|
||||
@ -482,7 +583,7 @@ public class ReturnYouTubeDislike {
|
||||
: formatDislikeCount(voteData.getDislikeCount()));
|
||||
}
|
||||
|
||||
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) {
|
||||
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
|
||||
SpannableString destination = new SpannableString(newSpanText);
|
||||
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
|
||||
for (Object span : spans) {
|
||||
|
@ -82,15 +82,12 @@ public final class RYDVoteData {
|
||||
|
||||
public void updateUsingVote(Vote vote) {
|
||||
if (vote == Vote.LIKE) {
|
||||
LogHelper.printDebug(() -> "Increasing like count");
|
||||
likeCount = fetchedLikeCount + 1;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
} else if (vote == Vote.DISLIKE) {
|
||||
LogHelper.printDebug(() -> "Increasing dislike count");
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount + 1;
|
||||
} else if (vote == Vote.LIKE_REMOVE) {
|
||||
LogHelper.printDebug(() -> "Resetting like/dislike to fetched values");
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
} else {
|
||||
|
@ -148,6 +148,7 @@ public enum SettingsEnum {
|
||||
RYD_ENABLED("ryd_enabled", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE),
|
||||
RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE),
|
||||
RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
|
||||
RYD_SHORTS("ryd_shorts", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
|
||||
RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
|
||||
|
||||
// SponsorBlock settings
|
||||
|
@ -19,6 +19,11 @@ import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
|
||||
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
|
||||
/**
|
||||
* If dislikes are shown on Shorts.
|
||||
*/
|
||||
private SwitchPreference shortsPreference;
|
||||
|
||||
/**
|
||||
* If dislikes are shown as percentage.
|
||||
*/
|
||||
@ -30,6 +35,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
private SwitchPreference compactLayoutPreference;
|
||||
|
||||
private void updateUIState() {
|
||||
shortsPreference.setEnabled(SettingsEnum.RYD_SHORTS.isAvailable());
|
||||
percentagePreference.setEnabled(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.isAvailable());
|
||||
compactLayoutPreference.setEnabled(SettingsEnum.RYD_USE_COMPACT_LAYOUT.isAvailable());
|
||||
}
|
||||
@ -58,6 +64,18 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
});
|
||||
preferenceScreen.addPreference(enabledPreference);
|
||||
|
||||
shortsPreference = new SwitchPreference(context);
|
||||
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
|
||||
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
|
||||
shortsPreference.setSummaryOn(str("revanced_ryd_shorts_summary_on"));
|
||||
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
|
||||
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
SettingsEnum.RYD_SHORTS.saveValue(newValue);
|
||||
updateUIState();
|
||||
return true;
|
||||
});
|
||||
preferenceScreen.addPreference(shortsPreference);
|
||||
|
||||
percentagePreference = new SwitchPreference(context);
|
||||
percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean());
|
||||
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
|
||||
|
@ -7,20 +7,39 @@ import app.revanced.integrations.utils.Event
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class PlayerType {
|
||||
NONE, // includes Shorts and Stories playback
|
||||
HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened
|
||||
/**
|
||||
* Includes Shorts and Stories playback.
|
||||
*/
|
||||
NONE,
|
||||
/**
|
||||
* A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened.
|
||||
*/
|
||||
HIDDEN,
|
||||
/**
|
||||
* When spoofing to an old version of YouTube, and watching a short with a regular video in the background,
|
||||
* the type will be this (and not [HIDDEN]).
|
||||
*/
|
||||
WATCH_WHILE_MINIMIZED,
|
||||
WATCH_WHILE_MAXIMIZED,
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
/**
|
||||
* When opening a short while a regular video is minimized, the type can momentarily be this.
|
||||
*/
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
INLINE_MINIMAL, // home feed video playback
|
||||
/**
|
||||
* Home feed video playback.
|
||||
*/
|
||||
INLINE_MINIMAL,
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
|
||||
companion object {
|
||||
|
||||
private val nameToPlayerType = values().associateBy { it.name }
|
||||
|
||||
/**
|
||||
* safely parse from a string
|
||||
*
|
||||
@ -29,11 +48,11 @@ enum class PlayerType {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun safeParseFromString(name: String): PlayerType? {
|
||||
return values().firstOrNull { it.name == name }
|
||||
return nameToPlayerType[name]
|
||||
}
|
||||
|
||||
/**
|
||||
* the current player type, as reported by [app.revanced.integrations.patches.PlayerTypeHookPatch.YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX]
|
||||
* The current player type.
|
||||
*/
|
||||
@JvmStatic
|
||||
var current
|
||||
@ -53,11 +72,30 @@ enum class PlayerType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current player type is [NONE] or [HIDDEN]
|
||||
* Check if the current player type is [NONE] or [HIDDEN].
|
||||
* Useful to check if a short is currently playing.
|
||||
*
|
||||
* @return True, if nothing, a Short, or a Story is playing.
|
||||
* Does not include the first moment after a short is opened when a regular video is minimized on screen,
|
||||
* or while watching a short with a regular video present on a spoofed old version of YouTube.
|
||||
* To include those situations instead use [isNoneHiddenOrMinimized].
|
||||
*/
|
||||
fun isNoneOrHidden(): Boolean {
|
||||
return this == NONE || this == HIDDEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current player type is [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED].
|
||||
*
|
||||
* Useful to check if a Short is being played,
|
||||
* although can return false positive if the player is minimized.
|
||||
*
|
||||
* @return If nothing, a Short, a Story,
|
||||
* or a regular minimized video is sliding off screen to a dismissed or hidden state.
|
||||
*/
|
||||
fun isNoneHiddenOrMinimized(): Boolean {
|
||||
return this == NONE || this == HIDDEN
|
||||
|| this == WATCH_WHILE_MINIMIZED
|
||||
|| this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user