From 6a94bd2237be9cde6256c83fcec72b3f0de83496 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 14 May 2023 11:28:49 +0400 Subject: [PATCH] fix(youtube/return-youtube-dislikes): fix temporarily frozen video after opening a shorts (#396) Co-authored-by: oSumAtrIX --- .../patches/ReturnYouTubeDislikePatch.java | 190 +++++++++++++++-- .../ReturnYouTubeDislike.java | 197 +++++++++++++----- .../requests/RYDVoteData.java | 3 - .../integrations/settings/SettingsEnum.java | 1 + .../ReturnYouTubeDislikeSettingsFragment.java | 18 ++ .../integrations/shared/PlayerType.kt | 52 ++++- 6 files changed, 383 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java index d621cf95..21014e7f 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -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> 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 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; } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index 5bee15ea..2e10d803 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -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 future; + final String videoId; + final long timeFetched; + RYDCachedFetch(@NonNull Future 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 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 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 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 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 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 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 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) { diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java index 969063e8..149f7ffd 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/RYDVoteData.java @@ -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 { diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index b50ca696..533aa74b 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -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 diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 6a8a0bb2..4fe380f8 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -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")); diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index 2cf2c4a9..47d2a097 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -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 + } + } \ No newline at end of file