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:
LisoUseInAIKyrios 2023-05-14 11:28:49 +04:00 committed by GitHub
parent 7ed0f46a7c
commit 6a94bd2237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 383 additions and 78 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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