mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 13:17:46 +02:00
refactor: Bump ReVanced Patcher & merge integrations by using ReVanced Patches Gradle plugin
BREAKING CHANGE: ReVanced Patcher >= 21 required
This commit is contained in:
@ -0,0 +1,46 @@
|
||||
package app.revanced.extension.youtube.patches.ads;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AdsPatch {
|
||||
private static final boolean hideGeneralAdsEnabled = Settings.HIDE_GENERAL_ADS.get();
|
||||
private static final boolean hideGetPremiumAdsEnabled = Settings.HIDE_GET_PREMIUM.get();
|
||||
private static final boolean hideVideoAdsEnabled = Settings.HIDE_VIDEO_ADS.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Hide the view, which shows ads in the homepage.
|
||||
*
|
||||
* @param view The view, which shows ads.
|
||||
*/
|
||||
public static void hideAdAttributionView(View view) {
|
||||
hideViewBy0dpUnderCondition(hideGeneralAdsEnabled, view);
|
||||
}
|
||||
|
||||
public static boolean hideGetPremium() {
|
||||
return hideGetPremiumAdsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideVideoAds() {
|
||||
return !hideVideoAdsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Only used by old clients.
|
||||
* It is presumed to have been deprecated, and if it is confirmed that it is no longer used, remove it.
|
||||
*/
|
||||
public static boolean hideVideoAds(boolean original) {
|
||||
return !hideVideoAdsEnabled && original;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,721 @@
|
||||
package app.revanced.extension.youtube.patches.alternativethumbnails;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_HOME;
|
||||
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY;
|
||||
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER;
|
||||
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH;
|
||||
import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
import org.chromium.net.impl.CronetUrlRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
* Alternative YouTube thumbnails.
|
||||
* <p>
|
||||
* Can show YouTube provided screen captures of beginning/middle/end of the video.
|
||||
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
|
||||
* <p>
|
||||
* Or can show crowd-sourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
|
||||
* <p>
|
||||
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
|
||||
* <p>
|
||||
* Has an additional option to use 'fast' video still thumbnails,
|
||||
* where it forces sd thumbnail quality and skips verifying if the alt thumbnail image exists.
|
||||
* The UI loading time will be the same or better than using original thumbnails,
|
||||
* but thumbnails will initially fail to load for all live streams, unreleased, and occasionally very old videos.
|
||||
* If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail
|
||||
* is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution,
|
||||
* because a noticeable number of videos do not have hq720 and too much fail to load.
|
||||
*/
|
||||
public final class AlternativeThumbnailsPatch {
|
||||
|
||||
// These must be class declarations if declared here,
|
||||
// otherwise the app will not load due to cyclic initialization errors.
|
||||
public static final class DeArrowAvailability implements Setting.Availability {
|
||||
public static boolean usingDeArrowAnywhere() {
|
||||
return ALT_THUMBNAIL_HOME.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_LIBRARY.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_PLAYER.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_SEARCH.get().useDeArrow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return usingDeArrowAnywhere();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StillImagesAvailability implements Setting.Availability {
|
||||
public static boolean usingStillImagesAnywhere() {
|
||||
return ALT_THUMBNAIL_HOME.get().useStillImages
|
||||
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages
|
||||
|| ALT_THUMBNAIL_LIBRARY.get().useStillImages
|
||||
|| ALT_THUMBNAIL_PLAYER.get().useStillImages
|
||||
|| ALT_THUMBNAIL_SEARCH.get().useStillImages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return usingStillImagesAnywhere();
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThumbnailOption {
|
||||
ORIGINAL(false, false),
|
||||
DEARROW(true, false),
|
||||
DEARROW_STILL_IMAGES(true, true),
|
||||
STILL_IMAGES(false, true);
|
||||
|
||||
final boolean useDeArrow;
|
||||
final boolean useStillImages;
|
||||
|
||||
ThumbnailOption(boolean useDeArrow, boolean useStillImages) {
|
||||
this.useDeArrow = useDeArrow;
|
||||
this.useStillImages = useStillImages;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThumbnailStillTime {
|
||||
BEGINNING(1),
|
||||
MIDDLE(2),
|
||||
END(3);
|
||||
|
||||
/**
|
||||
* The url alt image number. Such as the 2 in 'hq720_2.jpg'
|
||||
*/
|
||||
final int altImageNumber;
|
||||
|
||||
ThumbnailStillTime(int altImageNumber) {
|
||||
this.altImageNumber = altImageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Uri dearrowApiUri;
|
||||
|
||||
/**
|
||||
* The scheme and host of {@link #dearrowApiUri}.
|
||||
*/
|
||||
private static final String deArrowApiUrlPrefix;
|
||||
|
||||
/**
|
||||
* How long to temporarily turn off DeArrow if it fails for any reason.
|
||||
*/
|
||||
private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||
|
||||
/**
|
||||
* Regex to match youtube static thumbnails domain.
|
||||
* Used to find and replace blocked domain with a working ones
|
||||
*/
|
||||
private static final String YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX = "(yt[3-4]|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com";
|
||||
|
||||
private static final Pattern YOUTUBE_STATIC_THUMBNAILS_DOMAIN_PATTERN = Pattern.compile(YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX);
|
||||
|
||||
/**
|
||||
* If non zero, then the system time of when DeArrow API calls can resume.
|
||||
*/
|
||||
private static volatile long timeToResumeDeArrowAPICalls;
|
||||
|
||||
static {
|
||||
dearrowApiUri = validateSettings();
|
||||
final int port = dearrowApiUri.getPort();
|
||||
String portString = port == -1 ? "" : (":" + port);
|
||||
deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/";
|
||||
Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix any bad imported data.
|
||||
*/
|
||||
private static Uri validateSettings() {
|
||||
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
||||
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
||||
String scheme = apiUri.getScheme();
|
||||
if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) {
|
||||
Utils.showToastLong(str("revanced_alt_thumbnail_dearrow_api_url_invalid_toast"));
|
||||
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
|
||||
Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
|
||||
return validateSettings();
|
||||
}
|
||||
return apiUri;
|
||||
}
|
||||
|
||||
private static ThumbnailOption optionSettingForCurrentNavigation() {
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (RootView.isPlayerActive()) {
|
||||
return ALT_THUMBNAIL_PLAYER.get();
|
||||
}
|
||||
|
||||
// Must check second, as search can be from any tab.
|
||||
if (RootView.isSearchBarActive()) {
|
||||
return ALT_THUMBNAIL_SEARCH.get();
|
||||
}
|
||||
|
||||
// Avoid checking which navigation button is selected, if all other settings are the same.
|
||||
ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get();
|
||||
ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get();
|
||||
ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get();
|
||||
if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) {
|
||||
return homeOption; // All are the same option.
|
||||
}
|
||||
|
||||
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
|
||||
if (selectedNavButton == null) {
|
||||
// Unknown tab, treat as the home tab;
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.HOME) {
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
|
||||
return subscriptionsOption;
|
||||
}
|
||||
// A library tab variant is active.
|
||||
return libraryOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using YouTube provided still video captures.
|
||||
*
|
||||
* @param decodedUrl Decoded original thumbnail request url.
|
||||
* @return The alternative thumbnail url, or the original url. Both without tracking parameters.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
|
||||
@NonNull ThumbnailQuality qualityToUse) {
|
||||
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
|
||||
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
|
||||
return sanitizedReplacement;
|
||||
}
|
||||
return decodedUrl.sanitizedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using DeArrow thumbnail cache.
|
||||
*
|
||||
* @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short).
|
||||
* @param fallbackUrl URL to fall back to in case.
|
||||
* @return The alternative thumbnail url, without tracking parameters.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) {
|
||||
// Build thumbnail request url.
|
||||
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
|
||||
return dearrowApiUri
|
||||
.buildUpon()
|
||||
.appendQueryParameter("videoID", videoId)
|
||||
.appendQueryParameter("redirectUrl", fallbackUrl)
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static boolean urlIsDeArrow(@NonNull String imageUrl) {
|
||||
return imageUrl.startsWith(deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If this client has not recently experienced any DeArrow API errors.
|
||||
*/
|
||||
private static boolean canUseDeArrowAPI() {
|
||||
if (timeToResumeDeArrowAPICalls == 0) {
|
||||
return true;
|
||||
}
|
||||
if (timeToResumeDeArrowAPICalls < System.currentTimeMillis()) {
|
||||
Logger.printDebug(() -> "Resuming DeArrow API calls");
|
||||
timeToResumeDeArrowAPICalls = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void handleDeArrowError(@NonNull String url, int statusCode) {
|
||||
Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url);
|
||||
final long now = System.currentTimeMillis();
|
||||
if (timeToResumeDeArrowAPICalls < now) {
|
||||
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
|
||||
if (Settings.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.get()) {
|
||||
String toastMessage = (statusCode != 0)
|
||||
? str("revanced_alt_thumbnail_dearrow_error", statusCode)
|
||||
: str("revanced_alt_thumbnail_dearrow_error_generic");
|
||||
Utils.showToastLong(toastMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all url images loaded, including video thumbnails.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
ThumbnailOption option = optionSettingForCurrentNavigation();
|
||||
|
||||
if (option == ThumbnailOption.ORIGINAL) {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
|
||||
if (decodedUrl == null) {
|
||||
return originalUrl; // Not a thumbnail.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality);
|
||||
if (qualityToUse == null) {
|
||||
// Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these).
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
String sanitizedReplacementUrl;
|
||||
final boolean includeTracking;
|
||||
if (option.useDeArrow && canUseDeArrowAPI()) {
|
||||
includeTracking = false; // Do not include view tracking parameters with API call.
|
||||
final String fallbackUrl = option.useStillImages
|
||||
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
|
||||
: decodedUrl.sanitizedUrl;
|
||||
|
||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
||||
} else if (option.useStillImages) {
|
||||
includeTracking = true; // Include view tracking parameters if present.
|
||||
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
} else {
|
||||
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
|
||||
}
|
||||
|
||||
// Do not log any tracking parameters.
|
||||
Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl);
|
||||
|
||||
return includeTracking
|
||||
? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters
|
||||
: sanitizedReplacementUrl;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Cronet considers all completed connections as a success, even if the response is 404 or 5xx.
|
||||
*/
|
||||
public static void handleCronetSuccess(UrlRequest request, @NonNull UrlResponseInfo responseInfo) {
|
||||
try {
|
||||
final int statusCode = responseInfo.getHttpStatusCode();
|
||||
if (statusCode == 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = responseInfo.getUrl();
|
||||
|
||||
if (urlIsDeArrow(url)) {
|
||||
Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
|
||||
if (statusCode == 304) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
|
||||
return; // Normal response.
|
||||
}
|
||||
handleDeArrowError(url, statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 404) {
|
||||
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
||||
// The video is:
|
||||
// - live stream
|
||||
// - upcoming unreleased video
|
||||
// - very old
|
||||
// - very low view count
|
||||
// Take note of this, so if the image reloads the original thumbnail will be used.
|
||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url);
|
||||
if (decodedUrl == null) {
|
||||
return; // Not a thumbnail.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + url);
|
||||
|
||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
||||
if (quality == null) {
|
||||
// Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen.
|
||||
Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Callback success error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* To test failure cases, try changing the API URL to each of:
|
||||
* - A non-existent domain.
|
||||
* - A url path of something incorrect (ie: /v1/nonExistentEndPoint).
|
||||
* <p>
|
||||
* Cronet uses a very timeout (several minutes), so if the API never responds this hook can take a while to be called.
|
||||
* But this does not appear to be a problem, as the DeArrow API has not been observed to 'go silent'
|
||||
* Instead if there's a problem it returns an error code status response, which is handled in this patch.
|
||||
*/
|
||||
public static void handleCronetFailure(UrlRequest request,
|
||||
@Nullable UrlResponseInfo responseInfo,
|
||||
IOException exception) {
|
||||
try {
|
||||
String url = ((CronetUrlRequest) request).getHookedUrl();
|
||||
if (urlIsDeArrow(url)) {
|
||||
Logger.printDebug(() -> "handleCronetFailure, exception: " + exception);
|
||||
final int statusCode = (responseInfo != null)
|
||||
? responseInfo.getHttpStatusCode()
|
||||
: 0;
|
||||
handleDeArrowError(url, statusCode);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Callback failure error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ThumbnailQuality {
|
||||
// In order of lowest to highest resolution.
|
||||
DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg
|
||||
MQDEFAULT("mqdefault", "mq"),
|
||||
HQDEFAULT("hqdefault", "hq"),
|
||||
SDDEFAULT("sddefault", "sd"),
|
||||
HQ720("hq720", "hq720_"),
|
||||
MAXRESDEFAULT("maxresdefault", "maxres");
|
||||
|
||||
/**
|
||||
* Lookup map of original name to enum.
|
||||
*/
|
||||
private static final Map<String, ThumbnailQuality> originalNameToEnum = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Lookup map of alt name to enum. ie: "hq720_1" to {@link #HQ720}.
|
||||
*/
|
||||
private static final Map<String, ThumbnailQuality> altNameToEnum = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (ThumbnailQuality quality : values()) {
|
||||
originalNameToEnum.put(quality.originalName, quality);
|
||||
|
||||
for (ThumbnailStillTime time : ThumbnailStillTime.values()) {
|
||||
// 'custom' thumbnails set by the content creator.
|
||||
// These show up in place of regular thumbnails
|
||||
// and seem to be limited to the same [1, 3] range as the still captures.
|
||||
originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality);
|
||||
|
||||
altNameToEnum.put(quality.altImageName + time.altImageNumber, quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an alt image name to enum.
|
||||
* ie: "hq720_2" returns {@link #HQ720}.
|
||||
*/
|
||||
@Nullable
|
||||
static ThumbnailQuality altImageNameToQuality(@NonNull String altImageName) {
|
||||
return altNameToEnum.get(altImageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Original quality to effective alt quality to use.
|
||||
* ie: If fast alt image is enabled, then "hq720" returns {@link #SDDEFAULT}.
|
||||
*/
|
||||
@Nullable
|
||||
static ThumbnailQuality getQualityToUse(@NonNull String originalSize) {
|
||||
ThumbnailQuality quality = originalNameToEnum.get(originalSize);
|
||||
if (quality == null) {
|
||||
return null; // Not a thumbnail for a regular video.
|
||||
}
|
||||
|
||||
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
||||
switch (quality) {
|
||||
case SDDEFAULT:
|
||||
// SD alt images have somewhat worse quality with washed out color and poor contrast.
|
||||
// But the 720 images look much better and don't suffer from these issues.
|
||||
// For unknown reasons, the 720 thumbnails are used only for the home feed,
|
||||
// while SD is used for the search and subscription feed
|
||||
// (even though search and subscriptions use the exact same layout as the home feed).
|
||||
// Of note, this image quality issue only appears with the alt thumbnail images,
|
||||
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
||||
// Fix this by falling thru and upgrading SD to 720.
|
||||
case HQ720:
|
||||
if (useFastQuality) {
|
||||
return SDDEFAULT; // SD is max resolution for fast alt images.
|
||||
}
|
||||
return HQ720;
|
||||
case MAXRESDEFAULT:
|
||||
if (useFastQuality) {
|
||||
return SDDEFAULT;
|
||||
}
|
||||
return MAXRESDEFAULT;
|
||||
default:
|
||||
return quality;
|
||||
}
|
||||
}
|
||||
|
||||
final String originalName;
|
||||
final String altImageName;
|
||||
|
||||
ThumbnailQuality(String originalName, String altImageName) {
|
||||
this.originalName = originalName;
|
||||
this.altImageName = altImageName;
|
||||
}
|
||||
|
||||
String getAltImageNameToUse() {
|
||||
return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses HTTP HEAD requests to verify and keep track of which thumbnail sizes
|
||||
* are available and not available.
|
||||
*/
|
||||
private static class VerifiedQualities {
|
||||
/**
|
||||
* After a quality is verified as not available, how long until the quality is re-verified again.
|
||||
* Used only if fast mode is not enabled. Intended for live streams and unreleased videos
|
||||
* that are now finished and available (and thus, the alt thumbnails are also now available).
|
||||
*/
|
||||
private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes.
|
||||
|
||||
/**
|
||||
* Cache used to verify if an alternative thumbnails exists for a given video id.
|
||||
*/
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, VerifiedQualities> altVideoIdLookup = new LinkedHashMap<>(100) {
|
||||
private static final int CACHE_LIMIT = 1000;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
};
|
||||
|
||||
private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) {
|
||||
synchronized (altVideoIdLookup) {
|
||||
VerifiedQualities verified = altVideoIdLookup.get(videoId);
|
||||
if (verified == null) {
|
||||
if (returnNullIfDoesNotExist) {
|
||||
return null;
|
||||
}
|
||||
verified = new VerifiedQualities();
|
||||
altVideoIdLookup.put(videoId, verified);
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||
@NonNull String imageUrl) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get());
|
||||
if (verified == null) return true; // Fast alt thumbnails is enabled.
|
||||
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl);
|
||||
}
|
||||
|
||||
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, false);
|
||||
//noinspection ConstantConditions
|
||||
verified.setQualityVerified(videoId, quality, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highest quality verified as existing.
|
||||
*/
|
||||
@Nullable
|
||||
private ThumbnailQuality highestQualityVerified;
|
||||
/**
|
||||
* Lowest quality verified as not existing.
|
||||
*/
|
||||
@Nullable
|
||||
private ThumbnailQuality lowestQualityNotAvailable;
|
||||
|
||||
/**
|
||||
* System time, of when to invalidate {@link #lowestQualityNotAvailable}.
|
||||
* Used only if fast mode is not enabled.
|
||||
*/
|
||||
private long timeToReVerifyLowestQuality;
|
||||
|
||||
private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) {
|
||||
if (isVerified) {
|
||||
if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) {
|
||||
highestQualityVerified = quality;
|
||||
}
|
||||
} else {
|
||||
if (lowestQualityNotAvailable == null || lowestQualityNotAvailable.ordinal() > quality.ordinal()) {
|
||||
lowestQualityNotAvailable = quality;
|
||||
timeToReVerifyLowestQuality = System.currentTimeMillis() + NOT_AVAILABLE_TIMEOUT_MILLISECONDS;
|
||||
}
|
||||
Logger.printDebug(() -> quality + " not available for video: " + videoId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request.
|
||||
*/
|
||||
synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||
@NonNull String imageUrl) {
|
||||
if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) {
|
||||
return true; // Previously verified as existing.
|
||||
}
|
||||
|
||||
final boolean fastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
||||
if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) {
|
||||
if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) {
|
||||
return false; // Previously verified as not existing.
|
||||
}
|
||||
// Enough time has passed, and should re-verify again.
|
||||
Logger.printDebug(() -> "Resetting lowest verified quality for: " + videoId);
|
||||
lowestQualityNotAvailable = null;
|
||||
}
|
||||
|
||||
if (fastQuality) {
|
||||
return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails.
|
||||
}
|
||||
|
||||
boolean imageFileFound;
|
||||
try {
|
||||
// This hooked code is running on a low priority thread, and it's slightly faster
|
||||
// to run the url connection thru the integrations thread pool which runs at the highest priority.
|
||||
final long start = System.currentTimeMillis();
|
||||
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
||||
final int connectionTimeoutMillis = 10000; // 10 seconds.
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
|
||||
connection.setConnectTimeout(connectionTimeoutMillis);
|
||||
connection.setReadTimeout(connectionTimeoutMillis);
|
||||
connection.setRequestMethod("HEAD");
|
||||
// Even with a HEAD request, the response is the same size as a full GET request.
|
||||
// Using an empty range fixes this.
|
||||
connection.setRequestProperty("Range", "bytes=0-0");
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
||||
String contentType = connection.getContentType();
|
||||
return (contentType != null && contentType.startsWith("image"));
|
||||
}
|
||||
if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl);
|
||||
}
|
||||
return false;
|
||||
}).get();
|
||||
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl);
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
|
||||
imageFileFound = false;
|
||||
}
|
||||
|
||||
setQualityVerified(videoId, quality, imageFileFound);
|
||||
return imageFileFound;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube video thumbnail url, decoded into it's relevant parts.
|
||||
*/
|
||||
private static class DecodedThumbnailUrl {
|
||||
/**
|
||||
* YouTube thumbnail URL prefix. Can be '/vi/' or '/vi_webp/'
|
||||
*/
|
||||
private static final String YOUTUBE_THUMBNAIL_PREFIX = "https://i.ytimg.com/vi";
|
||||
|
||||
@Nullable
|
||||
static DecodedThumbnailUrl decodeImageUrl(String url) {
|
||||
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
|
||||
if (videoIdStartIndex <= 0) return null;
|
||||
|
||||
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
|
||||
if (videoIdEndIndex < 0) return null;
|
||||
|
||||
final int imageSizeStartIndex = videoIdEndIndex + 1;
|
||||
final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex);
|
||||
if (imageSizeEndIndex < 0) return null;
|
||||
|
||||
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
||||
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
||||
|
||||
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
|
||||
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
final String originalFullUrl;
|
||||
/**
|
||||
* Full usable url, but stripped of any tracking information.
|
||||
*/
|
||||
final String sanitizedUrl;
|
||||
/**
|
||||
* Url up to the video ID.
|
||||
*/
|
||||
final String urlPrefix;
|
||||
final String videoId;
|
||||
/**
|
||||
* Quality, such as hq720 or sddefault.
|
||||
*/
|
||||
final String imageQuality;
|
||||
/**
|
||||
* JPG or WEBP
|
||||
*/
|
||||
final String imageExtension;
|
||||
/**
|
||||
* User view tracking parameters, only present on some images.
|
||||
*/
|
||||
final String viewTrackingParameters;
|
||||
|
||||
DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
|
||||
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
||||
originalFullUrl = fullUrl;
|
||||
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
|
||||
urlPrefix = fullUrl.substring(0, videoIdStartIndex);
|
||||
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
|
||||
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
|
||||
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
||||
viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
|
||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection SameParameterValue
|
||||
*/
|
||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||
// especially for new videos uploaded in the last hour.
|
||||
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
|
||||
// (as much as 4x slower has been observed, despite the alt webp image being a smaller file).
|
||||
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
|
||||
builder.append(urlPrefix);
|
||||
builder.append(videoId).append('/');
|
||||
builder.append(qualityToUse.getAltImageNameToUse());
|
||||
builder.append('.').append(imageExtension);
|
||||
if (includeViewTracking) {
|
||||
builder.append(viewTrackingParameters);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ActionButtonsFilter extends Filter {
|
||||
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
|
||||
private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
|
||||
|
||||
private final StringFilterGroup actionBarRule;
|
||||
private final StringFilterGroup bufferFilterPathRule;
|
||||
private final StringFilterGroup likeSubscribeGlow;
|
||||
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
public ActionButtonsFilter() {
|
||||
actionBarRule = new StringFilterGroup(
|
||||
null,
|
||||
VIDEO_ACTION_BAR_PATH_PREFIX
|
||||
);
|
||||
addIdentifierCallbacks(actionBarRule);
|
||||
|
||||
bufferFilterPathRule = new StringFilterGroup(
|
||||
null,
|
||||
"|ContainerType|button.eml|"
|
||||
);
|
||||
likeSubscribeGlow = new StringFilterGroup(
|
||||
Settings.DISABLE_LIKE_DISLIKE_GLOW,
|
||||
"animated_button_border.eml"
|
||||
);
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_LIKE_DISLIKE_BUTTON,
|
||||
"|segmented_like_dislike_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_DOWNLOAD_BUTTON,
|
||||
"|download_button.eml|"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_CLIP_BUTTON,
|
||||
"|clip_button.eml|"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_PLAYLIST_BUTTON,
|
||||
"|save_to_playlist_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_REWARDS_BUTTON,
|
||||
"account_link_button"
|
||||
),
|
||||
bufferFilterPathRule,
|
||||
likeSubscribeGlow
|
||||
);
|
||||
|
||||
bufferButtonsGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_REPORT_BUTTON,
|
||||
"yt_outline_flag"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHARE_BUTTON,
|
||||
"yt_outline_share"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_REMIX_BUTTON,
|
||||
"yt_outline_youtube_shorts_plus"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHOP_BUTTON,
|
||||
"yt_outline_bag"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_THANKS_BUTTON,
|
||||
"yt_outline_dollar_sign_heart"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isEveryFilterGroupEnabled() {
|
||||
for (StringFilterGroup group : pathCallbacks)
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
for (ByteArrayFilterGroup group : bufferButtonsGroupList)
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) {
|
||||
return false;
|
||||
}
|
||||
if (matchedGroup == likeSubscribeGlow) {
|
||||
if (!path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (matchedGroup == bufferFilterPathRule) {
|
||||
// In case the group list has no match, return false.
|
||||
if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* If A/B testing is applied, ad components can only be filtered by identifier
|
||||
* <p>
|
||||
* Before A/B testing:
|
||||
* Identifier: video_display_button_group_layout.eml
|
||||
* Path: video_display_button_group_layout.eml|ContainerType|....
|
||||
* (Path always starts with an Identifier)
|
||||
* <p>
|
||||
* After A/B testing:
|
||||
* Identifier: video_display_button_group_layout.eml
|
||||
* Path: video_lockup_with_attachment.eml|ContainerType|....
|
||||
* (Path does not contain an Identifier)
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class AdsFilter extends Filter {
|
||||
|
||||
private final StringFilterGroup playerShoppingShelf;
|
||||
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
||||
|
||||
public AdsFilter() {
|
||||
|
||||
// Identifiers.
|
||||
|
||||
final StringFilterGroup alertBannerPromo = new StringFilterGroup(
|
||||
Settings.HIDE_PROMOTION_ALERT_BANNER,
|
||||
"alert_banner_promo.eml"
|
||||
);
|
||||
|
||||
// Keywords checked in 2024:
|
||||
final StringFilterGroup generalAdsIdentifier = new StringFilterGroup(
|
||||
Settings.HIDE_GENERAL_ADS,
|
||||
// "brand_video_shelf.eml"
|
||||
"brand_video",
|
||||
|
||||
// "carousel_footered_layout.eml"
|
||||
"carousel_footered_layout",
|
||||
|
||||
// "composite_concurrent_carousel_layout"
|
||||
"composite_concurrent_carousel_layout",
|
||||
|
||||
// "landscape_image_wide_button_layout.eml"
|
||||
"landscape_image_wide_button_layout",
|
||||
|
||||
// "square_image_layout.eml"
|
||||
"square_image_layout",
|
||||
|
||||
// "statement_banner.eml"
|
||||
"statement_banner",
|
||||
|
||||
// "video_display_full_layout.eml"
|
||||
"video_display_full_layout",
|
||||
|
||||
// "text_image_button_group_layout.eml"
|
||||
// "video_display_button_group_layout.eml"
|
||||
"_button_group_layout",
|
||||
|
||||
// "banner_text_icon_buttoned_layout.eml"
|
||||
// "video_display_compact_buttoned_layout.eml"
|
||||
// "video_display_full_buttoned_layout.eml"
|
||||
"_buttoned_layout",
|
||||
|
||||
// "compact_landscape_image_layout.eml"
|
||||
// "full_width_portrait_image_layout.eml"
|
||||
// "full_width_square_image_layout.eml"
|
||||
"_image_layout"
|
||||
);
|
||||
|
||||
final StringFilterGroup merchandise = new StringFilterGroup(
|
||||
Settings.HIDE_MERCHANDISE_SHELF,
|
||||
"product_carousel",
|
||||
"shopping_carousel"
|
||||
);
|
||||
|
||||
final StringFilterGroup paidContent = new StringFilterGroup(
|
||||
Settings.HIDE_PAID_PROMOTION_LABEL,
|
||||
"paid_content_overlay"
|
||||
);
|
||||
|
||||
final StringFilterGroup selfSponsor = new StringFilterGroup(
|
||||
Settings.HIDE_SELF_SPONSOR_CARDS,
|
||||
"cta_shelf_card"
|
||||
);
|
||||
|
||||
final StringFilterGroup viewProducts = new StringFilterGroup(
|
||||
Settings.HIDE_VIEW_PRODUCTS,
|
||||
"product_item",
|
||||
"products_in_video",
|
||||
"shopping_overlay"
|
||||
);
|
||||
|
||||
final StringFilterGroup webSearchPanel = new StringFilterGroup(
|
||||
Settings.HIDE_WEB_SEARCH_RESULTS,
|
||||
"web_link_panel",
|
||||
"web_result_panel"
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(
|
||||
alertBannerPromo,
|
||||
generalAdsIdentifier,
|
||||
merchandise,
|
||||
paidContent,
|
||||
selfSponsor,
|
||||
viewProducts,
|
||||
webSearchPanel
|
||||
);
|
||||
|
||||
// Path.
|
||||
|
||||
final StringFilterGroup generalAdsPath = new StringFilterGroup(
|
||||
Settings.HIDE_GENERAL_ADS,
|
||||
"carousel_ad",
|
||||
"carousel_headered_layout",
|
||||
"hero_promo_image",
|
||||
"legal_disclosure",
|
||||
"lumiere_promo_carousel",
|
||||
"primetime_promo",
|
||||
"product_details",
|
||||
"text_image_button_layout",
|
||||
"video_display_carousel_button",
|
||||
"watch_metadata_app_promo"
|
||||
);
|
||||
|
||||
playerShoppingShelf = new StringFilterGroup(
|
||||
null,
|
||||
"horizontal_shelf.eml"
|
||||
);
|
||||
|
||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||
"shopping_item_card_list.eml"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
generalAdsPath,
|
||||
playerShoppingShelf,
|
||||
viewProducts
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == playerShoppingShelf) {
|
||||
if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class CarouselShelfFilter extends Filter {
|
||||
private static final String BROWSE_ID_HOME = "FEwhat_to_watch";
|
||||
private static final String BROWSE_ID_LIBRARY = "FElibrary";
|
||||
private static final String BROWSE_ID_NOTIFICATION = "FEactivity";
|
||||
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
|
||||
private static final String BROWSE_ID_PLAYLIST = "VLPL";
|
||||
private static final String BROWSE_ID_SUBSCRIPTION = "FEsubscriptions";
|
||||
|
||||
private static final Supplier<Stream<String>> knownBrowseId = () -> Stream.of(
|
||||
BROWSE_ID_HOME,
|
||||
BROWSE_ID_NOTIFICATION,
|
||||
BROWSE_ID_PLAYLIST,
|
||||
BROWSE_ID_SUBSCRIPTION
|
||||
);
|
||||
|
||||
private static final Supplier<Stream<String>> whitelistBrowseId = () -> Stream.of(
|
||||
BROWSE_ID_LIBRARY,
|
||||
BROWSE_ID_NOTIFICATION_INBOX
|
||||
);
|
||||
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
public final StringFilterGroup horizontalShelf;
|
||||
|
||||
public CarouselShelfFilter() {
|
||||
exceptions.addPattern("library_recent_shelf.eml");
|
||||
|
||||
final StringFilterGroup carouselShelf = new StringFilterGroup(
|
||||
Settings.HIDE_CAROUSEL_SHELF,
|
||||
"horizontal_shelf_inline.eml",
|
||||
"horizontal_tile_shelf.eml",
|
||||
"horizontal_video_shelf.eml"
|
||||
);
|
||||
|
||||
horizontalShelf = new StringFilterGroup(
|
||||
Settings.HIDE_CAROUSEL_SHELF,
|
||||
"horizontal_shelf.eml"
|
||||
);
|
||||
|
||||
addPathCallbacks(carouselShelf, horizontalShelf);
|
||||
}
|
||||
|
||||
private static boolean hideShelves(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) {
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (playerActive) {
|
||||
return false;
|
||||
}
|
||||
// Must check second, as search can be from any tab.
|
||||
if (searchBarActive) {
|
||||
return true;
|
||||
}
|
||||
// Unknown tab, treat the same as home.
|
||||
if (selectedNavButton == null) {
|
||||
return true;
|
||||
}
|
||||
return knownBrowseId.get().anyMatch(browseId::equals) || whitelistBrowseId.get().noneMatch(browseId::equals);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (exceptions.matches(path)) {
|
||||
return false;
|
||||
}
|
||||
final boolean playerActive = RootView.isPlayerActive();
|
||||
final boolean searchBarActive = RootView.isSearchBarActive();
|
||||
final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton();
|
||||
final String navigation = navigationButton == null ? "null" : navigationButton.name();
|
||||
final String browseId = RootView.getBrowseId();
|
||||
final boolean hideShelves = matchedGroup != horizontalShelf || hideShelves(playerActive, searchBarActive, navigationButton, browseId);
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation);
|
||||
if (!hideShelves) {
|
||||
return false;
|
||||
}
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class CommentsFilter extends Filter {
|
||||
private static final String COMMENT_COMPOSER_PATH = "comment_composer";
|
||||
private static final String COMMENT_ENTRY_POINT_TEASER_PATH = "comments_entry_point_teaser";
|
||||
private static final Pattern COMMENT_PREVIEW_TEXT_PATTERN = Pattern.compile("comments_entry_point_teaser.+ContainerType");
|
||||
private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment";
|
||||
private static final String VIDEO_METADATA_CAROUSEL_PATH = "video_metadata_carousel.eml";
|
||||
|
||||
private final StringFilterGroup comments;
|
||||
private final StringFilterGroup commentsPreviewDots;
|
||||
private final StringFilterGroup createShorts;
|
||||
private final StringFilterGroup previewCommentText;
|
||||
private final StringFilterGroup thanks;
|
||||
private final StringFilterGroup timeStampAndEmojiPicker;
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
|
||||
public CommentsFilter() {
|
||||
exceptions.addPatterns("macro_markers_list_item");
|
||||
|
||||
final StringFilterGroup channelGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_CHANNEL_GUIDELINES,
|
||||
"channel_guidelines_entry_banner",
|
||||
"community_guidelines",
|
||||
"sponsorships_comments_upsell"
|
||||
);
|
||||
|
||||
comments = new StringFilterGroup(
|
||||
null,
|
||||
VIDEO_METADATA_CAROUSEL_PATH,
|
||||
"comments_"
|
||||
);
|
||||
|
||||
commentsPreviewDots = new StringFilterGroup(
|
||||
Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD,
|
||||
"|ContainerType|ContainerType|ContainerType|"
|
||||
);
|
||||
|
||||
createShorts = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENT_CREATE_SHORTS_BUTTON,
|
||||
"composer_short_creation_button"
|
||||
);
|
||||
|
||||
final StringFilterGroup membersBanner = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_BY_MEMBERS,
|
||||
"sponsorships_comments_header.eml",
|
||||
"sponsorships_comments_footer.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup previewComment = new StringFilterGroup(
|
||||
Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD,
|
||||
"|carousel_item.",
|
||||
"|carousel_listener",
|
||||
COMMENT_ENTRY_POINT_TEASER_PATH,
|
||||
"comments_entry_point_simplebox"
|
||||
);
|
||||
|
||||
previewCommentText = new StringFilterGroup(
|
||||
Settings.HIDE_PREVIEW_COMMENT_NEW_METHOD,
|
||||
COMMENT_ENTRY_POINT_TEASER_PATH
|
||||
);
|
||||
|
||||
thanks = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENT_THANKS_BUTTON,
|
||||
"|super_thanks_button.eml"
|
||||
);
|
||||
|
||||
timeStampAndEmojiPicker = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS,
|
||||
"|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|"
|
||||
);
|
||||
|
||||
|
||||
addIdentifierCallbacks(channelGuidelines);
|
||||
|
||||
addPathCallbacks(
|
||||
comments,
|
||||
commentsPreviewDots,
|
||||
createShorts,
|
||||
membersBanner,
|
||||
previewComment,
|
||||
previewCommentText,
|
||||
thanks,
|
||||
timeStampAndEmojiPicker
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (exceptions.matches(path))
|
||||
return false;
|
||||
|
||||
if (matchedGroup == createShorts || matchedGroup == thanks || matchedGroup == timeStampAndEmojiPicker) {
|
||||
if (path.startsWith(COMMENT_COMPOSER_PATH)) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == comments) {
|
||||
if (path.startsWith(FEED_VIDEO_PATH)) {
|
||||
if (Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (Settings.HIDE_COMMENTS_SECTION.get()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == commentsPreviewDots) {
|
||||
if (path.startsWith(VIDEO_METADATA_CAROUSEL_PATH)) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == previewCommentText) {
|
||||
if (COMMENT_PREVIEW_TEXT_PATTERN.matcher(path).find()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Allows custom filtering using a path and optionally a proto buffer string.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class CustomFilter extends Filter {
|
||||
|
||||
private static void showInvalidSyntaxToast(@NonNull String expression) {
|
||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
||||
}
|
||||
|
||||
private static class CustomFilterGroup extends StringFilterGroup {
|
||||
/**
|
||||
* Optional character for the path that indicates the custom filter path must match the start.
|
||||
* Must be the first character of the expression.
|
||||
*/
|
||||
public static final String SYNTAX_STARTS_WITH = "^";
|
||||
|
||||
/**
|
||||
* Optional character that separates the path from a proto buffer string pattern.
|
||||
*/
|
||||
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
||||
|
||||
/**
|
||||
* @return the parsed objects
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
||||
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
|
||||
if (rawCustomFilterText.isBlank()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Map key is the path including optional special characters (^ and/or $)
|
||||
Map<String, CustomFilterGroup> result = new HashMap<>();
|
||||
Pattern pattern = Pattern.compile(
|
||||
"(" // map key group
|
||||
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
|
||||
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
|
||||
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
|
||||
+ ")" // end map key group
|
||||
+ "(.*)"); // optional buffer string
|
||||
|
||||
for (String expression : rawCustomFilterText.split("\n")) {
|
||||
if (expression.isBlank()) continue;
|
||||
|
||||
Matcher matcher = pattern.matcher(expression);
|
||||
if (!matcher.find()) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
continue;
|
||||
}
|
||||
|
||||
final String mapKey = matcher.group(1);
|
||||
final boolean pathStartsWith = !matcher.group(2).isEmpty();
|
||||
final String path = matcher.group(3);
|
||||
final boolean hasBufferSymbol = !matcher.group(4).isEmpty();
|
||||
final String bufferString = matcher.group(5);
|
||||
|
||||
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use one group object for all expressions with the same path.
|
||||
// This ensures the buffer is searched exactly once
|
||||
// when multiple paths are used with different buffer strings.
|
||||
CustomFilterGroup group = result.get(mapKey);
|
||||
if (group == null) {
|
||||
group = new CustomFilterGroup(pathStartsWith, path);
|
||||
result.put(mapKey, group);
|
||||
}
|
||||
if (hasBufferSymbol) {
|
||||
group.addBufferString(bufferString);
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
final boolean startsWith;
|
||||
ByteTrieSearch bufferSearch;
|
||||
|
||||
CustomFilterGroup(boolean startsWith, @NonNull String path) {
|
||||
super(Settings.CUSTOM_FILTER, path);
|
||||
this.startsWith = startsWith;
|
||||
}
|
||||
|
||||
void addBufferString(@NonNull String bufferString) {
|
||||
if (bufferSearch == null) {
|
||||
bufferSearch = new ByteTrieSearch();
|
||||
}
|
||||
bufferSearch.addPattern(bufferString.getBytes());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("CustomFilterGroup{");
|
||||
builder.append("path=");
|
||||
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
|
||||
builder.append(filters[0]);
|
||||
|
||||
if (bufferSearch != null) {
|
||||
String delimitingCharacter = "❙";
|
||||
builder.append(", bufferStrings=");
|
||||
builder.append(delimitingCharacter);
|
||||
for (byte[] bufferString : bufferSearch.getPatterns()) {
|
||||
builder.append(new String(bufferString));
|
||||
builder.append(delimitingCharacter);
|
||||
}
|
||||
}
|
||||
builder.append("}");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public CustomFilter() {
|
||||
Collection<CustomFilterGroup> groups = CustomFilterGroup.parseCustomFilterGroups();
|
||||
|
||||
if (!groups.isEmpty()) {
|
||||
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
|
||||
Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray));
|
||||
addPathCallbacks(groupsArray);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// All callbacks are custom filter groups.
|
||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||
if (custom.startsWith && contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class DescriptionsFilter extends Filter {
|
||||
private final ByteArrayFilterGroupList macroMarkerShelfGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup howThisWasMadeSection;
|
||||
private final StringFilterGroup infoCardsSection;
|
||||
private final StringFilterGroup macroMarkerShelf;
|
||||
private final StringFilterGroup shoppingLinks;
|
||||
|
||||
public DescriptionsFilter() {
|
||||
// game section, music section and places section now use the same identifier in the latest version.
|
||||
final StringFilterGroup attributesSection = new StringFilterGroup(
|
||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||
"gaming_section.eml",
|
||||
"music_section.eml",
|
||||
"place_section.eml",
|
||||
"video_attributes_section.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup podcastSection = new StringFilterGroup(
|
||||
Settings.HIDE_PODCAST_SECTION,
|
||||
"playlist_section.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup transcriptSection = new StringFilterGroup(
|
||||
Settings.HIDE_TRANSCRIPT_SECTION,
|
||||
"transcript_section.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup videoSummarySection = new StringFilterGroup(
|
||||
Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION,
|
||||
"cell_expandable_metadata.eml-js"
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(
|
||||
attributesSection,
|
||||
podcastSection,
|
||||
transcriptSection,
|
||||
videoSummarySection
|
||||
);
|
||||
|
||||
howThisWasMadeSection = new StringFilterGroup(
|
||||
Settings.HIDE_CONTENTS_SECTION,
|
||||
"how_this_was_made_section.eml"
|
||||
);
|
||||
|
||||
infoCardsSection = new StringFilterGroup(
|
||||
Settings.HIDE_INFO_CARDS_SECTION,
|
||||
"infocards_section.eml"
|
||||
);
|
||||
|
||||
macroMarkerShelf = new StringFilterGroup(
|
||||
null,
|
||||
"macro_markers_carousel.eml"
|
||||
);
|
||||
|
||||
shoppingLinks = new StringFilterGroup(
|
||||
Settings.HIDE_SHOPPING_LINKS,
|
||||
"expandable_list.",
|
||||
"shopping_description_shelf"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
howThisWasMadeSection,
|
||||
infoCardsSection,
|
||||
macroMarkerShelf,
|
||||
shoppingLinks
|
||||
);
|
||||
|
||||
macroMarkerShelfGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_CHAPTERS_SECTION,
|
||||
"chapters_horizontal_shelf"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_KEY_CONCEPTS_SECTION,
|
||||
"learning_concept_macro_markers_carousel_shelf"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// Check for the index because of likelihood of false positives.
|
||||
if (matchedGroup == howThisWasMadeSection || matchedGroup == infoCardsSection || matchedGroup == shoppingLinks) {
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
} else if (matchedGroup == macroMarkerShelf) {
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (!macroMarkerShelfGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class FeedComponentsFilter extends Filter {
|
||||
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
|
||||
"horizontalCollectionSwipeProtector=null";
|
||||
private static final String CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER =
|
||||
"heightConstraint=null";
|
||||
private static final String INLINE_EXPANSION_PATH = "inline_expansion";
|
||||
private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment";
|
||||
|
||||
private static final ByteArrayFilterGroup inlineExpansion =
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_EXPANDABLE_CHIP,
|
||||
"inline_expansion"
|
||||
);
|
||||
|
||||
private static final ByteArrayFilterGroup mixPlaylists =
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_MIX_PLAYLISTS,
|
||||
"&list="
|
||||
);
|
||||
private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions =
|
||||
new ByteArrayFilterGroup(
|
||||
null,
|
||||
"cell_description_body",
|
||||
"channel_profile"
|
||||
);
|
||||
private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch();
|
||||
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final StringFilterGroup communityPosts;
|
||||
private final StringFilterGroup expandableChip;
|
||||
private final ByteArrayFilterGroup visitStoreButton;
|
||||
private final StringFilterGroup videoLockup;
|
||||
|
||||
private static final StringTrieSearch communityPostsFeedGroupSearch = new StringTrieSearch();
|
||||
private final StringFilterGroupList communityPostsFeedGroup = new StringFilterGroupList();
|
||||
|
||||
|
||||
public FeedComponentsFilter() {
|
||||
communityPostsFeedGroupSearch.addPatterns(
|
||||
CONVERSATION_CONTEXT_FEED_IDENTIFIER,
|
||||
CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER
|
||||
);
|
||||
mixPlaylistsContextExceptions.addPatterns(
|
||||
"V.ED", // playlist browse id
|
||||
"java.lang.ref.WeakReference"
|
||||
);
|
||||
|
||||
// Identifiers.
|
||||
|
||||
final StringFilterGroup chipsShelf = new StringFilterGroup(
|
||||
Settings.HIDE_CHIPS_SHELF,
|
||||
"chips_shelf"
|
||||
);
|
||||
|
||||
communityPosts = new StringFilterGroup(
|
||||
null,
|
||||
"post_base_wrapper",
|
||||
"images_post_root",
|
||||
"images_post_slim",
|
||||
"text_post_root"
|
||||
);
|
||||
|
||||
final StringFilterGroup expandableShelf = new StringFilterGroup(
|
||||
Settings.HIDE_EXPANDABLE_SHELF,
|
||||
"expandable_section"
|
||||
);
|
||||
|
||||
final StringFilterGroup feedSearchBar = new StringFilterGroup(
|
||||
Settings.HIDE_FEED_SEARCH_BAR,
|
||||
"search_bar_entry_point"
|
||||
);
|
||||
|
||||
final StringFilterGroup tasteBuilder = new StringFilterGroup(
|
||||
Settings.HIDE_FEED_SURVEY,
|
||||
"selectable_item.eml",
|
||||
"cell_button.eml"
|
||||
);
|
||||
|
||||
videoLockup = new StringFilterGroup(
|
||||
null,
|
||||
FEED_VIDEO_PATH
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(
|
||||
chipsShelf,
|
||||
communityPosts,
|
||||
expandableShelf,
|
||||
feedSearchBar,
|
||||
tasteBuilder,
|
||||
videoLockup
|
||||
);
|
||||
|
||||
// Paths.
|
||||
|
||||
final StringFilterGroup albumCard = new StringFilterGroup(
|
||||
Settings.HIDE_ALBUM_CARDS,
|
||||
"browsy_bar",
|
||||
"official_card"
|
||||
);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
Settings.HIDE_BROWSE_STORE_BUTTON,
|
||||
"channel_profile.eml",
|
||||
"page_header.eml" // new layout
|
||||
);
|
||||
|
||||
visitStoreButton = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"header_store_button"
|
||||
);
|
||||
|
||||
final StringFilterGroup channelMemberShelf = new StringFilterGroup(
|
||||
Settings.HIDE_CHANNEL_MEMBER_SHELF,
|
||||
"member_recognition_shelf"
|
||||
);
|
||||
|
||||
final StringFilterGroup channelProfileLinks = new StringFilterGroup(
|
||||
Settings.HIDE_CHANNEL_PROFILE_LINKS,
|
||||
"channel_header_links",
|
||||
"attribution.eml" // new layout
|
||||
);
|
||||
|
||||
expandableChip = new StringFilterGroup(
|
||||
Settings.HIDE_EXPANDABLE_CHIP,
|
||||
INLINE_EXPANSION_PATH,
|
||||
"inline_expander",
|
||||
"expandable_metadata.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup feedSurvey = new StringFilterGroup(
|
||||
Settings.HIDE_FEED_SURVEY,
|
||||
"feed_nudge",
|
||||
"_survey"
|
||||
);
|
||||
|
||||
final StringFilterGroup forYouShelf = new StringFilterGroup(
|
||||
Settings.HIDE_FOR_YOU_SHELF,
|
||||
"mixed_content_shelf"
|
||||
);
|
||||
|
||||
final StringFilterGroup imageShelf = new StringFilterGroup(
|
||||
Settings.HIDE_IMAGE_SHELF,
|
||||
"image_shelf"
|
||||
);
|
||||
|
||||
final StringFilterGroup latestPosts = new StringFilterGroup(
|
||||
Settings.HIDE_LATEST_POSTS,
|
||||
"post_shelf"
|
||||
);
|
||||
|
||||
final StringFilterGroup movieShelf = new StringFilterGroup(
|
||||
Settings.HIDE_MOVIE_SHELF,
|
||||
"compact_movie",
|
||||
"horizontal_movie_shelf",
|
||||
"movie_and_show_upsell_card",
|
||||
"compact_tvfilm_item",
|
||||
"offer_module"
|
||||
);
|
||||
|
||||
final StringFilterGroup notifyMe = new StringFilterGroup(
|
||||
Settings.HIDE_NOTIFY_ME_BUTTON,
|
||||
"set_reminder_button"
|
||||
);
|
||||
|
||||
final StringFilterGroup playables = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYABLES,
|
||||
"horizontal_gaming_shelf.eml",
|
||||
"mini_game_card.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup subscriptionsChannelBar = new StringFilterGroup(
|
||||
Settings.HIDE_SUBSCRIPTIONS_CAROUSEL,
|
||||
"subscriptions_channel_bar"
|
||||
);
|
||||
|
||||
final StringFilterGroup ticketShelf = new StringFilterGroup(
|
||||
Settings.HIDE_TICKET_SHELF,
|
||||
"ticket_horizontal_shelf",
|
||||
"ticket_shelf"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
albumCard,
|
||||
channelProfile,
|
||||
channelMemberShelf,
|
||||
channelProfileLinks,
|
||||
expandableChip,
|
||||
feedSurvey,
|
||||
forYouShelf,
|
||||
imageShelf,
|
||||
latestPosts,
|
||||
movieShelf,
|
||||
notifyMe,
|
||||
playables,
|
||||
subscriptionsChannelBar,
|
||||
ticketShelf,
|
||||
videoLockup
|
||||
);
|
||||
|
||||
final StringFilterGroup communityPostsHomeAndRelatedVideos =
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS,
|
||||
CONVERSATION_CONTEXT_FEED_IDENTIFIER
|
||||
);
|
||||
|
||||
final StringFilterGroup communityPostsSubscriptions =
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS,
|
||||
CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER
|
||||
);
|
||||
|
||||
communityPostsFeedGroup.addAll(communityPostsHomeAndRelatedVideos, communityPostsSubscriptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(final Object conversionContext, final byte[] bytes) {
|
||||
return bytes != null
|
||||
&& mixPlaylists.check(bytes).isFiltered()
|
||||
&& !mixPlaylistsBufferExceptions.check(bytes).isFiltered()
|
||||
&& !mixPlaylistsContextExceptions.matches(conversionContext.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == channelProfile) {
|
||||
if (contentIndex == 0 && visitStoreButton.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == communityPosts) {
|
||||
if (!communityPostsFeedGroupSearch.matches(allValue) && Settings.HIDE_COMMUNITY_POSTS_CHANNEL.get()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
if (!communityPostsFeedGroup.check(allValue).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
} else if (matchedGroup == expandableChip) {
|
||||
if (path.startsWith(FEED_VIDEO_PATH)) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == videoLockup) {
|
||||
if (contentIndex == 0 && path.startsWith("CellType|") && inlineExpansion.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class FeedVideoFilter extends Filter {
|
||||
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
|
||||
"horizontalCollectionSwipeProtector=null";
|
||||
private static final String ENDORSEMENT_FOOTER_PATH = "endorsement_header_footer";
|
||||
|
||||
private static final StringTrieSearch feedOnlyVideoPattern = new StringTrieSearch();
|
||||
// In search results, vertical video with shorts labels mostly include videos with gray descriptions.
|
||||
// Filters without check process.
|
||||
private final StringFilterGroup inlineShorts;
|
||||
// Used for home, related videos, subscriptions, and search results.
|
||||
private final StringFilterGroup videoLockup = new StringFilterGroup(
|
||||
null,
|
||||
"video_lockup_with_attachment.eml"
|
||||
);
|
||||
private final ByteArrayFilterGroupList feedAndDrawerGroupList = new ByteArrayFilterGroupList();
|
||||
private final ByteArrayFilterGroupList feedOnlyGroupList = new ByteArrayFilterGroupList();
|
||||
private final StringFilterGroupList videoLockupFilterGroup = new StringFilterGroupList();
|
||||
private static final ByteArrayFilterGroup relatedVideo =
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_RELATED_VIDEOS,
|
||||
"relatedH"
|
||||
);
|
||||
|
||||
public FeedVideoFilter() {
|
||||
feedOnlyVideoPattern.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER);
|
||||
|
||||
inlineShorts = new StringFilterGroup(
|
||||
Settings.HIDE_RECOMMENDED_VIDEO,
|
||||
"inline_shorts.eml" // vertical video with shorts label
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(inlineShorts);
|
||||
|
||||
addPathCallbacks(videoLockup);
|
||||
|
||||
feedAndDrawerGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_RECOMMENDED_VIDEO,
|
||||
ENDORSEMENT_FOOTER_PATH, // videos with gray descriptions
|
||||
"high-ptsZ" // videos for membership only
|
||||
)
|
||||
);
|
||||
|
||||
feedOnlyGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_LOW_VIEWS_VIDEO,
|
||||
"g-highZ" // videos with less than 1000 views
|
||||
)
|
||||
);
|
||||
|
||||
videoLockupFilterGroup.addAll(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_RECOMMENDED_VIDEO,
|
||||
ENDORSEMENT_FOOTER_PATH
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == inlineShorts) {
|
||||
if (RootView.isSearchBarActive()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == videoLockup) {
|
||||
if (relatedVideo.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
if (feedOnlyVideoPattern.matches(allValue)) {
|
||||
if (feedOnlyGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
} else if (videoLockupFilterGroup.check(allValue).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
} else {
|
||||
if (feedAndDrawerGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public final class FeedVideoViewsFilter extends Filter {
|
||||
|
||||
private final StringFilterGroup feedVideoFilter = new StringFilterGroup(
|
||||
null,
|
||||
"video_lockup_with_attachment.eml"
|
||||
);
|
||||
|
||||
public FeedVideoViewsFilter() {
|
||||
addPathCallbacks(feedVideoFilter);
|
||||
}
|
||||
|
||||
private boolean hideFeedVideoViewsSettingIsActive() {
|
||||
final boolean hideHome = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_HOME.get();
|
||||
final boolean hideSearch = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH.get();
|
||||
final boolean hideSubscriptions = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS.get();
|
||||
|
||||
if (!hideHome && !hideSearch && !hideSubscriptions) {
|
||||
return false;
|
||||
} else if (hideHome && hideSearch && hideSubscriptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (RootView.isPlayerActive()) {
|
||||
// For now, consider the under video results the same as the home feed.
|
||||
return hideHome;
|
||||
}
|
||||
|
||||
// Must check second, as search can be from any tab.
|
||||
if (RootView.isSearchBarActive()) {
|
||||
return hideSearch;
|
||||
}
|
||||
|
||||
NavigationBar.NavigationButton selectedNavButton = NavigationBar.NavigationButton.getSelectedNavigationButton();
|
||||
if (selectedNavButton == null) {
|
||||
return hideHome; // Unknown tab, treat the same as home.
|
||||
} else if (selectedNavButton == NavigationBar.NavigationButton.HOME) {
|
||||
return hideHome;
|
||||
} else if (selectedNavButton == NavigationBar.NavigationButton.SUBSCRIPTIONS) {
|
||||
return hideSubscriptions;
|
||||
}
|
||||
// User is in the Library or Notifications tab.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (hideFeedVideoViewsSettingIsActive() &&
|
||||
filterByViews(protobufBufferArray)) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private final String ARROW = " -> ";
|
||||
private final String VIEWS = "views";
|
||||
private final String[] parts = Settings.HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER.get().split("\\n");
|
||||
private Pattern[] viewCountPatterns = null;
|
||||
|
||||
/**
|
||||
* Hide videos based on views count
|
||||
*/
|
||||
private synchronized boolean filterByViews(byte[] protobufBufferArray) {
|
||||
final String protobufString = new String(protobufBufferArray);
|
||||
final long lessThan = Settings.HIDE_VIDEO_VIEW_COUNTS_LESS_THAN.get();
|
||||
final long greaterThan = Settings.HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN.get();
|
||||
|
||||
if (viewCountPatterns == null) {
|
||||
viewCountPatterns = getViewCountPatterns(parts);
|
||||
}
|
||||
|
||||
for (Pattern pattern : viewCountPatterns) {
|
||||
final Matcher matcher = pattern.matcher(protobufString);
|
||||
if (matcher.find()) {
|
||||
String numString = Objects.requireNonNull(matcher.group(1));
|
||||
double num = parseNumber(numString);
|
||||
String multiplierKey = matcher.group(2);
|
||||
long multiplierValue = getMultiplierValue(parts, multiplierKey);
|
||||
return num * multiplierValue < lessThan || num * multiplierValue > greaterThan;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private synchronized double parseNumber(String numString) {
|
||||
/**
|
||||
* Some languages have comma (,) as a decimal separator.
|
||||
* In order to detect those numbers as doubles in Java
|
||||
* we convert commas (,) to dots (.).
|
||||
* Unless we find a language that has commas used in
|
||||
* a different manner, it should work.
|
||||
*/
|
||||
numString = numString.replace(",", ".");
|
||||
|
||||
/**
|
||||
* Some languages have dot (.) as a kilo separator.
|
||||
* So we check with regex if there is a number with 3+
|
||||
* digits after dot (.), we replace it with nothing
|
||||
* to make Java understand the number as a whole.
|
||||
*/
|
||||
if (numString.matches("\\d+\\.\\d{3,}")) {
|
||||
numString = numString.replace(".", "");
|
||||
}
|
||||
|
||||
return Double.parseDouble(numString);
|
||||
}
|
||||
|
||||
private synchronized Pattern[] getViewCountPatterns(String[] parts) {
|
||||
StringBuilder prefixPatternBuilder = new StringBuilder("(\\d+(?:[.,]\\d+)?)\\s?("); // LTR layout
|
||||
StringBuilder secondPatternBuilder = new StringBuilder(); // RTL layout
|
||||
StringBuilder suffixBuilder = getSuffixBuilder(parts, prefixPatternBuilder, secondPatternBuilder);
|
||||
|
||||
prefixPatternBuilder.deleteCharAt(prefixPatternBuilder.length() - 1); // Remove the trailing |
|
||||
prefixPatternBuilder.append(")?\\s*");
|
||||
prefixPatternBuilder.append(suffixBuilder.length() > 0 ? suffixBuilder.toString() : VIEWS);
|
||||
|
||||
secondPatternBuilder.deleteCharAt(secondPatternBuilder.length() - 1); // Remove the trailing |
|
||||
secondPatternBuilder.append(")?");
|
||||
|
||||
final Pattern[] patterns = new Pattern[2];
|
||||
patterns[0] = Pattern.compile(prefixPatternBuilder.toString());
|
||||
patterns[1] = Pattern.compile(secondPatternBuilder.toString());
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private synchronized StringBuilder getSuffixBuilder(String[] parts, StringBuilder prefixPatternBuilder, StringBuilder secondPatternBuilder) {
|
||||
StringBuilder suffixBuilder = new StringBuilder();
|
||||
|
||||
for (String part : parts) {
|
||||
final String[] pair = part.split(ARROW);
|
||||
final String pair0 = pair[0].trim();
|
||||
final String pair1 = pair[1].trim();
|
||||
|
||||
if (pair.length == 2 && !pair1.equals(VIEWS)) {
|
||||
prefixPatternBuilder.append(pair0).append("|");
|
||||
}
|
||||
|
||||
if (pair.length == 2 && pair1.equals(VIEWS)) {
|
||||
suffixBuilder.append(pair0);
|
||||
secondPatternBuilder.append(pair0).append("\\s*").append(prefixPatternBuilder);
|
||||
}
|
||||
}
|
||||
return suffixBuilder;
|
||||
}
|
||||
|
||||
private synchronized long getMultiplierValue(String[] parts, String multiplier) {
|
||||
for (String part : parts) {
|
||||
final String[] pair = part.split(ARROW);
|
||||
final String pair0 = pair[0].trim();
|
||||
final String pair1 = pair[1].trim();
|
||||
|
||||
if (pair.length == 2 && pair0.equals(multiplier) && !pair1.equals(VIEWS)) {
|
||||
return Long.parseLong(pair[1].replaceAll("[^\\d]", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return 1L; // Default value if not found
|
||||
}
|
||||
}
|
@ -0,0 +1,632 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
|
||||
import static java.lang.Character.UnicodeBlock.HIRAGANA;
|
||||
import static java.lang.Character.UnicodeBlock.KATAKANA;
|
||||
import static java.lang.Character.UnicodeBlock.KHMER;
|
||||
import static java.lang.Character.UnicodeBlock.LAO;
|
||||
import static java.lang.Character.UnicodeBlock.MYANMAR;
|
||||
import static java.lang.Character.UnicodeBlock.THAI;
|
||||
import static java.lang.Character.UnicodeBlock.TIBETAN;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.shared.utils.TrieSearch;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Allows hiding home feed and search results based on video title keywords and/or channel names.
|
||||
*
|
||||
* Limitations:
|
||||
* - Searching for a keyword phrase will give no search results.
|
||||
* This is because the buffer for each video contains the text the user searched for, and everything
|
||||
* will be filtered away (even if that video title/channel does not contain any keywords).
|
||||
* - Filtering a channel name can still show Shorts from that channel in the search results.
|
||||
* The most common Shorts layouts do not include the channel name, so they will not be filtered.
|
||||
* - Some layout component residue will remain, such as the video chapter previews for some search results.
|
||||
* These components do not include the video title or channel name, and they
|
||||
* appear outside the filtered components so they are not caught.
|
||||
* - Keywords are case sensitive, but some casing variation is manually added.
|
||||
* (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
|
||||
* - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
|
||||
* will always be hidden. This patch checks for some words of these words.
|
||||
* - When using whole word syntax, some keywords may need additional pluralized variations.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class KeywordContentFilter extends Filter {
|
||||
|
||||
/**
|
||||
* Strings found in the buffer for every videos. Full strings should be specified.
|
||||
* <p>
|
||||
* This list does not include every common buffer string, and this can be added/changed as needed.
|
||||
* Words must be entered with the exact casing as found in the buffer.
|
||||
*/
|
||||
private static final String[] STRINGS_IN_EVERY_BUFFER = {
|
||||
// Video playback data.
|
||||
"googlevideo.com/initplayback?source=youtube", // Video url.
|
||||
"ANDROID", // Video url parameter.
|
||||
"https://i.ytimg.com/vi/", // Thumbnail url.
|
||||
"mqdefault.jpg",
|
||||
"hqdefault.jpg",
|
||||
"sddefault.jpg",
|
||||
"hq720.jpg",
|
||||
"webp",
|
||||
"_custom_", // Custom thumbnail set by video creator.
|
||||
// Video decoders.
|
||||
"OMX.ffmpeg.vp9.decoder",
|
||||
"OMX.Intel.sw_vd.vp9",
|
||||
"OMX.MTK.VIDEO.DECODER.SW.VP9",
|
||||
"OMX.google.vp9.decoder",
|
||||
"OMX.google.av1.decoder",
|
||||
"OMX.sprd.av1.decoder",
|
||||
"c2.android.av1.decoder",
|
||||
"c2.android.av1-dav1d.decoder",
|
||||
"c2.android.vp9.decoder",
|
||||
"c2.mtk.sw.vp9.decoder",
|
||||
// Analytics.
|
||||
"searchR",
|
||||
"browse-feed",
|
||||
"FEwhat_to_watch",
|
||||
"FEsubscriptions",
|
||||
"search_vwc_description_transition_key",
|
||||
"g-high-recZ",
|
||||
// Text and litho components found in the buffer that belong to path filters.
|
||||
"expandable_metadata.eml",
|
||||
"thumbnail.eml",
|
||||
"avatar.eml",
|
||||
"overflow_button.eml",
|
||||
"shorts-lockup-image",
|
||||
"shorts-lockup.overlay-metadata.secondary-text",
|
||||
"YouTubeSans-SemiBold",
|
||||
"sans-serif"
|
||||
};
|
||||
|
||||
/**
|
||||
* Substrings that are always first in the identifier.
|
||||
*/
|
||||
private final StringFilterGroup startsWithFilter = new StringFilterGroup(
|
||||
null, // Multiple settings are used and must be individually checked if active.
|
||||
"video_lockup_with_attachment.eml",
|
||||
"compact_video.eml",
|
||||
"inline_shorts",
|
||||
"shorts_video_cell",
|
||||
"shorts_pivot_item.eml"
|
||||
);
|
||||
|
||||
/**
|
||||
* Substrings that are never at the start of the path.
|
||||
*/
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final StringFilterGroup containsFilter = new StringFilterGroup(
|
||||
null,
|
||||
"modern_type_shelf_header_content.eml",
|
||||
"shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml'
|
||||
"video_card.eml" // Shorts that appear in a horizontal shelf.
|
||||
);
|
||||
|
||||
/**
|
||||
* Path components to not filter. Cannot filter the buffer when these are present,
|
||||
* otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword).
|
||||
* <p>
|
||||
* This is also a small performance improvement since
|
||||
* the buffer of the parent component was already searched and passed.
|
||||
*/
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch(
|
||||
"metadata.eml",
|
||||
"thumbnail.eml",
|
||||
"avatar.eml",
|
||||
"overflow_button.eml"
|
||||
);
|
||||
|
||||
/**
|
||||
* Minimum keyword/phrase length to prevent excessively broad content filtering.
|
||||
* Only applies when not using whole word syntax.
|
||||
*/
|
||||
private static final int MINIMUM_KEYWORD_LENGTH = 3;
|
||||
|
||||
/**
|
||||
* Threshold for {@link #filteredVideosPercentage}
|
||||
* that indicates all or nearly all videos have been filtered.
|
||||
* This should be close to 100% to reduce false positives.
|
||||
*/
|
||||
private static final float ALL_VIDEOS_FILTERED_THRESHOLD = 0.95f;
|
||||
|
||||
private static final float ALL_VIDEOS_FILTERED_SAMPLE_SIZE = 50;
|
||||
|
||||
private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds
|
||||
|
||||
private static final int UTF8_MAX_BYTE_COUNT = 4;
|
||||
|
||||
/**
|
||||
* Rolling average of how many videos were filtered by a keyword.
|
||||
* Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER}
|
||||
* but a keyword is still hiding all videos.
|
||||
* <p>
|
||||
* This check can still fail if some extra UI elements pass the keywords,
|
||||
* such as the video chapter preview or any other elements.
|
||||
* <p>
|
||||
* To test this, add a filter that appears in all videos (such as 'ovd='),
|
||||
* and open the subscription feed. In practice this does not always identify problems
|
||||
* in the home feed and search, because the home feed has a finite amount of content and
|
||||
* search results have a lot of extra video junk that is not hidden and interferes with the detection.
|
||||
*/
|
||||
private volatile float filteredVideosPercentage;
|
||||
|
||||
/**
|
||||
* If filtering is temporarily turned off, the time to resume filtering.
|
||||
* Field is zero if no timeout is in effect.
|
||||
*/
|
||||
private volatile long timeToResumeFiltering;
|
||||
|
||||
private final StringFilterGroup commentsFilter;
|
||||
|
||||
private final StringTrieSearch commentsFilterExceptions = new StringTrieSearch();
|
||||
|
||||
/**
|
||||
* The last value of {@link Settings#HIDE_KEYWORD_CONTENT_PHRASES}
|
||||
* parsed and loaded into {@link #bufferSearch}.
|
||||
* Allows changing the keywords without restarting the app.
|
||||
*/
|
||||
private volatile String lastKeywordPhrasesParsed;
|
||||
|
||||
private volatile ByteTrieSearch bufferSearch;
|
||||
|
||||
private static void logNavigationState(String state) {
|
||||
// Enable locally to debug filtering. Default off to reduce log spam.
|
||||
final boolean LOG_NAVIGATION_STATE = false;
|
||||
// noinspection ConstantValue
|
||||
if (LOG_NAVIGATION_STATE) {
|
||||
Logger.printDebug(() -> "Navigation state: " + state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change first letter of the first word to use title case.
|
||||
*/
|
||||
private static String titleCaseFirstWordOnly(String sentence) {
|
||||
if (sentence.isEmpty()) {
|
||||
return sentence;
|
||||
}
|
||||
final int firstCodePoint = sentence.codePointAt(0);
|
||||
// In some non English languages title case is different than uppercase.
|
||||
return new StringBuilder()
|
||||
.appendCodePoint(Character.toTitleCase(firstCodePoint))
|
||||
.append(sentence, Character.charCount(firstCodePoint), sentence.length())
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uppercase the first letter of each word.
|
||||
*/
|
||||
private static String capitalizeAllFirstLetters(String sentence) {
|
||||
if (sentence.isEmpty()) {
|
||||
return sentence;
|
||||
}
|
||||
|
||||
final int delimiter = ' ';
|
||||
// Use code points and not characters to handle unicode surrogates.
|
||||
int[] codePoints = sentence.codePoints().toArray();
|
||||
boolean capitalizeNext = true;
|
||||
for (int i = 0, length = codePoints.length; i < length; i++) {
|
||||
final int codePoint = codePoints[i];
|
||||
if (codePoint == delimiter) {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
codePoints[i] = Character.toUpperCase(codePoint);
|
||||
capitalizeNext = false;
|
||||
}
|
||||
}
|
||||
return new String(codePoints, 0, codePoints.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the string contains any characters from languages that do not use spaces between words.
|
||||
*/
|
||||
private static boolean isLanguageWithNoSpaces(String text) {
|
||||
for (int i = 0, length = text.length(); i < length; ) {
|
||||
final int codePoint = text.codePointAt(i);
|
||||
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
|
||||
if (block == CJK_UNIFIED_IDEOGRAPHS // Chinese and Kanji
|
||||
|| block == HIRAGANA // Japanese Hiragana
|
||||
|| block == KATAKANA // Japanese Katakana
|
||||
|| block == THAI
|
||||
|| block == LAO
|
||||
|| block == MYANMAR
|
||||
|| block == KHMER
|
||||
|| block == TIBETAN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
i += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return If the phrase will hide all videos. Not an exhaustive check.
|
||||
*/
|
||||
private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boolean matchWholeWords) {
|
||||
for (String phrase : phrases) {
|
||||
for (String commonString : STRINGS_IN_EVERY_BUFFER) {
|
||||
if (matchWholeWords) {
|
||||
byte[] commonStringBytes = commonString.getBytes(StandardCharsets.UTF_8);
|
||||
int matchIndex = 0;
|
||||
while (true) {
|
||||
matchIndex = commonString.indexOf(phrase, matchIndex);
|
||||
if (matchIndex < 0) break;
|
||||
|
||||
if (keywordMatchIsWholeWord(commonStringBytes, matchIndex, phrase.length())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
matchIndex++;
|
||||
}
|
||||
} else if (Utils.containsAny(commonString, phrases)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the start and end indexes are not surrounded by other letters.
|
||||
* If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word.
|
||||
*/
|
||||
private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) {
|
||||
final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex);
|
||||
if (codePointBefore != null && Character.isLetter(codePointBefore)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Integer codePointAfter = getUtf8CodePointAt(text, keywordStartIndex + keywordLength);
|
||||
//noinspection RedundantIfStatement
|
||||
if (codePointAfter != null && Character.isLetter(codePointAfter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The UTF8 character point immediately before the index,
|
||||
* or null if the bytes before the index is not a valid UTF8 character.
|
||||
*/
|
||||
@Nullable
|
||||
private static Integer getUtf8CodePointBefore(byte[] data, int index) {
|
||||
int characterByteCount = 0;
|
||||
while (--index >= 0 && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
|
||||
if (isValidUtf8(data, index, characterByteCount)) {
|
||||
return decodeUtf8ToCodePoint(data, index, characterByteCount);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The UTF8 character point at the index,
|
||||
* or null if the index holds no valid UTF8 character.
|
||||
*/
|
||||
@Nullable
|
||||
private static Integer getUtf8CodePointAt(byte[] data, int index) {
|
||||
int characterByteCount = 0;
|
||||
final int dataLength = data.length;
|
||||
while (index + characterByteCount < dataLength && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) {
|
||||
if (isValidUtf8(data, index, characterByteCount)) {
|
||||
return decodeUtf8ToCodePoint(data, index, characterByteCount);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isValidUtf8(byte[] data, int startIndex, int numberOfBytes) {
|
||||
switch (numberOfBytes) {
|
||||
case 1 -> { // 0xxxxxxx (ASCII)
|
||||
return (data[startIndex] & 0x80) == 0;
|
||||
}
|
||||
case 2 -> { // 110xxxxx, 10xxxxxx
|
||||
return (data[startIndex] & 0xE0) == 0xC0
|
||||
&& (data[startIndex + 1] & 0xC0) == 0x80;
|
||||
}
|
||||
case 3 -> { // 1110xxxx, 10xxxxxx, 10xxxxxx
|
||||
return (data[startIndex] & 0xF0) == 0xE0
|
||||
&& (data[startIndex + 1] & 0xC0) == 0x80
|
||||
&& (data[startIndex + 2] & 0xC0) == 0x80;
|
||||
}
|
||||
case 4 -> { // 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx
|
||||
return (data[startIndex] & 0xF8) == 0xF0
|
||||
&& (data[startIndex + 1] & 0xC0) == 0x80
|
||||
&& (data[startIndex + 2] & 0xC0) == 0x80
|
||||
&& (data[startIndex + 3] & 0xC0) == 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
|
||||
}
|
||||
|
||||
public static int decodeUtf8ToCodePoint(byte[] data, int startIndex, int numberOfBytes) {
|
||||
switch (numberOfBytes) {
|
||||
case 1 -> {
|
||||
return data[startIndex];
|
||||
}
|
||||
case 2 -> {
|
||||
return ((data[startIndex] & 0x1F) << 6) |
|
||||
(data[startIndex + 1] & 0x3F);
|
||||
}
|
||||
case 3 -> {
|
||||
return ((data[startIndex] & 0x0F) << 12) |
|
||||
((data[startIndex + 1] & 0x3F) << 6) |
|
||||
(data[startIndex + 2] & 0x3F);
|
||||
}
|
||||
case 4 -> {
|
||||
return ((data[startIndex] & 0x07) << 18) |
|
||||
((data[startIndex + 1] & 0x3F) << 12) |
|
||||
((data[startIndex + 2] & 0x3F) << 6) |
|
||||
(data[startIndex + 3] & 0x3F);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes);
|
||||
}
|
||||
|
||||
private static boolean phraseUsesWholeWordSyntax(String phrase) {
|
||||
return phrase.startsWith("\"") && phrase.endsWith("\"");
|
||||
}
|
||||
|
||||
private static String stripWholeWordSyntax(String phrase) {
|
||||
return phrase.substring(1, phrase.length() - 1);
|
||||
}
|
||||
|
||||
private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded.
|
||||
String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get();
|
||||
|
||||
//noinspection StringEquality
|
||||
if (rawKeywords == lastKeywordPhrasesParsed) {
|
||||
Logger.printDebug(() -> "Using previously initialized search");
|
||||
return; // Another thread won the race, and search is already initialized.
|
||||
}
|
||||
|
||||
ByteTrieSearch search = new ByteTrieSearch();
|
||||
String[] split = rawKeywords.split("\n");
|
||||
if (split.length != 0) {
|
||||
// Linked Set so log statement are more organized and easier to read.
|
||||
// Map is: Phrase -> isWholeWord
|
||||
Map<String, Boolean> keywords = new LinkedHashMap<>(10 * split.length);
|
||||
|
||||
for (String phrase : split) {
|
||||
// Remove any trailing spaces the user may have accidentally included.
|
||||
phrase = phrase.stripTrailing();
|
||||
if (phrase.isBlank()) continue;
|
||||
|
||||
final boolean wholeWordMatching;
|
||||
if (phraseUsesWholeWordSyntax(phrase)) {
|
||||
if (phrase.length() == 2) {
|
||||
continue; // Empty "" phrase
|
||||
}
|
||||
phrase = stripWholeWordSyntax(phrase);
|
||||
wholeWordMatching = true;
|
||||
} else if (phrase.length() < MINIMUM_KEYWORD_LENGTH && !isLanguageWithNoSpaces(phrase)) {
|
||||
// Allow phrases of 1 and 2 characters if using a
|
||||
// language that does not use spaces between words.
|
||||
|
||||
// Do not reset the setting. Keep the invalid keywords so the user can fix the mistake.
|
||||
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH));
|
||||
continue;
|
||||
} else {
|
||||
wholeWordMatching = false;
|
||||
}
|
||||
|
||||
// Common casing that might appear.
|
||||
//
|
||||
// This could be simplified by adding case insensitive search to the prefix search,
|
||||
// which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII.
|
||||
//
|
||||
// But to support Unicode with ByteTrieSearch would require major changes because
|
||||
// UTF-8 characters can be different byte lengths, which does
|
||||
// not allow comparing two different byte arrays using simple plain array indexes.
|
||||
//
|
||||
// Instead use all common case variations of the words.
|
||||
String[] phraseVariations = {
|
||||
phrase,
|
||||
phrase.toLowerCase(),
|
||||
titleCaseFirstWordOnly(phrase),
|
||||
capitalizeAllFirstLetters(phrase),
|
||||
phrase.toUpperCase()
|
||||
};
|
||||
if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) {
|
||||
String toastMessage;
|
||||
// If whole word matching is off, but would pass with on, then show a different toast.
|
||||
if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) {
|
||||
toastMessage = "revanced_hide_keyword_toast_invalid_common_whole_word_required";
|
||||
} else {
|
||||
toastMessage = "revanced_hide_keyword_toast_invalid_common";
|
||||
}
|
||||
|
||||
Utils.showToastLong(str(toastMessage, phrase));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String variation : phraseVariations) {
|
||||
// Check if the same phrase is declared both with and without quotes.
|
||||
Boolean existing = keywords.get(variation);
|
||||
if (existing == null) {
|
||||
keywords.put(variation, wholeWordMatching);
|
||||
} else if (existing != wholeWordMatching) {
|
||||
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_conflicting", phrase));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Boolean> entry : keywords.entrySet()) {
|
||||
String keyword = entry.getKey();
|
||||
//noinspection ExtractMethodRecommender
|
||||
final boolean isWholeWord = entry.getValue();
|
||||
TrieSearch.TriePatternMatchedCallback<byte[]> callback =
|
||||
(textSearched, startIndex, matchLength, callbackParameter) -> {
|
||||
if (isWholeWord && !keywordMatchIsWholeWord(textSearched, startIndex, matchLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> (isWholeWord ? "Matched whole keyword: '"
|
||||
: "Matched keyword: '") + keyword + "'");
|
||||
// noinspection unchecked
|
||||
((MutableReference<String>) callbackParameter).value = keyword;
|
||||
return true;
|
||||
};
|
||||
byte[] stringBytes = keyword.getBytes(StandardCharsets.UTF_8);
|
||||
search.addPattern(stringBytes, callback);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords.keySet());
|
||||
}
|
||||
|
||||
bufferSearch = search;
|
||||
timeToResumeFiltering = 0;
|
||||
filteredVideosPercentage = 0;
|
||||
lastKeywordPhrasesParsed = rawKeywords; // Must set last.
|
||||
}
|
||||
|
||||
public KeywordContentFilter() {
|
||||
commentsFilterExceptions.addPatterns("engagement_toolbar");
|
||||
|
||||
commentsFilter = new StringFilterGroup(
|
||||
Settings.HIDE_KEYWORD_CONTENT_COMMENTS,
|
||||
"comment_thread.eml"
|
||||
);
|
||||
|
||||
// Keywords are parsed on first call to isFiltered()
|
||||
addPathCallbacks(startsWithFilter, containsFilter, commentsFilter);
|
||||
}
|
||||
|
||||
private boolean hideKeywordSettingIsActive() {
|
||||
if (timeToResumeFiltering != 0) {
|
||||
if (System.currentTimeMillis() < timeToResumeFiltering) {
|
||||
return false;
|
||||
}
|
||||
|
||||
timeToResumeFiltering = 0;
|
||||
filteredVideosPercentage = 0;
|
||||
Logger.printDebug(() -> "Resuming keyword filtering");
|
||||
}
|
||||
|
||||
final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get();
|
||||
final boolean hideSearch = Settings.HIDE_KEYWORD_CONTENT_SEARCH.get();
|
||||
final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get();
|
||||
|
||||
if (!hideHome && !hideSearch && !hideSubscriptions) {
|
||||
return false;
|
||||
} else if (hideHome && hideSearch && hideSubscriptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (RootView.isPlayerActive()) {
|
||||
// For now, consider the under video results the same as the home feed.
|
||||
return hideHome;
|
||||
}
|
||||
|
||||
// Must check second, as search can be from any tab.
|
||||
if (RootView.isSearchBarActive()) {
|
||||
return hideSearch;
|
||||
}
|
||||
|
||||
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
|
||||
if (selectedNavButton == null) {
|
||||
return hideHome; // Unknown tab, treat the same as home.
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.HOME) {
|
||||
return hideHome;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) {
|
||||
return hideSubscriptions;
|
||||
}
|
||||
// User is in the Library or Notifications tab.
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateStats(boolean videoWasHidden, @Nullable String keyword) {
|
||||
float updatedAverage = filteredVideosPercentage
|
||||
* ((ALL_VIDEOS_FILTERED_SAMPLE_SIZE - 1) / ALL_VIDEOS_FILTERED_SAMPLE_SIZE);
|
||||
if (videoWasHidden) {
|
||||
updatedAverage += 1 / ALL_VIDEOS_FILTERED_SAMPLE_SIZE;
|
||||
}
|
||||
|
||||
if (updatedAverage <= ALL_VIDEOS_FILTERED_THRESHOLD) {
|
||||
filteredVideosPercentage = updatedAverage;
|
||||
return;
|
||||
}
|
||||
|
||||
// A keyword is hiding everything.
|
||||
// Inform the user, and temporarily turn off filtering.
|
||||
timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS;
|
||||
|
||||
Logger.printDebug(() -> "Temporarily turning off filtering due to excessively broad filter: " + keyword);
|
||||
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_broad", keyword));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not filter if comments path includes an engagement toolbar (like, dislike...)
|
||||
if (matchedGroup == commentsFilter && commentsFilterExceptions.matches(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Field is intentionally compared using reference equality.
|
||||
//noinspection StringEquality
|
||||
if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
|
||||
// User changed the keywords or whole word setting.
|
||||
parseKeywords();
|
||||
}
|
||||
|
||||
if (matchedGroup != commentsFilter && !hideKeywordSettingIsActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) {
|
||||
return false; // Do not update statistics.
|
||||
}
|
||||
|
||||
MutableReference<String> matchRef = new MutableReference<>();
|
||||
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
||||
updateStats(true, matchRef.value);
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
updateStats(false, null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple non-atomic wrapper since {@link AtomicReference#setPlain(Object)} is not available with Android 8.0.
|
||||
*/
|
||||
final class MutableReference<T> {
|
||||
T value;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LayoutComponentsFilter extends Filter {
|
||||
private static final String ACCOUNT_HEADER_PATH = "account_header.eml";
|
||||
|
||||
public LayoutComponentsFilter() {
|
||||
addIdentifierCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_GRAY_SEPARATOR,
|
||||
"cell_divider"
|
||||
)
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_HANDLE,
|
||||
"|CellType|ContainerType|ContainerType|ContainerType|TextType|"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentType == FilterContentType.PATH && !path.startsWith(ACCOUNT_HEADER_PATH)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}.
|
||||
*/
|
||||
public final class PlaybackSpeedMenuFilter extends Filter {
|
||||
/**
|
||||
* Old litho based speed selection menu.
|
||||
*/
|
||||
public static volatile boolean isOldPlaybackSpeedMenuVisible;
|
||||
|
||||
/**
|
||||
* 0.05x speed selection menu.
|
||||
*/
|
||||
public static volatile boolean isPlaybackRateSelectorMenuVisible;
|
||||
|
||||
private final StringFilterGroup oldPlaybackMenuGroup;
|
||||
|
||||
public PlaybackSpeedMenuFilter() {
|
||||
// 0.05x litho speed menu.
|
||||
final StringFilterGroup playbackRateSelectorGroup = new StringFilterGroup(
|
||||
Settings.ENABLE_CUSTOM_PLAYBACK_SPEED,
|
||||
"playback_rate_selector_menu_sheet.eml-js"
|
||||
);
|
||||
|
||||
// Old litho based speed menu.
|
||||
oldPlaybackMenuGroup = new StringFilterGroup(
|
||||
Settings.ENABLE_CUSTOM_PLAYBACK_SPEED,
|
||||
"playback_speed_sheet_content.eml-js");
|
||||
|
||||
addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
||||
isOldPlaybackSpeedMenuVisible = true;
|
||||
} else {
|
||||
isPlaybackRateSelectorMenuVisible = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroupList;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class PlayerComponentsFilter extends Filter {
|
||||
private final StringFilterGroupList channelBarGroupList = new StringFilterGroupList();
|
||||
private final StringFilterGroup channelBar;
|
||||
private final StringTrieSearch suggestedActionsException = new StringTrieSearch();
|
||||
private final StringFilterGroup suggestedActions;
|
||||
|
||||
public PlayerComponentsFilter() {
|
||||
suggestedActionsException.addPatterns(
|
||||
"channel_bar",
|
||||
"shorts"
|
||||
);
|
||||
|
||||
// The player audio track button does the exact same function as the audio track flyout menu option.
|
||||
// But if the copy url button is shown, these button clashes and the the audio button does not work.
|
||||
// Previously this was a setting to show/hide the player button.
|
||||
// But it was decided it's simpler to always hide this button because:
|
||||
// - it doesn't work with copy video url feature
|
||||
// - the button is rare
|
||||
// - always hiding makes the ReVanced settings simpler and easier to understand
|
||||
// - nobody is going to notice the redundant button is always hidden
|
||||
final StringFilterGroup audioTrackButton = new StringFilterGroup(
|
||||
null,
|
||||
"multi_feed_icon_button"
|
||||
);
|
||||
|
||||
channelBar = new StringFilterGroup(
|
||||
null,
|
||||
"channel_bar_inner"
|
||||
);
|
||||
|
||||
final StringFilterGroup channelWaterMark = new StringFilterGroup(
|
||||
Settings.HIDE_CHANNEL_WATERMARK,
|
||||
"featured_channel_watermark_overlay.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup infoCards = new StringFilterGroup(
|
||||
Settings.HIDE_INFO_CARDS,
|
||||
"info_card_teaser_overlay.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup infoPanel = new StringFilterGroup(
|
||||
Settings.HIDE_INFO_PANEL,
|
||||
"compact_banner",
|
||||
"publisher_transparency_panel",
|
||||
"single_item_information_panel"
|
||||
);
|
||||
|
||||
final StringFilterGroup liveChat = new StringFilterGroup(
|
||||
Settings.HIDE_LIVE_CHAT_MESSAGES,
|
||||
"live_chat_text_message",
|
||||
"viewer_engagement_message" // message about poll, not poll itself
|
||||
);
|
||||
|
||||
final StringFilterGroup medicalPanel = new StringFilterGroup(
|
||||
Settings.HIDE_MEDICAL_PANEL,
|
||||
"emergency_onebox",
|
||||
"medical_panel"
|
||||
);
|
||||
|
||||
suggestedActions = new StringFilterGroup(
|
||||
Settings.HIDE_SUGGESTED_ACTION,
|
||||
"|suggested_action.eml|"
|
||||
);
|
||||
|
||||
final StringFilterGroup timedReactions = new StringFilterGroup(
|
||||
Settings.HIDE_TIMED_REACTIONS,
|
||||
"emoji_control_panel",
|
||||
"timed_reaction"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
audioTrackButton,
|
||||
channelBar,
|
||||
channelWaterMark,
|
||||
infoCards,
|
||||
infoPanel,
|
||||
liveChat,
|
||||
medicalPanel,
|
||||
suggestedActions,
|
||||
timedReactions
|
||||
);
|
||||
|
||||
final StringFilterGroup joinMembership = new StringFilterGroup(
|
||||
Settings.HIDE_JOIN_BUTTON,
|
||||
"compact_sponsor_button",
|
||||
"|ContainerType|button.eml|"
|
||||
);
|
||||
|
||||
final StringFilterGroup startTrial = new StringFilterGroup(
|
||||
Settings.HIDE_START_TRIAL_BUTTON,
|
||||
"channel_purchase_button"
|
||||
);
|
||||
|
||||
channelBarGroupList.addAll(
|
||||
joinMembership,
|
||||
startTrial
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == suggestedActions) {
|
||||
// suggested actions button on shorts and the suggested actions button on video players use the same path builder.
|
||||
// Check PlayerType to make each setting work independently.
|
||||
if (suggestedActionsException.matches(path) || PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return false;
|
||||
}
|
||||
} else if (matchedGroup == channelBar) {
|
||||
if (!channelBarGroupList.check(path).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class PlayerFlyoutMenuFilter extends Filter {
|
||||
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final ByteArrayFilterGroup byteArrayException;
|
||||
private final StringTrieSearch pathBuilderException = new StringTrieSearch();
|
||||
private final StringTrieSearch playerFlyoutMenuFooter = new StringTrieSearch();
|
||||
private final StringFilterGroup playerFlyoutMenu;
|
||||
private final StringFilterGroup qualityHeader;
|
||||
|
||||
public PlayerFlyoutMenuFilter() {
|
||||
byteArrayException = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"quality_sheet"
|
||||
);
|
||||
pathBuilderException.addPattern(
|
||||
"bottom_sheet_list_option"
|
||||
);
|
||||
playerFlyoutMenuFooter.addPatterns(
|
||||
"captions_sheet_content.eml",
|
||||
"quality_sheet_content.eml"
|
||||
);
|
||||
|
||||
final StringFilterGroup captionsFooter = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER,
|
||||
"|ContainerType|ContainerType|ContainerType|TextType|",
|
||||
"|divider.eml|"
|
||||
);
|
||||
|
||||
final StringFilterGroup qualityFooter = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER,
|
||||
"quality_sheet_footer.eml",
|
||||
"|divider.eml|"
|
||||
);
|
||||
|
||||
qualityHeader = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER,
|
||||
"quality_sheet_header.eml"
|
||||
);
|
||||
|
||||
playerFlyoutMenu = new StringFilterGroup(null, "overflow_menu_item.eml|");
|
||||
|
||||
// Using pathFilterGroupList due to new flyout panel(A/B)
|
||||
addPathCallbacks(
|
||||
captionsFooter,
|
||||
qualityFooter,
|
||||
qualityHeader,
|
||||
playerFlyoutMenu
|
||||
);
|
||||
|
||||
flyoutFilterGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT,
|
||||
"yt_outline_screen_light"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK,
|
||||
"yt_outline_person_radar"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS,
|
||||
"closed_caption"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_HELP,
|
||||
"yt_outline_question_circle"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN,
|
||||
"yt_outline_lock"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP,
|
||||
"yt_outline_arrow_repeat_1_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_MORE,
|
||||
"yt_outline_info_circle"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_PIP,
|
||||
"yt_fill_picture_in_picture",
|
||||
"yt_outline_picture_in_picture"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED,
|
||||
"yt_outline_play_arrow_half_circle"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS,
|
||||
"yt_outline_adjust"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS,
|
||||
"yt_outline_gear"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_REPORT,
|
||||
"yt_outline_flag"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME,
|
||||
"volume_stable"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER,
|
||||
"yt_outline_moon_z_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS,
|
||||
"yt_outline_statistics_graph"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR,
|
||||
"yt_outline_vr"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC,
|
||||
"yt_outline_open_new"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == playerFlyoutMenu) {
|
||||
// Overflow menu is always the start of the path.
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
// Shorts also use this player flyout panel
|
||||
if (PlayerType.getCurrent().isNoneOrHidden() || byteArrayException.check(protobufBufferArray).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
} else if (matchedGroup == qualityHeader) {
|
||||
// Quality header is always the start of the path.
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
} else {
|
||||
// Components other than the footer separator are not filtered.
|
||||
if (pathBuilderException.matches(path) || !playerFlyoutMenuFooter.matches(path)) {
|
||||
return false;
|
||||
}
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class QuickActionFilter extends Filter {
|
||||
private static final String QUICK_ACTION_PATH = "quick_actions.eml";
|
||||
private final StringFilterGroup quickActionRule;
|
||||
|
||||
private final StringFilterGroup bufferFilterPathRule;
|
||||
private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup liveChatReplay;
|
||||
|
||||
public QuickActionFilter() {
|
||||
quickActionRule = new StringFilterGroup(null, QUICK_ACTION_PATH);
|
||||
addIdentifierCallbacks(quickActionRule);
|
||||
bufferFilterPathRule = new StringFilterGroup(
|
||||
null,
|
||||
"|ContainerType|button.eml|",
|
||||
"|fullscreen_video_action_button.eml|"
|
||||
);
|
||||
|
||||
liveChatReplay = new StringFilterGroup(
|
||||
Settings.HIDE_LIVE_CHAT_REPLAY_BUTTON,
|
||||
"live_chat_ep_entrypoint.eml"
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(liveChatReplay);
|
||||
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
|
||||
"|like_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
|
||||
"dislike_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
|
||||
"comments_entry_point_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
|
||||
"|save_to_playlist_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
|
||||
"|overflow_menu_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_RELATED_VIDEO_OVERLAY,
|
||||
"fullscreen_related_videos"
|
||||
),
|
||||
bufferFilterPathRule
|
||||
);
|
||||
|
||||
bufferButtonsGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
|
||||
"yt_outline_message_bubble_right"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
|
||||
"yt_outline_message_bubble_overlap"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
|
||||
"yt_outline_youtube_mix"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
|
||||
"yt_outline_list_play_arrow"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON,
|
||||
"yt_outline_share"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isEveryFilterGroupEnabled() {
|
||||
for (StringFilterGroup group : pathCallbacks)
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
for (ByteArrayFilterGroup group : bufferButtonsGroupList)
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == liveChatReplay) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
if (!path.startsWith(QUICK_ACTION_PATH)) {
|
||||
return false;
|
||||
}
|
||||
if (matchedGroup == quickActionRule && !isEveryFilterGroupEnabled()) {
|
||||
return false;
|
||||
}
|
||||
if (matchedGroup == bufferFilterPathRule) {
|
||||
return bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Here is an unintended behavior:
|
||||
* <p>
|
||||
* 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise.
|
||||
* 2. Goes to the Subscriptions tab and scrolls to where Shorts is.
|
||||
* 3. Opens a regular video.
|
||||
* 4. Minimizes the video and turns off the screen.
|
||||
* 5. Turns the screen on and maximizes the video.
|
||||
* 6. Shorts belonging to related videos are not hidden.
|
||||
* <p>
|
||||
* Here is an explanation of this special issue:
|
||||
* <p>
|
||||
* When the user minimizes the video, turns off the screen, and then turns it back on,
|
||||
* the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED].
|
||||
* (Shorts belonging to related videos are also reloaded)
|
||||
* Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked.
|
||||
* (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video)
|
||||
* <p>
|
||||
* As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player,
|
||||
* it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED].
|
||||
*/
|
||||
public final class RelatedVideoFilter extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
|
||||
public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false);
|
||||
|
||||
public RelatedVideoFilter() {
|
||||
addIdentifierCallbacks(
|
||||
new StringFilterGroup(
|
||||
null,
|
||||
"video_action_bar.eml"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED &&
|
||||
isActionBarVisible.compareAndSet(false, true))
|
||||
Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 750);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeChannelNamePatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"unused", "CharsetObjectCanBeUsed"})
|
||||
public final class ReturnYouTubeChannelNameFilterPatch extends Filter {
|
||||
private static final String DELIMITING_CHARACTER = "❙";
|
||||
private static final String CHANNEL_ID_IDENTIFIER_CHARACTER = "UC";
|
||||
private static final String CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER =
|
||||
DELIMITING_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER;
|
||||
private static final String HANDLE_IDENTIFIER_CHARACTER = "@";
|
||||
private static final String HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER =
|
||||
HANDLE_IDENTIFIER_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER;
|
||||
|
||||
private final ByteArrayFilterGroupList shortsChannelBarAvatarFilterGroup = new ByteArrayFilterGroupList();
|
||||
|
||||
public ReturnYouTubeChannelNameFilterPatch() {
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "|reel_channel_bar_inner.eml|")
|
||||
);
|
||||
shortsChannelBarAvatarFilterGroup.addAll(
|
||||
new ByteArrayFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "/@")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (shortsChannelBarAvatarFilterGroup.check(protobufBufferArray).isFiltered()) {
|
||||
setLastShortsChannelId(protobufBufferArray);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setLastShortsChannelId(byte[] protobufBufferArray) {
|
||||
try {
|
||||
String[] splitArr;
|
||||
final String bufferString = findAsciiStrings(protobufBufferArray);
|
||||
splitArr = bufferString.split(CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER);
|
||||
if (splitArr.length < 2) {
|
||||
return;
|
||||
}
|
||||
final String splitedBufferString = CHANNEL_ID_IDENTIFIER_CHARACTER + splitArr[1];
|
||||
splitArr = splitedBufferString.split(HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER);
|
||||
if (splitArr.length < 2) {
|
||||
return;
|
||||
}
|
||||
splitArr = splitArr[1].split(DELIMITING_CHARACTER);
|
||||
if (splitArr.length < 1) {
|
||||
return;
|
||||
}
|
||||
final String cachedHandle = HANDLE_IDENTIFIER_CHARACTER + splitArr[0];
|
||||
splitArr = splitedBufferString.split(DELIMITING_CHARACTER);
|
||||
if (splitArr.length < 1) {
|
||||
return;
|
||||
}
|
||||
final String channelId = splitArr[0].replaceAll("\"", "").trim();
|
||||
final String handle = URLDecoder.decode(cachedHandle, "UTF-8").trim();
|
||||
|
||||
ReturnYouTubeChannelNamePatch.setLastShortsChannelId(handle, channelId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setLastShortsChannelId failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String findAsciiStrings(byte[] buffer) {
|
||||
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
||||
builder.append("");
|
||||
|
||||
// Valid ASCII values (ignore control characters).
|
||||
final int minimumAscii = 32; // 32 = space character
|
||||
final int maximumAscii = 126; // 127 = delete character
|
||||
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
|
||||
String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
|
||||
|
||||
final int length = buffer.length;
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
while (end < length) {
|
||||
int value = buffer[end];
|
||||
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
|
||||
if (end - start >= minimumAsciiStringLength) {
|
||||
for (int i = start; i < end; i++) {
|
||||
builder.append((char) buffer[i]);
|
||||
}
|
||||
builder.append(delimitingCharacter);
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.FilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.TrieSearch;
|
||||
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
* <p>
|
||||
* Searches for video id's in the proto buffer of Shorts dislike.
|
||||
* <p>
|
||||
* Because multiple litho dislike spans are created in the background
|
||||
* (and also anytime litho refreshes the components, which is somewhat arbitrary),
|
||||
* that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()}
|
||||
* unreliable to determine which video id a Shorts litho span belongs to.
|
||||
* <p>
|
||||
* But the correct video id does appear in the protobuffer just before a Shorts litho span is created.
|
||||
* <p>
|
||||
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
|
||||
*/
|
||||
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
|
||||
/**
|
||||
* Last unique video id's loaded. Value is ignored and Map is treated as a Set.
|
||||
* Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry().
|
||||
*/
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
|
||||
/**
|
||||
* Number of video id's to keep track of for searching thru the buffer.
|
||||
* A minimum value of 3 should be sufficient, but check a few more just in case.
|
||||
*/
|
||||
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
|
||||
}
|
||||
};
|
||||
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
|
||||
|
||||
public ReturnYouTubeDislikeFilterPatch() {
|
||||
// When a new Short is opened, the like buttons always seem to load before the dislike.
|
||||
// But if swiping back to a previous video and liking/disliking, then only that single button reloads.
|
||||
// So must check for both buttons.
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(null, "|shorts_like_button.eml"),
|
||||
new StringFilterGroup(null, "|shorts_dislike_button.eml")
|
||||
);
|
||||
|
||||
// After the likes icon name is some binary data and then the video id for that specific short.
|
||||
videoIdFilterGroup.addAll(
|
||||
// on_shadowed = Video was previously like/disliked before opening.
|
||||
// off_shadowed = Video was not previously liked/disliked before opening.
|
||||
new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"),
|
||||
new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed"),
|
||||
|
||||
new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
|
||||
new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
|
||||
);
|
||||
}
|
||||
|
||||
private volatile static String shortsVideoId = "";
|
||||
|
||||
public static String getShortsVideoId() {
|
||||
return shortsVideoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (!Settings.RYD_SHORTS.get()) {
|
||||
return;
|
||||
}
|
||||
if (shortsVideoId.equals(newlyLoadedVideoId)) {
|
||||
return;
|
||||
}
|
||||
Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
|
||||
shortsVideoId = newlyLoadedVideoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return;
|
||||
}
|
||||
synchronized (lastVideoIds) {
|
||||
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
|
||||
Logger.printDebug(() -> "New Short video id: " + videoId);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "newPlayerResponseVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This could use {@link TrieSearch}, but since the patterns are constantly changing
|
||||
* the overhead of updating the Trie might negate the search performance gain.
|
||||
*/
|
||||
private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) {
|
||||
for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) {
|
||||
boolean found = true;
|
||||
for (int j = 0, textLength = text.length(); j < textLength; j++) {
|
||||
if (array[i + j] != (byte) text.charAt(j)) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
||||
if (result.isFiltered()) {
|
||||
String matchedVideoId = findVideoId(protobufBufferArray);
|
||||
// Matched video will be null if in incognito mode.
|
||||
// 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.setLastLithoShortsVideoId(matchedVideoId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String findVideoId(byte[] protobufBufferArray) {
|
||||
synchronized (lastVideoIds) {
|
||||
for (String videoId : lastVideoIds.keySet()) {
|
||||
if (byteArrayContainsString(protobufBufferArray, videoId)) {
|
||||
return videoId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.patches.misc.ShareSheetPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Abuse LithoFilter for {@link ShareSheetPatch}.
|
||||
*/
|
||||
public final class ShareSheetMenuFilter extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
|
||||
public static volatile boolean isShareSheetMenuVisible;
|
||||
|
||||
public ShareSheetMenuFilter() {
|
||||
addIdentifierCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.CHANGE_SHARE_SHEET,
|
||||
"share_sheet_container.eml"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
isShareSheetMenuVisible = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,274 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ShortsButtonFilter extends Filter {
|
||||
private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
||||
private final static String REEL_LIVE_HEADER_PATH = "immersive_live_header.eml";
|
||||
/**
|
||||
* For paid promotion label and subscribe button that appears in the channel bar.
|
||||
*/
|
||||
private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml";
|
||||
|
||||
private final static String SHORTS_PAUSED_STATE_BUTTON_PATH = "|ScrollableContainerType|ContainerType|button.eml|";
|
||||
|
||||
private final StringFilterGroup subscribeButton;
|
||||
private final StringFilterGroup joinButton;
|
||||
private final StringFilterGroup pausedOverlayButtons;
|
||||
private final StringFilterGroup metaPanelButton;
|
||||
private final ByteArrayFilterGroupList pausedOverlayButtonsGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup suggestedAction;
|
||||
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup actionBar;
|
||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final ByteArrayFilterGroup useThisSoundButton = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
|
||||
"yt_outline_camera"
|
||||
);
|
||||
|
||||
public ShortsButtonFilter() {
|
||||
StringFilterGroup floatingButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_FLOATING_BUTTON,
|
||||
"floating_action_button"
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(floatingButton);
|
||||
|
||||
pausedOverlayButtons = new StringFilterGroup(
|
||||
null,
|
||||
"shorts_paused_state"
|
||||
);
|
||||
|
||||
StringFilterGroup channelBar = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_CHANNEL_BAR,
|
||||
REEL_CHANNEL_BAR_PATH
|
||||
);
|
||||
|
||||
StringFilterGroup fullVideoLinkLabel = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_FULL_VIDEO_LINK_LABEL,
|
||||
"reel_multi_format_link"
|
||||
);
|
||||
|
||||
StringFilterGroup videoTitle = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_VIDEO_TITLE,
|
||||
"shorts_video_title_item"
|
||||
);
|
||||
|
||||
StringFilterGroup reelSoundMetadata = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SOUND_METADATA_LABEL,
|
||||
"reel_sound_metadata"
|
||||
);
|
||||
|
||||
StringFilterGroup infoPanel = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_INFO_PANEL,
|
||||
"shorts_info_panel_overview"
|
||||
);
|
||||
|
||||
StringFilterGroup stickers = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_STICKERS,
|
||||
"stickers_layer.eml"
|
||||
);
|
||||
|
||||
StringFilterGroup liveHeader = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_LIVE_HEADER,
|
||||
"immersive_live_header"
|
||||
);
|
||||
|
||||
StringFilterGroup paidPromotionButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL,
|
||||
"reel_player_disclosure.eml"
|
||||
);
|
||||
|
||||
metaPanelButton = new StringFilterGroup(
|
||||
null,
|
||||
"|ContainerType|button.eml|"
|
||||
);
|
||||
|
||||
joinButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_JOIN_BUTTON,
|
||||
"sponsor_button"
|
||||
);
|
||||
|
||||
subscribeButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON,
|
||||
"subscribe_button"
|
||||
);
|
||||
|
||||
actionBar = new StringFilterGroup(
|
||||
null,
|
||||
"shorts_action_bar"
|
||||
);
|
||||
|
||||
suggestedAction = new StringFilterGroup(
|
||||
null,
|
||||
"|suggested_action_inner.eml|"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
suggestedAction, actionBar, joinButton, subscribeButton, metaPanelButton,
|
||||
paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel,
|
||||
videoTitle, reelSoundMetadata, infoPanel, liveHeader, stickers
|
||||
);
|
||||
|
||||
//
|
||||
// Action buttons
|
||||
//
|
||||
videoActionButtonGroupList.addAll(
|
||||
// This also appears as the path item 'shorts_like_button.eml'
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_LIKE_BUTTON,
|
||||
"reel_like_button",
|
||||
"reel_like_toggled_button"
|
||||
),
|
||||
// This also appears as the path item 'shorts_dislike_button.eml'
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
|
||||
"reel_dislike_button",
|
||||
"reel_dislike_toggled_button"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"reel_comment_button"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"reel_share_button"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"reel_remix_button"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION,
|
||||
"shorts_like_fountain"
|
||||
)
|
||||
);
|
||||
|
||||
//
|
||||
// Paused overlay buttons.
|
||||
//
|
||||
pausedOverlayButtonsGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_TRENDS_BUTTON,
|
||||
"yt_outline_fire_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHOPPING_BUTTON,
|
||||
"yt_outline_bag_"
|
||||
)
|
||||
);
|
||||
|
||||
//
|
||||
// Suggested actions.
|
||||
//
|
||||
suggestedActionsGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_TAGGED_PRODUCTS,
|
||||
// Product buttons show pictures of the products, and does not have any unique icons to identify.
|
||||
// Instead use a unique identifier found in the buffer.
|
||||
"PAproduct_listZ"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHOP_BUTTON,
|
||||
"yt_outline_bag_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_LOCATION_BUTTON,
|
||||
"yt_outline_location_point_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SAVE_MUSIC_BUTTON,
|
||||
"yt_outline_list_add_",
|
||||
"yt_outline_bookmark_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON,
|
||||
"yt_outline_search_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
|
||||
"yt_outline_dollar_sign_heart_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||
"yt_outline_template_add"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
|
||||
"shorts_green_screen"
|
||||
),
|
||||
useThisSoundButton
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isEverySuggestedActionFilterEnabled() {
|
||||
for (ByteArrayFilterGroup group : suggestedActionsGroupList)
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton) {
|
||||
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
||||
if (StringUtils.startsWithAny(path, REEL_CHANNEL_BAR_PATH, REEL_LIVE_HEADER_PATH, REEL_METAPANEL_PATH)) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == metaPanelButton) {
|
||||
if (path.startsWith(REEL_METAPANEL_PATH) && useThisSoundButton.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Video action buttons (like, dislike, comment, share, remix) have the same path.
|
||||
if (matchedGroup == actionBar) {
|
||||
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
if (isEverySuggestedActionFilterEnabled()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
// Suggested actions can be at the start or in the middle of a path.
|
||||
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == pausedOverlayButtons) {
|
||||
if (Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS.get()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
} else if (StringUtils.contains(path, SHORTS_PAUSED_STATE_BUTTON_PATH)) {
|
||||
if (pausedOverlayButtonsGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ShortsShelfFilter extends Filter {
|
||||
private static final String BROWSE_ID_HISTORY = "FEhistory";
|
||||
private static final String BROWSE_ID_LIBRARY = "FElibrary";
|
||||
private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox";
|
||||
private static final String BROWSE_ID_SUBSCRIPTIONS = "FEsubscriptions";
|
||||
private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER =
|
||||
"horizontalCollectionSwipeProtector=null";
|
||||
private static final String SHELF_HEADER_PATH = "shelf_header.eml";
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final StringFilterGroup compactFeedVideoPath;
|
||||
private final ByteArrayFilterGroup compactFeedVideoBuffer;
|
||||
private final StringFilterGroup shelfHeaderIdentifier;
|
||||
private final StringFilterGroup shelfHeaderPath;
|
||||
private static final StringTrieSearch feedGroup = new StringTrieSearch();
|
||||
private static final BooleanSetting hideShortsShelf = Settings.HIDE_SHORTS_SHELF;
|
||||
private static final BooleanSetting hideChannel = Settings.HIDE_SHORTS_SHELF_CHANNEL;
|
||||
private static final ByteArrayFilterGroup channelProfileShelfHeader =
|
||||
new ByteArrayFilterGroup(
|
||||
hideChannel,
|
||||
"Shorts"
|
||||
);
|
||||
|
||||
public ShortsShelfFilter() {
|
||||
feedGroup.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
hideChannel,
|
||||
"shorts_pivot_item"
|
||||
);
|
||||
|
||||
final StringFilterGroup shortsIdentifiers = new StringFilterGroup(
|
||||
hideShortsShelf,
|
||||
"shorts_shelf",
|
||||
"inline_shorts",
|
||||
"shorts_grid",
|
||||
"shorts_video_cell"
|
||||
);
|
||||
|
||||
shelfHeaderIdentifier = new StringFilterGroup(
|
||||
hideShortsShelf,
|
||||
SHELF_HEADER_PATH
|
||||
);
|
||||
|
||||
addIdentifierCallbacks(channelProfile, shortsIdentifiers, shelfHeaderIdentifier);
|
||||
|
||||
compactFeedVideoPath = new StringFilterGroup(
|
||||
hideShortsShelf,
|
||||
// Shorts that appear in the feed/search when the device is using tablet layout.
|
||||
"compact_video.eml",
|
||||
// 'video_lockup_with_attachment.eml' is used instead of 'compact_video.eml' for some users. (A/B tests)
|
||||
"video_lockup_with_attachment.eml",
|
||||
// Search results that appear in a horizontal shelf.
|
||||
"video_card.eml"
|
||||
);
|
||||
|
||||
// Filter out items that use the 'frame0' thumbnail.
|
||||
// This is a valid thumbnail for both regular videos and Shorts,
|
||||
// but it appears these thumbnails are used only for Shorts.
|
||||
compactFeedVideoBuffer = new ByteArrayFilterGroup(
|
||||
hideShortsShelf,
|
||||
"/frame0.jpg"
|
||||
);
|
||||
|
||||
// Feed Shorts shelf header.
|
||||
// Use a different filter group for this pattern, as it requires an additional check after matching.
|
||||
shelfHeaderPath = new StringFilterGroup(
|
||||
hideShortsShelf,
|
||||
SHELF_HEADER_PATH
|
||||
);
|
||||
|
||||
addPathCallbacks(compactFeedVideoPath, shelfHeaderPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
final boolean playerActive = RootView.isPlayerActive();
|
||||
final boolean searchBarActive = RootView.isSearchBarActive();
|
||||
final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton();
|
||||
final String navigation = navigationButton == null ? "null" : navigationButton.name();
|
||||
final String browseId = RootView.getBrowseId();
|
||||
final boolean hideShelves = shouldHideShortsFeedItems(playerActive, searchBarActive, navigationButton, browseId);
|
||||
Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation);
|
||||
if (contentType == FilterContentType.PATH) {
|
||||
if (matchedGroup == compactFeedVideoPath) {
|
||||
if (hideShelves && compactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
} else if (matchedGroup == shelfHeaderPath) {
|
||||
// Because the header is used in watch history and possibly other places, check for the index,
|
||||
// which is 0 when the shelf header is used for Shorts.
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (!channelProfileShelfHeader.check(protobufBufferArray).isFiltered()) {
|
||||
return false;
|
||||
}
|
||||
if (feedGroup.matches(allValue)) {
|
||||
return false;
|
||||
}
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
} else if (contentType == FilterContentType.IDENTIFIER) {
|
||||
// Feed/search identifier components.
|
||||
if (matchedGroup == shelfHeaderIdentifier) {
|
||||
// Check ConversationContext to not hide shelf header in channel profile
|
||||
// This value does not exist in the shelf header in the channel profile
|
||||
if (!feedGroup.matches(allValue)) {
|
||||
return false;
|
||||
}
|
||||
} else if (matchedGroup == channelProfile) {
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
if (!hideShelves) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
private static boolean shouldHideShortsFeedItems(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) {
|
||||
final boolean hideHomeAndRelatedVideos = Settings.HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS.get();
|
||||
final boolean hideSubscriptions = Settings.HIDE_SHORTS_SHELF_SUBSCRIPTIONS.get();
|
||||
final boolean hideSearch = Settings.HIDE_SHORTS_SHELF_SEARCH.get();
|
||||
|
||||
if (hideHomeAndRelatedVideos && hideSubscriptions && hideSearch) {
|
||||
// Shorts suggestions can load in the background if a video is opened and
|
||||
// then immediately minimized before any suggestions are loaded.
|
||||
// In this state the player type will show minimized, which makes it not possible to
|
||||
// distinguish between Shorts suggestions loading in the player and between
|
||||
// scrolling thru search/home/subscription tabs while a player is minimized.
|
||||
//
|
||||
// To avoid this situation for users that never want to show Shorts (all hide Shorts options are enabled)
|
||||
// then hide all Shorts everywhere including the Library history and Library playlists.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (playerActive) {
|
||||
// For now, consider the under video results the same as the home feed.
|
||||
return hideHomeAndRelatedVideos;
|
||||
}
|
||||
|
||||
// Must check second, as search can be from any tab.
|
||||
if (searchBarActive) {
|
||||
return hideSearch;
|
||||
}
|
||||
|
||||
// Avoid checking navigation button status if all other Shorts should show.
|
||||
if (!hideHomeAndRelatedVideos && !hideSubscriptions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedNavButton == null) {
|
||||
return hideHomeAndRelatedVideos; // Unknown tab, treat the same as home.
|
||||
}
|
||||
|
||||
switch (browseId) {
|
||||
case BROWSE_ID_HISTORY, BROWSE_ID_LIBRARY, BROWSE_ID_NOTIFICATION_INBOX -> {
|
||||
return false;
|
||||
}
|
||||
case BROWSE_ID_SUBSCRIPTIONS -> {
|
||||
return hideSubscriptions;
|
||||
}
|
||||
default -> {
|
||||
return hideHomeAndRelatedVideos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.patches.video.RestoreOldVideoQualityMenuPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}.
|
||||
*/
|
||||
public final class VideoQualityMenuFilter extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
|
||||
public static volatile boolean isVideoQualityMenuVisible;
|
||||
|
||||
public VideoQualityMenuFilter() {
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.RESTORE_OLD_VIDEO_QUALITY_MENU,
|
||||
"quick_quality_sheet_content.eml-js"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
isVideoQualityMenuVisible = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package app.revanced.extension.youtube.patches.feed;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class FeedPatch {
|
||||
|
||||
// region [Hide feed components] patch
|
||||
|
||||
public static int hideCategoryBarInFeed(final int height) {
|
||||
return Settings.HIDE_CATEGORY_BAR_IN_FEED.get() ? 0 : height;
|
||||
}
|
||||
|
||||
public static void hideCategoryBarInRelatedVideos(final View chipView) {
|
||||
Utils.hideViewBy0dpUnderCondition(
|
||||
Settings.HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS.get() || Settings.HIDE_RELATED_VIDEOS.get(),
|
||||
chipView
|
||||
);
|
||||
}
|
||||
|
||||
public static int hideCategoryBarInSearch(final int height) {
|
||||
return Settings.HIDE_CATEGORY_BAR_IN_SEARCH.get() ? 0 : height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rather than simply hiding the channel tab view, completely removes channel tab from list.
|
||||
* If a channel tab is removed from the list, users will not be able to open it by swiping.
|
||||
*
|
||||
* @param channelTabText Text to be assigned to channel tab, such as 'Shorts', 'Playlists', 'Community', 'Store'.
|
||||
* This text is hardcoded, so it follows the user's language.
|
||||
* @return Whether to remove the channel tab from the list.
|
||||
*/
|
||||
public static boolean hideChannelTab(String channelTabText) {
|
||||
if (!Settings.HIDE_CHANNEL_TAB.get()) {
|
||||
return false;
|
||||
}
|
||||
if (channelTabText == null || channelTabText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] blockList = Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS.get().split("\\n");
|
||||
|
||||
for (String filter : blockList) {
|
||||
if (!filter.isEmpty() && channelTabText.equals(filter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hideBreakingNewsShelf(View view) {
|
||||
hideViewBy0dpUnderCondition(
|
||||
Settings.HIDE_CAROUSEL_SHELF.get(),
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
public static View hideCaptionsButton(View view) {
|
||||
return Settings.HIDE_FEED_CAPTIONS_BUTTON.get() ? null : view;
|
||||
}
|
||||
|
||||
public static void hideCaptionsButtonContainer(View view) {
|
||||
hideViewUnderCondition(
|
||||
Settings.HIDE_FEED_CAPTIONS_BUTTON,
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean hideFloatingButton() {
|
||||
return Settings.HIDE_FLOATING_BUTTON.get();
|
||||
}
|
||||
|
||||
public static void hideLatestVideosButton(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_LATEST_VIDEOS_BUTTON.get(), view);
|
||||
}
|
||||
|
||||
public static boolean hideSubscriptionsChannelSection() {
|
||||
return Settings.HIDE_SUBSCRIPTIONS_CAROUSEL.get();
|
||||
}
|
||||
|
||||
public static void hideSubscriptionsChannelSection(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_SUBSCRIPTIONS_CAROUSEL, view);
|
||||
}
|
||||
|
||||
private static FrameLayout.LayoutParams layoutParams;
|
||||
private static int minimumHeight = -1;
|
||||
private static int paddingLeft = 12;
|
||||
private static int paddingTop = 0;
|
||||
private static int paddingRight = 12;
|
||||
private static int paddingBottom = 0;
|
||||
|
||||
/**
|
||||
* expandButtonContainer is used in channel profiles as well as search results.
|
||||
* We need to hide expandButtonContainer only in search results, not in channel profile.
|
||||
* <p>
|
||||
* If we hide expandButtonContainer with setVisibility, the empty space occupied by expandButtonContainer will still be left.
|
||||
* Therefore, we need to dynamically resize the View with LayoutParams.
|
||||
* <p>
|
||||
* Unlike other Views, expandButtonContainer cannot make a View invisible using the normal {@link Utils#hideViewByLayoutParams} method.
|
||||
* We should set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer.
|
||||
*
|
||||
* @param parentView Parent view of expandButtonContainer.
|
||||
*/
|
||||
public static void hideShowMoreButton(View parentView) {
|
||||
if (!Settings.HIDE_SHOW_MORE_BUTTON.get())
|
||||
return;
|
||||
|
||||
if (!(parentView instanceof ViewGroup viewGroup))
|
||||
return;
|
||||
|
||||
if (!(viewGroup.getChildAt(0) instanceof ViewGroup expandButtonContainer))
|
||||
return;
|
||||
|
||||
if (layoutParams == null) {
|
||||
// We need to get the original LayoutParams and paddings applied to expandButtonContainer.
|
||||
// Theses are used to make the expandButtonContainer visible again.
|
||||
if (expandButtonContainer.getLayoutParams() instanceof FrameLayout.LayoutParams lp) {
|
||||
layoutParams = lp;
|
||||
paddingLeft = parentView.getPaddingLeft();
|
||||
paddingTop = parentView.getPaddingTop();
|
||||
paddingRight = parentView.getPaddingRight();
|
||||
paddingBottom = parentView.getPaddingBottom();
|
||||
}
|
||||
}
|
||||
|
||||
// I'm not sure if 'Utils.runOnMainThreadDelayed' is absolutely necessary.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// MinimumHeight is also needed to make expandButtonContainer visible again.
|
||||
// Get original MinimumHeight.
|
||||
if (minimumHeight == -1) {
|
||||
minimumHeight = parentView.getMinimumHeight();
|
||||
}
|
||||
|
||||
// In the search results, the child view structure of expandButtonContainer is as follows:
|
||||
// expandButtonContainer
|
||||
// L TextView (first child view is SHOWN, 'Show more' text)
|
||||
// L ImageView (second child view is shown, dropdown arrow icon)
|
||||
|
||||
// In the channel profiles, the child view structure of expandButtonContainer is as follows:
|
||||
// expandButtonContainer
|
||||
// L TextView (first child view is HIDDEN, 'Show more' text)
|
||||
// L ImageView (second child view is shown, dropdown arrow icon)
|
||||
|
||||
if (expandButtonContainer.getChildAt(0).getVisibility() != View.VISIBLE && layoutParams != null) {
|
||||
// If the first child view (TextView) is HIDDEN, the channel profile is open.
|
||||
// Restore parent view's padding and MinimumHeight to make them visible.
|
||||
parentView.setMinimumHeight(minimumHeight);
|
||||
parentView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
|
||||
expandButtonContainer.setLayoutParams(layoutParams);
|
||||
} else {
|
||||
// If the first child view (TextView) is SHOWN, the search results is open.
|
||||
// Set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer.
|
||||
parentView.setMinimumHeight(0);
|
||||
parentView.setPadding(0, 0, 0, 0);
|
||||
expandButtonContainer.setLayoutParams(new FrameLayout.LayoutParams(0, 0));
|
||||
}
|
||||
}, 0
|
||||
);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide feed flyout menu] patch
|
||||
|
||||
/**
|
||||
* hide feed flyout menu for phone
|
||||
*
|
||||
* @param menuTitleCharSequence menu title
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence hideFlyoutMenu(@Nullable CharSequence menuTitleCharSequence) {
|
||||
if (menuTitleCharSequence != null && Settings.HIDE_FEED_FLYOUT_MENU.get()) {
|
||||
String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n");
|
||||
String menuTitleString = menuTitleCharSequence.toString();
|
||||
|
||||
for (String filter : blockList) {
|
||||
if (menuTitleString.equals(filter) && !filter.isEmpty())
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return menuTitleCharSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* hide feed flyout panel for tablet
|
||||
*
|
||||
* @param menuTextView flyout text view
|
||||
* @param menuTitleCharSequence raw text
|
||||
*/
|
||||
public static void hideFlyoutMenu(TextView menuTextView, CharSequence menuTitleCharSequence) {
|
||||
if (menuTitleCharSequence == null || !Settings.HIDE_FEED_FLYOUT_MENU.get())
|
||||
return;
|
||||
|
||||
if (!(menuTextView.getParent() instanceof View parentView))
|
||||
return;
|
||||
|
||||
String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n");
|
||||
String menuTitleString = menuTitleCharSequence.toString();
|
||||
|
||||
for (String filter : blockList) {
|
||||
if (menuTitleString.equals(filter) && !filter.isEmpty())
|
||||
Utils.hideViewByLayoutParams(parentView);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package app.revanced.extension.youtube.patches.feed;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.BottomSheetState;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class RelatedVideoPatch {
|
||||
private static final boolean HIDE_RELATED_VIDEOS = Settings.HIDE_RELATED_VIDEOS.get();
|
||||
|
||||
private static final int OFFSET = Settings.RELATED_VIDEOS_OFFSET.get();
|
||||
|
||||
// video title,channel bar, video action bar, comment
|
||||
private static final int MAX_ITEM_COUNT = 4 + OFFSET;
|
||||
|
||||
private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false);
|
||||
|
||||
public static void showEngagementPanel(@Nullable Object object) {
|
||||
engagementPanelOpen.set(object != null);
|
||||
}
|
||||
|
||||
public static void hideEngagementPanel() {
|
||||
engagementPanelOpen.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
public static int overrideItemCounts(int itemCounts) {
|
||||
if (!HIDE_RELATED_VIDEOS) {
|
||||
return itemCounts;
|
||||
}
|
||||
if (itemCounts < MAX_ITEM_COUNT) {
|
||||
return itemCounts;
|
||||
}
|
||||
if (!RootView.isPlayerActive()) {
|
||||
return itemCounts;
|
||||
}
|
||||
if (BottomSheetState.getCurrent().isOpen()) {
|
||||
return itemCounts;
|
||||
}
|
||||
if (engagementPanelOpen.get()) {
|
||||
return itemCounts;
|
||||
}
|
||||
return MAX_ITEM_COUNT;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ChangeStartPagePatch {
|
||||
|
||||
public enum StartPage {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL("", null),
|
||||
|
||||
/**
|
||||
* Browse id.
|
||||
*/
|
||||
BROWSE("FEguide_builder", TRUE),
|
||||
EXPLORE("FEexplore", TRUE),
|
||||
HISTORY("FEhistory", TRUE),
|
||||
LIBRARY("FElibrary", TRUE),
|
||||
MOVIE("FEstorefront", TRUE),
|
||||
SUBSCRIPTIONS("FEsubscriptions", TRUE),
|
||||
TRENDING("FEtrending", TRUE),
|
||||
|
||||
/**
|
||||
* Channel id, this can be used as a browseId.
|
||||
*/
|
||||
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
|
||||
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
|
||||
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
|
||||
SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE),
|
||||
|
||||
/**
|
||||
* Playlist id, this can be used as a browseId.
|
||||
*/
|
||||
LIKED_VIDEO("VLLL", TRUE),
|
||||
WATCH_LATER("VLWL", TRUE),
|
||||
|
||||
/**
|
||||
* Intent action.
|
||||
*/
|
||||
SEARCH("com.google.android.youtube.action.open.search", FALSE),
|
||||
SHORTS("com.google.android.youtube.action.open.shorts", FALSE);
|
||||
|
||||
@Nullable
|
||||
final Boolean isBrowseId;
|
||||
|
||||
@NonNull
|
||||
final String id;
|
||||
|
||||
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
|
||||
this.id = id;
|
||||
this.isBrowseId = isBrowseId;
|
||||
}
|
||||
|
||||
private boolean isBrowseId() {
|
||||
return BooleanUtils.isTrue(isBrowseId);
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean isIntentAction() {
|
||||
return BooleanUtils.isFalse(isBrowseId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent action when YouTube is cold started from the launcher.
|
||||
* <p>
|
||||
* If you don't check this, the hooking will also apply in the following cases:
|
||||
* Case 1. The user clicked Shorts button on the YouTube shortcut.
|
||||
* Case 2. The user clicked Shorts button on the YouTube widget.
|
||||
* In this case, instead of opening Shorts, the start page specified by the user is opened.
|
||||
*/
|
||||
private static final String ACTION_MAIN = "android.intent.action.MAIN";
|
||||
|
||||
private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get();
|
||||
private static final boolean ALWAYS_CHANGE_START_PAGE = Settings.CHANGE_START_PAGE_TYPE.get();
|
||||
|
||||
/**
|
||||
* There is an issue where the back button on the toolbar doesn't work properly.
|
||||
* As a workaround for this issue, instead of overriding the browserId multiple times, just override it once.
|
||||
*/
|
||||
private static boolean appLaunched = false;
|
||||
|
||||
public static String overrideBrowseId(@NonNull String original) {
|
||||
if (!START_PAGE.isBrowseId()) {
|
||||
return original;
|
||||
}
|
||||
if (!ALWAYS_CHANGE_START_PAGE && appLaunched) {
|
||||
Logger.printDebug(() -> "Ignore override browseId as the app already launched");
|
||||
return original;
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
final String browseId = START_PAGE.id;
|
||||
Logger.printDebug(() -> "Changing browseId to " + browseId);
|
||||
return browseId;
|
||||
}
|
||||
|
||||
public static void overrideIntentAction(@NonNull Intent intent) {
|
||||
if (!START_PAGE.isIntentAction()) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.equals(intent.getAction(), ACTION_MAIN)) {
|
||||
Logger.printDebug(() -> "Ignore override intent action" +
|
||||
" as the current activity is not the entry point of the application");
|
||||
return;
|
||||
}
|
||||
|
||||
final String intentAction = START_PAGE.id;
|
||||
Logger.printDebug(() -> "Changing intent action to " + intentAction);
|
||||
intent.setAction(intentAction);
|
||||
}
|
||||
|
||||
public static final class ChangeStartPageTypeAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Settings.CHANGE_START_PAGE.get() != StartPage.ORIGINAL;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class DownloadActionsPatch extends VideoUtils {
|
||||
|
||||
private static final BooleanSetting overrideVideoDownloadButton =
|
||||
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON;
|
||||
|
||||
private static final BooleanSetting overridePlaylistDownloadButton =
|
||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called from the in app download hook,
|
||||
* for both the player action button (below the video)
|
||||
* and the 'Download video' flyout option for feed videos.
|
||||
* <p>
|
||||
* Appears to always be called from the main thread.
|
||||
*/
|
||||
public static boolean inAppVideoDownloadButtonOnClick(String videoId) {
|
||||
try {
|
||||
if (!overrideVideoDownloadButton.get()) {
|
||||
return false;
|
||||
}
|
||||
if (videoId == null || videoId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
launchVideoExternalDownloader(videoId);
|
||||
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called from the in app playlist download hook.
|
||||
* <p>
|
||||
* Appears to always be called from the main thread.
|
||||
*/
|
||||
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
|
||||
try {
|
||||
if (!overridePlaylistDownloadButton.get()) {
|
||||
return playlistId;
|
||||
}
|
||||
if (playlistId == null || playlistId.isEmpty()) {
|
||||
return playlistId;
|
||||
}
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
|
||||
return "";
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
|
||||
}
|
||||
return playlistId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called from the 'Download playlist' flyout option.
|
||||
* <p>
|
||||
* Appears to always be called from the main thread.
|
||||
*/
|
||||
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
|
||||
try {
|
||||
if (!overridePlaylistDownloadButton.get()) {
|
||||
return false;
|
||||
}
|
||||
if (playlistId == null || playlistId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean overridePlaylistDownloadButtonVisibility() {
|
||||
return overridePlaylistDownloadButton.get();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,589 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.getChildView;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewByLayoutParams;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewGroupByMarginLayoutParams;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
|
||||
import static app.revanced.extension.youtube.patches.utils.PatchStatus.ImageSearchButton;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.apps.youtube.app.application.Shell_SettingsActivity;
|
||||
import com.google.android.apps.youtube.app.settings.SettingsActivity;
|
||||
import com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class GeneralPatch {
|
||||
|
||||
// region [Disable auto audio tracks] patch
|
||||
|
||||
private static final String DEFAULT_AUDIO_TRACKS_IDENTIFIER = "original";
|
||||
private static ArrayList<Object> formatStreamModelArray;
|
||||
|
||||
/**
|
||||
* Find the stream format containing the parameter {@link GeneralPatch#DEFAULT_AUDIO_TRACKS_IDENTIFIER}, and save to the array.
|
||||
*
|
||||
* @param formatStreamModel stream format model including audio tracks.
|
||||
*/
|
||||
public static void setFormatStreamModelArray(final Object formatStreamModel) {
|
||||
if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignoring, as the stream format model array has already been added.
|
||||
if (formatStreamModelArray != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignoring, as it is not an original audio track.
|
||||
if (!formatStreamModel.toString().contains(DEFAULT_AUDIO_TRACKS_IDENTIFIER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For some reason, when YouTube handles formatStreamModelArray,
|
||||
// it uses an array with duplicate values at the first and second indices.
|
||||
formatStreamModelArray = new ArrayList<>();
|
||||
formatStreamModelArray.add(formatStreamModel);
|
||||
formatStreamModelArray.add(formatStreamModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of stream format models containing the default audio tracks.
|
||||
*
|
||||
* @param localizedFormatStreamModelArray stream format model array consisting of audio tracks in the system's language.
|
||||
* @return stream format model array consisting of original audio tracks.
|
||||
*/
|
||||
public static ArrayList<Object> getFormatStreamModelArray(final ArrayList<Object> localizedFormatStreamModelArray) {
|
||||
if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) {
|
||||
return localizedFormatStreamModelArray;
|
||||
}
|
||||
|
||||
// Ignoring, as the stream format model array is empty.
|
||||
if (formatStreamModelArray == null || formatStreamModelArray.isEmpty()) {
|
||||
return localizedFormatStreamModelArray;
|
||||
}
|
||||
|
||||
// Initialize the array before returning it.
|
||||
ArrayList<Object> defaultFormatStreamModelArray = formatStreamModelArray;
|
||||
formatStreamModelArray = null;
|
||||
return defaultFormatStreamModelArray;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Disable splash animation] patch
|
||||
|
||||
public static boolean disableSplashAnimation(boolean original) {
|
||||
try {
|
||||
return !Settings.DISABLE_SPLASH_ANIMATION.get() && original;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to load disableSplashAnimation", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Enable gradient loading screen] patch
|
||||
|
||||
public static boolean enableGradientLoadingScreen() {
|
||||
return Settings.ENABLE_GRADIENT_LOADING_SCREEN.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide layout components] patch
|
||||
|
||||
private static String[] accountMenuBlockList;
|
||||
|
||||
static {
|
||||
accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n");
|
||||
// Some settings should not be hidden.
|
||||
accountMenuBlockList = Arrays.stream(accountMenuBlockList)
|
||||
.filter(item -> !Objects.equals(item, str("settings")))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* hide account menu in you tab
|
||||
*
|
||||
* @param menuTitleCharSequence menu title
|
||||
*/
|
||||
public static void hideAccountList(View view, CharSequence menuTitleCharSequence) {
|
||||
if (!Settings.HIDE_ACCOUNT_MENU.get())
|
||||
return;
|
||||
if (menuTitleCharSequence == null)
|
||||
return;
|
||||
if (!(view.getParent().getParent().getParent() instanceof ViewGroup viewGroup))
|
||||
return;
|
||||
|
||||
hideAccountMenu(viewGroup, menuTitleCharSequence.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* hide account menu for tablet and old clients
|
||||
*
|
||||
* @param menuTitleCharSequence menu title
|
||||
*/
|
||||
public static void hideAccountMenu(View view, CharSequence menuTitleCharSequence) {
|
||||
if (!Settings.HIDE_ACCOUNT_MENU.get())
|
||||
return;
|
||||
if (menuTitleCharSequence == null)
|
||||
return;
|
||||
if (!(view.getParent().getParent() instanceof ViewGroup viewGroup))
|
||||
return;
|
||||
|
||||
hideAccountMenu(viewGroup, menuTitleCharSequence.toString());
|
||||
}
|
||||
|
||||
private static void hideAccountMenu(ViewGroup viewGroup, String menuTitleString) {
|
||||
for (String filter : accountMenuBlockList) {
|
||||
if (!filter.isEmpty() && menuTitleString.equals(filter)) {
|
||||
if (viewGroup.getLayoutParams() instanceof MarginLayoutParams)
|
||||
hideViewGroupByMarginLayoutParams(viewGroup);
|
||||
else
|
||||
viewGroup.setLayoutParams(new LayoutParams(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int hideHandle(int originalValue) {
|
||||
return Settings.HIDE_HANDLE.get() ? 8 : originalValue;
|
||||
}
|
||||
|
||||
public static boolean hideFloatingMicrophone(boolean original) {
|
||||
return Settings.HIDE_FLOATING_MICROPHONE.get() || original;
|
||||
}
|
||||
|
||||
public static boolean hideSnackBar() {
|
||||
return Settings.HIDE_SNACK_BAR.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide navigation bar components] patch
|
||||
|
||||
private static final Map<NavigationButton, Boolean> shouldHideMap = new EnumMap<>(NavigationButton.class) {
|
||||
{
|
||||
put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get());
|
||||
put(NavigationButton.SHORTS, Settings.HIDE_NAVIGATION_SHORTS_BUTTON.get());
|
||||
put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON.get());
|
||||
put(NavigationButton.CREATE, Settings.HIDE_NAVIGATION_CREATE_BUTTON.get());
|
||||
put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON.get());
|
||||
put(NavigationButton.LIBRARY, Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get());
|
||||
}
|
||||
};
|
||||
|
||||
public static boolean enableNarrowNavigationButton(boolean original) {
|
||||
return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original;
|
||||
}
|
||||
|
||||
public static boolean enableTranslucentNavigationBar() {
|
||||
return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get();
|
||||
}
|
||||
|
||||
public static boolean switchCreateWithNotificationButton(boolean original) {
|
||||
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
|
||||
}
|
||||
|
||||
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
||||
if (BooleanUtils.isTrue(shouldHideMap.get(button))) {
|
||||
tabView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideNavigationLabel(TextView view) {
|
||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), view);
|
||||
}
|
||||
|
||||
public static void hideNavigationBar(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR.get(), view);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Remove viewer discretion dialog] patch
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
|
||||
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
|
||||
* <a href="https://stackoverflow.com/a/4604145"/>
|
||||
* <p>
|
||||
* That's why {@link AlertDialog#show()} is absolutely necessary.
|
||||
* Instead, use two tricks to hide Alertdialog.
|
||||
* <p>
|
||||
* 1. Change the size of AlertDialog to 0.
|
||||
* 2. Disable AlertDialog's background dim.
|
||||
* <p>
|
||||
* This way, AlertDialog will be completely hidden,
|
||||
* and {@link AlertDialog#getButton(int)} method can be used without issue.
|
||||
*/
|
||||
public static void confirmDialog(final AlertDialog dialog) {
|
||||
if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This method is called after AlertDialog#show(),
|
||||
// So we need to hide the AlertDialog before pressing the possitive button.
|
||||
final Window window = dialog.getWindow();
|
||||
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if (window != null && button != null) {
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
params.height = 0;
|
||||
params.width = 0;
|
||||
|
||||
// Change the size of AlertDialog to 0.
|
||||
window.setAttributes(params);
|
||||
|
||||
// Disable AlertDialog's background dim.
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
Utils.clickView(button);
|
||||
}
|
||||
}
|
||||
|
||||
public static void confirmDialogAgeVerified(final AlertDialog dialog) {
|
||||
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if (!button.getText().toString().equals(str("og_continue")))
|
||||
return;
|
||||
|
||||
confirmDialog(dialog);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Spoof app version] patch
|
||||
|
||||
public static String getVersionOverride(String appVersion) {
|
||||
return Settings.SPOOF_APP_VERSION.get()
|
||||
? Settings.SPOOF_APP_VERSION_TARGET.get()
|
||||
: appVersion;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Toolbar components] patch
|
||||
|
||||
private static final int generalHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytWordmarkHeader");
|
||||
private static final int premiumHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytPremiumWordmarkHeader");
|
||||
|
||||
public static void setDrawerNavigationHeader(View lithoView) {
|
||||
final int headerAttributeId = getHeaderAttributeId();
|
||||
|
||||
lithoView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
if (!(lithoView instanceof ViewGroup viewGroup))
|
||||
return;
|
||||
if (!(viewGroup.getChildAt(0) instanceof ImageView imageView))
|
||||
return;
|
||||
final Activity mActivity = Utils.getActivity();
|
||||
if (mActivity == null)
|
||||
return;
|
||||
imageView.setImageDrawable(getHeaderDrawable(mActivity, headerAttributeId));
|
||||
});
|
||||
}
|
||||
|
||||
public static int getHeaderAttributeId() {
|
||||
return Settings.CHANGE_YOUTUBE_HEADER.get()
|
||||
? premiumHeaderAttributeId
|
||||
: generalHeaderAttributeId;
|
||||
}
|
||||
|
||||
public static boolean overridePremiumHeader() {
|
||||
return Settings.CHANGE_YOUTUBE_HEADER.get();
|
||||
}
|
||||
|
||||
private static Drawable getHeaderDrawable(Activity mActivity, int resourceId) {
|
||||
// Rest of the implementation added by patch.
|
||||
return ResourceUtils.getDrawable("");
|
||||
}
|
||||
|
||||
private static final int searchBarId = ResourceUtils.getIdIdentifier("search_bar");
|
||||
private static final int youtubeTextId = ResourceUtils.getIdIdentifier("youtube_text");
|
||||
private static final int searchBoxId = ResourceUtils.getIdIdentifier("search_box");
|
||||
private static final int searchIconId = ResourceUtils.getIdIdentifier("search_icon");
|
||||
|
||||
private static final boolean wideSearchbarEnabled = Settings.ENABLE_WIDE_SEARCH_BAR.get();
|
||||
// Loads the search bar deprecated by Google.
|
||||
private static final boolean wideSearchbarWithHeaderEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_WITH_HEADER.get();
|
||||
private static final boolean wideSearchbarYouTabEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB.get();
|
||||
|
||||
public static boolean enableWideSearchBar(boolean original) {
|
||||
return wideSearchbarEnabled || original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limitation: Premium header will not be applied for YouTube Premium users if the user uses the 'Wide search bar with header' option.
|
||||
* This is because it forces the deprecated search bar to be loaded.
|
||||
* As a solution to this limitation, 'Change YouTube header' patch is required.
|
||||
*/
|
||||
public static boolean enableWideSearchBarWithHeader(boolean original) {
|
||||
if (!wideSearchbarEnabled)
|
||||
return original;
|
||||
else
|
||||
return wideSearchbarWithHeaderEnabled || original;
|
||||
}
|
||||
|
||||
public static boolean enableWideSearchBarWithHeaderInverse(boolean original) {
|
||||
if (!wideSearchbarEnabled)
|
||||
return original;
|
||||
else
|
||||
return !wideSearchbarWithHeaderEnabled && original;
|
||||
}
|
||||
|
||||
public static boolean enableWideSearchBarInYouTab(boolean original) {
|
||||
if (!wideSearchbarEnabled)
|
||||
return original;
|
||||
else
|
||||
return !wideSearchbarYouTabEnabled && original;
|
||||
}
|
||||
|
||||
public static void setWideSearchBarLayout(View view) {
|
||||
if (!wideSearchbarEnabled)
|
||||
return;
|
||||
if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView))
|
||||
return;
|
||||
|
||||
// When the deprecated search bar is loaded, two search bars overlap.
|
||||
// Manually hides another search bar.
|
||||
if (wideSearchbarWithHeaderEnabled) {
|
||||
final View searchIconView = searchBarView.findViewById(searchIconId);
|
||||
final View searchBoxView = searchBarView.findViewById(searchBoxId);
|
||||
final View textView = searchBarView.findViewById(youtubeTextId);
|
||||
if (textView != null) {
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(0, 0);
|
||||
layoutParams.setMargins(0, 0, 0, 0);
|
||||
textView.setLayoutParams(layoutParams);
|
||||
}
|
||||
// The search icon in the deprecated search bar is clickable, but onClickListener is not assigned.
|
||||
// Assign onClickListener and disable the effect when clicked.
|
||||
if (searchIconView != null && searchBoxView != null) {
|
||||
searchIconView.setOnClickListener(view1 -> searchBoxView.callOnClick());
|
||||
searchIconView.getBackground().setAlpha(0);
|
||||
}
|
||||
} else {
|
||||
// This is the legacy method - Wide search bar without YouTube header.
|
||||
// Since the padding start is 0, it does not look good.
|
||||
// Add a padding start of 8.0 dip.
|
||||
final int paddingLeft = searchBarView.getPaddingLeft();
|
||||
final int paddingRight = searchBarView.getPaddingRight();
|
||||
final int paddingTop = searchBarView.getPaddingTop();
|
||||
final int paddingBottom = searchBarView.getPaddingBottom();
|
||||
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, Utils.getResources().getDisplayMetrics());
|
||||
|
||||
// In RelativeLayout, paddingStart cannot be assigned programmatically.
|
||||
// Check RTL layout and set left padding or right padding.
|
||||
if (Utils.isRightToLeftTextLayout()) {
|
||||
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
||||
} else {
|
||||
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hideCastButton(boolean original) {
|
||||
return !Settings.HIDE_TOOLBAR_CAST_BUTTON.get() && original;
|
||||
}
|
||||
|
||||
public static void hideCastButton(MenuItem menuItem) {
|
||||
if (!Settings.HIDE_TOOLBAR_CAST_BUTTON.get())
|
||||
return;
|
||||
|
||||
menuItem.setVisible(false);
|
||||
menuItem.setEnabled(false);
|
||||
}
|
||||
|
||||
public static void hideCreateButton(String enumString, View view) {
|
||||
if (!Settings.HIDE_TOOLBAR_CREATE_BUTTON.get())
|
||||
return;
|
||||
|
||||
hideViewUnderCondition(isCreateButton(enumString), view);
|
||||
}
|
||||
|
||||
public static void hideNotificationButton(String enumString, View view) {
|
||||
if (!Settings.HIDE_TOOLBAR_NOTIFICATION_BUTTON.get())
|
||||
return;
|
||||
|
||||
hideViewUnderCondition(isNotificationButton(enumString), view);
|
||||
}
|
||||
|
||||
public static boolean hideSearchTermThumbnail() {
|
||||
return Settings.HIDE_SEARCH_TERM_THUMBNAIL.get();
|
||||
}
|
||||
|
||||
private static final boolean hideImageSearchButton = Settings.HIDE_IMAGE_SEARCH_BUTTON.get();
|
||||
private static final boolean hideVoiceSearchButton = Settings.HIDE_VOICE_SEARCH_BUTTON.get();
|
||||
|
||||
/**
|
||||
* If the user does not hide the Image search button but only the Voice search button,
|
||||
* {@link View#setVisibility(int)} cannot be used on the Voice search button.
|
||||
* (This breaks the search bar layout.)
|
||||
* <p>
|
||||
* In this case, {@link Utils#hideViewByLayoutParams(View)} should be used.
|
||||
*/
|
||||
private static final boolean showImageSearchButtonAndHideVoiceSearchButton = !hideImageSearchButton && hideVoiceSearchButton && ImageSearchButton();
|
||||
|
||||
public static boolean hideImageSearchButton(boolean original) {
|
||||
return !hideImageSearchButton && original;
|
||||
}
|
||||
|
||||
public static void hideVoiceSearchButton(View view) {
|
||||
if (showImageSearchButtonAndHideVoiceSearchButton) {
|
||||
hideViewByLayoutParams(view);
|
||||
} else {
|
||||
hideViewUnderCondition(hideVoiceSearchButton, view);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideVoiceSearchButton(View view, int visibility) {
|
||||
if (showImageSearchButtonAndHideVoiceSearchButton) {
|
||||
view.setVisibility(visibility);
|
||||
hideViewByLayoutParams(view);
|
||||
} else {
|
||||
view.setVisibility(
|
||||
hideVoiceSearchButton
|
||||
? View.GONE : visibility
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In ReVanced, image files are replaced to change the header,
|
||||
* Whereas in RVX, the header is changed programmatically.
|
||||
* There is an issue where the header is not changed in RVX when YouTube Doodles are hidden.
|
||||
* As a workaround, manually set the header when YouTube Doodles are hidden.
|
||||
*/
|
||||
public static void hideYouTubeDoodles(ImageView imageView, Drawable drawable) {
|
||||
final Activity mActivity = Utils.getActivity();
|
||||
if (Settings.HIDE_YOUTUBE_DOODLES.get() && mActivity != null) {
|
||||
drawable = getHeaderDrawable(mActivity, getHeaderAttributeId());
|
||||
}
|
||||
imageView.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
private static final int settingsDrawableId =
|
||||
ResourceUtils.getDrawableIdentifier("yt_outline_gear_black_24");
|
||||
|
||||
public static int getCreateButtonDrawableId(int original) {
|
||||
return Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get() &&
|
||||
settingsDrawableId != 0
|
||||
? settingsDrawableId
|
||||
: original;
|
||||
}
|
||||
|
||||
public static void replaceCreateButton(String enumString, View toolbarView) {
|
||||
if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get())
|
||||
return;
|
||||
// Check if the button is a create button.
|
||||
if (!isCreateButton(enumString))
|
||||
return;
|
||||
ImageView imageView = getChildView((ViewGroup) toolbarView, view -> view instanceof ImageView);
|
||||
if (imageView == null)
|
||||
return;
|
||||
|
||||
// Overriding is possible only after OnClickListener is assigned to the create button.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE.get()) {
|
||||
imageView.setOnClickListener(GeneralPatch::openRVXSettings);
|
||||
imageView.setOnLongClickListener(button -> {
|
||||
openYouTubeSettings(button);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
imageView.setOnClickListener(GeneralPatch::openYouTubeSettings);
|
||||
imageView.setOnLongClickListener(button -> {
|
||||
openRVXSettings(button);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private static void openYouTubeSettings(View view) {
|
||||
Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.setPackage(context.getPackageName());
|
||||
intent.setClass(context, Shell_SettingsActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void openRVXSettings(View view) {
|
||||
Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.setPackage(context.getPackageName());
|
||||
intent.setData(Uri.parse("revanced_extended_settings_intent"));
|
||||
intent.setClass(context, VideoQualitySettingsActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* The theme of {@link Shell_SettingsActivity} is dark theme.
|
||||
* Since this theme is hardcoded, we should manually specify the theme for the activity.
|
||||
* <p>
|
||||
* Since {@link Shell_SettingsActivity} only invokes {@link SettingsActivity}, finish activity after specifying a theme.
|
||||
*
|
||||
* @param base {@link Shell_SettingsActivity}
|
||||
*/
|
||||
public static void setShellActivityTheme(Activity base) {
|
||||
if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get())
|
||||
return;
|
||||
|
||||
base.setTheme(ThemeUtils.getThemeId());
|
||||
Utils.runOnMainThreadDelayed(base::finish, 0);
|
||||
}
|
||||
|
||||
|
||||
private static boolean isCreateButton(String enumString) {
|
||||
return StringUtils.equalsAny(
|
||||
enumString,
|
||||
"CREATION_ENTRY", // Create button for Phone layout
|
||||
"FAB_CAMERA" // Create button for Tablet layout
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isNotificationButton(String enumString) {
|
||||
return StringUtils.equalsAny(
|
||||
enumString,
|
||||
"TAB_ACTIVITY", // Notification button
|
||||
"TAB_ACTIVITY_CAIRO" // Notification button (new layout)
|
||||
);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.utils.PackageUtils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LayoutSwitchPatch {
|
||||
|
||||
public enum FormFactor {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL(null, null, null),
|
||||
SMALL_FORM_FACTOR(1, null, TRUE),
|
||||
SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
|
||||
LARGE_FORM_FACTOR(2, null, FALSE),
|
||||
LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
|
||||
|
||||
@Nullable
|
||||
final Integer formFactorType;
|
||||
|
||||
@Nullable
|
||||
final Integer widthDp;
|
||||
|
||||
@Nullable
|
||||
final Boolean setMinimumDp;
|
||||
|
||||
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
|
||||
this.formFactorType = formFactorType;
|
||||
this.widthDp = widthDp;
|
||||
this.setMinimumDp = setMinimumDp;
|
||||
}
|
||||
|
||||
private boolean setMinimumDp() {
|
||||
return BooleanUtils.isTrue(setMinimumDp);
|
||||
}
|
||||
}
|
||||
|
||||
private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
|
||||
|
||||
public static int getFormFactor(int original) {
|
||||
Integer formFactorType = FORM_FACTOR.formFactorType;
|
||||
return formFactorType == null
|
||||
? original
|
||||
: formFactorType;
|
||||
}
|
||||
|
||||
public static int getWidthDp(int original) {
|
||||
Integer widthDp = FORM_FACTOR.widthDp;
|
||||
if (widthDp == null) {
|
||||
return original;
|
||||
}
|
||||
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
|
||||
if (smallestScreenWidthDp == 0) {
|
||||
return original;
|
||||
}
|
||||
return FORM_FACTOR.setMinimumDp()
|
||||
? Math.min(smallestScreenWidthDp, widthDp)
|
||||
: Math.max(smallestScreenWidthDp, widthDp);
|
||||
}
|
||||
|
||||
public static boolean phoneLayoutEnabled() {
|
||||
return Objects.equals(FORM_FACTOR.formFactorType, 1);
|
||||
}
|
||||
|
||||
public static boolean tabletLayoutEnabled() {
|
||||
return Objects.equals(FORM_FACTOR.formFactorType, 2);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.ORIGINAL;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class MiniplayerPatch {
|
||||
|
||||
/**
|
||||
* Mini player type. Null fields indicates to use the original un-patched value.
|
||||
*/
|
||||
public enum MiniplayerType {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL(null, null),
|
||||
PHONE(false, null),
|
||||
TABLET(true, null),
|
||||
MODERN_1(null, 1),
|
||||
MODERN_2(null, 2),
|
||||
MODERN_3(null, 3);
|
||||
|
||||
/**
|
||||
* Legacy tablet hook value.
|
||||
*/
|
||||
@Nullable
|
||||
final Boolean legacyTabletOverride;
|
||||
|
||||
/**
|
||||
* Modern player type used by YT.
|
||||
*/
|
||||
@Nullable
|
||||
final Integer modernPlayerType;
|
||||
|
||||
MiniplayerType(@Nullable Boolean legacyTabletOverride, @Nullable Integer modernPlayerType) {
|
||||
this.legacyTabletOverride = legacyTabletOverride;
|
||||
this.modernPlayerType = modernPlayerType;
|
||||
}
|
||||
|
||||
public boolean isModern() {
|
||||
return modernPlayerType != null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern subtitle overlay for {@link MiniplayerType#MODERN_2}.
|
||||
* Resource is not present in older targets, and this field will be zero.
|
||||
*/
|
||||
private static final int MODERN_OVERLAY_SUBTITLE_TEXT
|
||||
= ResourceUtils.getIdIdentifier("modern_miniplayer_subtitle_text");
|
||||
|
||||
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
|
||||
|
||||
private static final boolean DOUBLE_TAP_ACTION_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get();
|
||||
|
||||
private static final boolean DRAG_AND_DROP_ENABLED =
|
||||
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
||||
|
||||
private static final boolean HIDE_EXPAND_CLOSE_AVAILABLE =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) &&
|
||||
!DOUBLE_TAP_ACTION_ENABLED &&
|
||||
!DRAG_AND_DROP_ENABLED;
|
||||
|
||||
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
|
||||
HIDE_EXPAND_CLOSE_AVAILABLE && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get();
|
||||
|
||||
private static final boolean HIDE_SUBTEXT_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
|
||||
|
||||
private static final boolean HIDE_REWIND_FORWARD_ENABLED =
|
||||
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get();
|
||||
|
||||
private static final int OPACITY_LEVEL;
|
||||
|
||||
static {
|
||||
final int opacity = validateValue(
|
||||
Settings.MINIPLAYER_OPACITY,
|
||||
0,
|
||||
100,
|
||||
"revanced_miniplayer_opacity_invalid_toast"
|
||||
);
|
||||
|
||||
OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getLegacyTabletMiniplayerOverride(boolean original) {
|
||||
Boolean isTablet = CURRENT_TYPE.legacyTabletOverride;
|
||||
return isTablet == null
|
||||
? original
|
||||
: isTablet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getModernMiniplayerOverride(boolean original) {
|
||||
return CURRENT_TYPE == ORIGINAL
|
||||
? original
|
||||
: CURRENT_TYPE.isModern();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getModernMiniplayerOverrideType(int original) {
|
||||
Integer modernValue = CURRENT_TYPE.modernPlayerType;
|
||||
return modernValue == null
|
||||
? original
|
||||
: modernValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void adjustMiniplayerOpacity(ImageView view) {
|
||||
if (CURRENT_TYPE == MODERN_1) {
|
||||
view.setImageAlpha(OPACITY_LEVEL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDoubleTapAction() {
|
||||
return DOUBLE_TAP_ACTION_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDragAndDrop() {
|
||||
return DRAG_AND_DROP_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideMiniplayerExpandClose(ImageView view) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_EXPAND_CLOSE_ENABLED, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hideMiniplayerRewindForward(ImageView view) {
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(HIDE_REWIND_FORWARD_ENABLED, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideMiniplayerSubTexts(View view) {
|
||||
// Different subviews are passed in, but only TextView and layouts are of interest here.
|
||||
final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout);
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(hideView, view);
|
||||
return hideView || view == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void playerOverlayGroupCreated(View group) {
|
||||
// Modern 2 has an half broken subtitle that is always present.
|
||||
// Always hide it to make the miniplayer mostly usable.
|
||||
if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) {
|
||||
if (group instanceof ViewGroup viewGroup) {
|
||||
View subtitleText = Utils.getChildView(viewGroup, true,
|
||||
view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT);
|
||||
|
||||
if (subtitleText != null) {
|
||||
subtitleText.setVisibility(View.GONE);
|
||||
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import app.revanced.extension.shared.patches.BaseSettingsMenuPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SettingsMenuPatch extends BaseSettingsMenuPatch {
|
||||
|
||||
public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) {
|
||||
if (mPreferenceScreen == null) return;
|
||||
for (SettingsMenuComponent component : SettingsMenuComponent.values())
|
||||
if (component.enabled)
|
||||
removePreference(mPreferenceScreen, component.key);
|
||||
}
|
||||
|
||||
private enum SettingsMenuComponent {
|
||||
YOUTUBE_TV("yt_unplugged_pref_key", Settings.HIDE_SETTINGS_MENU_YOUTUBE_TV.get()),
|
||||
PARENT_TOOLS("parent_tools_key", Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get()),
|
||||
PRE_PURCHASE("yt_unlimited_pre_purchase_key", Settings.HIDE_SETTINGS_MENU_PRE_PURCHASE.get()),
|
||||
GENERAL("general_key", Settings.HIDE_SETTINGS_MENU_GENERAL.get()),
|
||||
ACCOUNT("account_switcher_key", Settings.HIDE_SETTINGS_MENU_ACCOUNT.get()),
|
||||
DATA_SAVING("data_saving_settings_key", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()),
|
||||
AUTOPLAY("auto_play_key", Settings.HIDE_SETTINGS_MENU_AUTOPLAY.get()),
|
||||
VIDEO_QUALITY_PREFERENCES("video_quality_settings_key", Settings.HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES.get()),
|
||||
POST_PURCHASE("yt_unlimited_post_purchase_key", Settings.HIDE_SETTINGS_MENU_POST_PURCHASE.get()),
|
||||
OFFLINE("offline_key", Settings.HIDE_SETTINGS_MENU_OFFLINE.get()),
|
||||
WATCH_ON_TV("pair_with_tv_key", Settings.HIDE_SETTINGS_MENU_WATCH_ON_TV.get()),
|
||||
MANAGE_ALL_HISTORY("history_key", Settings.HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY.get()),
|
||||
YOUR_DATA_IN_YOUTUBE("your_data_key", Settings.HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE.get()),
|
||||
PRIVACY("privacy_key", Settings.HIDE_SETTINGS_MENU_PRIVACY.get()),
|
||||
TRY_EXPERIMENTAL_NEW_FEATURES("premium_early_access_browse_page_key", Settings.HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES.get()),
|
||||
PURCHASES_AND_MEMBERSHIPS("subscription_product_setting_key", Settings.HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS.get()),
|
||||
BILLING_AND_PAYMENTS("billing_and_payment_key", Settings.HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS.get()),
|
||||
NOTIFICATIONS("notification_key", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()),
|
||||
THIRD_PARTY("third_party_key", Settings.HIDE_SETTINGS_MENU_THIRD_PARTY.get()),
|
||||
CONNECTED_APPS("connected_accounts_browse_page_key", Settings.HIDE_SETTINGS_MENU_CONNECTED_APPS.get()),
|
||||
LIVE_CHAT("live_chat_key", Settings.HIDE_SETTINGS_MENU_LIVE_CHAT.get()),
|
||||
CAPTIONS("captions_key", Settings.HIDE_SETTINGS_MENU_CAPTIONS.get()),
|
||||
ACCESSIBILITY("accessibility_settings_key", Settings.HIDE_SETTINGS_MENU_ACCESSIBILITY.get()),
|
||||
ABOUT("about_key", Settings.HIDE_SETTINGS_MENU_ABOUT.get());
|
||||
|
||||
private final String key;
|
||||
private final boolean enabled;
|
||||
|
||||
SettingsMenuComponent(String key, boolean enabled) {
|
||||
this.key = key;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.general;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class YouTubeMusicActionsPatch extends VideoUtils {
|
||||
|
||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
||||
|
||||
private static final boolean isOverrideYouTubeMusicEnabled =
|
||||
Settings.OVERRIDE_YOUTUBE_MUSIC_BUTTON.get();
|
||||
|
||||
private static final boolean overrideYouTubeMusicEnabled =
|
||||
isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled();
|
||||
|
||||
public static String overridePackageName(@NonNull String packageName) {
|
||||
if (!overrideYouTubeMusicEnabled) {
|
||||
return packageName;
|
||||
}
|
||||
if (!StringUtils.equals(PACKAGE_NAME_YOUTUBE_MUSIC, packageName)) {
|
||||
return packageName;
|
||||
}
|
||||
final String thirdPartyPackageName = Settings.THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME.get();
|
||||
if (!ExtendedUtils.isPackageEnabled(thirdPartyPackageName)) {
|
||||
return packageName;
|
||||
}
|
||||
return thirdPartyPackageName;
|
||||
}
|
||||
|
||||
private static boolean isYouTubeMusicEnabled() {
|
||||
return ExtendedUtils.isPackageEnabled(PACKAGE_NAME_YOUTUBE_MUSIC);
|
||||
}
|
||||
|
||||
public static final class HookYouTubeMusicAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return isYouTubeMusicEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HookYouTubeMusicPackageNameAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BackgroundPlaybackPatch {
|
||||
|
||||
public static boolean allowBackgroundPlayback(boolean original) {
|
||||
return original || ShortsPlayerState.getCurrent().isClosed();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ExternalBrowserPatch {
|
||||
|
||||
public static String enableExternalBrowser(final String original) {
|
||||
if (!Settings.ENABLE_EXTERNAL_BROWSER.get())
|
||||
return original;
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class OpenLinksDirectlyPatch {
|
||||
private static final String YOUTUBE_REDIRECT_PATH = "/redirect";
|
||||
|
||||
public static Uri enableBypassRedirect(String uri) {
|
||||
final Uri parsed = Uri.parse(uri);
|
||||
if (!Settings.ENABLE_OPEN_LINKS_DIRECTLY.get())
|
||||
return parsed;
|
||||
|
||||
if (Objects.equals(parsed.getPath(), YOUTUBE_REDIRECT_PATH)) {
|
||||
return Uri.parse(Uri.decode(parsed.getQueryParameter("q")));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class OpusCodecPatch {
|
||||
|
||||
public static boolean enableOpusCodec() {
|
||||
return Settings.ENABLE_OPUS_CODEC.get();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class QUICProtocolPatch {
|
||||
|
||||
public static boolean disableQUICProtocol(boolean original) {
|
||||
try {
|
||||
return !Settings.DISABLE_QUIC_PROTOCOL.get() && original;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to load disableQUICProtocol", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.components.ShareSheetMenuFilter;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ShareSheetPatch {
|
||||
private static final boolean changeShareSheetEnabled = Settings.CHANGE_SHARE_SHEET.get();
|
||||
|
||||
private static void clickSystemShareButton(final RecyclerView bottomSheetRecyclerView,
|
||||
final RecyclerView appsContainerRecyclerView) {
|
||||
if (appsContainerRecyclerView.getChildAt(appsContainerRecyclerView.getChildCount() - 1) instanceof ViewGroup parentView &&
|
||||
parentView.getChildAt(0) instanceof ViewGroup shareWithOtherAppsView) {
|
||||
ShareSheetMenuFilter.isShareSheetMenuVisible = false;
|
||||
|
||||
bottomSheetRecyclerView.setVisibility(View.GONE);
|
||||
Utils.clickView(shareWithOtherAppsView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onShareSheetMenuCreate(final RecyclerView recyclerView) {
|
||||
if (!changeShareSheetEnabled)
|
||||
return;
|
||||
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
if (!ShareSheetMenuFilter.isShareSheetMenuVisible) {
|
||||
return;
|
||||
}
|
||||
if (!(recyclerView.getChildAt(0) instanceof ViewGroup parentView4th)) {
|
||||
return;
|
||||
}
|
||||
if (parentView4th.getChildAt(0) instanceof ViewGroup parentView3rd &&
|
||||
parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) {
|
||||
clickSystemShareButton(recyclerView, appsContainerRecyclerView);
|
||||
} else if (parentView4th.getChildAt(1) instanceof ViewGroup parentView3rd &&
|
||||
parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) {
|
||||
clickSystemShareButton(recyclerView, appsContainerRecyclerView);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onShareSheetMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String overridePackageName(String original) {
|
||||
return changeShareSheetEnabled ? "" : original;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.youtube.patches.misc.requests.StreamingDataRequest;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofStreamingDataPatch {
|
||||
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get();
|
||||
|
||||
/**
|
||||
* Any unreachable ip address. Used to intentionally fail requests.
|
||||
*/
|
||||
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
|
||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Blocks /get_watch requests by returning an unreachable URI.
|
||||
*
|
||||
* @param playerRequestUri The URI of the player request.
|
||||
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
|
||||
*/
|
||||
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
String path = playerRequestUri.getPath();
|
||||
|
||||
if (path != null && path.contains("get_watch")) {
|
||||
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
|
||||
|
||||
return UNREACHABLE_HOST_URI;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "blockGetWatchRequest failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return playerRequestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Blocks /initplayback requests.
|
||||
*/
|
||||
public static String blockInitPlaybackRequest(String originalUrlString) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
var originalUri = Uri.parse(originalUrlString);
|
||||
String path = originalUri.getPath();
|
||||
|
||||
if (path != null && path.contains("initplayback")) {
|
||||
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
|
||||
|
||||
return UNREACHABLE_HOST_URI_STRING;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return originalUrlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isSpoofingEnabled() {
|
||||
return SPOOF_STREAMING_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
Uri uri = Uri.parse(url);
|
||||
String path = uri.getPath();
|
||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
|
||||
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
|
||||
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "buildRequest failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Fix playback by replace the streaming data.
|
||||
* Called after {@link #fetchStreams(String, Map)} .
|
||||
*/
|
||||
@Nullable
|
||||
public static ByteBuffer getStreamingData(String videoId) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
|
||||
if (request != null) {
|
||||
// This hook is always called off the main thread,
|
||||
// but this can later be called for the same video id from the main thread.
|
||||
// This is not a concern, since the fetch will always be finished
|
||||
// and never block the main thread.
|
||||
// But if debugging, then still verify this is the situation.
|
||||
if (Settings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
|
||||
Logger.printException(() -> "Error: Blocking main thread");
|
||||
}
|
||||
var stream = request.getStream();
|
||||
if (stream != null) {
|
||||
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getStreamingData failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called after {@link #getStreamingData(String)}.
|
||||
*/
|
||||
@Nullable
|
||||
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
try {
|
||||
final int methodPost = 2;
|
||||
if (method == methodPost) {
|
||||
String path = uri.getPath();
|
||||
if (path != null && path.contains("videoplayback")) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String appendSpoofedClient(String videoFormat) {
|
||||
try {
|
||||
if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
|
||||
&& !TextUtils.isEmpty(videoFormat)) {
|
||||
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
|
||||
return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "appendSpoofedClient failure", ex);
|
||||
}
|
||||
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
public static final class iOSAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package app.revanced.extension.youtube.patches.misc;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class WatchHistoryPatch {
|
||||
|
||||
public enum WatchHistoryType {
|
||||
ORIGINAL,
|
||||
REPLACE,
|
||||
BLOCK
|
||||
}
|
||||
|
||||
private static final Uri UNREACHABLE_HOST_URI = Uri.parse("https://127.0.0.0");
|
||||
private static final String WWW_TRACKING_URL_AUTHORITY = "www.youtube.com";
|
||||
|
||||
public static Uri replaceTrackingUrl(Uri trackingUrl) {
|
||||
final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get();
|
||||
if (watchHistoryType != WatchHistoryType.ORIGINAL) {
|
||||
try {
|
||||
if (watchHistoryType == WatchHistoryType.REPLACE) {
|
||||
return trackingUrl.buildUpon().authority(WWW_TRACKING_URL_AUTHORITY).build();
|
||||
} else if (watchHistoryType == WatchHistoryType.BLOCK) {
|
||||
return UNREACHABLE_HOST_URI;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "replaceTrackingUrl failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return trackingUrl;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package app.revanced.extension.youtube.patches.misc.client;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AppClient {
|
||||
|
||||
// ANDROID
|
||||
private static final String OS_NAME_ANDROID = "Android";
|
||||
|
||||
// IOS
|
||||
/**
|
||||
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
||||
*
|
||||
* <p>
|
||||
* It can be extracted by getting the latest release version of the app on
|
||||
* <a href="https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/">the App
|
||||
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
||||
* </p>
|
||||
*/
|
||||
private static final String CLIENT_VERSION_IOS = "19.47.7";
|
||||
private static final String DEVICE_MAKE_IOS = "Apple";
|
||||
/**
|
||||
* The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps.
|
||||
* The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding.
|
||||
*
|
||||
* <p>
|
||||
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
|
||||
* information.
|
||||
* </p>
|
||||
*/
|
||||
private static final String DEVICE_MODEL_IOS = DeviceHardwareSupport.allowAV1()
|
||||
? "iPhone17,2"
|
||||
: "iPhone11,4";
|
||||
private static final String OS_NAME_IOS = "iOS";
|
||||
/**
|
||||
* The minimum supported OS version for the iOS YouTube client is iOS 14.0.
|
||||
* Using an invalid OS version will use the AVC codec.
|
||||
*/
|
||||
private static final String OS_VERSION_IOS = DeviceHardwareSupport.allowVP9()
|
||||
? "18.1.1.22B91"
|
||||
: "13.7.17H35";
|
||||
private static final String USER_AGENT_VERSION_IOS = DeviceHardwareSupport.allowVP9()
|
||||
? "18_1_1"
|
||||
: "13_7";
|
||||
private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
|
||||
CLIENT_VERSION_IOS +
|
||||
"(" +
|
||||
DEVICE_MODEL_IOS +
|
||||
"; U; CPU iOS " +
|
||||
USER_AGENT_VERSION_IOS +
|
||||
" like Mac OS X)";
|
||||
|
||||
// ANDROID VR
|
||||
/**
|
||||
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
|
||||
*
|
||||
* <p>
|
||||
* It can be extracted by getting the latest release version of the app on
|
||||
* <a href="https://www.meta.com/en-us/experiences/2002317119880945/">the App
|
||||
* Store page of the YouTube app</a>, in the {@code Additional details} section.
|
||||
* </p>
|
||||
*/
|
||||
private static final String CLIENT_VERSION_ANDROID_VR = "1.60.19";
|
||||
/**
|
||||
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
|
||||
*
|
||||
* <p>
|
||||
* See <a href="https://dumps.tadiphone.dev/dumps/oculus/eureka">this GitLab</a> for more
|
||||
* information.
|
||||
* </p>
|
||||
*/
|
||||
private static final String DEVICE_MODEL_ANDROID_VR = "Quest 3";
|
||||
private static final String OS_VERSION_ANDROID_VR = "12";
|
||||
/**
|
||||
* The SDK version for Android 12 is 31,
|
||||
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
|
||||
*/
|
||||
private static final int ANDROID_SDK_VERSION_ANDROID_VR = 32;
|
||||
/**
|
||||
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
|
||||
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
|
||||
* Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
|
||||
*/
|
||||
private static final String USER_AGENT_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus/" +
|
||||
CLIENT_VERSION_ANDROID_VR +
|
||||
" (Linux; U; Android " +
|
||||
OS_VERSION_ANDROID_VR +
|
||||
"; GB) gzip";
|
||||
|
||||
// ANDROID UNPLUGGED
|
||||
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.47.0";
|
||||
/**
|
||||
* The device machine id for the Chromecast with Google TV 4K.
|
||||
*
|
||||
* <p>
|
||||
* See <a href="https://dumps.tadiphone.dev/dumps/google/kirkwood">this GitLab</a> for more
|
||||
* information.
|
||||
* </p>
|
||||
*/
|
||||
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
|
||||
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
|
||||
private static final int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 34;
|
||||
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
|
||||
CLIENT_VERSION_ANDROID_UNPLUGGED +
|
||||
" (Linux; U; Android " +
|
||||
OS_VERSION_ANDROID_UNPLUGGED +
|
||||
"; GB) gzip";
|
||||
|
||||
private AppClient() {
|
||||
}
|
||||
|
||||
public enum ClientType {
|
||||
IOS(5,
|
||||
DEVICE_MAKE_IOS,
|
||||
DEVICE_MODEL_IOS,
|
||||
CLIENT_VERSION_IOS,
|
||||
OS_NAME_IOS,
|
||||
OS_VERSION_IOS,
|
||||
null,
|
||||
USER_AGENT_IOS,
|
||||
false
|
||||
),
|
||||
ANDROID_VR(28,
|
||||
null,
|
||||
DEVICE_MODEL_ANDROID_VR,
|
||||
CLIENT_VERSION_ANDROID_VR,
|
||||
OS_NAME_ANDROID,
|
||||
OS_VERSION_ANDROID_VR,
|
||||
ANDROID_SDK_VERSION_ANDROID_VR,
|
||||
USER_AGENT_ANDROID_VR,
|
||||
true
|
||||
),
|
||||
ANDROID_UNPLUGGED(29,
|
||||
null,
|
||||
DEVICE_MODEL_ANDROID_UNPLUGGED,
|
||||
CLIENT_VERSION_ANDROID_UNPLUGGED,
|
||||
OS_NAME_ANDROID,
|
||||
OS_VERSION_ANDROID_UNPLUGGED,
|
||||
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
|
||||
USER_AGENT_ANDROID_UNPLUGGED,
|
||||
true
|
||||
);
|
||||
|
||||
public final String friendlyName;
|
||||
|
||||
/**
|
||||
* YouTube
|
||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||
*/
|
||||
public final int id;
|
||||
|
||||
/**
|
||||
* Device manufacturer.
|
||||
*/
|
||||
@Nullable
|
||||
public final String make;
|
||||
|
||||
/**
|
||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
||||
*/
|
||||
public final String deviceModel;
|
||||
|
||||
/**
|
||||
* Device OS name.
|
||||
*/
|
||||
@Nullable
|
||||
public final String osName;
|
||||
|
||||
/**
|
||||
* Device OS version.
|
||||
*/
|
||||
public final String osVersion;
|
||||
|
||||
/**
|
||||
* Player user-agent.
|
||||
*/
|
||||
public final String userAgent;
|
||||
|
||||
/**
|
||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||
* Field is null if not applicable.
|
||||
*/
|
||||
public final Integer androidSdkVersion;
|
||||
|
||||
/**
|
||||
* App version.
|
||||
*/
|
||||
public final String clientVersion;
|
||||
|
||||
/**
|
||||
* If the client can access the API logged in.
|
||||
*/
|
||||
public final boolean canLogin;
|
||||
|
||||
ClientType(int id,
|
||||
@Nullable String make,
|
||||
String deviceModel,
|
||||
String clientVersion,
|
||||
@Nullable String osName,
|
||||
String osVersion,
|
||||
Integer androidSdkVersion,
|
||||
String userAgent,
|
||||
boolean canLogin
|
||||
) {
|
||||
this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
|
||||
this.id = id;
|
||||
this.make = make;
|
||||
this.deviceModel = deviceModel;
|
||||
this.clientVersion = clientVersion;
|
||||
this.osName = osName;
|
||||
this.osVersion = osVersion;
|
||||
this.androidSdkVersion = androidSdkVersion;
|
||||
this.userAgent = userAgent;
|
||||
this.canLogin = canLogin;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package app.revanced.extension.youtube.patches.misc.client;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
public class DeviceHardwareSupport {
|
||||
private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
|
||||
private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
|
||||
|
||||
static {
|
||||
boolean vp9found = false;
|
||||
boolean av1found = false;
|
||||
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
|
||||
final boolean deviceIsAndroidTenOrLater = isSDKAbove(29);
|
||||
|
||||
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
|
||||
final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
|
||||
? codecInfo.isHardwareAccelerated()
|
||||
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
|
||||
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
|
||||
for (String type : codecInfo.getSupportedTypes()) {
|
||||
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
|
||||
vp9found = true;
|
||||
} else if (type.equalsIgnoreCase("video/av01")) {
|
||||
av1found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
|
||||
DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
|
||||
|
||||
Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
|
||||
? "Device supports AV1 hardware decoding\n"
|
||||
: "Device does not support AV1 hardware decoding\n"
|
||||
+ (DEVICE_HAS_HARDWARE_DECODING_VP9
|
||||
? "Device supports VP9 hardware decoding"
|
||||
: "Device does not support VP9 hardware decoding"));
|
||||
}
|
||||
|
||||
public static boolean allowVP9() {
|
||||
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get();
|
||||
}
|
||||
|
||||
public static boolean allowAV1() {
|
||||
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package app.revanced.extension.youtube.patches.misc.requests;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public final class PlayerRoutes {
|
||||
/**
|
||||
* The base URL of requests of non-web clients to the InnerTube internal API.
|
||||
*/
|
||||
private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
||||
|
||||
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
||||
Route.Method.POST,
|
||||
"player" +
|
||||
"?fields=streamingData" +
|
||||
"&alt=proto"
|
||||
).compile();
|
||||
|
||||
static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
|
||||
Route.Method.POST,
|
||||
"next" +
|
||||
"?fields=contents.singleColumnWatchNextResults.playlist.playlist"
|
||||
).compile();
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout
|
||||
*/
|
||||
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
|
||||
|
||||
private PlayerRoutes() {
|
||||
}
|
||||
|
||||
static String createInnertubeBody(ClientType clientType, String videoId) {
|
||||
return createInnertubeBody(clientType, videoId, null);
|
||||
}
|
||||
|
||||
static String createInnertubeBody(ClientType clientType, String videoId, String playlistId) {
|
||||
JSONObject innerTubeBody = new JSONObject();
|
||||
|
||||
try {
|
||||
JSONObject context = new JSONObject();
|
||||
|
||||
JSONObject client = new JSONObject();
|
||||
client.put("clientName", clientType.name());
|
||||
client.put("clientVersion", clientType.clientVersion);
|
||||
client.put("deviceModel", clientType.deviceModel);
|
||||
client.put("osVersion", clientType.osVersion);
|
||||
if (clientType.make != null) {
|
||||
client.put("deviceMake", clientType.make);
|
||||
}
|
||||
if (clientType.osName != null) {
|
||||
client.put("osName", clientType.osName);
|
||||
}
|
||||
if (clientType.androidSdkVersion != null) {
|
||||
client.put("androidSdkVersion", clientType.androidSdkVersion.toString());
|
||||
}
|
||||
String languageCode = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale.getLanguage();
|
||||
client.put("hl", languageCode);
|
||||
|
||||
context.put("client", client);
|
||||
|
||||
innerTubeBody.put("context", context);
|
||||
innerTubeBody.put("contentCheckOk", true);
|
||||
innerTubeBody.put("racyCheckOk", true);
|
||||
innerTubeBody.put("videoId", videoId);
|
||||
if (playlistId != null) {
|
||||
innerTubeBody.put("playlistId", playlistId);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||
}
|
||||
|
||||
return innerTubeBody.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection SameParameterValue
|
||||
*/
|
||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||
var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
|
||||
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
||||
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||
return connection;
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
package app.revanced.extension.youtube.patches.misc.requests;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
public class PlaylistRequest {
|
||||
|
||||
/**
|
||||
* How long to keep fetches until they are expired.
|
||||
*/
|
||||
private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute
|
||||
|
||||
private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds
|
||||
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, PlaylistRequest> cache = new HashMap<>();
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
public static void fetchRequestIfNeeded(@Nullable String videoId) {
|
||||
Objects.requireNonNull(videoId);
|
||||
synchronized (cache) {
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
cache.values().removeIf(request -> {
|
||||
final boolean expired = request.isExpired(now);
|
||||
if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId);
|
||||
return expired;
|
||||
});
|
||||
|
||||
if (!cache.containsKey(videoId)) {
|
||||
cache.put(videoId, new PlaylistRequest(videoId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PlaylistRequest getRequestForVideoId(@Nullable String videoId) {
|
||||
synchronized (cache) {
|
||||
return cache.get(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
|
||||
Logger.printInfo(() -> toastMessage, ex);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject send(ClientType clientType, String videoId) {
|
||||
Objects.requireNonNull(clientType);
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
final long startTime = System.currentTimeMillis();
|
||||
String clientTypeName = clientType.name();
|
||||
Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName);
|
||||
|
||||
try {
|
||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
|
||||
|
||||
String innerTubeBody = PlayerRoutes.createInnertubeBody(
|
||||
clientType,
|
||||
videoId,
|
||||
"RD" + videoId
|
||||
);
|
||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||
connection.getOutputStream().write(requestBody);
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||
|
||||
handleConnectionError(clientTypeName + " not available with response code: "
|
||||
+ responseCode + " message: " + connection.getResponseMessage(),
|
||||
null);
|
||||
} catch (SocketTimeoutException ex) {
|
||||
handleConnectionError("Connection timeout", ex);
|
||||
} catch (IOException ex) {
|
||||
handleConnectionError("Network error", ex);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "send failed", ex);
|
||||
} finally {
|
||||
Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Boolean fetch(@NonNull String videoId) {
|
||||
final ClientType clientType = ClientType.ANDROID_VR;
|
||||
final JSONObject playlistJson = send(clientType, videoId);
|
||||
if (playlistJson != null) {
|
||||
try {
|
||||
final JSONObject singleColumnWatchNextResultsJsonObject = playlistJson
|
||||
.getJSONObject("contents")
|
||||
.getJSONObject("singleColumnWatchNextResults");
|
||||
|
||||
if (!singleColumnWatchNextResultsJsonObject.has("playlist")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final JSONObject playlistJsonObject = singleColumnWatchNextResultsJsonObject
|
||||
.getJSONObject("playlist")
|
||||
.getJSONObject("playlist");
|
||||
|
||||
final Object currentStreamObject = playlistJsonObject
|
||||
.getJSONArray("contents")
|
||||
.get(0);
|
||||
|
||||
if (!(currentStreamObject instanceof JSONObject currentStreamJsonObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final JSONObject watchEndpointJsonObject = currentStreamJsonObject
|
||||
.getJSONObject("playlistPanelVideoRenderer")
|
||||
.getJSONObject("navigationEndpoint")
|
||||
.getJSONObject("watchEndpoint");
|
||||
|
||||
Logger.printDebug(() -> "watchEndpoint: " + watchEndpointJsonObject);
|
||||
|
||||
return watchEndpointJsonObject.has("playerParams") &&
|
||||
VideoInformation.isMixPlaylistsOpenedByUser(watchEndpointJsonObject.getString("playerParams"));
|
||||
} catch (JSONException e) {
|
||||
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time this instance and the fetch future was created.
|
||||
*/
|
||||
private final long timeFetched;
|
||||
private final String videoId;
|
||||
private final Future<Boolean> future;
|
||||
|
||||
private PlaylistRequest(String videoId) {
|
||||
this.timeFetched = System.currentTimeMillis();
|
||||
this.videoId = videoId;
|
||||
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId));
|
||||
}
|
||||
|
||||
public boolean isExpired(long now) {
|
||||
final long timeSinceCreation = now - timeFetched;
|
||||
if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only expired if the fetch failed (API null response).
|
||||
return (fetchCompleted() && getStream() == null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the fetch call has completed.
|
||||
*/
|
||||
public boolean fetchCompleted() {
|
||||
return future.isDone();
|
||||
}
|
||||
|
||||
public Boolean getStream() {
|
||||
try {
|
||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
Logger.printInfo(() -> "getStream timed out", ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Logger.printException(() -> "getStream interrupted", ex);
|
||||
Thread.currentThread().interrupt(); // Restore interrupt status flag.
|
||||
} catch (ExecutionException ex) {
|
||||
Logger.printException(() -> "getStream failure", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
package app.revanced.extension.youtube.patches.misc.requests;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
public class StreamingDataRequest {
|
||||
private static final ClientType[] ALL_CLIENT_TYPES = ClientType.values();
|
||||
private static final ClientType[] CLIENT_ORDER_TO_USE;
|
||||
|
||||
static {
|
||||
ClientType preferredClient = Settings.SPOOF_STREAMING_DATA_TYPE.get();
|
||||
CLIENT_ORDER_TO_USE = new ClientType[ALL_CLIENT_TYPES.length];
|
||||
|
||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
||||
|
||||
int i = 1;
|
||||
for (ClientType c : ALL_CLIENT_TYPES) {
|
||||
if (c != preferredClient) {
|
||||
CLIENT_ORDER_TO_USE[i++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ClientType lastSpoofedClientType;
|
||||
|
||||
public static String getLastSpoofedClientName() {
|
||||
return lastSpoofedClientType == null
|
||||
? "Unknown"
|
||||
: lastSpoofedClientType.friendlyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout.
|
||||
*/
|
||||
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
|
||||
|
||||
/**
|
||||
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
|
||||
*/
|
||||
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
|
||||
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(100) {
|
||||
/**
|
||||
* Cache limit must be greater than the maximum number of videos open at once,
|
||||
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
|
||||
* But instead use a much larger value, to handle if a video viewed a while ago
|
||||
* is somehow still referenced. Each stream is a small array of Strings
|
||||
* so memory usage is not a concern.
|
||||
*/
|
||||
private static final int CACHE_LIMIT = 50;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
});
|
||||
|
||||
public static void fetchRequest(@NonNull String videoId, Map<String, String> fetchHeaders) {
|
||||
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) {
|
||||
return cache.get(videoId);
|
||||
}
|
||||
|
||||
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
|
||||
Logger.printInfo(() -> toastMessage, ex);
|
||||
}
|
||||
|
||||
// Available only to logged in users.
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
|
||||
private static final String[] REQUEST_HEADER_KEYS = {
|
||||
AUTHORIZATION_HEADER,
|
||||
"X-GOOG-API-FORMAT-VERSION",
|
||||
"X-Goog-Visitor-Id"
|
||||
};
|
||||
|
||||
private static void writeInnerTubeBody(HttpURLConnection connection, ClientType clientType,
|
||||
String videoId, Map<String, String> playerHeaders) {
|
||||
try {
|
||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
|
||||
if (playerHeaders != null) {
|
||||
for (String key : REQUEST_HEADER_KEYS) {
|
||||
if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) {
|
||||
continue;
|
||||
}
|
||||
String value = playerHeaders.get(key);
|
||||
if (value != null) {
|
||||
connection.setRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
|
||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||
connection.getOutputStream().write(requestBody);
|
||||
} catch (IOException ex) {
|
||||
handleConnectionError("Network error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static HttpURLConnection send(ClientType clientType, String videoId,
|
||||
Map<String, String> playerHeaders) {
|
||||
Objects.requireNonNull(clientType);
|
||||
Objects.requireNonNull(videoId);
|
||||
Objects.requireNonNull(playerHeaders);
|
||||
|
||||
final long startTime = System.currentTimeMillis();
|
||||
String clientTypeName = clientType.name();
|
||||
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
|
||||
|
||||
try {
|
||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
||||
writeInnerTubeBody(connection, clientType, videoId, playerHeaders);
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) return connection;
|
||||
|
||||
handleConnectionError(clientTypeName + " not available with response code: "
|
||||
+ responseCode + " message: " + connection.getResponseMessage(),
|
||||
null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "send failed", ex);
|
||||
} finally {
|
||||
Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final ByteArrayFilterGroup liveStreams =
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
|
||||
"yt_live_broadcast",
|
||||
"yt_premiere_broadcast"
|
||||
);
|
||||
|
||||
private static ByteBuffer fetch(@NonNull String videoId, Map<String, String> playerHeaders) {
|
||||
try {
|
||||
lastSpoofedClientType = null;
|
||||
|
||||
// Retry with different client if empty response body is received.
|
||||
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
|
||||
|
||||
// gzip encoding doesn't response with content length (-1),
|
||||
// but empty response body does.
|
||||
if (connection == null || connection.getContentLength() == 0) {
|
||||
continue;
|
||||
}
|
||||
InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[2048];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
||||
baos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
inputStream.close();
|
||||
if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
|
||||
Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
|
||||
continue;
|
||||
}
|
||||
lastSpoofedClientType = clientType;
|
||||
|
||||
return ByteBuffer.wrap(baos.toByteArray());
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Fetch failed while processing response data", ex);
|
||||
}
|
||||
|
||||
handleConnectionError("Could not fetch any client streams", null);
|
||||
return null;
|
||||
}
|
||||
|
||||
private final String videoId;
|
||||
private final Future<ByteBuffer> future;
|
||||
|
||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||
Objects.requireNonNull(playerHeaders);
|
||||
this.videoId = videoId;
|
||||
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
|
||||
}
|
||||
|
||||
public boolean fetchCompleted() {
|
||||
return future.isDone();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ByteBuffer getStream() {
|
||||
try {
|
||||
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
Logger.printInfo(() -> "getStream timed out", ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Logger.printException(() -> "getStream interrupted", ex);
|
||||
Thread.currentThread().interrupt(); // Restore interrupt status flag.
|
||||
} catch (ExecutionException ex) {
|
||||
Logger.printException(() -> "getStream failure", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AlwaysRepeat extends BottomControlButton {
|
||||
@Nullable
|
||||
private static AlwaysRepeat instance;
|
||||
|
||||
public AlwaysRepeat(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"always_repeat_button",
|
||||
Settings.OVERLAY_BUTTON_ALWAYS_REPEAT,
|
||||
Settings.ALWAYS_REPEAT,
|
||||
Settings.ALWAYS_REPEAT_PAUSE,
|
||||
view -> {
|
||||
if (instance != null)
|
||||
instance.changeSelected(!view.isSelected());
|
||||
},
|
||||
view -> {
|
||||
if (instance != null)
|
||||
instance.changeColorFilter();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new AlwaysRepeat(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getAnimation;
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getInteger;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.getChildView;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
public abstract class BottomControlButton {
|
||||
private static final Animation fadeIn;
|
||||
private static final Animation fadeOut;
|
||||
private static final Animation fadeOutImmediate;
|
||||
|
||||
private final ColorFilter cf =
|
||||
new PorterDuffColorFilter(Color.parseColor("#fffffc79"), PorterDuff.Mode.SRC_ATOP);
|
||||
|
||||
private final WeakReference<ImageView> buttonRef;
|
||||
private final BooleanSetting setting;
|
||||
private final BooleanSetting primaryInteractionSetting;
|
||||
private final BooleanSetting secondaryInteractionSetting;
|
||||
protected boolean isVisible;
|
||||
|
||||
static {
|
||||
fadeIn = getAnimation("fade_in");
|
||||
// android.R.integer.config_shortAnimTime, 200
|
||||
fadeIn.setDuration(getInteger("fade_duration_fast"));
|
||||
|
||||
fadeOut = getAnimation("fade_out");
|
||||
// android.R.integer.config_mediumAnimTime, 400
|
||||
fadeOut.setDuration(getInteger("fade_overlay_fade_duration"));
|
||||
|
||||
fadeOutImmediate = getAnimation("abc_fade_out");
|
||||
// android.R.integer.config_shortAnimTime, 200
|
||||
fadeOutImmediate.setDuration(getInteger("fade_duration_fast"));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Animation getButtonFadeIn() {
|
||||
return fadeIn;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Animation getButtonFadeOut() {
|
||||
return fadeOut;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Animation getButtonFadeOutImmediate() {
|
||||
return fadeOutImmediate;
|
||||
}
|
||||
|
||||
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting,
|
||||
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
|
||||
this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, null, null, onClickListener, longClickListener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting, @Nullable BooleanSetting primaryInteractionSetting,
|
||||
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
|
||||
this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, primaryInteractionSetting, null, onClickListener, longClickListener);
|
||||
}
|
||||
|
||||
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting,
|
||||
@Nullable BooleanSetting primaryInteractionSetting, @Nullable BooleanSetting secondaryInteractionSetting,
|
||||
@NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) {
|
||||
Logger.printDebug(() -> "Initializing button: " + imageViewButtonId);
|
||||
|
||||
setting = booleanSetting;
|
||||
|
||||
// Create the button.
|
||||
ImageView imageView = Objects.requireNonNull(getChildView(bottomControlsViewGroup, imageViewButtonId));
|
||||
imageView.setOnClickListener(onClickListener);
|
||||
this.primaryInteractionSetting = primaryInteractionSetting;
|
||||
this.secondaryInteractionSetting = secondaryInteractionSetting;
|
||||
if (primaryInteractionSetting != null) {
|
||||
imageView.setSelected(primaryInteractionSetting.get());
|
||||
}
|
||||
if (secondaryInteractionSetting != null) {
|
||||
setColorFilter(imageView, secondaryInteractionSetting.get());
|
||||
}
|
||||
if (longClickListener != null) {
|
||||
imageView.setOnLongClickListener(longClickListener);
|
||||
}
|
||||
imageView.setVisibility(View.GONE);
|
||||
buttonRef = new WeakReference<>(imageView);
|
||||
}
|
||||
|
||||
public void changeActivated(boolean activated) {
|
||||
ImageView imageView = buttonRef.get();
|
||||
if (imageView == null)
|
||||
return;
|
||||
imageView.setActivated(activated);
|
||||
}
|
||||
|
||||
public void changeSelected(boolean selected) {
|
||||
ImageView imageView = buttonRef.get();
|
||||
if (imageView == null || primaryInteractionSetting == null)
|
||||
return;
|
||||
|
||||
if (imageView.getColorFilter() == cf) {
|
||||
Utils.showToastShort(str("revanced_overlay_button_not_allowed_warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
imageView.setSelected(selected);
|
||||
primaryInteractionSetting.save(selected);
|
||||
}
|
||||
|
||||
public void changeColorFilter() {
|
||||
ImageView imageView = buttonRef.get();
|
||||
if (imageView == null) return;
|
||||
if (primaryInteractionSetting == null || secondaryInteractionSetting == null)
|
||||
return;
|
||||
|
||||
imageView.setSelected(true);
|
||||
primaryInteractionSetting.save(true);
|
||||
|
||||
final boolean newValue = !secondaryInteractionSetting.get();
|
||||
secondaryInteractionSetting.save(newValue);
|
||||
setColorFilter(imageView, newValue);
|
||||
}
|
||||
|
||||
public void setColorFilter(ImageView imageView, boolean selected) {
|
||||
if (selected)
|
||||
imageView.setColorFilter(cf);
|
||||
else
|
||||
imageView.clearColorFilter();
|
||||
}
|
||||
|
||||
public void setVisibility(boolean visible, boolean animation) {
|
||||
ImageView imageView = buttonRef.get();
|
||||
if (imageView == null || isVisible == visible) return;
|
||||
isVisible = visible;
|
||||
|
||||
imageView.clearAnimation();
|
||||
if (visible && setting.get()) {
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
if (animation) imageView.startAnimation(fadeIn);
|
||||
return;
|
||||
}
|
||||
if (imageView.getVisibility() == View.VISIBLE) {
|
||||
if (animation) imageView.startAnimation(fadeOut);
|
||||
imageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisibilityNegatedImmediate() {
|
||||
ImageView imageView = buttonRef.get();
|
||||
if (imageView == null) return;
|
||||
if (!setting.get()) return;
|
||||
|
||||
imageView.clearAnimation();
|
||||
imageView.startAnimation(fadeOutImmediate);
|
||||
imageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CopyVideoUrl extends BottomControlButton {
|
||||
@Nullable
|
||||
private static CopyVideoUrl instance;
|
||||
|
||||
public CopyVideoUrl(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"copy_video_url_button",
|
||||
Settings.OVERLAY_BUTTON_COPY_VIDEO_URL,
|
||||
view -> VideoUtils.copyUrl(false),
|
||||
view -> {
|
||||
VideoUtils.copyUrl(true);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new CopyVideoUrl(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CopyVideoUrlTimestamp extends BottomControlButton {
|
||||
@Nullable
|
||||
private static CopyVideoUrlTimestamp instance;
|
||||
|
||||
public CopyVideoUrlTimestamp(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"copy_video_url_timestamp_button",
|
||||
Settings.OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP,
|
||||
view -> VideoUtils.copyUrl(true),
|
||||
view -> {
|
||||
VideoUtils.copyTimeStamp();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new CopyVideoUrlTimestamp(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ExternalDownload extends BottomControlButton {
|
||||
@Nullable
|
||||
private static ExternalDownload instance;
|
||||
|
||||
public ExternalDownload(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"external_download_button",
|
||||
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER,
|
||||
view -> VideoUtils.launchVideoExternalDownloader(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new ExternalDownload(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class MuteVolume extends BottomControlButton {
|
||||
@Nullable
|
||||
private static MuteVolume instance;
|
||||
private static AudioManager audioManager;
|
||||
private static final int stream = AudioManager.STREAM_MUSIC;
|
||||
|
||||
public MuteVolume(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"mute_volume_button",
|
||||
Settings.OVERLAY_BUTTON_MUTE_VOLUME,
|
||||
view -> {
|
||||
if (instance != null && audioManager != null) {
|
||||
boolean unMuted = !audioManager.isStreamMute(stream);
|
||||
audioManager.setStreamMute(stream, unMuted);
|
||||
instance.changeActivated(unMuted);
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new MuteVolume(viewGroup);
|
||||
}
|
||||
if (bottomControlsViewGroup.getContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager am) {
|
||||
audioManager = am;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) {
|
||||
instance.setVisibility(showing, animation);
|
||||
changeActivated(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) {
|
||||
instance.setVisibilityNegatedImmediate();
|
||||
changeActivated(instance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void changeActivated(MuteVolume instance) {
|
||||
if (audioManager != null) {
|
||||
boolean muted = audioManager.isStreamMute(stream);
|
||||
instance.changeActivated(muted);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayAll extends BottomControlButton {
|
||||
|
||||
@Nullable
|
||||
private static PlayAll instance;
|
||||
|
||||
public PlayAll(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"play_all_button",
|
||||
Settings.OVERLAY_BUTTON_PLAY_ALL,
|
||||
view -> VideoUtils.openVideo(Settings.OVERLAY_BUTTON_PLAY_ALL_TYPE.get()),
|
||||
view -> {
|
||||
VideoUtils.openVideo();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new PlayAll(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.showToastShort;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpeedDialog extends BottomControlButton {
|
||||
@Nullable
|
||||
private static SpeedDialog instance;
|
||||
|
||||
public SpeedDialog(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"speed_dialog_button",
|
||||
Settings.OVERLAY_BUTTON_SPEED_DIALOG,
|
||||
view -> VideoUtils.showPlaybackSpeedDialog(view.getContext()),
|
||||
view -> {
|
||||
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
|
||||
VideoInformation.getPlaybackSpeed() == Settings.DEFAULT_PLAYBACK_SPEED.get()) {
|
||||
VideoInformation.overridePlaybackSpeed(1.0f);
|
||||
showToastShort(str("revanced_overlay_button_speed_dialog_reset", "1.0"));
|
||||
} else {
|
||||
float defaultSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get();
|
||||
VideoInformation.overridePlaybackSpeed(defaultSpeed);
|
||||
showToastShort(str("revanced_overlay_button_speed_dialog_reset", defaultSpeed));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new SpeedDialog(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package app.revanced.extension.youtube.patches.overlaybutton;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.settings.preference.WhitelistedChannelsPreference;
|
||||
import app.revanced.extension.youtube.whitelist.Whitelist;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class Whitelists extends BottomControlButton {
|
||||
@Nullable
|
||||
private static Whitelists instance;
|
||||
|
||||
public Whitelists(ViewGroup bottomControlsViewGroup) {
|
||||
super(
|
||||
bottomControlsViewGroup,
|
||||
"whitelist_button",
|
||||
Settings.OVERLAY_BUTTON_WHITELIST,
|
||||
view -> Whitelist.showWhitelistDialog(view.getContext()),
|
||||
view -> {
|
||||
WhitelistedChannelsPreference.showWhitelistedChannelDialog(view.getContext());
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(View bottomControlsViewGroup) {
|
||||
try {
|
||||
if (bottomControlsViewGroup instanceof ViewGroup viewGroup) {
|
||||
instance = new Whitelists(viewGroup);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation) {
|
||||
if (instance != null) instance.setVisibility(showing, animation);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,730 @@
|
||||
package app.revanced.extension.youtube.patches.player;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromParentUnderCondition;
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Color;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.IntegerSetting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.utils.InitializationPatch;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.RootView;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerPatch {
|
||||
private static final IntegerSetting quickActionsMarginTopSetting = Settings.QUICK_ACTIONS_TOP_MARGIN;
|
||||
|
||||
private static final int PLAYER_OVERLAY_OPACITY_LEVEL;
|
||||
private static final int QUICK_ACTIONS_MARGIN_TOP;
|
||||
private static final float SPEED_OVERLAY_VALUE;
|
||||
|
||||
static {
|
||||
final int opacity = validateValue(
|
||||
Settings.CUSTOM_PLAYER_OVERLAY_OPACITY,
|
||||
0,
|
||||
100,
|
||||
"revanced_custom_player_overlay_opacity_invalid_toast"
|
||||
);
|
||||
PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
|
||||
SPEED_OVERLAY_VALUE = validateValue(
|
||||
Settings.SPEED_OVERLAY_VALUE,
|
||||
0.0f,
|
||||
8.0f,
|
||||
"revanced_speed_overlay_value_invalid_toast"
|
||||
);
|
||||
|
||||
final int topMargin = validateValue(
|
||||
Settings.QUICK_ACTIONS_TOP_MARGIN,
|
||||
0,
|
||||
32,
|
||||
"revanced_quick_actions_top_margin_invalid_toast"
|
||||
);
|
||||
|
||||
QUICK_ACTIONS_MARGIN_TOP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) topMargin, Utils.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
// region [Ambient mode control] patch
|
||||
|
||||
public static boolean bypassAmbientModeRestrictions(boolean original) {
|
||||
return (!Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS.get() && original) || Settings.DISABLE_AMBIENT_MODE.get();
|
||||
}
|
||||
|
||||
public static boolean disableAmbientModeInFullscreen() {
|
||||
return !Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Change player flyout menu toggles] patch
|
||||
|
||||
public static boolean changeSwitchToggle(boolean original) {
|
||||
return !Settings.CHANGE_PLAYER_FLYOUT_MENU_TOGGLE.get() && original;
|
||||
}
|
||||
|
||||
public static String getToggleString(String str) {
|
||||
return ResourceUtils.getString(str);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Description components] patch
|
||||
|
||||
public static boolean disableRollingNumberAnimations() {
|
||||
return Settings.DISABLE_ROLLING_NUMBER_ANIMATIONS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* view id R.id.content
|
||||
*/
|
||||
private static final int contentId = ResourceUtils.getIdIdentifier("content");
|
||||
private static final boolean expandDescriptionEnabled = Settings.EXPAND_VIDEO_DESCRIPTION.get();
|
||||
private static final String descriptionString = Settings.EXPAND_VIDEO_DESCRIPTION_STRINGS.get();
|
||||
|
||||
private static boolean isDescriptionPanel = false;
|
||||
|
||||
public static void setContentDescription(String contentDescription) {
|
||||
if (!expandDescriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
if (contentDescription == null || contentDescription.isEmpty()) {
|
||||
isDescriptionPanel = false;
|
||||
return;
|
||||
}
|
||||
if (descriptionString.isEmpty()) {
|
||||
isDescriptionPanel = false;
|
||||
return;
|
||||
}
|
||||
isDescriptionPanel = descriptionString.equals(contentDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last time the clickDescriptionView method was called.
|
||||
*/
|
||||
private static long lastTimeDescriptionViewInvoked;
|
||||
|
||||
|
||||
public static void onVideoDescriptionCreate(RecyclerView recyclerView) {
|
||||
if (!expandDescriptionEnabled)
|
||||
return;
|
||||
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
// Video description panel is only open when the player is active.
|
||||
if (!RootView.isPlayerActive()) {
|
||||
return;
|
||||
}
|
||||
// Video description's recyclerView is a child view of [contentId].
|
||||
if (!(recyclerView.getParent().getParent() instanceof View contentView)) {
|
||||
return;
|
||||
}
|
||||
if (contentView.getId() != contentId) {
|
||||
return;
|
||||
}
|
||||
// This method is invoked whenever the Engagement panel is opened. (Description, Chapters, Comments, etc.)
|
||||
// Check the title of the Engagement panel to prevent unnecessary clicking.
|
||||
if (!isDescriptionPanel) {
|
||||
return;
|
||||
}
|
||||
// The first view group contains information such as the video's title, like count, and number of views.
|
||||
if (!(recyclerView.getChildAt(0) instanceof ViewGroup primaryViewGroup)) {
|
||||
return;
|
||||
}
|
||||
if (primaryViewGroup.getChildCount() < 2) {
|
||||
return;
|
||||
}
|
||||
// Typically, descriptionView is placed as the second child of recyclerView.
|
||||
if (recyclerView.getChildAt(1) instanceof ViewGroup viewGroup) {
|
||||
clickDescriptionView(viewGroup);
|
||||
}
|
||||
// In some videos, descriptionView is placed as the third child of recyclerView.
|
||||
if (recyclerView.getChildAt(2) instanceof ViewGroup viewGroup) {
|
||||
clickDescriptionView(viewGroup);
|
||||
}
|
||||
// Even if both methods are performed, there is no major issue with the operation of the patch.
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onVideoDescriptionCreate failed.", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void clickDescriptionView(@NonNull ViewGroup descriptionViewGroup) {
|
||||
final View descriptionView = descriptionViewGroup.getChildAt(0);
|
||||
if (descriptionView == null) {
|
||||
return;
|
||||
}
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimeDescriptionViewInvoked < 1000) {
|
||||
return;
|
||||
}
|
||||
lastTimeDescriptionViewInvoked = now;
|
||||
|
||||
// The type of descriptionView can be either ViewGroup or TextView. (A/B tests)
|
||||
// If the type of descriptionView is TextView, longer delay is required.
|
||||
final long delayMillis = descriptionView instanceof TextView
|
||||
? 500
|
||||
: 100;
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked only when the view type of descriptionView is {@link TextView}. (A/B tests)
|
||||
*
|
||||
* @param textView descriptionView.
|
||||
* @param original Whether to apply {@link TextView#setTextIsSelectable}.
|
||||
* Patch replaces the {@link TextView#setTextIsSelectable} method invoke.
|
||||
*/
|
||||
public static void disableVideoDescriptionInteraction(TextView textView, boolean original) {
|
||||
if (textView != null) {
|
||||
textView.setTextIsSelectable(
|
||||
!Settings.DISABLE_VIDEO_DESCRIPTION_INTERACTION.get() && original
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Disable haptic feedback] patch
|
||||
|
||||
public static boolean disableChapterVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
||||
}
|
||||
|
||||
|
||||
public static boolean disableSeekVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK.get();
|
||||
}
|
||||
|
||||
public static boolean disableSeekUndoVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
|
||||
}
|
||||
|
||||
public static boolean disableScrubbingVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_SCRUBBING.get();
|
||||
}
|
||||
|
||||
public static boolean disableZoomVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Fullscreen components] patch
|
||||
|
||||
public static void disableEngagementPanels(CoordinatorLayout coordinatorLayout) {
|
||||
if (!Settings.DISABLE_ENGAGEMENT_PANEL.get()) return;
|
||||
coordinatorLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public static void showVideoTitleSection(FrameLayout frameLayout, View view) {
|
||||
final boolean isEnabled = Settings.SHOW_VIDEO_TITLE_SECTION.get() || !Settings.DISABLE_ENGAGEMENT_PANEL.get();
|
||||
|
||||
if (isEnabled) {
|
||||
frameLayout.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hideAutoPlayPreview() {
|
||||
return Settings.HIDE_AUTOPLAY_PREVIEW.get();
|
||||
}
|
||||
|
||||
public static boolean hideRelatedVideoOverlay() {
|
||||
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
||||
}
|
||||
|
||||
public static void hideQuickActions(View view) {
|
||||
final boolean isEnabled = Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get();
|
||||
|
||||
Utils.hideViewBy0dpUnderCondition(
|
||||
isEnabled,
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
public static void setQuickActionMargin(View view) {
|
||||
int topMarginPx = getQuickActionsTopMargin();
|
||||
if (topMarginPx == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams mlp))
|
||||
return;
|
||||
|
||||
mlp.setMargins(
|
||||
mlp.leftMargin,
|
||||
topMarginPx,
|
||||
mlp.rightMargin,
|
||||
mlp.bottomMargin
|
||||
);
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
public static boolean enableCompactControlsOverlay(boolean original) {
|
||||
return Settings.ENABLE_COMPACT_CONTROLS_OVERLAY.get() || original;
|
||||
}
|
||||
|
||||
public static boolean disableLandScapeMode(boolean original) {
|
||||
return Settings.DISABLE_LANDSCAPE_MODE.get() || original;
|
||||
}
|
||||
|
||||
private static volatile boolean isScreenOn;
|
||||
|
||||
public static boolean keepFullscreen(boolean original) {
|
||||
if (!Settings.KEEP_LANDSCAPE_MODE.get())
|
||||
return original;
|
||||
|
||||
return isScreenOn;
|
||||
}
|
||||
|
||||
public static void setScreenOn() {
|
||||
if (!Settings.KEEP_LANDSCAPE_MODE.get())
|
||||
return;
|
||||
|
||||
isScreenOn = true;
|
||||
Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get());
|
||||
}
|
||||
|
||||
private static WeakReference<Activity> watchDescriptorActivityRef = new WeakReference<>(null);
|
||||
private static volatile boolean isLandScapeVideo = true;
|
||||
|
||||
public static void setWatchDescriptorActivity(Activity activity) {
|
||||
watchDescriptorActivityRef = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
public static boolean forceFullscreen(boolean original) {
|
||||
if (!Settings.FORCE_FULLSCREEN.get())
|
||||
return original;
|
||||
|
||||
Utils.runOnMainThreadDelayed(PlayerPatch::setOrientation, 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void setOrientation() {
|
||||
final Activity watchDescriptorActivity = watchDescriptorActivityRef.get();
|
||||
final int requestedOrientation = isLandScapeVideo
|
||||
? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
: watchDescriptorActivity.getRequestedOrientation();
|
||||
|
||||
watchDescriptorActivity.setRequestedOrientation(requestedOrientation);
|
||||
}
|
||||
|
||||
public static void setVideoPortrait(int width, int height) {
|
||||
if (!Settings.FORCE_FULLSCREEN.get())
|
||||
return;
|
||||
|
||||
isLandScapeVideo = width > height;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide comments component] patch
|
||||
|
||||
public static void changeEmojiPickerOpacity(ImageView imageView) {
|
||||
if (!Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get())
|
||||
return;
|
||||
|
||||
imageView.setImageAlpha(0);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Object disableEmojiPickerOnClickListener(@Nullable Object object) {
|
||||
return Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get() ? null : object;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide player buttons] patch
|
||||
|
||||
public static boolean hideAutoPlayButton() {
|
||||
return Settings.HIDE_PLAYER_AUTOPLAY_BUTTON.get();
|
||||
}
|
||||
|
||||
public static boolean hideCaptionsButton(boolean original) {
|
||||
return !Settings.HIDE_PLAYER_CAPTIONS_BUTTON.get() && original;
|
||||
}
|
||||
|
||||
public static int hideCastButton(int original) {
|
||||
return Settings.HIDE_PLAYER_CAST_BUTTON.get()
|
||||
? View.GONE
|
||||
: original;
|
||||
}
|
||||
|
||||
public static void hideCaptionsButton(View view) {
|
||||
Utils.hideViewUnderCondition(Settings.HIDE_PLAYER_CAPTIONS_BUTTON, view);
|
||||
}
|
||||
|
||||
public static void hideCollapseButton(ImageView imageView) {
|
||||
if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get())
|
||||
return;
|
||||
|
||||
imageView.setImageResource(android.R.color.transparent);
|
||||
imageView.setImageAlpha(0);
|
||||
imageView.setEnabled(false);
|
||||
|
||||
var layoutParams = imageView.getLayoutParams();
|
||||
if (layoutParams instanceof RelativeLayout.LayoutParams) {
|
||||
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(0, 0);
|
||||
imageView.setLayoutParams(lp);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Unknown collapse button layout params: " + layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTitleAnchorStartMargin(View titleAnchorView) {
|
||||
if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get())
|
||||
return;
|
||||
|
||||
var layoutParams = titleAnchorView.getLayoutParams();
|
||||
if (titleAnchorView.getLayoutParams() instanceof RelativeLayout.LayoutParams lp) {
|
||||
lp.setMarginStart(0);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Unknown title anchor layout params: " + layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
public static ImageView hideFullscreenButton(ImageView imageView) {
|
||||
final boolean hideView = Settings.HIDE_PLAYER_FULLSCREEN_BUTTON.get();
|
||||
|
||||
Utils.hideViewUnderCondition(hideView, imageView);
|
||||
return hideView ? null : imageView;
|
||||
}
|
||||
|
||||
public static boolean hidePreviousNextButton(boolean previousOrNextButtonVisible) {
|
||||
return !Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTON.get() && previousOrNextButtonVisible;
|
||||
}
|
||||
|
||||
public static boolean hideMusicButton() {
|
||||
return Settings.HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Player components] patch
|
||||
|
||||
public static void changeOpacity(ImageView imageView) {
|
||||
imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL);
|
||||
}
|
||||
|
||||
private static boolean isAutoPopupPanel;
|
||||
|
||||
public static boolean disableAutoPlayerPopupPanels(boolean isLiveChatOrPlaylistPanel) {
|
||||
if (!Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get()) {
|
||||
return false;
|
||||
}
|
||||
if (isLiveChatOrPlaylistPanel) {
|
||||
return true;
|
||||
}
|
||||
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
|
||||
}
|
||||
|
||||
public static void setInitVideoPanel(boolean initVideoPanel) {
|
||||
isAutoPopupPanel = initVideoPanel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String videoId = "";
|
||||
|
||||
public static void disableAutoSwitchMixPlaylists(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (!Settings.DISABLE_AUTO_SWITCH_MIX_PLAYLISTS.get()) {
|
||||
return;
|
||||
}
|
||||
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) {
|
||||
return;
|
||||
}
|
||||
if (Objects.equals(newlyLoadedVideoId, videoId)) {
|
||||
return;
|
||||
}
|
||||
videoId = newlyLoadedVideoId;
|
||||
|
||||
if (!VideoInformation.lastPlayerResponseIsAutoGeneratedMixPlaylist()) {
|
||||
return;
|
||||
}
|
||||
VideoUtils.pauseMedia();
|
||||
VideoUtils.openVideo(videoId);
|
||||
}
|
||||
|
||||
public static boolean disableSpeedOverlay() {
|
||||
return disableSpeedOverlay(true);
|
||||
}
|
||||
|
||||
public static boolean disableSpeedOverlay(boolean original) {
|
||||
return !Settings.DISABLE_SPEED_OVERLAY.get() && original;
|
||||
}
|
||||
|
||||
public static double speedOverlayValue() {
|
||||
return speedOverlayValue(2.0f);
|
||||
}
|
||||
|
||||
public static float speedOverlayValue(float original) {
|
||||
return SPEED_OVERLAY_VALUE;
|
||||
}
|
||||
|
||||
public static boolean hideChannelWatermark(boolean original) {
|
||||
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
|
||||
}
|
||||
|
||||
public static void hideCrowdfundingBox(View view) {
|
||||
hideViewBy0dpUnderCondition(Settings.HIDE_CROWDFUNDING_BOX.get(), view);
|
||||
}
|
||||
|
||||
public static void hideDoubleTapOverlayFilter(View view) {
|
||||
hideViewByRemovingFromParentUnderCondition(Settings.HIDE_DOUBLE_TAP_OVERLAY_FILTER, view);
|
||||
}
|
||||
|
||||
public static void hideEndScreenCards(View view) {
|
||||
if (Settings.HIDE_END_SCREEN_CARDS.get()) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hideFilmstripOverlay() {
|
||||
return Settings.HIDE_FILMSTRIP_OVERLAY.get();
|
||||
}
|
||||
|
||||
public static boolean hideInfoCard(boolean original) {
|
||||
return !Settings.HIDE_INFO_CARDS.get() && original;
|
||||
}
|
||||
|
||||
public static boolean hideSeekMessage() {
|
||||
return Settings.HIDE_SEEK_MESSAGE.get();
|
||||
}
|
||||
|
||||
public static boolean hideSeekUndoMessage() {
|
||||
return Settings.HIDE_SEEK_UNDO_MESSAGE.get();
|
||||
}
|
||||
|
||||
public static void hideSuggestedActions(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_SUGGESTED_ACTION.get(), view);
|
||||
}
|
||||
|
||||
public static boolean hideSuggestedVideoEndScreen() {
|
||||
return Settings.HIDE_SUGGESTED_VIDEO_END_SCREEN.get();
|
||||
}
|
||||
|
||||
public static void skipAutoPlayCountdown(View view) {
|
||||
if (!hideSuggestedVideoEndScreen())
|
||||
return;
|
||||
if (!Settings.SKIP_AUTOPLAY_COUNTDOWN.get())
|
||||
return;
|
||||
|
||||
Utils.clickView(view);
|
||||
}
|
||||
|
||||
public static boolean hideZoomOverlay() {
|
||||
return Settings.HIDE_ZOOM_OVERLAY.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Hide player flyout menu] patch
|
||||
|
||||
private static final String QUALITY_LABEL_PREMIUM = "1080p Premium";
|
||||
|
||||
public static String hidePlayerFlyoutMenuEnhancedBitrate(String qualityLabel) {
|
||||
return Settings.HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE.get() &&
|
||||
Objects.equals(QUALITY_LABEL_PREMIUM, qualityLabel)
|
||||
? null
|
||||
: qualityLabel;
|
||||
}
|
||||
|
||||
public static void hidePlayerFlyoutMenuCaptionsFooter(View view) {
|
||||
Utils.hideViewUnderCondition(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER.get(),
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
public static void hidePlayerFlyoutMenuQualityFooter(View view) {
|
||||
Utils.hideViewUnderCondition(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER.get(),
|
||||
view
|
||||
);
|
||||
}
|
||||
|
||||
public static View hidePlayerFlyoutMenuQualityHeader(View view) {
|
||||
return Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER.get()
|
||||
? new View(view.getContext()) // empty view
|
||||
: view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriding this values is possible only after the litho component has been loaded.
|
||||
* Otherwise, crash will occur.
|
||||
* See {@link InitializationPatch#onCreate}.
|
||||
*
|
||||
* @param original original value.
|
||||
* @return whether to enable PiP Mode in the player flyout menu.
|
||||
*/
|
||||
public static boolean hidePiPModeMenu(boolean original) {
|
||||
if (!BaseSettings.SETTINGS_INITIALIZED.get()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return !Settings.HIDE_PLAYER_FLYOUT_MENU_PIP.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region [Seekbar components] patch
|
||||
|
||||
public static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
|
||||
|
||||
public static String appendTimeStampInformation(String original) {
|
||||
if (!Settings.APPEND_TIME_STAMP_INFORMATION.get()) return original;
|
||||
|
||||
String appendString = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE.get()
|
||||
? VideoUtils.getFormattedQualityString(null)
|
||||
: VideoUtils.getFormattedSpeedString(null);
|
||||
|
||||
// Encapsulate the entire appendString with bidi control characters
|
||||
appendString = "\u2066" + appendString + "\u2069";
|
||||
|
||||
// Format the original string with the appended timestamp information
|
||||
return String.format(
|
||||
"%s\u2009•\u2009%s", // Add the separator and the appended information
|
||||
original, appendString
|
||||
);
|
||||
}
|
||||
|
||||
public static void setContainerClickListener(View view) {
|
||||
if (!Settings.APPEND_TIME_STAMP_INFORMATION.get())
|
||||
return;
|
||||
|
||||
if (!(view.getParent() instanceof View containerView))
|
||||
return;
|
||||
|
||||
final BooleanSetting appendTypeSetting = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE;
|
||||
final boolean previousBoolean = appendTypeSetting.get();
|
||||
|
||||
containerView.setOnLongClickListener(timeStampContainerView -> {
|
||||
appendTypeSetting.save(!previousBoolean);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
if (Settings.REPLACE_TIME_STAMP_ACTION.get()) {
|
||||
containerView.setOnClickListener(timeStampContainerView -> VideoUtils.showFlyoutMenu());
|
||||
}
|
||||
}
|
||||
|
||||
public static int getSeekbarClickedColorValue(final int colorValue) {
|
||||
return colorValue == ORIGINAL_SEEKBAR_COLOR
|
||||
? overrideSeekbarColor(colorValue)
|
||||
: colorValue;
|
||||
}
|
||||
|
||||
public static int resumedProgressBarColor(final int colorValue) {
|
||||
return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get()
|
||||
? getSeekbarClickedColorValue(colorValue)
|
||||
: colorValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides all drawable color that use the YouTube seekbar color.
|
||||
* Used only for the video thumbnails seekbar.
|
||||
* <p>
|
||||
* If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color.
|
||||
*/
|
||||
public static int getColor(int colorValue) {
|
||||
if (colorValue == ORIGINAL_SEEKBAR_COLOR) {
|
||||
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
||||
return 0x00000000;
|
||||
}
|
||||
return overrideSeekbarColor(ORIGINAL_SEEKBAR_COLOR);
|
||||
}
|
||||
return colorValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Points where errors occur when playing videos on the PlayStore (ROOT Build)
|
||||
*/
|
||||
public static int overrideSeekbarColor(final int colorValue) {
|
||||
try {
|
||||
return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get()
|
||||
? Color.parseColor(Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.get())
|
||||
: colorValue;
|
||||
} catch (Exception ignored) {
|
||||
Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.resetToDefault();
|
||||
}
|
||||
return colorValue;
|
||||
}
|
||||
|
||||
public static boolean enableSeekbarTapping() {
|
||||
return Settings.ENABLE_SEEKBAR_TAPPING.get();
|
||||
}
|
||||
|
||||
public static boolean enableHighQualityFullscreenThumbnails() {
|
||||
return Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
|
||||
}
|
||||
|
||||
private static final int timeBarChapterViewId =
|
||||
ResourceUtils.getIdIdentifier("time_bar_chapter_title");
|
||||
|
||||
public static boolean hideSeekbar() {
|
||||
return Settings.HIDE_SEEKBAR.get();
|
||||
}
|
||||
|
||||
public static boolean disableSeekbarChapters() {
|
||||
return Settings.DISABLE_SEEKBAR_CHAPTERS.get();
|
||||
}
|
||||
|
||||
public static boolean hideSeekbarChapterLabel(View view) {
|
||||
return Settings.HIDE_SEEKBAR_CHAPTER_LABEL.get() && view.getId() == timeBarChapterViewId;
|
||||
}
|
||||
|
||||
public static boolean hideTimeStamp() {
|
||||
return Settings.HIDE_TIME_STAMP.get();
|
||||
}
|
||||
|
||||
public static boolean restoreOldSeekbarThumbnails() {
|
||||
return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
|
||||
}
|
||||
|
||||
public static boolean enableCairoSeekbar() {
|
||||
return Settings.ENABLE_CAIRO_SEEKBAR.get();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
public static int getQuickActionsTopMargin() {
|
||||
if (!PatchStatus.QuickActions()) {
|
||||
return 0;
|
||||
}
|
||||
return QUICK_ACTIONS_MARGIN_TOP;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package app.revanced.extension.youtube.patches.shorts;
|
||||
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getRawIdentifier;
|
||||
import static app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType.ORIGINAL;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.youtube.patches.utils.LottieAnimationViewPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class AnimationFeedbackPatch {
|
||||
|
||||
public enum AnimationType {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL(null),
|
||||
THUMBS_UP("like_tap_feedback"),
|
||||
THUMBS_UP_CAIRO("like_tap_feedback_cairo"),
|
||||
HEART("like_tap_feedback_heart"),
|
||||
HEART_TINT("like_tap_feedback_heart_tint"),
|
||||
HIDDEN("like_tap_feedback_hidden");
|
||||
|
||||
/**
|
||||
* Animation id.
|
||||
*/
|
||||
final int rawRes;
|
||||
|
||||
AnimationType(@Nullable String jsonName) {
|
||||
this.rawRes = jsonName != null
|
||||
? getRawIdentifier(jsonName)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final AnimationType CURRENT_TYPE = Settings.ANIMATION_TYPE.get();
|
||||
|
||||
private static final boolean HIDE_PLAY_PAUSE_FEEDBACK = Settings.HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND.get();
|
||||
|
||||
private static final int PAUSE_TAP_FEEDBACK_HIDDEN
|
||||
= ResourceUtils.getRawIdentifier("pause_tap_feedback_hidden");
|
||||
|
||||
private static final int PLAY_TAP_FEEDBACK_HIDDEN
|
||||
= ResourceUtils.getRawIdentifier("play_tap_feedback_hidden");
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setShortsLikeFeedback(LottieAnimationView lottieAnimationView) {
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, CURRENT_TYPE.rawRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setShortsPauseFeedback(LottieAnimationView lottieAnimationView) {
|
||||
if (!HIDE_PLAY_PAUSE_FEEDBACK) {
|
||||
return;
|
||||
}
|
||||
|
||||
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PAUSE_TAP_FEEDBACK_HIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setShortsPlayFeedback(LottieAnimationView lottieAnimationView) {
|
||||
if (!HIDE_PLAY_PAUSE_FEEDBACK) {
|
||||
return;
|
||||
}
|
||||
|
||||
LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PLAY_TAP_FEEDBACK_HIDDEN);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
package app.revanced.extension.youtube.patches.shorts;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue;
|
||||
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ShortsPatch {
|
||||
private static final boolean ENABLE_TIME_STAMP = Settings.ENABLE_TIME_STAMP.get();
|
||||
public static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
|
||||
|
||||
private static final int META_PANEL_BOTTOM_MARGIN;
|
||||
private static final double NAVIGATION_BAR_HEIGHT_PERCENTAGE;
|
||||
|
||||
static {
|
||||
if (HIDE_SHORTS_NAVIGATION_BAR) {
|
||||
ShortsPlayerState.getOnChange().addObserver((ShortsPlayerState state) -> {
|
||||
setNavigationBarLayoutParams(state);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
final int bottomMargin = validateValue(
|
||||
Settings.META_PANEL_BOTTOM_MARGIN,
|
||||
0,
|
||||
64,
|
||||
"revanced_shorts_meta_panel_bottom_margin_invalid_toast"
|
||||
);
|
||||
|
||||
META_PANEL_BOTTOM_MARGIN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) bottomMargin, Utils.getResources().getDisplayMetrics());
|
||||
|
||||
final int heightPercentage = validateValue(
|
||||
Settings.SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE,
|
||||
0,
|
||||
100,
|
||||
"revanced_shorts_navigation_bar_height_percentage_invalid_toast"
|
||||
);
|
||||
|
||||
NAVIGATION_BAR_HEIGHT_PERCENTAGE = heightPercentage / 100d;
|
||||
}
|
||||
|
||||
public static Enum<?> repeat;
|
||||
public static Enum<?> singlePlay;
|
||||
public static Enum<?> endScreen;
|
||||
|
||||
public static Enum<?> changeShortsRepeatState(Enum<?> currentState) {
|
||||
switch (Settings.CHANGE_SHORTS_REPEAT_STATE.get()) {
|
||||
case 1 -> currentState = repeat;
|
||||
case 2 -> currentState = singlePlay;
|
||||
case 3 -> currentState = endScreen;
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public static boolean disableResumingStartupShortsPlayer() {
|
||||
return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get();
|
||||
}
|
||||
|
||||
public static boolean enableShortsTimeStamp(boolean original) {
|
||||
return ENABLE_TIME_STAMP || original;
|
||||
}
|
||||
|
||||
public static int enableShortsTimeStamp(int original) {
|
||||
return ENABLE_TIME_STAMP ? 10010 : original;
|
||||
}
|
||||
|
||||
public static void setShortsMetaPanelBottomMargin(View view) {
|
||||
if (!ENABLE_TIME_STAMP)
|
||||
return;
|
||||
|
||||
if (!(view.getLayoutParams() instanceof RelativeLayout.LayoutParams lp))
|
||||
return;
|
||||
|
||||
lp.setMargins(0, 0, 0, META_PANEL_BOTTOM_MARGIN);
|
||||
lp.setMarginEnd(ResourceUtils.getDimension("reel_player_right_dyn_bar_width"));
|
||||
}
|
||||
|
||||
public static void setShortsTimeStampChangeRepeatState(View view) {
|
||||
if (!ENABLE_TIME_STAMP)
|
||||
return;
|
||||
if (!Settings.TIME_STAMP_CHANGE_REPEAT_STATE.get())
|
||||
return;
|
||||
if (view == null)
|
||||
return;
|
||||
|
||||
view.setLongClickable(true);
|
||||
view.setOnLongClickListener(view1 -> {
|
||||
VideoUtils.showShortsRepeatDialog(view1.getContext());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public static void hideShortsCommentsButton(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_SHORTS_COMMENTS_BUTTON.get(), view);
|
||||
}
|
||||
|
||||
public static boolean hideShortsDislikeButton() {
|
||||
return Settings.HIDE_SHORTS_DISLIKE_BUTTON.get();
|
||||
}
|
||||
|
||||
public static ViewGroup hideShortsInfoPanel(ViewGroup viewGroup) {
|
||||
return Settings.HIDE_SHORTS_INFO_PANEL.get() ? null : viewGroup;
|
||||
}
|
||||
|
||||
public static boolean hideShortsLikeButton() {
|
||||
return Settings.HIDE_SHORTS_LIKE_BUTTON.get();
|
||||
}
|
||||
|
||||
public static boolean hideShortsPaidPromotionLabel() {
|
||||
return Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get();
|
||||
}
|
||||
|
||||
public static void hideShortsPaidPromotionLabel(TextView textView) {
|
||||
hideViewUnderCondition(Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get(), textView);
|
||||
}
|
||||
|
||||
public static void hideShortsRemixButton(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_SHORTS_REMIX_BUTTON.get(), view);
|
||||
}
|
||||
|
||||
public static void hideShortsShareButton(View view) {
|
||||
hideViewUnderCondition(Settings.HIDE_SHORTS_SHARE_BUTTON.get(), view);
|
||||
}
|
||||
|
||||
public static boolean hideShortsSoundButton() {
|
||||
return Settings.HIDE_SHORTS_SOUND_BUTTON.get();
|
||||
}
|
||||
|
||||
private static final int zeroPaddingDimenId =
|
||||
ResourceUtils.getDimenIdentifier("revanced_zero_padding");
|
||||
|
||||
public static int getShortsSoundButtonDimenId(int dimenId) {
|
||||
return Settings.HIDE_SHORTS_SOUND_BUTTON.get()
|
||||
? zeroPaddingDimenId
|
||||
: dimenId;
|
||||
}
|
||||
|
||||
public static int hideShortsSubscribeButton(int original) {
|
||||
return Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON.get() ? 0 : original;
|
||||
}
|
||||
|
||||
// YouTube 18.29.38 ~ YouTube 19.28.42
|
||||
public static boolean hideShortsPausedHeader() {
|
||||
return Settings.HIDE_SHORTS_PAUSED_HEADER.get();
|
||||
}
|
||||
|
||||
// YouTube 19.29.42 ~
|
||||
public static boolean hideShortsPausedHeader(boolean original) {
|
||||
return Settings.HIDE_SHORTS_PAUSED_HEADER.get() || original;
|
||||
}
|
||||
|
||||
public static boolean hideShortsToolBar(boolean original) {
|
||||
return !Settings.HIDE_SHORTS_TOOLBAR.get() && original;
|
||||
}
|
||||
|
||||
/**
|
||||
* BottomBarContainer is the parent view of {@link PivotBar},
|
||||
* And can be hidden using {@link View#setVisibility} only when it is initialized.
|
||||
* <p>
|
||||
* If it was not hidden with {@link View#setVisibility} when it was initialized,
|
||||
* it should be hidden with {@link FrameLayout.LayoutParams}.
|
||||
* <p>
|
||||
* When Shorts is opened, {@link FrameLayout.LayoutParams} should be changed to 0dp,
|
||||
* When Shorts is closed, {@link FrameLayout.LayoutParams} should be changed to the original.
|
||||
*/
|
||||
private static WeakReference<View> bottomBarContainerRef = new WeakReference<>(null);
|
||||
|
||||
private static FrameLayout.LayoutParams originalLayoutParams;
|
||||
private static final FrameLayout.LayoutParams zeroLayoutParams =
|
||||
new FrameLayout.LayoutParams(0, 0);
|
||||
|
||||
public static void setNavigationBar(View view) {
|
||||
if (!HIDE_SHORTS_NAVIGATION_BAR) {
|
||||
return;
|
||||
}
|
||||
bottomBarContainerRef = new WeakReference<>(view);
|
||||
if (!(view.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) {
|
||||
return;
|
||||
}
|
||||
if (originalLayoutParams == null) {
|
||||
originalLayoutParams = lp;
|
||||
}
|
||||
}
|
||||
|
||||
public static int setNavigationBarHeight(int original) {
|
||||
return HIDE_SHORTS_NAVIGATION_BAR
|
||||
? (int) Math.round(original * NAVIGATION_BAR_HEIGHT_PERCENTAGE)
|
||||
: original;
|
||||
}
|
||||
|
||||
private static void setNavigationBarLayoutParams(@NonNull ShortsPlayerState shortsPlayerState) {
|
||||
final View navigationBar = bottomBarContainerRef.get();
|
||||
if (navigationBar == null) {
|
||||
return;
|
||||
}
|
||||
if (!(navigationBar.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) {
|
||||
return;
|
||||
}
|
||||
navigationBar.setLayoutParams(
|
||||
shortsPlayerState.isClosed()
|
||||
? originalLayoutParams
|
||||
: zeroLayoutParams
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package app.revanced.extension.youtube.patches.spans;
|
||||
|
||||
import android.text.SpannableString;
|
||||
|
||||
import app.revanced.extension.shared.patches.spans.Filter;
|
||||
import app.revanced.extension.shared.patches.spans.SpanType;
|
||||
import app.revanced.extension.shared.patches.spans.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"})
|
||||
public final class SanitizeVideoSubtitleFilter extends Filter {
|
||||
|
||||
public SanitizeVideoSubtitleFilter() {
|
||||
addCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.SANITIZE_VIDEO_SUBTITLE,
|
||||
"|video_subtitle.eml|"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean skip(String conversionContext, SpannableString spannableString, Object span,
|
||||
int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
|
||||
if (isWord) {
|
||||
if (spanType == SpanType.IMAGE) {
|
||||
hideImageSpan(spannableString, start, end, flags);
|
||||
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
|
||||
} else if (spanType == SpanType.CUSTOM_CHARACTER_STYLE) {
|
||||
hideSpan(spannableString, start, end, flags);
|
||||
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.extension.youtube.patches.spans;
|
||||
|
||||
import android.text.SpannableString;
|
||||
|
||||
import app.revanced.extension.shared.patches.spans.Filter;
|
||||
import app.revanced.extension.shared.patches.spans.SpanType;
|
||||
import app.revanced.extension.shared.patches.spans.StringFilterGroup;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"})
|
||||
public final class SearchLinksFilter extends Filter {
|
||||
/**
|
||||
* Located in front of the search icon.
|
||||
*/
|
||||
private final String WORD_JOINER_CHARACTER = "\u2060";
|
||||
|
||||
public SearchLinksFilter() {
|
||||
addCallbacks(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS,
|
||||
"|comment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the word contains a search icon or not.
|
||||
*/
|
||||
private boolean isSearchLinks(SpannableString original, int end) {
|
||||
String originalString = original.toString();
|
||||
int wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER);
|
||||
// There may be more than one highlight keyword in the comment.
|
||||
// Check the index of all highlight keywords.
|
||||
while (wordJoinerIndex != -1) {
|
||||
if (end - wordJoinerIndex == 2) return true;
|
||||
wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER, wordJoinerIndex + 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean skip(String conversionContext, SpannableString spannableString, Object span,
|
||||
int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) {
|
||||
if (isWord && isSearchLinks(spannableString, end)) {
|
||||
if (spanType == SpanType.IMAGE) {
|
||||
hideSpan(spannableString, start, end, flags);
|
||||
}
|
||||
return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package app.revanced.extension.youtube.patches.swipe;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SwipeControlsPatch {
|
||||
private static WeakReference<View> fullscreenEngagementOverlayViewRef = new WeakReference<>(null);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableHDRAutoBrightness() {
|
||||
return Settings.DISABLE_HDR_AUTO_BRIGHTNESS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableSwipeToSwitchVideo() {
|
||||
return Settings.ENABLE_SWIPE_TO_SWITCH_VIDEO.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableWatchPanelGestures() {
|
||||
return Settings.ENABLE_WATCH_PANEL_GESTURES.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param fullscreenEngagementOverlayView R.layout.fullscreen_engagement_overlay
|
||||
*/
|
||||
public static void setFullscreenEngagementOverlayView(View fullscreenEngagementOverlayView) {
|
||||
fullscreenEngagementOverlayViewRef = new WeakReference<>(fullscreenEngagementOverlayView);
|
||||
}
|
||||
|
||||
public static boolean isEngagementOverlayVisible() {
|
||||
final View engagementOverlayView = fullscreenEngagementOverlayViewRef.get();
|
||||
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.youtube.utils.VideoUtils.pauseMedia;
|
||||
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AlwaysRepeatPatch extends Utils {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @return video is repeated.
|
||||
*/
|
||||
public static boolean alwaysRepeat() {
|
||||
return alwaysRepeatEnabled() && VideoInformation.overrideVideoTime(0);
|
||||
}
|
||||
|
||||
public static boolean alwaysRepeatEnabled() {
|
||||
final boolean alwaysRepeat = Settings.ALWAYS_REPEAT.get();
|
||||
final boolean alwaysRepeatPause = Settings.ALWAYS_REPEAT_PAUSE.get();
|
||||
|
||||
if (alwaysRepeat && alwaysRepeatPause) pauseMedia();
|
||||
return alwaysRepeat;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import app.revanced.extension.youtube.shared.BottomSheetState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BottomSheetHookPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onAttachedToWindow() {
|
||||
BottomSheetState.set(BottomSheetState.OPEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onDetachedFromWindow() {
|
||||
BottomSheetState.set(BottomSheetState.CLOSED);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CastButtonPatch {
|
||||
|
||||
/**
|
||||
* The [Hide cast button] setting is separated into the [Hide cast button in player] setting and the [Hide cast button in toolbar] setting.
|
||||
* Always hide the cast button when both settings are true.
|
||||
* <p>
|
||||
* These two settings belong to different patches, and since the default value for this setting is true,
|
||||
* it is essential to ensure that each patch is included to ensure independent operation.
|
||||
*/
|
||||
public static int hideCastButton(int original) {
|
||||
return Settings.HIDE_TOOLBAR_CAST_BUTTON.get()
|
||||
&& PatchStatus.ToolBarComponents()
|
||||
&& Settings.HIDE_PLAYER_CAST_BUTTON.get()
|
||||
&& PatchStatus.PlayerButtons()
|
||||
? View.GONE
|
||||
: original;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
*/
|
||||
public class DoubleBackToClosePatch {
|
||||
/**
|
||||
* Time between two back button presses
|
||||
*/
|
||||
private static final long PRESSED_TIMEOUT_MILLISECONDS = Settings.DOUBLE_BACK_TO_CLOSE_TIMEOUT.get();
|
||||
|
||||
/**
|
||||
* Last time back button was pressed
|
||||
*/
|
||||
private static long lastTimeBackPressed = 0;
|
||||
|
||||
/**
|
||||
* State whether scroll position reaches the top
|
||||
*/
|
||||
private static boolean isScrollTop = false;
|
||||
|
||||
/**
|
||||
* Detect event when back button is pressed
|
||||
*
|
||||
* @param activity is used when closing the app
|
||||
*/
|
||||
public static void closeActivityOnBackPressed(@NonNull Activity activity) {
|
||||
// Check scroll position reaches the top in home feed
|
||||
if (!isScrollTop)
|
||||
return;
|
||||
|
||||
final long currentTime = System.currentTimeMillis();
|
||||
|
||||
// If the time between two back button presses does not reach PRESSED_TIMEOUT_MILLISECONDS,
|
||||
// set lastTimeBackPressed to the current time.
|
||||
if (currentTime - lastTimeBackPressed < PRESSED_TIMEOUT_MILLISECONDS ||
|
||||
PRESSED_TIMEOUT_MILLISECONDS == 0)
|
||||
activity.finish();
|
||||
else
|
||||
lastTimeBackPressed = currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect event when ScrollView is created by RecyclerView
|
||||
* <p>
|
||||
* start of ScrollView
|
||||
*/
|
||||
public static void onStartScrollView() {
|
||||
isScrollTop = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect event when the scroll position reaches the top by the back button
|
||||
* <p>
|
||||
* stop of ScrollView
|
||||
*/
|
||||
public static void onStopScrollView() {
|
||||
isScrollTop = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DrawableColorPatch {
|
||||
private static final int[] WHITE_VALUES = {
|
||||
-1, // comments chip background
|
||||
-394759, // music related results panel background
|
||||
-83886081 // video chapters list background
|
||||
};
|
||||
|
||||
private static final int[] DARK_VALUES = {
|
||||
-14145496, // drawer content view background
|
||||
-14606047, // comments chip background
|
||||
-15198184, // music related results panel background
|
||||
-15790321, // comments chip background (new layout)
|
||||
-98492127 // video chapters list background
|
||||
};
|
||||
|
||||
// background colors
|
||||
private static int whiteColor = 0;
|
||||
private static int blackColor = 0;
|
||||
|
||||
public static int getColor(int originalValue) {
|
||||
if (anyEquals(originalValue, DARK_VALUES)) {
|
||||
return getBlackColor();
|
||||
} else if (anyEquals(originalValue, WHITE_VALUES)) {
|
||||
return getWhiteColor();
|
||||
}
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
private static int getBlackColor() {
|
||||
if (blackColor == 0) blackColor = ResourceUtils.getColor("yt_black1");
|
||||
return blackColor;
|
||||
}
|
||||
|
||||
private static int getWhiteColor() {
|
||||
if (whiteColor == 0) whiteColor = ResourceUtils.getColor("yt_white1");
|
||||
return whiteColor;
|
||||
}
|
||||
|
||||
private static boolean anyEquals(int value, int... of) {
|
||||
for (int v : of) if (value == v) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,39 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class InitializationPatch {
|
||||
private static final BooleanSetting SETTINGS_INITIALIZED = BaseSettings.SETTINGS_INITIALIZED;
|
||||
|
||||
/**
|
||||
* Some layouts that depend on litho do not load when the app is first installed.
|
||||
* (Also reproduced on unPatched YouTube)
|
||||
* <p>
|
||||
* To fix this, show the restart dialog when the app is installed for the first time.
|
||||
*/
|
||||
public static void onCreate(@NonNull Activity mActivity) {
|
||||
if (SETTINGS_INITIALIZED.get()) {
|
||||
return;
|
||||
}
|
||||
runOnMainThreadDelayed(() -> showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500), 500);
|
||||
runOnMainThreadDelayed(() -> SETTINGS_INITIALIZED.save(true), 1000);
|
||||
}
|
||||
|
||||
public static void setExtendedUtils(@NonNull Activity mActivity) {
|
||||
ExtendedUtils.setApplicationLabel();
|
||||
ExtendedUtils.setSmallestScreenWidthDp();
|
||||
ExtendedUtils.setVersionName();
|
||||
ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.shared.LockModeState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LockModeStateHookPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setLockModeState(@Nullable Enum<?> lockModeState) {
|
||||
if (lockModeState == null) return;
|
||||
|
||||
LockModeState.setFromString(lockModeState.name());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
|
||||
public class LottieAnimationViewPatch {
|
||||
|
||||
public static void setLottieAnimationRawResources(LottieAnimationView lottieAnimationView, int rawRes) {
|
||||
if (lottieAnimationView == null) {
|
||||
Logger.printDebug(() -> "View is null");
|
||||
return;
|
||||
}
|
||||
if (rawRes == 0) {
|
||||
Logger.printDebug(() -> "Resource is not found");
|
||||
return;
|
||||
}
|
||||
setAnimation(lottieAnimationView, rawRes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void setAnimation(LottieAnimationView lottieAnimationView, int rawRes) {
|
||||
// Rest of the implementation added by patch.
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
public class PatchStatus {
|
||||
|
||||
public static boolean ImageSearchButton() {
|
||||
// Replace this with true if the Hide image search buttons patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean MinimalHeader() {
|
||||
// Replace this with true If the Custom header patch succeeds and the patch option was `youtube_minimal_header`
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean PlayerButtons() {
|
||||
// Replace this with true if the Hide player buttons patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean QuickActions() {
|
||||
// Replace this with true if the Fullscreen components patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean RememberPlaybackSpeed() {
|
||||
// Replace this with true if the Video playback patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean SponsorBlock() {
|
||||
// Replace this with true if the SponsorBlock patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean ToolBarComponents() {
|
||||
// Replace this with true if the Toolbar components patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
public static String RVXMusicPackageName() {
|
||||
return "com.google.android.apps.youtube.music";
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getIdIdentifier;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
*/
|
||||
public class PlayerControlsPatch {
|
||||
private static WeakReference<View> playerOverflowButtonViewRef = new WeakReference<>(null);
|
||||
private static final int playerOverflowButtonId =
|
||||
getIdIdentifier("player_overflow_button");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initializeBottomControlButton(View bottomControlsViewGroup) {
|
||||
// AlwaysRepeat.initialize(bottomControlsViewGroup);
|
||||
// CopyVideoUrl.initialize(bottomControlsViewGroup);
|
||||
// CopyVideoUrlTimestamp.initialize(bottomControlsViewGroup);
|
||||
// MuteVolume.initialize(bottomControlsViewGroup);
|
||||
// ExternalDownload.initialize(bottomControlsViewGroup);
|
||||
// SpeedDialog.initialize(bottomControlsViewGroup);
|
||||
// TimeOrderedPlaylist.initialize(bottomControlsViewGroup);
|
||||
// Whitelists.initialize(bottomControlsViewGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initializeTopControlButton(View youtubeControlsLayout) {
|
||||
// CreateSegmentButtonController.initialize(youtubeControlsLayout);
|
||||
// VotingButtonController.initialize(youtubeControlsLayout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Legacy method.
|
||||
* <p>
|
||||
* Player overflow button view does not attach to windows immediately after cold start.
|
||||
* Player overflow button view is not attached to the windows until the user touches the player at least once, and the overlay buttons are hidden until then.
|
||||
* To prevent this, uses the legacy method to show the overlay button until the player overflow button view is attached to the windows.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing) {
|
||||
if (playerOverflowButtonViewRef.get() != null) {
|
||||
return;
|
||||
}
|
||||
changeVisibility(showing, false);
|
||||
}
|
||||
|
||||
private static void changeVisibility(boolean showing, boolean animation) {
|
||||
// AlwaysRepeat.changeVisibility(showing, animation);
|
||||
// CopyVideoUrl.changeVisibility(showing, animation);
|
||||
// CopyVideoUrlTimestamp.changeVisibility(showing, animation);
|
||||
// MuteVolume.changeVisibility(showing, animation);
|
||||
// ExternalDownload.changeVisibility(showing, animation);
|
||||
// SpeedDialog.changeVisibility(showing, animation);
|
||||
// TimeOrderedPlaylist.changeVisibility(showing, animation);
|
||||
// Whitelists.changeVisibility(showing, animation);
|
||||
|
||||
// CreateSegmentButtonController.changeVisibility(showing, animation);
|
||||
// VotingButtonController.changeVisibility(showing, animation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* New method.
|
||||
* <p>
|
||||
* Show or hide the overlay button when the player overflow button view is visible and hidden, respectively.
|
||||
* <p>
|
||||
* Inject the current view into {@link PlayerControlsPatch#playerOverflowButtonView} to check that the player overflow button view is attached to the window.
|
||||
* From this point on, the legacy method is deprecated.
|
||||
*/
|
||||
public static void changeVisibility(boolean showing, boolean animation, @NonNull View view) {
|
||||
if (view.getId() != playerOverflowButtonId) {
|
||||
return;
|
||||
}
|
||||
if (playerOverflowButtonViewRef.get() == null) {
|
||||
Utils.runOnMainThreadDelayed(() -> playerOverflowButtonViewRef = new WeakReference<>(view), 1400);
|
||||
}
|
||||
changeVisibility(showing, animation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called whenever a motion event occurs on the player controller.
|
||||
* <p>
|
||||
* When the user touches the player overlay (motion event occurs), the player overlay disappears immediately.
|
||||
* In this case, the overlay buttons should also disappear immediately.
|
||||
* <p>
|
||||
* In other words, this method detects when the player overlay disappears immediately upon the user's touch,
|
||||
* and quickly fades out all overlay buttons.
|
||||
*/
|
||||
public static void changeVisibilityNegatedImmediate() {
|
||||
if (PlayerControlsVisibility.getCurrent() == PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN) {
|
||||
changeVisibilityNegatedImmediately();
|
||||
}
|
||||
}
|
||||
|
||||
private static void changeVisibilityNegatedImmediately() {
|
||||
// AlwaysRepeat.changeVisibilityNegatedImmediate();
|
||||
// CopyVideoUrl.changeVisibilityNegatedImmediate();
|
||||
// CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate();
|
||||
// MuteVolume.changeVisibilityNegatedImmediate();
|
||||
// ExternalDownload.changeVisibilityNegatedImmediate();
|
||||
// SpeedDialog.changeVisibilityNegatedImmediate();
|
||||
// TimeOrderedPlaylist.changeVisibilityNegatedImmediate();
|
||||
// Whitelists.changeVisibilityNegatedImmediate();
|
||||
|
||||
// CreateSegmentButtonController.changeVisibilityNegatedImmediate();
|
||||
// VotingButtonController.changeVisibilityNegatedImmediate();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerControlsVisibilityHookPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setPlayerControlsVisibility(@Nullable Enum<?> youTubePlayerControlsVisibility) {
|
||||
if (youTubePlayerControlsVisibility == null) return;
|
||||
|
||||
PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.shared.VideoState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerTypeHookPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setPlayerType(@Nullable Enum<?> youTubePlayerType) {
|
||||
if (youTubePlayerType == null) return;
|
||||
|
||||
PlayerType.setFromString(youTubePlayerType.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setVideoState(@Nullable Enum<?> youTubeVideoState) {
|
||||
if (youTubeVideoState == null) return;
|
||||
|
||||
VideoState.setFromString(youTubeVideoState.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Add a listener to the shorts player overlay View.
|
||||
* Triggered when a shorts player is attached or detached to Windows.
|
||||
*
|
||||
* @param view shorts player overlay (R.id.reel_watch_player).
|
||||
*/
|
||||
public static void onShortsCreate(View view) {
|
||||
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@Nullable View v) {
|
||||
ShortsPlayerState.set(ShortsPlayerState.OPEN);
|
||||
}
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@Nullable View v) {
|
||||
ShortsPlayerState.set(ShortsPlayerState.CLOSED);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.player.PlayerPatch.ORIGINAL_SEEKBAR_COLOR;
|
||||
import static app.revanced.extension.youtube.patches.player.PlayerPatch.resumedProgressBarColor;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ProgressBarDrawable extends Drawable {
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
||||
return;
|
||||
}
|
||||
paint.setColor(resumedProgressBarColor(ORIGINAL_SEEKBAR_COLOR));
|
||||
canvas.drawRect(getBounds(), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeChannelNamePatch {
|
||||
|
||||
private static final boolean REPLACE_CHANNEL_HANDLE = Settings.REPLACE_CHANNEL_HANDLE.get();
|
||||
/**
|
||||
* The last character of some handles is an official channel certification mark.
|
||||
* This was in the form of nonBreakSpaceCharacter before SpannableString was made.
|
||||
*/
|
||||
private static final String NON_BREAK_SPACE_CHARACTER = "\u00A0";
|
||||
private volatile static String channelName = "";
|
||||
|
||||
/**
|
||||
* Key: channelId, Value: channelName.
|
||||
*/
|
||||
private static final Map<String, String> channelIdMap = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(20) {
|
||||
private static final int CACHE_LIMIT = 10;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Key: handle, Value: channelName.
|
||||
*/
|
||||
private static final Map<String, String> channelHandleMap = Collections.synchronizedMap(
|
||||
new LinkedHashMap<>(20) {
|
||||
private static final int CACHE_LIMIT = 10;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This method is only invoked on Shorts and is updated whenever the user swipes up or down on the Shorts.
|
||||
*/
|
||||
public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (!REPLACE_CHANNEL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
if (channelIdMap.get(newlyLoadedChannelId) != null) {
|
||||
return;
|
||||
}
|
||||
if (channelIdMap.put(newlyLoadedChannelId, newlyLoadedChannelName) == null) {
|
||||
channelName = newlyLoadedChannelName;
|
||||
Logger.printDebug(() -> "New video started, ChannelId " + newlyLoadedChannelId + ", Channel Name: " + newlyLoadedChannelName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence charSequence) {
|
||||
try {
|
||||
if (!REPLACE_CHANNEL_HANDLE) {
|
||||
return charSequence;
|
||||
}
|
||||
final String conversionContextString = conversionContext.toString();
|
||||
if (!conversionContextString.contains("|reel_channel_bar_inner.eml|")) {
|
||||
return charSequence;
|
||||
}
|
||||
final String originalString = charSequence.toString();
|
||||
if (!originalString.startsWith("@")) {
|
||||
return charSequence;
|
||||
}
|
||||
return getChannelName(originalString);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onCharSequenceLoaded failed", ex);
|
||||
}
|
||||
return charSequence;
|
||||
}
|
||||
|
||||
private static CharSequence getChannelName(@NonNull String handle) {
|
||||
final String trimmedHandle = handle.replaceAll(NON_BREAK_SPACE_CHARACTER, "");
|
||||
|
||||
String cachedChannelName = channelHandleMap.get(trimmedHandle);
|
||||
if (cachedChannelName == null) {
|
||||
if (!channelName.isEmpty() && channelHandleMap.put(handle, channelName) == null) {
|
||||
Logger.printDebug(() -> "Set Handle from last fetched Channel Name, Handle: " + handle + ", Channel Name: " + channelName);
|
||||
cachedChannelName = channelName;
|
||||
} else {
|
||||
Logger.printDebug(() -> "Channel handle is not found: " + trimmedHandle);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
if (handle.contains(NON_BREAK_SPACE_CHARACTER)) {
|
||||
cachedChannelName += NON_BREAK_SPACE_CHARACTER;
|
||||
}
|
||||
String replacedChannelName = cachedChannelName;
|
||||
Logger.printDebug(() -> "Replace Handle " + handle + " to " + replacedChannelName);
|
||||
return replacedChannelName;
|
||||
}
|
||||
|
||||
public synchronized static void setLastShortsChannelId(@NonNull String handle, @NonNull String channelId) {
|
||||
try {
|
||||
if (channelHandleMap.get(handle) != null) {
|
||||
return;
|
||||
}
|
||||
final String channelName = channelIdMap.get(channelId);
|
||||
if (channelName == null) {
|
||||
Logger.printDebug(() -> "Channel name is not found!");
|
||||
return;
|
||||
}
|
||||
if (channelHandleMap.put(handle, channelName) == null) {
|
||||
Logger.printDebug(() -> "Set Handle from Shorts, Handle: " + handle + ", Channel Name: " + channelName);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setLastShortsChannelId failure ", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,690 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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.Objects;
|
||||
|
||||
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
/**
|
||||
* Handles all interaction of UI patch components.
|
||||
* <p>
|
||||
* Known limitation:
|
||||
* The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
|
||||
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
|
||||
* not completed yet then the UI will be temporarily frozen.
|
||||
* <p>
|
||||
* A (yet to be implemented) solution that fixes this problem. Any one of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text 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.
|
||||
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
isSpoofingToLessThan("18.34.00");
|
||||
|
||||
/**
|
||||
* RYD data for the current video on screen.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile ReturnYouTubeDislike currentVideoData;
|
||||
|
||||
/**
|
||||
* The last litho based Shorts loaded.
|
||||
* May be the same value as {@link #currentVideoData}, but usually is the next short to swipe to.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile ReturnYouTubeDislike lastLithoShortsVideoData;
|
||||
|
||||
/**
|
||||
* Because the litho Shorts spans are created after {@link ReturnYouTubeDislikeFilterPatch}
|
||||
* detects the video ids, after the user votes the litho will update
|
||||
* but {@link #lastLithoShortsVideoData} is not the correct data to use.
|
||||
* If this is true, then instead use {@link #currentVideoData}.
|
||||
*/
|
||||
private static volatile boolean lithoShortsShouldUseCurrentData;
|
||||
|
||||
/**
|
||||
* Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile String lastPrefetchedVideoId;
|
||||
|
||||
public static void onRYDStatusChange() {
|
||||
ReturnYouTubeDislikeApi.resetRateLimits();
|
||||
// Must remove all values to protect against using stale data
|
||||
// if the user enables RYD while a video is on screen.
|
||||
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.
|
||||
}
|
||||
|
||||
//
|
||||
// Litho player for both regular videos and Shorts.
|
||||
//
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* For Litho segmented buttons and Litho Shorts player.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence original) {
|
||||
return onLithoTextLoaded(conversionContext, original, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
* <p>
|
||||
* 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 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
|
||||
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence original,
|
||||
boolean isRollingNumber) {
|
||||
try {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
|
||||
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
|
||||
// Regular video.
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
return original; // User enabled RYD while a video was on screen.
|
||||
}
|
||||
if (!(original instanceof Spanned)) {
|
||||
original = new SpannableString(original);
|
||||
}
|
||||
return videoData.getDislikesSpanForRegularVideo((Spanned) original,
|
||||
true, isRollingNumber);
|
||||
}
|
||||
|
||||
if (isRollingNumber) {
|
||||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_like_button.eml")
|
||||
&& !Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onLithoTextLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
//
|
||||
// Litho Shorts player in the incognito mode / live stream.
|
||||
//
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* This method is used in the following situations.
|
||||
* <p>
|
||||
* 1. When the dislike counts are fetched in the Incognito mode.
|
||||
* 2. When the dislike counts are fetched in the live stream.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence original) {
|
||||
try {
|
||||
String conversionContextString = conversionContext.toString();
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return original;
|
||||
}
|
||||
if (!Settings.RYD_SHORTS.get()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
final boolean fetchDislikeLiveStream =
|
||||
conversionContextString.contains("immersive_live_video_action_bar.eml")
|
||||
&& conversionContextString.contains("|dislike_button.eml|");
|
||||
|
||||
if (!fetchDislikeLiveStream) {
|
||||
return original;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(ReturnYouTubeDislikeFilterPatch.getShortsVideoId());
|
||||
videoData.setVideoIdIsShort(true);
|
||||
lastLithoShortsVideoData = videoData;
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
|
||||
return videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onCharSequenceLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
|
||||
private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) {
|
||||
// Litho Shorts player.
|
||||
if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get())
|
||||
|| (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) {
|
||||
return original;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
|
||||
if (videoData == null) {
|
||||
// The Shorts litho video id filter did not detect the video id.
|
||||
// This is normal in incognito mode, but otherwise is abnormal.
|
||||
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
|
||||
return original;
|
||||
}
|
||||
|
||||
// Use the correct dislikes data after voting.
|
||||
if (lithoShortsShouldUseCurrentData) {
|
||||
if (isDislikesSpan) {
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
}
|
||||
videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printException(() -> "currentVideoData is null"); // Should never happen
|
||||
return original;
|
||||
}
|
||||
Logger.printDebug(() -> "Using current video data for litho span");
|
||||
}
|
||||
|
||||
return isDislikesSpan
|
||||
? videoData.getDislikeSpanForShort((Spanned) original)
|
||||
: videoData.getLikeSpanForShort((Spanned) 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, original, true);
|
||||
|
||||
String replacementString = replacement.toString();
|
||||
if (!replacementString.equals(original)) {
|
||||
rollingNumberSpan = replacement;
|
||||
return replacementString;
|
||||
} // Else, the text was not a likes count but instead the view count or something else.
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onRollingNumberLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called for all usage of Rolling Number.
|
||||
* Modifies the measured String text width to include the left separator and padding, if needed.
|
||||
*/
|
||||
public static float onRollingNumberMeasured(String text, float measuredTextWidth) {
|
||||
try {
|
||||
if (Settings.RYD_ENABLED.get()) {
|
||||
if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) {
|
||||
// +1 pixel is needed for some foreign languages that measure
|
||||
// the text different from what is used for layout (Greek in particular).
|
||||
// Probably a bug in Android, but who knows.
|
||||
// Single line mode is also used as an additional fix for this issue.
|
||||
if (Settings.RYD_COMPACT_LAYOUT.get()) {
|
||||
return measuredTextWidth + 1;
|
||||
}
|
||||
|
||||
return measuredTextWidth + 1
|
||||
+ ReturnYouTubeDislike.leftSeparatorBounds.right
|
||||
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onRollingNumberMeasured failure", ex);
|
||||
}
|
||||
|
||||
return measuredTextWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Rolling Number text view modifications.
|
||||
*/
|
||||
private static void addRollingNumberPatchChanges(TextView view) {
|
||||
// YouTube Rolling Numbers do not use compound drawables or drawable padding.
|
||||
if (view.getCompoundDrawablePadding() == 0) {
|
||||
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||
if (Utils.isRightToLeftTextLayout()) {
|
||||
view.setCompoundDrawables(null, null, separator, null);
|
||||
} else {
|
||||
view.setCompoundDrawables(separator, null, null, null);
|
||||
}
|
||||
|
||||
// Disliking can cause the span to grow in size, which is ok and is laid out correctly,
|
||||
// but if the user then removes their dislike the layout will not adjust to the new shorter width.
|
||||
// Use a center alignment to take up any extra space.
|
||||
view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
|
||||
|
||||
// Single line mode does not clip words if the span is larger than the view bounds.
|
||||
// The styled span applied to the view should always have the same bounds,
|
||||
// but use this feature just in case the measurements are somehow off by a few pixels.
|
||||
view.setSingleLine(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
Logger.printDebug(() -> "Removing rolling number TextView changes");
|
||||
view.setCompoundDrawablePadding(0);
|
||||
view.setCompoundDrawables(null, null, null, null);
|
||||
view.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY); // Default alignment
|
||||
view.setSingleLine(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static CharSequence updateRollingNumber(TextView view, CharSequence original) {
|
||||
try {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
removeRollingNumberPatchChanges(view);
|
||||
return original;
|
||||
}
|
||||
final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent
|
||||
&& viewGroupParent.getChildCount() < 2;
|
||||
// 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()) || isDescriptionPanel) {
|
||||
// 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.
|
||||
Logger.printDebug(() -> "Cannot update rolling number (field is null)");
|
||||
removeRollingNumberPatchChanges(view);
|
||||
return original;
|
||||
}
|
||||
|
||||
if (Settings.RYD_COMPACT_LAYOUT.get()) {
|
||||
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) {
|
||||
Logger.printException(() -> "updateRollingNumber failure", ex);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Non litho Shorts player.
|
||||
//
|
||||
|
||||
/**
|
||||
* Replacement text to use for "Dislikes" while RYD is fetching.
|
||||
*/
|
||||
private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-");
|
||||
|
||||
/**
|
||||
* Dislikes TextViews used by Shorts.
|
||||
* <p>
|
||||
* 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() {
|
||||
shortsTextViewRefs.removeIf(ref -> ref.get() == null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called when a Shorts dislike is updated. Always on main thread.
|
||||
* 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 (!Settings.RYD_ENABLED.get()) {
|
||||
return false;
|
||||
}
|
||||
if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) {
|
||||
// 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).
|
||||
clearData();
|
||||
return false;
|
||||
}
|
||||
Logger.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)) {
|
||||
Logger.printDebug(() -> "Shorts dislike is already selected");
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData != null) videoData.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) {
|
||||
Logger.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;
|
||||
}
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "updateShortsTextViews");
|
||||
|
||||
Runnable update = () -> {
|
||||
Spanned shortsDislikesSpan = videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
|
||||
Utils.runOnMainThreadNowOrLater(() -> {
|
||||
String videoId = videoData.getVideoId();
|
||||
if (!videoId.equals(VideoInformation.getVideoId())) {
|
||||
// User swiped to new video before fetch completed
|
||||
Logger.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()))) {
|
||||
Logger.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan);
|
||||
textView.setText(shortsDislikesSpan);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
if (videoData.fetchCompleted()) {
|
||||
update.run(); // Network call is completed, no need to wait on background thread.
|
||||
} else {
|
||||
Utils.runOnBackgroundThread(update);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "updateOnScreenShortsTextViews 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();
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Video Id and voting hooks (all players).
|
||||
//
|
||||
|
||||
private static volatile boolean lastPlayerResponseWasShort;
|
||||
|
||||
/**
|
||||
* Injection point. Uses 'playback response' video id hook to preload RYD.
|
||||
*/
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
||||
// 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 (videoIdIsShort && (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get())) {
|
||||
return;
|
||||
}
|
||||
final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
&& videoIdIsShort && !lastPlayerResponseWasShort;
|
||||
|
||||
Logger.printDebug(() -> "Prefetching RYD for video: " + videoId);
|
||||
ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
if (waitForFetchToComplete && !fetch.fetchCompleted()) {
|
||||
// This call is off the main thread, so wait until the RYD fetch completely finishes,
|
||||
// otherwise if this returns before the fetch completes then the UI can
|
||||
// become frozen when the main thread tries to modify the litho Shorts dislikes and
|
||||
// it must wait for the fetch.
|
||||
// Only need to do this for the first Short opened, as the next Short to swipe to
|
||||
// are preloaded in the background.
|
||||
//
|
||||
// If an asynchronous litho Shorts solution is found, then this blocking call should be removed.
|
||||
Logger.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
|
||||
fetch.getFetchData(20000); // Any arbitrarily large max wait time.
|
||||
}
|
||||
|
||||
// Set the fields after the fetch completes, so any concurrent calls will also wait.
|
||||
lastPlayerResponseWasShort = videoIdIsShort;
|
||||
lastPrefetchedVideoId = videoId;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "preloadVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Uses 'current playing' video id hook. Always called on main thread.
|
||||
*/
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
try {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return;
|
||||
}
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
final PlayerType currentPlayerType = PlayerType.getCurrent();
|
||||
final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized();
|
||||
if (isNoneHiddenOrSlidingMinimized && !Settings.RYD_SHORTS.get()) {
|
||||
// Must clear here, otherwise the wrong data can be used for a minimized regular video.
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoIdIsSame(currentVideoData, videoId)) {
|
||||
return;
|
||||
}
|
||||
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
||||
|
||||
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 (isNoneHiddenOrSlidingMinimized) {
|
||||
updateOnScreenShortsTextViews(true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
Logger.printDebug(() -> "Litho filter did not find any video ids");
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote int that matches {@link Vote#value}
|
||||
*/
|
||||
public static void sendVote(int vote) {
|
||||
try {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return;
|
||||
}
|
||||
final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) {
|
||||
return;
|
||||
}
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printDebug(() -> "Cannot send vote, as current video data is null");
|
||||
return; // User enabled RYD while a regular video was minimized.
|
||||
}
|
||||
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
videoData.sendVote(v);
|
||||
|
||||
if (isNoneHiddenOrMinimized && lastLithoShortsVideoData != null) {
|
||||
lithoShortsShouldUseCurrentData = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ToolBarPatch {
|
||||
|
||||
public static void hookToolBar(Enum<?> buttonEnum, ImageView imageView) {
|
||||
final String enumString = buttonEnum.name();
|
||||
if (enumString.isEmpty() ||
|
||||
imageView == null ||
|
||||
!(imageView.getParent() instanceof View view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "enumString: " + enumString);
|
||||
|
||||
hookToolBar(enumString, view);
|
||||
}
|
||||
|
||||
private static void hookToolBar(String enumString, View parentView) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,53 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AV1CodecPatch {
|
||||
private static final int LITERAL_VALUE_AV01 = 1635135811;
|
||||
private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
|
||||
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
|
||||
private static long lastTimeResponse = 0;
|
||||
|
||||
/**
|
||||
* Replace the SW AV01 codec to VP9 codec.
|
||||
* May not be valid on some clients.
|
||||
*
|
||||
* @param original hardcoded value - "video/av01"
|
||||
*/
|
||||
public static String replaceCodec(String original) {
|
||||
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the SW AV01 codec request with a Dolby Vision codec request.
|
||||
* This request is invalid, so it falls back to codecs other than AV01.
|
||||
* <p>
|
||||
* Limitation: Fallback process causes about 15-20 seconds of buffering.
|
||||
*
|
||||
* @param literalValue literal value of the codec
|
||||
*/
|
||||
public static int rejectResponse(int literalValue) {
|
||||
if (!Settings.REJECT_AV1_CODEC.get())
|
||||
return literalValue;
|
||||
|
||||
Logger.printDebug(() -> "Response: " + literalValue);
|
||||
|
||||
if (literalValue != LITERAL_VALUE_AV01)
|
||||
return literalValue;
|
||||
|
||||
final long currentTime = System.currentTimeMillis();
|
||||
|
||||
// Ignore the invoke within 20 seconds.
|
||||
if (currentTime - lastTimeResponse > 20000) {
|
||||
lastTimeResponse = currentTime;
|
||||
Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
|
||||
}
|
||||
|
||||
return LITERAL_VALUE_DOLBY_VISION;
|
||||
}
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CustomPlaybackSpeedPatch {
|
||||
/**
|
||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
||||
* <p>
|
||||
* Going over 8x does not increase the actual playback speed any higher,
|
||||
* and the UI selector starts flickering and acting weird.
|
||||
* Over 10x and the speeds show up out of order in the UI selector.
|
||||
*/
|
||||
public static final float MAXIMUM_PLAYBACK_SPEED = 8;
|
||||
private static final String[] defaultSpeedEntries;
|
||||
private static final String[] defaultSpeedEntryValues;
|
||||
/**
|
||||
* Custom playback speeds.
|
||||
*/
|
||||
private static float[] playbackSpeeds;
|
||||
private static String[] customSpeedEntries;
|
||||
private static String[] customSpeedEntryValues;
|
||||
|
||||
private static String[] playbackSpeedEntries;
|
||||
private static String[] playbackSpeedEntryValues;
|
||||
|
||||
/**
|
||||
* The last time the old playback menu was forcefully called.
|
||||
*/
|
||||
private static long lastTimeOldPlaybackMenuInvoked;
|
||||
|
||||
static {
|
||||
defaultSpeedEntries = new String[]{getString("quality_auto"), "0.25x", "0.5x", "0.75x", getString("revanced_playback_speed_normal"), "1.25x", "1.5x", "1.75x", "2.0x"};
|
||||
defaultSpeedEntryValues = new String[]{"-2.0", "0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0"};
|
||||
|
||||
loadCustomSpeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static float[] getArray(float[] original) {
|
||||
return isCustomPlaybackSpeedEnabled() ? playbackSpeeds : original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getLength(int original) {
|
||||
return isCustomPlaybackSpeedEnabled() ? playbackSpeeds.length : original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getSize(int original) {
|
||||
return isCustomPlaybackSpeedEnabled() ? 0 : original;
|
||||
}
|
||||
|
||||
public static String[] getListEntries() {
|
||||
return isCustomPlaybackSpeedEnabled()
|
||||
? customSpeedEntries
|
||||
: defaultSpeedEntries;
|
||||
}
|
||||
|
||||
public static String[] getListEntryValues() {
|
||||
return isCustomPlaybackSpeedEnabled()
|
||||
? customSpeedEntryValues
|
||||
: defaultSpeedEntryValues;
|
||||
}
|
||||
|
||||
public static String[] getTrimmedListEntries() {
|
||||
if (playbackSpeedEntries == null) {
|
||||
final String[] playbackSpeedWithAutoEntries = getListEntries();
|
||||
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
|
||||
}
|
||||
|
||||
return playbackSpeedEntries;
|
||||
}
|
||||
|
||||
public static String[] getTrimmedListEntryValues() {
|
||||
if (playbackSpeedEntryValues == null) {
|
||||
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
|
||||
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
|
||||
}
|
||||
|
||||
return playbackSpeedEntryValues;
|
||||
}
|
||||
|
||||
private static void resetCustomSpeeds(@NonNull String toastMessage) {
|
||||
Utils.showToastLong(toastMessage);
|
||||
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
|
||||
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||
}
|
||||
|
||||
private static void loadCustomSpeeds() {
|
||||
try {
|
||||
if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+");
|
||||
Arrays.sort(speedStrings);
|
||||
if (speedStrings.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
playbackSpeeds = new float[speedStrings.length];
|
||||
int i = 0;
|
||||
for (String speedString : speedStrings) {
|
||||
final float speedFloat = Float.parseFloat(speedString);
|
||||
if (speedFloat <= 0 || arrayContains(playbackSpeeds, speedFloat)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
if (speedFloat > MAXIMUM_PLAYBACK_SPEED) {
|
||||
resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED));
|
||||
loadCustomSpeeds();
|
||||
return;
|
||||
}
|
||||
|
||||
playbackSpeeds[i] = speedFloat;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (customSpeedEntries != null) return;
|
||||
|
||||
customSpeedEntries = new String[playbackSpeeds.length + 1];
|
||||
customSpeedEntryValues = new String[playbackSpeeds.length + 1];
|
||||
customSpeedEntries[0] = getString("quality_auto");
|
||||
customSpeedEntryValues[0] = "-2.0";
|
||||
|
||||
i = 1;
|
||||
for (float speed : playbackSpeeds) {
|
||||
String speedString = String.valueOf(speed);
|
||||
customSpeedEntries[i] = speed != 1.0f
|
||||
? speedString + "x"
|
||||
: getString("revanced_playback_speed_normal");
|
||||
customSpeedEntryValues[i] = speedString;
|
||||
i++;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "parse error", ex);
|
||||
resetCustomSpeeds(str("revanced_custom_playback_speeds_parse_exception"));
|
||||
loadCustomSpeeds();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean arrayContains(float[] array, float value) {
|
||||
for (float arrayValue : array) {
|
||||
if (arrayValue == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isCustomPlaybackSpeedEnabled() {
|
||||
return Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get() && playbackSpeeds != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
|
||||
if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
if (PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible) {
|
||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) {
|
||||
PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible) {
|
||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) {
|
||||
PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible = false;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
|
||||
if (recyclerView.getChildCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(recyclerView.getChildAt(0) instanceof ViewGroup PlaybackSpeedParentView)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup parentView3rd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(parentView3rd.getParent() instanceof ViewGroup parentView4th)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
|
||||
// This only shows in phone layout.
|
||||
Utils.clickView(parentView4th.getChildAt(0));
|
||||
|
||||
// In tablet layout there is no Dismiss View, instead we just hide all two parent views.
|
||||
parentView3rd.setVisibility(View.GONE);
|
||||
parentView4th.setVisibility(View.GONE);
|
||||
|
||||
// Show old playback speed menu.
|
||||
showCustomPlaybackSpeedMenu(recyclerView.getContext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is sometimes used multiple times
|
||||
* To prevent this, ignore method reuse within 1 second.
|
||||
*
|
||||
* @param context Context for [playbackSpeedDialogListener]
|
||||
*/
|
||||
private static void showCustomPlaybackSpeedMenu(@NonNull Context context) {
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
|
||||
return;
|
||||
}
|
||||
lastTimeOldPlaybackMenuInvoked = now;
|
||||
|
||||
if (Settings.CUSTOM_PLAYBACK_SPEED_MENU_TYPE.get()) {
|
||||
// Open playback speed dialog
|
||||
VideoUtils.showPlaybackSpeedDialog(context);
|
||||
} else {
|
||||
// Open old style flyout menu
|
||||
VideoUtils.showPlaybackSpeedFlyoutMenu();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HDRVideoPatch {
|
||||
|
||||
public static boolean disableHDRVideo() {
|
||||
return !Settings.DISABLE_HDR_VIDEO.get();
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.requests.PlaylistRequest;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
import app.revanced.extension.youtube.whitelist.Whitelist;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlaybackSpeedPatch {
|
||||
private static final long TOAST_DELAY_MILLISECONDS = 750;
|
||||
private static long lastTimeSpeedChanged;
|
||||
private static boolean isLiveStream;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
isLiveStream = newlyLoadedLiveStreamValue;
|
||||
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
|
||||
|
||||
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
|
||||
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
|
||||
|
||||
VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void fetchPlaylistData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get()) {
|
||||
try {
|
||||
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
||||
// 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 (videoIdIsShort && !isShortAndOpeningOrPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistRequest.fetchRequestIfNeeded(videoId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "fetchPlaylistData failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
|
||||
if (!VideoInformation.lastPlayerResponseIsShort())
|
||||
return playbackSpeed;
|
||||
if (!Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get())
|
||||
return playbackSpeed;
|
||||
|
||||
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
|
||||
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
|
||||
|
||||
return defaultPlaybackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when user selects a playback speed.
|
||||
*
|
||||
* @param playbackSpeed The playback speed the user selected
|
||||
*/
|
||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||
if (PatchStatus.RememberPlaybackSpeed() &&
|
||||
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
||||
// then the menu will allow increasing without bounds but the max speed is
|
||||
// still capped to under 8.0x.
|
||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.MAXIMUM_PLAYBACK_SPEED - 0.05f);
|
||||
|
||||
// Prevent toast spamming if using the 0.05x adjustments.
|
||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||
final long now = System.currentTimeMillis();
|
||||
lastTimeSpeedChanged = now;
|
||||
|
||||
final float finalPlaybackSpeed = playbackSpeed;
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (lastTimeSpeedChanged != now) {
|
||||
// The user made additional speed adjustments and this call is outdated.
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) {
|
||||
// User changed to a different speed and immediately changed back.
|
||||
// Or the user is going past 8.0x in the glitched out 0.05x menu.
|
||||
return;
|
||||
}
|
||||
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
|
||||
|
||||
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
|
||||
return;
|
||||
}
|
||||
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
}, TOAST_DELAY_MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
|
||||
return (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) ||
|
||||
Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) ||
|
||||
getPlaylistData(videoId)
|
||||
? 1.0f
|
||||
: Settings.DEFAULT_PLAYBACK_SPEED.get();
|
||||
}
|
||||
|
||||
private static boolean getPlaylistData(@Nullable String videoId) {
|
||||
if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get() && videoId != null) {
|
||||
try {
|
||||
PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId);
|
||||
final boolean isPlaylist = request != null && BooleanUtils.toBoolean(request.getStream());
|
||||
Logger.printDebug(() -> "isPlaylist: " + isPlaylist);
|
||||
|
||||
return isPlaylist;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getPlaylistData failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ReloadVideoPatch {
|
||||
private static final long RELOAD_VIDEO_TIME_MILLISECONDS = 15000L;
|
||||
|
||||
@NonNull
|
||||
public static String videoId = "";
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (!Settings.SKIP_PRELOADED_BUFFER.get())
|
||||
return;
|
||||
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
|
||||
return;
|
||||
if (videoId.equals(newlyLoadedVideoId))
|
||||
return;
|
||||
videoId = newlyLoadedVideoId;
|
||||
|
||||
if (newlyLoadedVideoLength < RELOAD_VIDEO_TIME_MILLISECONDS || newlyLoadedLiveStreamValue)
|
||||
return;
|
||||
|
||||
final long seekTime = Math.max(RELOAD_VIDEO_TIME_MILLISECONDS, (long) (newlyLoadedVideoLength * 0.5));
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> reloadVideo(seekTime), 250);
|
||||
}
|
||||
|
||||
private static void reloadVideo(final long videoLength) {
|
||||
final long lastVideoTime = VideoInformation.getVideoTime();
|
||||
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
|
||||
final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 300);
|
||||
VideoInformation.overrideVideoTime(videoLength);
|
||||
VideoInformation.overrideVideoTime(lastVideoTime + speedAdjustedTimeThreshold);
|
||||
|
||||
if (!Settings.SKIP_PRELOADED_BUFFER_TOAST.get())
|
||||
return;
|
||||
|
||||
Utils.showToastShort(str("revanced_skipped_preloaded_buffer"));
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.components.VideoQualityMenuFilter;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RestoreOldVideoQualityMenuPatch {
|
||||
|
||||
public static boolean restoreOldVideoQualityMenu() {
|
||||
return Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get();
|
||||
}
|
||||
|
||||
public static void restoreOldVideoQualityMenu(ListView listView) {
|
||||
if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get())
|
||||
return;
|
||||
|
||||
listView.setVisibility(View.GONE);
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
listView.setSoundEffectsEnabled(false);
|
||||
listView.performItemClick(null, 2, 0);
|
||||
},
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public static void onFlyoutMenuCreate(final RecyclerView recyclerView) {
|
||||
if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get())
|
||||
return;
|
||||
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
// Check if the current view is the quality menu.
|
||||
if (!VideoQualityMenuFilter.isVideoQualityMenuVisible || recyclerView.getChildCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup quickQualityViewParent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(recyclerView.getChildAt(0) instanceof ViewGroup advancedQualityParentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (advancedQualityParentView.getChildCount() < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
View advancedQualityView = advancedQualityParentView.getChildAt(3);
|
||||
if (advancedQualityView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
quickQualityViewParent.setVisibility(View.GONE);
|
||||
|
||||
// Click the "Advanced" quality menu to show the "old" quality menu.
|
||||
advancedQualityView.callOnClick();
|
||||
|
||||
VideoQualityMenuFilter.isVideoQualityMenuVisible = false;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofDeviceDimensionsPatch {
|
||||
private static final boolean SPOOF = Settings.SPOOF_DEVICE_DIMENSIONS.get();
|
||||
|
||||
public static int getMinHeightOrWidth(int minHeightOrWidth) {
|
||||
return SPOOF ? 64 : minHeightOrWidth;
|
||||
}
|
||||
|
||||
public static int getMaxHeightOrWidth(int maxHeightOrWidth) {
|
||||
return SPOOF ? 4096 : maxHeightOrWidth;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VP9CodecPatch {
|
||||
|
||||
public static boolean disableVP9Codec() {
|
||||
return !Settings.DISABLE_VP9_CODEC.get();
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.settings.IntegerSetting;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VideoQualityPatch {
|
||||
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
|
||||
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
|
||||
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
|
||||
|
||||
@NonNull
|
||||
public static String videoId = "";
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoStarted() {
|
||||
setVideoQuality(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
|
||||
return;
|
||||
if (videoId.equals(newlyLoadedVideoId))
|
||||
return;
|
||||
videoId = newlyLoadedVideoId;
|
||||
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void userSelectedVideoQuality() {
|
||||
Utils.runOnMainThreadDelayed(() ->
|
||||
userSelectedVideoQuality(VideoInformation.getVideoQuality()),
|
||||
300
|
||||
);
|
||||
}
|
||||
|
||||
private static void setVideoQuality(final long delayMillis) {
|
||||
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
|
||||
? mobileQualitySetting.get()
|
||||
: wifiQualitySetting.get();
|
||||
|
||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
||||
return;
|
||||
|
||||
Utils.runOnMainThreadDelayed(() ->
|
||||
VideoInformation.overrideVideoQuality(
|
||||
VideoInformation.getAvailableVideoQuality(defaultQuality)
|
||||
),
|
||||
delayMillis
|
||||
);
|
||||
}
|
||||
|
||||
private static void userSelectedVideoQuality(final int defaultQuality) {
|
||||
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
|
||||
return;
|
||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
||||
return;
|
||||
|
||||
final Utils.NetworkType networkType = Utils.getNetworkType();
|
||||
|
||||
switch (networkType) {
|
||||
case NONE -> {
|
||||
Utils.showToastShort(str("revanced_remember_video_quality_none"));
|
||||
return;
|
||||
}
|
||||
case MOBILE -> mobileQualitySetting.save(defaultQuality);
|
||||
default -> wifiQualitySetting.save(defaultQuality);
|
||||
}
|
||||
|
||||
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
|
||||
return;
|
||||
|
||||
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
|
||||
}
|
||||
}
|
@ -0,0 +1,776 @@
|
||||
package app.revanced.extension.youtube.returnyoutubedislike;
|
||||
|
||||
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.icu.text.CompactDecimalFormat;
|
||||
import android.icu.text.DecimalFormat;
|
||||
import android.icu.text.DecimalFormatSymbols;
|
||||
import android.icu.text.NumberFormat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
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;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
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;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.returnyoutubedislike.requests.RYDVoteData;
|
||||
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
|
||||
/**
|
||||
* Handles fetching and creation/replacing of RYD dislike text spans.
|
||||
* <p>
|
||||
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
|
||||
*/
|
||||
public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
|
||||
* <p>
|
||||
* Must be less than 5 seconds, as per:
|
||||
* <a href="https://developer.android.com/topic/performance/vitals/anr">...</a>
|
||||
*/
|
||||
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
|
||||
|
||||
/**
|
||||
* How long to retain successful RYD fetches.
|
||||
*/
|
||||
private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes
|
||||
|
||||
/**
|
||||
* How long to retain unsuccessful RYD fetches,
|
||||
* and also the minimum time before retrying again.
|
||||
*/
|
||||
private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes
|
||||
|
||||
/**
|
||||
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
|
||||
* 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 = '◎'; // 'bullseye'
|
||||
|
||||
public static final boolean IS_SPOOFING_TO_OLD_SEPARATOR_COLOR =
|
||||
isSpoofingToLessThan("18.10.00");
|
||||
|
||||
/**
|
||||
* Cached lookup of all video ids.
|
||||
*/
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, ReturnYouTubeDislike> fetchCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Used to send votes, one by one, in the same order the user created them.
|
||||
*/
|
||||
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* For formatting dislikes as number.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* For formatting dislikes as percentage.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class")
|
||||
private static NumberFormat dislikePercentageFormatter;
|
||||
|
||||
// Used for segmented dislike spans in Litho regular player.
|
||||
public 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;
|
||||
public static final Locale locale;
|
||||
|
||||
static {
|
||||
final Resources resources = Utils.getResources();
|
||||
DisplayMetrics dp = resources.getDisplayMetrics();
|
||||
|
||||
leftSeparatorBounds = new Rect(0, 0,
|
||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
|
||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp));
|
||||
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);
|
||||
locale = resources.getConfiguration().getLocales().get(0);
|
||||
|
||||
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
|
||||
}
|
||||
|
||||
private final String videoId;
|
||||
|
||||
/**
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes.
|
||||
* Absolutely cannot be holding any lock during calls to {@link Future#get()}.
|
||||
*/
|
||||
private final Future<RYDVoteData> future;
|
||||
|
||||
/**
|
||||
* Time this instance and the fetch future was created.
|
||||
*/
|
||||
private final long timeFetched;
|
||||
|
||||
/**
|
||||
* If this instance was previously used for a Short.
|
||||
*/
|
||||
@GuardedBy("this")
|
||||
private boolean isShort;
|
||||
|
||||
/**
|
||||
* Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("this")
|
||||
private Vote userVote;
|
||||
|
||||
/**
|
||||
* Original dislike span, before modifications.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("this")
|
||||
private Spanned originalDislikeSpan;
|
||||
|
||||
/**
|
||||
* Replacement like/dislike span that includes formatted dislikes.
|
||||
* Used to prevent recreating the same span multiple times.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("this")
|
||||
private SpannableString replacementLikeDislikeSpan;
|
||||
|
||||
/**
|
||||
* Color of the left and middle separator, based on the color of the right separator.
|
||||
* It's unknown where YT gets the color from, and the values here are approximated by hand.
|
||||
* Ideally, this would be the actual color YT uses at runtime.
|
||||
* <p>
|
||||
* Older versions before the 'Me' library tab use a slightly different color.
|
||||
* If spoofing was previously used and is now turned off,
|
||||
* or an old version was recently upgraded then the old colors are sometimes still used.
|
||||
*/
|
||||
private static int getSeparatorColor() {
|
||||
if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) {
|
||||
return ThemeUtils.isDarkTheme()
|
||||
? 0x29AAAAAA // transparent dark gray
|
||||
: 0xFFD9D9D9; // light gray
|
||||
}
|
||||
|
||||
return ThemeUtils.isDarkTheme()
|
||||
? 0x33FFFFFF
|
||||
: 0xFFD9D9D9;
|
||||
}
|
||||
|
||||
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,
|
||||
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).
|
||||
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
|
||||
CharSequence oldLikes = oldSpannable;
|
||||
|
||||
// YouTube creators can hide the like count on a video,
|
||||
// and the like count appears as a device language specific string that says 'Like'.
|
||||
// Check if the string contains any numbers.
|
||||
if (!Utils.containsNumber(oldLikes)) {
|
||||
if (Settings.RYD_ESTIMATED_LIKE.get()) {
|
||||
// Likes are hidden by video creator
|
||||
//
|
||||
// RYD does not directly provide like data, but can use an estimated likes
|
||||
// using the same scale factor RYD applied to the raw dislikes.
|
||||
//
|
||||
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
|
||||
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
|
||||
Logger.printDebug(() -> "Using estimated likes");
|
||||
oldLikes = formatDislikeCount(voteData.getLikeCount());
|
||||
} else {
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden.
|
||||
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
|
||||
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
|
||||
}
|
||||
}
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
||||
|
||||
if (!compactLayout) {
|
||||
String leftSeparatorString = getTextDirectionString();
|
||||
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);
|
||||
}
|
||||
|
||||
// likes
|
||||
builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes));
|
||||
|
||||
// middle separator
|
||||
String middleSeparatorString = compactLayout
|
||||
? " " + MIDDLE_SEPARATOR_CHARACTER + " "
|
||||
: " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
|
||||
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
|
||||
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
|
||||
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
|
||||
shapeDrawable.getPaint().setColor(getSeparatorColor());
|
||||
shapeDrawable.setBounds(middleSeparatorBounds);
|
||||
// Use original text width if using Rolling Number,
|
||||
// to ensure the replacement styled span has the same width as the measured String,
|
||||
// otherwise layout can be broken (especially on devices with small system font sizes).
|
||||
middleSeparatorSpan.setSpan(
|
||||
new VerticallyCenteredImageSpan(shapeDrawable, isRollingNumber),
|
||||
shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
builder.append(middleSeparatorSpan);
|
||||
|
||||
// dislikes
|
||||
builder.append(newSpannableWithDislikes(oldSpannable, voteData));
|
||||
|
||||
return new SpannableString(builder);
|
||||
}
|
||||
|
||||
private static @NonNull String getTextDirectionString() {
|
||||
return Utils.isRightToLeftTextLayout()
|
||||
? "\u200F" // u200F = right to left character
|
||||
: "\u200E"; // u200E = left to right character
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) {
|
||||
// Cannot use equals on the span, because many of the inner styling spans do not implement equals.
|
||||
// Instead, compare the underlying text and the text color to handle when dark mode is changed.
|
||||
// Cannot compare the status of device dark mode, as Litho components are updated just before dark mode status changes.
|
||||
if (!one.toString().equals(two.toString())) {
|
||||
return false;
|
||||
}
|
||||
ForegroundColorSpan[] oneColors = one.getSpans(0, one.length(), ForegroundColorSpan.class);
|
||||
ForegroundColorSpan[] twoColors = two.getSpans(0, two.length(), ForegroundColorSpan.class);
|
||||
final int oneLength = oneColors.length;
|
||||
if (oneLength != twoColors.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < oneLength; i++) {
|
||||
if (oneColors[i].getForegroundColor() != twoColors[i].getForegroundColor()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SpannableString newSpannableWithLikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
return newSpanUsingStylingOfAnotherSpan(sourceStyling, formatDislikeCount(voteData.getLikeCount()));
|
||||
}
|
||||
|
||||
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
|
||||
Settings.RYD_DISLIKE_PERCENTAGE.get()
|
||||
? formatDislikePercentage(voteData.getDislikePercentage())
|
||||
: formatDislikeCount(voteData.getDislikeCount()));
|
||||
}
|
||||
|
||||
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
|
||||
if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString spannableString) {
|
||||
return spannableString; // Nothing to do.
|
||||
}
|
||||
|
||||
SpannableString destination = new SpannableString(newSpanText);
|
||||
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
|
||||
for (Object span : spans) {
|
||||
destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static String formatDislikeCount(long dislikeCount) {
|
||||
if (isSDKAbove(24)) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikeCountFormatter == null) {
|
||||
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
|
||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
|
||||
// YouTube disregards locale specific number characters
|
||||
// and instead shows english number characters everywhere.
|
||||
// To use the same behavior, override the digit characters to use English
|
||||
// so languages such as Arabic will show "1.234" instead of the native "۱,۲۳٤"
|
||||
if (isSDKAbove(28)) {
|
||||
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
|
||||
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
|
||||
dislikeCountFormatter.setDecimalFormatSymbols(symbols);
|
||||
}
|
||||
}
|
||||
return dislikeCountFormatter.format(dislikeCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Will never be reached, as the oldest supported YouTube app requires Android N or greater.
|
||||
return String.valueOf(dislikeCount);
|
||||
}
|
||||
|
||||
private static String formatDislikePercentage(float dislikePercentage) {
|
||||
if (isSDKAbove(24)) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
|
||||
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||
|
||||
// Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns.
|
||||
if (isSDKAbove(28) && dislikePercentageFormatter instanceof DecimalFormat decimalFormat) {
|
||||
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
|
||||
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
|
||||
decimalFormat.setDecimalFormatSymbols(symbols);
|
||||
}
|
||||
}
|
||||
if (dislikePercentage >= 0.01) { // at least 1%
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
|
||||
} else {
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
|
||||
}
|
||||
return dislikePercentageFormatter.format(dislikePercentage);
|
||||
}
|
||||
}
|
||||
|
||||
// Will never be reached, as the oldest supported YouTube app requires Android N or greater.
|
||||
return String.valueOf((int) (dislikePercentage * 100));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) {
|
||||
Objects.requireNonNull(videoId);
|
||||
synchronized (fetchCache) {
|
||||
// Remove any expired entries.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (isSDKAbove(24)) {
|
||||
fetchCache.values().removeIf(value -> {
|
||||
final boolean expired = value.isExpired(now);
|
||||
if (expired)
|
||||
Logger.printDebug(() -> "Removing expired fetch: " + value.videoId);
|
||||
return expired;
|
||||
});
|
||||
} else {
|
||||
final Iterator<Map.Entry<String, ReturnYouTubeDislike>> itr = fetchCache.entrySet().iterator();
|
||||
while (itr.hasNext()) {
|
||||
final Map.Entry<String, ReturnYouTubeDislike> entry = itr.next();
|
||||
if (entry.getValue().isExpired(now)) {
|
||||
Logger.printDebug(() -> "Removing expired fetch: " + entry.getValue().videoId);
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike fetch = fetchCache.get(videoId);
|
||||
if (fetch == null) {
|
||||
fetch = new ReturnYouTubeDislike(videoId);
|
||||
fetchCache.put(videoId, fetch);
|
||||
}
|
||||
return fetch;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called if the user changes dislikes appearance settings.
|
||||
*/
|
||||
public static void clearAllUICaches() {
|
||||
synchronized (fetchCache) {
|
||||
for (ReturnYouTubeDislike fetch : fetchCache.values()) {
|
||||
fetch.clearUICache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ReturnYouTubeDislike(@NonNull String videoId) {
|
||||
this.videoId = Objects.requireNonNull(videoId);
|
||||
this.timeFetched = System.currentTimeMillis();
|
||||
this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
|
||||
private boolean isExpired(long now) {
|
||||
final long timeSinceCreation = now - timeFetched;
|
||||
if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) {
|
||||
return false; // Not expired, even if the API call failed.
|
||||
}
|
||||
if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) {
|
||||
return true; // Always expired.
|
||||
}
|
||||
// Only expired if the fetch failed (API null response).
|
||||
return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public RYDVoteData getFetchData(long maxTimeToWait) {
|
||||
try {
|
||||
return future.get(maxTimeToWait, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException ex) {
|
||||
Logger.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms");
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
Logger.printException(() -> "Future failure ", ex); // will never happen
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the RYD fetch call has completed.
|
||||
*/
|
||||
public boolean fetchCompleted() {
|
||||
return future.isDone();
|
||||
}
|
||||
|
||||
private synchronized void clearUICache() {
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
Logger.printDebug(() -> "Clearing replacement span for: " + videoId);
|
||||
}
|
||||
replacementLikeDislikeSpan = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered.
|
||||
*
|
||||
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
||||
* and the network call fails, this returns NULL.
|
||||
*/
|
||||
@Nullable
|
||||
private static String getUserId() {
|
||||
Utils.verifyOffMainThread();
|
||||
|
||||
String userId = Settings.RYD_USER_ID.get();
|
||||
if (!userId.isEmpty()) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser();
|
||||
if (userId != null) {
|
||||
Settings.RYD_USER_ID.save(userId);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getVideoId() {
|
||||
return videoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-emptively set this as a Short.
|
||||
*/
|
||||
public synchronized void setVideoIdIsShort(boolean isShort) {
|
||||
this.isShort = isShort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
boolean isRollingNumber) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton,
|
||||
isRollingNumber, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Shorts like Spannable is created.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getLikeSpanForShort(@NonNull Spanned original) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false,
|
||||
false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Shorts dislike Spannable is created.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false,
|
||||
false, true, false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber,
|
||||
boolean spanIsForShort,
|
||||
boolean spanIsForLikes) {
|
||||
try {
|
||||
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
|
||||
if (votingData == null) {
|
||||
Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
|
||||
return original;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (spanIsForShort) {
|
||||
// Cannot set this to false if span is not for a Short.
|
||||
// When spoofing to an old version and a Short is opened while a regular video
|
||||
// is on screen, this instance can be loaded for the minimized regular video.
|
||||
// But this Shorts data won't be displayed for that call
|
||||
// and when it is un-minimized it will reload again and the load will be ignored.
|
||||
isShort = true;
|
||||
} else if (isShort) {
|
||||
// user:
|
||||
// 1, opened a video
|
||||
// 2. opened a short (without closing the regular video)
|
||||
// 3. closed the short
|
||||
// 4. regular video is now present, but the videoId and RYD data is still for the short
|
||||
Logger.printDebug(() -> "Ignoring regular video dislike span,"
|
||||
+ " as data loaded was previously used for a Short: " + videoId);
|
||||
return original;
|
||||
}
|
||||
|
||||
// prevents reproducible bugs with the following steps:
|
||||
// (user is using YouTube with RollingNumber applied)
|
||||
// 1. opened a video
|
||||
// 2. switched to fullscreen
|
||||
// 3. click video's title to open the video description
|
||||
// 4. dislike count may be replaced in the like count area or view count area of the video description
|
||||
if (PlayerType.getCurrent().isFullScreenOrSlidingFullScreen()) {
|
||||
Logger.printDebug(() -> "Ignoring fullscreen video description panel: " + videoId);
|
||||
return original;
|
||||
}
|
||||
|
||||
if (spanIsForLikes) {
|
||||
// Scrolling Shorts does not cause the Spans to be reloaded,
|
||||
// so there is no need to cache the likes for this situations.
|
||||
Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId);
|
||||
return newSpannableWithLikes(original, votingData);
|
||||
}
|
||||
|
||||
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null
|
||||
&& spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
|
||||
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
|
||||
// No replacement span exist, create it now.
|
||||
|
||||
if (userVote != null) {
|
||||
votingData.updateUsingVote(userVote);
|
||||
}
|
||||
originalDislikeSpan = original;
|
||||
replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, isRollingNumber, votingData);
|
||||
Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '"
|
||||
+ replacementLikeDislikeSpan + "'" + " using video: " + videoId);
|
||||
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", ex);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
public void sendVote(@NonNull Vote vote) {
|
||||
Utils.verifyOnMainThread();
|
||||
Objects.requireNonNull(vote);
|
||||
try {
|
||||
if (isShort != PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
// Shorts was loaded with regular video present, then Shorts was closed.
|
||||
// and then user voted on the now visible original video.
|
||||
// Cannot send a vote, because this instance is for the wrong video.
|
||||
Utils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
|
||||
return;
|
||||
}
|
||||
|
||||
setUserVote(vote);
|
||||
|
||||
voteSerialExecutor.execute(() -> {
|
||||
try { // Must wrap in try/catch to properly log exceptions.
|
||||
ReturnYouTubeDislikeApi.sendVote(getUserId(), videoId, vote);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to send vote", ex);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Error trying to send vote", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current user vote value, and does not send the vote to the RYD API.
|
||||
* <p>
|
||||
* Only used to set value if thumbs up/down is already selected on video load.
|
||||
*/
|
||||
public void setUserVote(@NonNull Vote vote) {
|
||||
Objects.requireNonNull(vote);
|
||||
try {
|
||||
Logger.printDebug(() -> "setUserVote: " + vote);
|
||||
|
||||
synchronized (this) {
|
||||
userVote = vote;
|
||||
clearUICache();
|
||||
}
|
||||
|
||||
if (future.isDone()) {
|
||||
// Update the fetched vote data.
|
||||
RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
|
||||
if (voteData == null) {
|
||||
// RYD fetch failed.
|
||||
Logger.printDebug(() -> "Cannot update UI (vote data not available)");
|
||||
return;
|
||||
}
|
||||
voteData.updateUsingVote(vote);
|
||||
} // Else, vote will be applied after fetch completes.
|
||||
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setUserVote failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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
|
||||
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
|
||||
int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) {
|
||||
Drawable drawable = getDrawable();
|
||||
Rect bounds = drawable.getBounds();
|
||||
if (fontMetrics != null) {
|
||||
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 - halfDrawHeight;
|
||||
fontMetrics.top = fontMetrics.ascent;
|
||||
fontMetrics.bottom = yCenter + halfDrawHeight;
|
||||
fontMetrics.descent = fontMetrics.bottom;
|
||||
}
|
||||
if (useOriginalWidth) {
|
||||
return (int) paint.measureText(text, start, end);
|
||||
}
|
||||
return bounds.right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
|
||||
float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
Drawable drawable = getDrawable();
|
||||
canvas.save();
|
||||
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
|
||||
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(translateX, translateY);
|
||||
drawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
@ -0,0 +1,654 @@
|
||||
package app.revanced.extension.youtube.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.shared.settings.Setting.migrateFromOldPreferences;
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
import static app.revanced.extension.shared.settings.Setting.parentsAny;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.EnumSetting;
|
||||
import app.revanced.extension.shared.settings.FloatSetting;
|
||||
import app.revanced.extension.shared.settings.IntegerSetting;
|
||||
import app.revanced.extension.shared.settings.LongSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability;
|
||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
|
||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
|
||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
|
||||
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
|
||||
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
|
||||
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
|
||||
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
|
||||
import app.revanced.extension.youtube.patches.misc.SpoofStreamingDataPatch;
|
||||
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class Settings extends BaseSettings {
|
||||
// PreferenceScreen: Ads
|
||||
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
|
||||
public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE, true);
|
||||
public static final BooleanSetting HIDE_MERCHANDISE_SHELF = new BooleanSetting("revanced_hide_merchandise_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_STORE_SHELF = new BooleanSetting("revanced_hide_player_store_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE);
|
||||
public static final BooleanSetting HIDE_SELF_SPONSOR_CARDS = new BooleanSetting("revanced_hide_self_sponsor_cards", TRUE);
|
||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
|
||||
public static final BooleanSetting HIDE_VIEW_PRODUCTS = new BooleanSetting("revanced_hide_view_products", TRUE);
|
||||
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
|
||||
|
||||
|
||||
// PreferenceScreen: Alternative Thumbnails
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscriptions", ThumbnailOption.ORIGINAL);
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_LIBRARY = new EnumSetting<>("revanced_alt_thumbnail_library", ThumbnailOption.ORIGINAL);
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_PLAYER = new EnumSetting<>("revanced_alt_thumbnail_player", ThumbnailOption.ORIGINAL);
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SEARCH = new EnumSetting<>("revanced_alt_thumbnail_search", ThumbnailOption.ORIGINAL);
|
||||
public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url",
|
||||
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, new DeArrowAvailability());
|
||||
public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", FALSE, new DeArrowAvailability());
|
||||
public static final EnumSetting<ThumbnailStillTime> ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability());
|
||||
public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability());
|
||||
|
||||
|
||||
// PreferenceScreen: Feed
|
||||
public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_card", TRUE);
|
||||
public static final BooleanSetting HIDE_CAROUSEL_SHELF = new BooleanSetting("revanced_hide_carousel_shelf", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
|
||||
public static final BooleanSetting HIDE_EXPANDABLE_SHELF = new BooleanSetting("revanced_hide_expandable_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_FEED_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_feed_captions_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FEED_SEARCH_BAR = new BooleanSetting("revanced_hide_feed_search_bar", FALSE);
|
||||
public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE);
|
||||
public static final BooleanSetting HIDE_FLOATING_BUTTON = new BooleanSetting("revanced_hide_floating_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE);
|
||||
public static final BooleanSetting HIDE_LATEST_VIDEOS_BUTTON = new BooleanSetting("revanced_hide_latest_videos_button", TRUE);
|
||||
public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", FALSE);
|
||||
public static final BooleanSetting HIDE_MOVIE_SHELF = new BooleanSetting("revanced_hide_movie_shelf", FALSE);
|
||||
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
||||
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_SUBSCRIPTIONS_CAROUSEL = new BooleanSetting("revanced_hide_subscriptions_carousel", FALSE, true);
|
||||
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", TRUE);
|
||||
|
||||
|
||||
// PreferenceScreen: Feed - Category bar
|
||||
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_FEED = new BooleanSetting("revanced_hide_category_bar_in_feed", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_SEARCH = new BooleanSetting("revanced_hide_category_bar_in_search", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_category_bar_in_related_videos", FALSE, true);
|
||||
|
||||
// PreferenceScreen: Feed - Channel profile
|
||||
public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE);
|
||||
public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB));
|
||||
public static final BooleanSetting HIDE_BROWSE_STORE_BUTTON = new BooleanSetting("revanced_hide_browse_store_button", TRUE);
|
||||
public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_CHANNEL_PROFILE_LINKS = new BooleanSetting("revanced_hide_channel_profile_links", TRUE);
|
||||
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", TRUE);
|
||||
|
||||
// PreferenceScreen: Feed - Community posts
|
||||
public static final BooleanSetting HIDE_COMMUNITY_POSTS_CHANNEL = new BooleanSetting("revanced_hide_community_posts_channel", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_community_posts_home_related_videos", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_community_posts_subscriptions", FALSE);
|
||||
|
||||
// PreferenceScreen: Feed - Flyout menu
|
||||
public static final BooleanSetting HIDE_FEED_FLYOUT_MENU = new BooleanSetting("revanced_hide_feed_flyout_menu", FALSE);
|
||||
public static final StringSetting HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_feed_flyout_menu_filter_strings", "", true, parent(HIDE_FEED_FLYOUT_MENU));
|
||||
|
||||
// PreferenceScreen: Feed - Video filter
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_COMMENTS = new BooleanSetting("revanced_hide_keyword_content_comments", FALSE);
|
||||
public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "",
|
||||
parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SEARCH, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_COMMENTS));
|
||||
|
||||
public static final BooleanSetting HIDE_RECOMMENDED_VIDEO = new BooleanSetting("revanced_hide_recommended_video", FALSE);
|
||||
public static final BooleanSetting HIDE_LOW_VIEWS_VIDEO = new BooleanSetting("revanced_hide_low_views_video", TRUE);
|
||||
|
||||
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_HOME = new BooleanSetting("revanced_hide_video_by_view_counts_home", FALSE);
|
||||
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH = new BooleanSetting("revanced_hide_video_by_view_counts_search", FALSE);
|
||||
public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_video_by_view_counts_subscriptions", FALSE);
|
||||
public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_LESS_THAN = new LongSetting("revanced_hide_video_view_counts_less_than", 1000L,
|
||||
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
|
||||
public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN = new LongSetting("revanced_hide_video_view_counts_greater_than", 1_000_000_000_000L,
|
||||
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
|
||||
public static final StringSetting HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER = new StringSetting("revanced_hide_video_view_counts_multiplier", str("revanced_hide_video_view_counts_multiplier_default_value"), true,
|
||||
parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS));
|
||||
|
||||
// Experimental Flags
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE, true, "revanced_hide_related_videos_user_dialog_message");
|
||||
public static final IntegerSetting RELATED_VIDEOS_OFFSET = new IntegerSetting("revanced_related_videos_offset", 2, true, parent(HIDE_RELATED_VIDEOS));
|
||||
|
||||
|
||||
// PreferenceScreen: General
|
||||
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true);
|
||||
public static final BooleanSetting CHANGE_START_PAGE_TYPE = new BooleanSetting("revanced_change_start_page_type", FALSE, true,
|
||||
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
|
||||
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
|
||||
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
|
||||
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
|
||||
public static final BooleanSetting HIDE_SNACK_BAR = new BooleanSetting("revanced_hide_snack_bar", FALSE);
|
||||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
|
||||
|
||||
public static final EnumSetting<FormFactor> CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
|
||||
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
|
||||
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "18.17.43", true, parent(SPOOF_APP_VERSION));
|
||||
|
||||
// PreferenceScreen: General - Account menu
|
||||
public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE);
|
||||
public static final StringSetting HIDE_ACCOUNT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_account_menu_filter_strings", "", true, parent(HIDE_ACCOUNT_MENU));
|
||||
public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("revanced_hide_handle", TRUE, true);
|
||||
|
||||
// PreferenceScreen: General - Custom filter
|
||||
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
|
||||
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
||||
|
||||
// PreferenceScreen: General - Miniplayer
|
||||
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true);
|
||||
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3));
|
||||
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true);
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
|
||||
// PreferenceScreen: General - Navigation bar
|
||||
public static final BooleanSetting ENABLE_NARROW_NAVIGATION_BUTTONS = new BooleanSetting("revanced_enable_narrow_navigation_buttons", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_CREATE_BUTTON = new BooleanSetting("revanced_hide_navigation_create_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_LIBRARY_BUTTON = new BooleanSetting("revanced_hide_navigation_library_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_notifications_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_SHORTS_BUTTON = new BooleanSetting("revanced_hide_navigation_shorts_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
|
||||
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
|
||||
public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
|
||||
|
||||
// PreferenceScreen: General - Override buttons
|
||||
public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE);
|
||||
public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE);
|
||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl");
|
||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl");
|
||||
public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true
|
||||
, new YouTubeMusicActionsPatch.HookYouTubeMusicAvailability());
|
||||
public static final StringSetting THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME = new StringSetting("revanced_third_party_youtube_music_package_name", PatchStatus.RVXMusicPackageName(), true
|
||||
, new YouTubeMusicActionsPatch.HookYouTubeMusicPackageNameAvailability());
|
||||
|
||||
// PreferenceScreen: General - Settings menu
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_PARENT_TOOLS = new BooleanSetting("revanced_hide_settings_menu_parent_tools", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_GENERAL = new BooleanSetting("revanced_hide_settings_menu_general", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_ACCOUNT = new BooleanSetting("revanced_hide_settings_menu_account", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_DATA_SAVING = new BooleanSetting("revanced_hide_settings_menu_data_saving", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_AUTOPLAY = new BooleanSetting("revanced_hide_settings_menu_auto_play", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES = new BooleanSetting("revanced_hide_settings_menu_video_quality", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_OFFLINE = new BooleanSetting("revanced_hide_settings_menu_offline", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_WATCH_ON_TV = new BooleanSetting("revanced_hide_settings_menu_pair_with_tv", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY = new BooleanSetting("revanced_hide_settings_menu_history", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE = new BooleanSetting("revanced_hide_settings_menu_your_data", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_PRIVACY = new BooleanSetting("revanced_hide_settings_menu_privacy", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES = new BooleanSetting("revanced_hide_settings_menu_premium_early_access", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS = new BooleanSetting("revanced_hide_settings_menu_subscription_product", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS = new BooleanSetting("revanced_hide_settings_menu_billing_and_payment", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_NOTIFICATIONS = new BooleanSetting("revanced_hide_settings_menu_notification", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_CONNECTED_APPS = new BooleanSetting("revanced_hide_settings_menu_connected_accounts", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_LIVE_CHAT = new BooleanSetting("revanced_hide_settings_menu_live_chat", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_CAPTIONS = new BooleanSetting("revanced_hide_settings_menu_captions", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_ACCESSIBILITY = new BooleanSetting("revanced_hide_settings_menu_accessibility", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_ABOUT = new BooleanSetting("revanced_hide_settings_menu_about", FALSE, true);
|
||||
// dummy data
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_YOUTUBE_TV = new BooleanSetting("revanced_hide_settings_menu_youtube_tv", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_PRE_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_pre_purchase", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_POST_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_post_purchase", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SETTINGS_MENU_THIRD_PARTY = new BooleanSetting("revanced_hide_settings_menu_third_party", FALSE, true);
|
||||
|
||||
// PreferenceScreen: General - Toolbar
|
||||
public static final BooleanSetting CHANGE_YOUTUBE_HEADER = new BooleanSetting("revanced_change_youtube_header", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR = new BooleanSetting("revanced_enable_wide_search_bar", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_WITH_HEADER = new BooleanSetting("revanced_enable_wide_search_bar_with_header", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB = new BooleanSetting("revanced_enable_wide_search_bar_in_you_tab", FALSE, true);
|
||||
public static final BooleanSetting HIDE_TOOLBAR_CAST_BUTTON = new BooleanSetting("revanced_hide_toolbar_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_hide_toolbar_create_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_TOOLBAR_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_toolbar_notification_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEARCH_TERM_THUMBNAIL = new BooleanSetting("revanced_hide_search_term_thumbnail", FALSE);
|
||||
public static final BooleanSetting HIDE_IMAGE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_image_search_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_YOUTUBE_DOODLES = new BooleanSetting("revanced_hide_youtube_doodles", FALSE, true, "revanced_hide_youtube_doodles_user_dialog_message");
|
||||
public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_replace_toolbar_create_button", FALSE, true);
|
||||
public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON_TYPE = new BooleanSetting("revanced_replace_toolbar_create_button_type", FALSE, true);
|
||||
|
||||
|
||||
// PreferenceScreen: Player
|
||||
public static final IntegerSetting CUSTOM_PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_custom_player_overlay_opacity", 100, true);
|
||||
public static final BooleanSetting DISABLE_AUTO_PLAYER_POPUP_PANELS = new BooleanSetting("revanced_disable_auto_player_popup_panels", TRUE, true);
|
||||
public static final BooleanSetting DISABLE_AUTO_SWITCH_MIX_PLAYLISTS = new BooleanSetting("revanced_disable_auto_switch_mix_playlists", FALSE, true, "revanced_disable_auto_switch_mix_playlists_user_dialog_message");
|
||||
public static final BooleanSetting DISABLE_SPEED_OVERLAY = new BooleanSetting("revanced_disable_speed_overlay", FALSE, true);
|
||||
public static final FloatSetting SPEED_OVERLAY_VALUE = new FloatSetting("revanced_speed_overlay_value", 2.0f, true);
|
||||
public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
|
||||
public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", TRUE, true);
|
||||
public static final BooleanSetting HIDE_DOUBLE_TAP_OVERLAY_FILTER = new BooleanSetting("revanced_hide_double_tap_overlay_filter", FALSE, true);
|
||||
public static final BooleanSetting HIDE_END_SCREEN_CARDS = new BooleanSetting("revanced_hide_end_screen_cards", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FILMSTRIP_OVERLAY = new BooleanSetting("revanced_hide_filmstrip_overlay", FALSE, true);
|
||||
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE, true);
|
||||
public static final BooleanSetting HIDE_INFO_PANEL = new BooleanSetting("revanced_hide_info_panel", TRUE);
|
||||
public static final BooleanSetting HIDE_LIVE_CHAT_MESSAGES = new BooleanSetting("revanced_hide_live_chat_messages", FALSE);
|
||||
public static final BooleanSetting HIDE_MEDICAL_PANEL = new BooleanSetting("revanced_hide_medical_panel", TRUE);
|
||||
public static final BooleanSetting HIDE_SEEK_MESSAGE = new BooleanSetting("revanced_hide_seek_message", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEEK_UNDO_MESSAGE = new BooleanSetting("revanced_hide_seek_undo_message", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SUGGESTED_ACTION = new BooleanSetting("revanced_hide_suggested_actions", TRUE, true);
|
||||
public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE);
|
||||
public static final BooleanSetting HIDE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_hide_suggested_video_end_screen", TRUE, true);
|
||||
public static final BooleanSetting SKIP_AUTOPLAY_COUNTDOWN = new BooleanSetting("revanced_skip_autoplay_countdown", FALSE, true, parent(HIDE_SUGGESTED_VIDEO_END_SCREEN));
|
||||
public static final BooleanSetting HIDE_ZOOM_OVERLAY = new BooleanSetting("revanced_hide_zoom_overlay", FALSE, true);
|
||||
public static final BooleanSetting SANITIZE_VIDEO_SUBTITLE = new BooleanSetting("revanced_sanitize_video_subtitle", FALSE);
|
||||
|
||||
|
||||
// PreferenceScreen: Player - Action buttons
|
||||
public static final BooleanSetting DISABLE_LIKE_DISLIKE_GLOW = new BooleanSetting("revanced_disable_like_dislike_glow", FALSE);
|
||||
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE);
|
||||
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
|
||||
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE);
|
||||
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", FALSE);
|
||||
public static final BooleanSetting HIDE_REWARDS_BUTTON = new BooleanSetting("revanced_hide_rewards_button", FALSE);
|
||||
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE);
|
||||
public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE);
|
||||
|
||||
// PreferenceScreen: Player - Ambient mode
|
||||
public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE);
|
||||
public static final BooleanSetting DISABLE_AMBIENT_MODE = new BooleanSetting("revanced_disable_ambient_mode", FALSE, true);
|
||||
public static final BooleanSetting DISABLE_AMBIENT_MODE_IN_FULLSCREEN = new BooleanSetting("revanced_disable_ambient_mode_in_fullscreen", FALSE, true);
|
||||
|
||||
// PreferenceScreen: Player - Channel bar
|
||||
public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", TRUE);
|
||||
public static final BooleanSetting HIDE_START_TRIAL_BUTTON = new BooleanSetting("revanced_hide_start_trial_button", TRUE);
|
||||
|
||||
// PreferenceScreen: Player - Comments
|
||||
public static final BooleanSetting HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS = new BooleanSetting("revanced_hide_comments_by_members", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS = new BooleanSetting("revanced_hide_comment_highlighted_search_links", FALSE, true);
|
||||
public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE);
|
||||
public static final BooleanSetting HIDE_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_preview_comment", FALSE);
|
||||
public static final BooleanSetting HIDE_PREVIEW_COMMENT_TYPE = new BooleanSetting("revanced_hide_preview_comment_type", FALSE);
|
||||
public static final BooleanSetting HIDE_PREVIEW_COMMENT_OLD_METHOD = new BooleanSetting("revanced_hide_preview_comment_old_method", FALSE);
|
||||
public static final BooleanSetting HIDE_PREVIEW_COMMENT_NEW_METHOD = new BooleanSetting("revanced_hide_preview_comment_new_method", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENT_CREATE_SHORTS_BUTTON = new BooleanSetting("revanced_hide_comment_create_shorts_button", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENT_THANKS_BUTTON = new BooleanSetting("revanced_hide_comment_thanks_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comment_timestamp_and_emoji_buttons", FALSE);
|
||||
|
||||
// PreferenceScreen: Player - Flyout menu
|
||||
public static final BooleanSetting CHANGE_PLAYER_FLYOUT_MENU_TOGGLE = new BooleanSetting("revanced_change_player_flyout_menu_toggle", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE = new BooleanSetting("revanced_hide_player_flyout_menu_enhanced_bitrate", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK = new BooleanSetting("revanced_hide_player_flyout_menu_audio_track", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS = new BooleanSetting("revanced_hide_player_flyout_menu_captions", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_captions_footer", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN = new BooleanSetting("revanced_hide_player_flyout_menu_lock_screen", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_MORE = new BooleanSetting("revanced_hide_player_flyout_menu_more_info", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED = new BooleanSetting("revanced_hide_player_flyout_menu_playback_speed", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_header", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_footer", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_player_flyout_menu_report", TRUE);
|
||||
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_menu_additional_settings", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AMBIENT = new BooleanSetting("revanced_hide_player_flyout_menu_ambient_mode", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_player_flyout_menu_help", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOOP = new BooleanSetting("revanced_hide_player_flyout_menu_loop_video", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PIP = new BooleanSetting("revanced_hide_player_flyout_menu_pip", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS = new BooleanSetting("revanced_hide_player_flyout_menu_premium_controls", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_menu_sleep_timer", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_menu_stable_volume", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_player_flyout_menu_stats_for_nerds", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_menu_watch_in_vr", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE);
|
||||
|
||||
// PreferenceScreen: Player - Fullscreen
|
||||
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true);
|
||||
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
|
||||
public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, true);
|
||||
public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
|
||||
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE, true);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_COMMENT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_comment_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_LIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_like_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_live_chat_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_MORE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_more_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_mix_playlist_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_playlist_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_save_to_playlist_button", FALSE);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS_SHARE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_share_button", FALSE);
|
||||
public static final IntegerSetting QUICK_ACTIONS_TOP_MARGIN = new IntegerSetting("revanced_quick_actions_top_margin", 0, true);
|
||||
|
||||
public static final BooleanSetting DISABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_disable_landscape_mode", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_COMPACT_CONTROLS_OVERLAY = new BooleanSetting("revanced_enable_compact_controls_overlay", FALSE, true);
|
||||
public static final BooleanSetting FORCE_FULLSCREEN = new BooleanSetting("revanced_force_fullscreen", FALSE, true);
|
||||
public static final BooleanSetting KEEP_LANDSCAPE_MODE = new BooleanSetting("revanced_keep_landscape_mode", FALSE, true);
|
||||
public static final LongSetting KEEP_LANDSCAPE_MODE_TIMEOUT = new LongSetting("revanced_keep_landscape_mode_timeout", 3000L, true);
|
||||
|
||||
// PreferenceScreen: Player - Haptic feedback
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SCRUBBING = new BooleanSetting("revanced_disable_haptic_feedback_scrubbing", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK = new BooleanSetting("revanced_disable_haptic_feedback_seek", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE);
|
||||
|
||||
// PreferenceScreen: Player - Player buttons
|
||||
public static final BooleanSetting HIDE_PLAYER_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_player_autoplay_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_player_captions_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_CAST_BUTTON = new BooleanSetting("revanced_hide_player_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_COLLAPSE_BUTTON = new BooleanSetting("revanced_hide_player_collapse_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_FULLSCREEN_BUTTON = new BooleanSetting("revanced_hide_player_fullscreen_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTON = new BooleanSetting("revanced_hide_player_previous_next_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_player_youtube_music_button", FALSE);
|
||||
|
||||
public static final BooleanSetting ALWAYS_REPEAT = new BooleanSetting("revanced_always_repeat", FALSE);
|
||||
public static final BooleanSetting ALWAYS_REPEAT_PAUSE = new BooleanSetting("revanced_always_repeat_pause", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_ALWAYS_REPEAT = new BooleanSetting("revanced_overlay_button_always_repeat", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL = new BooleanSetting("revanced_overlay_button_copy_video_url", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", FALSE);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE);
|
||||
public static final EnumSetting<PlaylistIdPrefix> OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_DESCENDING);
|
||||
public static final BooleanSetting OVERLAY_BUTTON_WHITELIST = new BooleanSetting("revanced_overlay_button_whitelist", FALSE);
|
||||
|
||||
// PreferenceScreen: Player - Seekbar
|
||||
public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION = new BooleanSetting("revanced_append_time_stamp_information", TRUE, true);
|
||||
public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION_TYPE = new BooleanSetting("revanced_append_time_stamp_information_type", TRUE, parent(APPEND_TIME_STAMP_INFORMATION));
|
||||
public static final BooleanSetting REPLACE_TIME_STAMP_ACTION = new BooleanSetting("revanced_replace_time_stamp_action", TRUE, true, parent(APPEND_TIME_STAMP_INFORMATION));
|
||||
public static final BooleanSetting ENABLE_CUSTOM_SEEKBAR_COLOR = new BooleanSetting("revanced_enable_custom_seekbar_color", FALSE, true);
|
||||
public static final StringSetting ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE = new StringSetting("revanced_custom_seekbar_color_value", "#FF0000", true, parent(ENABLE_CUSTOM_SEEKBAR_COLOR));
|
||||
public static final BooleanSetting ENABLE_SEEKBAR_TAPPING = new BooleanSetting("revanced_enable_seekbar_tapping", TRUE);
|
||||
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE);
|
||||
public static final BooleanSetting DISABLE_SEEKBAR_CHAPTERS = new BooleanSetting("revanced_disable_seekbar_chapters", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEEKBAR_CHAPTER_LABEL = new BooleanSetting("revanced_hide_seekbar_chapter_label", FALSE, true);
|
||||
public static final BooleanSetting HIDE_TIME_STAMP = new BooleanSetting("revanced_hide_time_stamp", FALSE, true);
|
||||
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails",
|
||||
PatchStatus.OldSeekbarThumbnailsDefaultBoolean(), true);
|
||||
public static final BooleanSetting ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY = new BooleanSetting("revanced_enable_seekbar_thumbnails_high_quality", FALSE, true, "revanced_enable_seekbar_thumbnails_high_quality_dialog_message");
|
||||
public static final BooleanSetting ENABLE_CAIRO_SEEKBAR = new BooleanSetting("revanced_enable_cairo_seekbar", FALSE, true);
|
||||
|
||||
// PreferenceScreen: Player - Video description
|
||||
public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE);
|
||||
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
||||
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
|
||||
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", FALSE);
|
||||
public static final BooleanSetting HIDE_CONTENTS_SECTION = new BooleanSetting("revanced_hide_contents_section", FALSE);
|
||||
public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", FALSE);
|
||||
public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE);
|
||||
public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", FALSE);
|
||||
public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE);
|
||||
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", FALSE);
|
||||
public static final BooleanSetting DISABLE_VIDEO_DESCRIPTION_INTERACTION = new BooleanSetting("revanced_disable_video_description_interaction", FALSE, true);
|
||||
public static final BooleanSetting EXPAND_VIDEO_DESCRIPTION = new BooleanSetting("revanced_expand_video_description", FALSE, true);
|
||||
public static final StringSetting EXPAND_VIDEO_DESCRIPTION_STRINGS = new StringSetting("revanced_expand_video_description_strings", str("revanced_expand_video_description_strings_default_value"), true, parent(EXPAND_VIDEO_DESCRIPTION));
|
||||
|
||||
|
||||
// PreferenceScreen: Shorts
|
||||
public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_FLOATING_BUTTON = new BooleanSetting("revanced_hide_shorts_floating_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF = new BooleanSetting("revanced_hide_shorts_shelf", TRUE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF_CHANNEL = new BooleanSetting("revanced_hide_shorts_shelf_channel", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_shorts_shelf_home_related_videos", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_shelf_subscriptions", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF_SEARCH = new BooleanSetting("revanced_hide_shorts_shelf_search", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHELF_HISTORY = new BooleanSetting("revanced_hide_shorts_shelf_history", FALSE);
|
||||
public static final IntegerSetting CHANGE_SHORTS_REPEAT_STATE = new IntegerSetting("revanced_change_shorts_repeat_state", 0);
|
||||
|
||||
// PreferenceScreen: Shorts - Shorts player components
|
||||
public static final BooleanSetting HIDE_SHORTS_JOIN_BUTTON = new BooleanSetting("revanced_hide_shorts_join_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_shorts_subscribe_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PAUSED_HEADER = new BooleanSetting("revanced_hide_shorts_paused_header", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_TRENDS_BUTTON = new BooleanSetting("revanced_hide_shorts_trends_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHOPPING_BUTTON = new BooleanSetting("revanced_hide_shorts_shopping_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_STICKERS = new BooleanSetting("revanced_hide_shorts_stickers", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_shorts_paid_promotion_label", TRUE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LIVE_HEADER = new BooleanSetting("revanced_hide_shorts_live_header", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", TRUE);
|
||||
|
||||
// PreferenceScreen: Shorts - Shorts player components - Suggested actions
|
||||
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SAVE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_shorts_save_music_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LOCATION_BUTTON = new BooleanSetting("revanced_hide_shorts_location_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON = new BooleanSetting("revanced_hide_shorts_search_suggestions_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
|
||||
|
||||
// PreferenceScreen: Shorts - Shorts player components - Action buttons
|
||||
public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_REMIX_BUTTON = new BooleanSetting("revanced_hide_shorts_remix_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", TRUE);
|
||||
|
||||
public static final BooleanSetting DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION = new BooleanSetting("revanced_disable_shorts_like_button_fountain_animation", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND = new BooleanSetting("revanced_hide_shorts_play_pause_button_background", FALSE, true);
|
||||
public static final EnumSetting<AnimationType> ANIMATION_TYPE = new EnumSetting<>("revanced_shorts_double_tap_to_like_animation", AnimationType.ORIGINAL, true);
|
||||
|
||||
|
||||
// Experimental Flags
|
||||
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
|
||||
public static final BooleanSetting TIME_STAMP_CHANGE_REPEAT_STATE = new BooleanSetting("revanced_shorts_time_stamp_change_repeat_state", TRUE, true, parent(ENABLE_TIME_STAMP));
|
||||
public static final IntegerSetting META_PANEL_BOTTOM_MARGIN = new IntegerSetting("revanced_shorts_meta_panel_bottom_margin", 32, true, parent(ENABLE_TIME_STAMP));
|
||||
public static final BooleanSetting HIDE_SHORTS_TOOLBAR = new BooleanSetting("revanced_hide_shorts_toolbar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true);
|
||||
public static final IntegerSetting SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE = new IntegerSetting("revanced_shorts_navigation_bar_height_percentage", 45, true, parent(HIDE_SHORTS_NAVIGATION_BAR));
|
||||
public static final BooleanSetting REPLACE_CHANNEL_HANDLE = new BooleanSetting("revanced_replace_channel_handle", FALSE, true);
|
||||
|
||||
// PreferenceScreen: Swipe controls
|
||||
public static final BooleanSetting ENABLE_SWIPE_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_brightness", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_SWIPE_VOLUME = new BooleanSetting("revanced_enable_swipe_volume", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_SWIPE_LOWEST_VALUE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_lowest_value_auto_brightness", TRUE, parent(ENABLE_SWIPE_BRIGHTNESS));
|
||||
public static final BooleanSetting ENABLE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_enable_save_and_restore_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS));
|
||||
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||
|
||||
public static final IntegerSetting SWIPE_BRIGHTNESS_SENSITIVITY = new IntegerSetting("revanced_swipe_brightness_sensitivity", 100, true, parent(ENABLE_SWIPE_BRIGHTNESS));
|
||||
public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 100, true, parent(ENABLE_SWIPE_VOLUME));
|
||||
/**
|
||||
* @noinspection DeprecatedIsStillUsed
|
||||
*/
|
||||
@Deprecated // Patch is obsolete and no longer works with 19.09+
|
||||
public static final BooleanSetting DISABLE_HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_disable_hdr_auto_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS));
|
||||
public static final BooleanSetting ENABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_enable_swipe_to_switch_video", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_WATCH_PANEL_GESTURES = new BooleanSetting("revanced_enable_watch_panel_gestures", FALSE, true);
|
||||
public static final BooleanSetting SWIPE_BRIGHTNESS_AUTO = new BooleanSetting("revanced_swipe_brightness_auto", TRUE, false, false);
|
||||
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
|
||||
|
||||
|
||||
// PreferenceScreen: Video
|
||||
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
|
||||
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
|
||||
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
|
||||
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
|
||||
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE = new BooleanSetting("revanced_disable_default_playback_speed_live", TRUE);
|
||||
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
|
||||
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
||||
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
|
||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
|
||||
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
|
||||
// Experimental Flags
|
||||
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
|
||||
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
|
||||
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
|
||||
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
|
||||
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
|
||||
public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
|
||||
|
||||
|
||||
// PreferenceScreen: Miscellaneous
|
||||
public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE);
|
||||
public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true);
|
||||
|
||||
// Experimental Flags
|
||||
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
|
||||
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
|
||||
|
||||
/**
|
||||
* @noinspection DeprecatedIsStillUsed
|
||||
*/
|
||||
@Deprecated
|
||||
public static final LongSetting DOUBLE_BACK_TO_CLOSE_TIMEOUT = new LongSetting("revanced_double_back_to_close_timeout", 2000L);
|
||||
|
||||
// PreferenceScreen: Miscellaneous - Watch history
|
||||
public static final EnumSetting<WatchHistoryType> WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
|
||||
|
||||
// PreferenceScreen: Miscellaneous - Spoof streaming data
|
||||
// The order of the settings should not be changed otherwise the app may crash
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true,
|
||||
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.iOSAvailability());
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true, new SpoofStreamingDataPatch.iOSAvailability());
|
||||
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA));
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA));
|
||||
|
||||
// PreferenceScreen: Return YouTube Dislike
|
||||
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
|
||||
public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "");
|
||||
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED));
|
||||
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED));
|
||||
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED));
|
||||
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", FALSE, true, parent(RYD_ENABLED));
|
||||
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", FALSE, parent(RYD_ENABLED));
|
||||
|
||||
|
||||
// PreferenceScreen: SponsorBlock
|
||||
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
||||
/**
|
||||
* Do not use directly, instead use {@link SponsorBlockSettings}
|
||||
*/
|
||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
|
||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
||||
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED));
|
||||
public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app", parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
|
||||
public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0);
|
||||
public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L);
|
||||
|
||||
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
|
||||
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
|
||||
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
|
||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
|
||||
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
|
||||
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
|
||||
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
|
||||
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
|
||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
|
||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
|
||||
|
||||
// SB Setting not exported
|
||||
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
|
||||
public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false);
|
||||
public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false);
|
||||
|
||||
static {
|
||||
// region Migration initialized
|
||||
// Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
|
||||
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
|
||||
SB_CATEGORY_SPONSOR,
|
||||
SB_CATEGORY_SPONSOR_COLOR,
|
||||
SB_CATEGORY_SELF_PROMO,
|
||||
SB_CATEGORY_SELF_PROMO_COLOR,
|
||||
SB_CATEGORY_INTERACTION,
|
||||
SB_CATEGORY_INTERACTION_COLOR,
|
||||
SB_CATEGORY_HIGHLIGHT,
|
||||
SB_CATEGORY_HIGHLIGHT_COLOR,
|
||||
SB_CATEGORY_INTRO,
|
||||
SB_CATEGORY_INTRO_COLOR,
|
||||
SB_CATEGORY_OUTRO,
|
||||
SB_CATEGORY_OUTRO_COLOR,
|
||||
SB_CATEGORY_PREVIEW,
|
||||
SB_CATEGORY_PREVIEW_COLOR,
|
||||
SB_CATEGORY_FILLER,
|
||||
SB_CATEGORY_FILLER_COLOR,
|
||||
SB_CATEGORY_MUSIC_OFFTOPIC,
|
||||
SB_CATEGORY_MUSIC_OFFTOPIC_COLOR,
|
||||
SB_CATEGORY_UNSUBMITTED,
|
||||
SB_CATEGORY_UNSUBMITTED_COLOR));
|
||||
|
||||
SharedPrefCategory ytPrefs = new SharedPrefCategory("youtube");
|
||||
SharedPrefCategory rydPrefs = new SharedPrefCategory("ryd");
|
||||
SharedPrefCategory sbPrefs = new SharedPrefCategory("sponsor-block");
|
||||
for (Setting<?> setting : Setting.allLoadedSettings()) {
|
||||
String key = setting.key;
|
||||
if (setting.key.startsWith("sb_")) {
|
||||
if (sbCategories.contains(setting)) {
|
||||
key = key.substring(3); // Remove the "sb_" prefix, as old categories are saved without it.
|
||||
}
|
||||
migrateFromOldPreferences(sbPrefs, setting, key);
|
||||
} else if (setting.key.startsWith("ryd_")) {
|
||||
migrateFromOldPreferences(rydPrefs, setting, key);
|
||||
} else {
|
||||
migrateFromOldPreferences(ytPrefs, setting, key);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.shared.settings.preference.YouTubeDataAPIDialogBuilder;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class AboutYouTubeDataAPIPreference extends Preference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public AboutYouTubeDataAPIPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (getContext() instanceof Activity mActivity) {
|
||||
YouTubeDataAPIDialogBuilder.showDialog(mActivity);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://dearrow.ajay.app"));
|
||||
pref.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ExternalDownloaderPlaylistPreference extends Preference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST;
|
||||
private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_label");
|
||||
private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_playlist_package_name");
|
||||
private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_website");
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static EditText mEditText;
|
||||
private static String packageName;
|
||||
private static int mClickedDialogEntryIndex;
|
||||
|
||||
private final TextWatcher textWatcher = new TextWatcher() {
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable s) {
|
||||
packageName = s.toString();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
}
|
||||
};
|
||||
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderPlaylistPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
packageName = settings.get();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
|
||||
final Context context = getContext();
|
||||
AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context);
|
||||
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(15, 0, 15, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
|
||||
mEditText = new EditText(context);
|
||||
mEditText.setHint(settings.defaultValue);
|
||||
mEditText.setText(packageName);
|
||||
mEditText.addTextChangedListener(textWatcher);
|
||||
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9);
|
||||
mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(mEditText);
|
||||
|
||||
table.addView(row);
|
||||
builder.setView(table);
|
||||
|
||||
builder.setTitle(str("revanced_external_downloader_dialog_title"));
|
||||
builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> {
|
||||
mClickedDialogEntryIndex = which;
|
||||
mEditText.setText(mEntryValues[which]);
|
||||
});
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
final String packageName = mEditText.getText().toString().trim();
|
||||
settings.save(packageName);
|
||||
checkPackageIsValid(context, packageName);
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault());
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
builder.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkPackageIsValid(Context context, String packageName) {
|
||||
String appName = "";
|
||||
String website = "";
|
||||
|
||||
if (mClickedDialogEntryIndex >= 0) {
|
||||
appName = mEntries[mClickedDialogEntryIndex];
|
||||
website = mWebsiteEntries[mClickedDialogEntryIndex];
|
||||
}
|
||||
|
||||
return showToastOrOpenWebsites(context, appName, packageName, website);
|
||||
}
|
||||
|
||||
private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) {
|
||||
if (ExtendedUtils.isPackageEnabled(packageName))
|
||||
return true;
|
||||
|
||||
if (website.isEmpty()) {
|
||||
Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName));
|
||||
return false;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_external_downloader_not_installed_dialog_title"))
|
||||
.setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
|
||||
context.startActivity(i);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean checkPackageIsDisabled() {
|
||||
final Context context = Utils.getActivity();
|
||||
packageName = settings.get();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
return !checkPackageIsValid(context, packageName);
|
||||
}
|
||||
|
||||
public static String getExternalDownloaderPackageName() {
|
||||
String downloaderPackageName = settings.get().trim();
|
||||
|
||||
if (downloaderPackageName.isEmpty()) {
|
||||
settings.resetToDefault();
|
||||
downloaderPackageName = settings.defaultValue;
|
||||
}
|
||||
|
||||
return downloaderPackageName;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ExternalDownloaderVideoPreference extends Preference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO;
|
||||
private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_label");
|
||||
private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_video_package_name");
|
||||
private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_website");
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static EditText mEditText;
|
||||
private static String packageName;
|
||||
private static int mClickedDialogEntryIndex;
|
||||
|
||||
private final TextWatcher textWatcher = new TextWatcher() {
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable s) {
|
||||
packageName = s.toString();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
}
|
||||
};
|
||||
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ExternalDownloaderVideoPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
packageName = settings.get();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
|
||||
final Context context = getContext();
|
||||
AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context);
|
||||
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(15, 0, 15, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
|
||||
mEditText = new EditText(context);
|
||||
mEditText.setHint(settings.defaultValue);
|
||||
mEditText.setText(packageName);
|
||||
mEditText.addTextChangedListener(textWatcher);
|
||||
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9);
|
||||
mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(mEditText);
|
||||
|
||||
table.addView(row);
|
||||
builder.setView(table);
|
||||
|
||||
builder.setTitle(str("revanced_external_downloader_dialog_title"));
|
||||
builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> {
|
||||
mClickedDialogEntryIndex = which;
|
||||
mEditText.setText(mEntryValues[which]);
|
||||
});
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
final String packageName = mEditText.getText().toString().trim();
|
||||
settings.save(packageName);
|
||||
checkPackageIsValid(context, packageName);
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault());
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
builder.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean checkPackageIsValid(Context context, String packageName) {
|
||||
String appName = "";
|
||||
String website = "";
|
||||
|
||||
if (mClickedDialogEntryIndex >= 0) {
|
||||
appName = mEntries[mClickedDialogEntryIndex];
|
||||
website = mWebsiteEntries[mClickedDialogEntryIndex];
|
||||
}
|
||||
|
||||
return showToastOrOpenWebsites(context, appName, packageName, website);
|
||||
}
|
||||
|
||||
private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) {
|
||||
if (ExtendedUtils.isPackageEnabled(packageName))
|
||||
return true;
|
||||
|
||||
if (website.isEmpty()) {
|
||||
Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName));
|
||||
return false;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_external_downloader_not_installed_dialog_title"))
|
||||
.setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
|
||||
context.startActivity(i);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean checkPackageIsDisabled() {
|
||||
final Context context = Utils.getActivity();
|
||||
packageName = settings.get();
|
||||
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
|
||||
return !checkPackageIsValid(context, packageName);
|
||||
}
|
||||
|
||||
public static String getExternalDownloaderPackageName() {
|
||||
String downloaderPackageName = settings.get().trim();
|
||||
|
||||
if (downloaderPackageName.isEmpty()) {
|
||||
settings.resetToDefault();
|
||||
downloaderPackageName = settings.defaultValue;
|
||||
}
|
||||
|
||||
return downloaderPackageName;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private String existingSettings;
|
||||
|
||||
@TargetApi(26)
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
|
||||
EditText editText = getEditText();
|
||||
editText.setTextIsSelectable(true);
|
||||
editText.setAutofillHints((String) null);
|
||||
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap.
|
||||
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public ImportExportPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ImportExportPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(null);
|
||||
getEditText().setText(existingSettings);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder, true);
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_extended_settings_import_copy"), (dialog, which) ->
|
||||
Utils.setClipboard(getEditText().getText().toString(), str("revanced_share_copy_settings_success")))
|
||||
.setPositiveButton(str("revanced_extended_settings_import"), (dialog, which) ->
|
||||
importSettings(getEditText().getText().toString()));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importSettings(String replacementSettings) {
|
||||
try {
|
||||
if (replacementSettings.equals(existingSettings)) {
|
||||
return;
|
||||
}
|
||||
ReVancedPreferenceFragment.settingImportInProgress = true;
|
||||
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings, true);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
} finally {
|
||||
ReVancedPreferenceFragment.settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class OpenDefaultAppSettingsPreference extends Preference {
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
try {
|
||||
Context context = Utils.getActivity();
|
||||
final Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
final Intent intent = isSDKAbove(31)
|
||||
? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
||||
: new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
|
||||
context.startActivity(intent);
|
||||
} catch (Exception exception) {
|
||||
Logger.printException(() -> "OpenDefaultAppSettings Failed");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public OpenDefaultAppSettingsPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,689 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
|
||||
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.getChildView;
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
import static app.revanced.extension.shared.utils.Utils.showToastShort;
|
||||
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
|
||||
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
|
||||
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
private static final int READ_REQUEST_CODE = 42;
|
||||
private static final int WRITE_REQUEST_CODE = 43;
|
||||
static boolean settingImportInProgress = false;
|
||||
static boolean showingUserDialogMessage;
|
||||
|
||||
@SuppressLint("SuspiciousIndentation")
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
if (str == null) return;
|
||||
Setting<?> setting = Setting.getSettingFromPath(str);
|
||||
|
||||
if (setting == null) return;
|
||||
|
||||
Preference mPreference = findPreference(str);
|
||||
|
||||
if (mPreference == null) return;
|
||||
|
||||
if (mPreference instanceof SwitchPreference switchPreference) {
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
if (settingImportInProgress) {
|
||||
switchPreference.setChecked(boolSetting.get());
|
||||
} else {
|
||||
BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked());
|
||||
}
|
||||
|
||||
if (ExtendedUtils.anyMatchSetting(setting)) {
|
||||
ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings();
|
||||
} else if (setting.equals(HIDE_PREVIEW_COMMENT) || setting.equals(HIDE_PREVIEW_COMMENT_TYPE)) {
|
||||
ExtendedUtils.setCommentPreviewSettings();
|
||||
}
|
||||
} else if (mPreference instanceof EditTextPreference editTextPreference) {
|
||||
if (settingImportInProgress) {
|
||||
editTextPreference.setText(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, editTextPreference.getText());
|
||||
}
|
||||
} else if (mPreference instanceof ListPreference listPreference) {
|
||||
if (settingImportInProgress) {
|
||||
listPreference.setValue(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, listPreference.getValue());
|
||||
}
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
||||
}
|
||||
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
||||
updateListPreferenceSummary(listPreference, setting);
|
||||
}
|
||||
} else {
|
||||
Logger.printException(() -> "Setting cannot be handled: " + mPreference.getClass() + " " + mPreference);
|
||||
return;
|
||||
}
|
||||
|
||||
ReVancedSettingsPreference.initializeReVancedSettings();
|
||||
|
||||
if (settingImportInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showingUserDialogMessage) {
|
||||
final Context context = getActivity();
|
||||
|
||||
if (setting.userDialogMessage != null
|
||||
&& mPreference instanceof SwitchPreference switchPreference
|
||||
&& setting.defaultValue instanceof Boolean defaultValue
|
||||
&& switchPreference.isChecked() != defaultValue) {
|
||||
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
|
||||
} else if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||
}
|
||||
};
|
||||
|
||||
private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
showingUserDialogMessage = true;
|
||||
assert setting.userDialogMessage != null;
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
|
||||
.setMessage(setting.userDialogMessage.toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
|
||||
})
|
||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
static PreferenceManager mPreferenceManager;
|
||||
private SharedPreferences mSharedPreferences;
|
||||
|
||||
private PreferenceScreen originalPreferenceScreen;
|
||||
|
||||
public ReVancedPreferenceFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
private void putPreferenceScreenMap(SortedMap<String, PreferenceScreen> preferenceScreenMap, PreferenceGroup preferenceGroup) {
|
||||
if (preferenceGroup instanceof PreferenceScreen mPreferenceScreen) {
|
||||
preferenceScreenMap.put(mPreferenceScreen.getKey(), mPreferenceScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPreferenceScreenToolbar() {
|
||||
SortedMap<String, PreferenceScreen> preferenceScreenMap = new TreeMap<>();
|
||||
|
||||
PreferenceScreen rootPreferenceScreen = getPreferenceScreen();
|
||||
for (Preference preference : getAllPreferencesBy(rootPreferenceScreen)) {
|
||||
if (!(preference instanceof PreferenceGroup preferenceGroup)) continue;
|
||||
putPreferenceScreenMap(preferenceScreenMap, preferenceGroup);
|
||||
for (Preference childPreference : getAllPreferencesBy(preferenceGroup)) {
|
||||
if (!(childPreference instanceof PreferenceGroup nestedPreferenceGroup)) continue;
|
||||
putPreferenceScreenMap(preferenceScreenMap, nestedPreferenceGroup);
|
||||
for (Preference nestedPreference : getAllPreferencesBy(nestedPreferenceGroup)) {
|
||||
if (!(nestedPreference instanceof PreferenceGroup childPreferenceGroup))
|
||||
continue;
|
||||
putPreferenceScreenMap(preferenceScreenMap, childPreferenceGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
|
||||
mPreferenceScreen.setOnPreferenceClickListener(
|
||||
preferenceScreen -> {
|
||||
Dialog preferenceScreenDialog = mPreferenceScreen.getDialog();
|
||||
ViewGroup rootView = (ViewGroup) preferenceScreenDialog
|
||||
.findViewById(android.R.id.content)
|
||||
.getParent();
|
||||
|
||||
Toolbar toolbar = new Toolbar(preferenceScreen.getContext());
|
||||
|
||||
toolbar.setTitle(preferenceScreen.getTitle());
|
||||
toolbar.setNavigationIcon(ThemeUtils.getBackButtonDrawable());
|
||||
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
||||
|
||||
int margin = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
toolbar.setTitleMargin(margin, 0, margin, 0);
|
||||
|
||||
TextView toolbarTextView = getChildView(toolbar, TextView.class::isInstance);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(ThemeUtils.getForegroundColor());
|
||||
}
|
||||
|
||||
rootView.addView(toolbar, 0);
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Map to store dependencies: key is the preference key, value is a list of dependent preferences
|
||||
private final Map<String, List<Preference>> dependencyMap = new HashMap<>();
|
||||
// Set to track already added preferences to avoid duplicates
|
||||
private final Set<String> addedPreferences = new HashSet<>();
|
||||
// Map to store preferences grouped by their parent PreferenceGroup
|
||||
private final Map<PreferenceGroup, List<Preference>> groupedPreferences = new LinkedHashMap<>();
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
try {
|
||||
mPreferenceManager = getPreferenceManager();
|
||||
mPreferenceManager.setSharedPreferencesName(Setting.preferences.name);
|
||||
mSharedPreferences = mPreferenceManager.getSharedPreferences();
|
||||
addPreferencesFromResource(getXmlIdentifier("revanced_prefs"));
|
||||
|
||||
// Initialize toolbars and other UI elements
|
||||
setPreferenceScreenToolbar();
|
||||
|
||||
// Initialize ReVanced settings
|
||||
ReVancedSettingsPreference.initializeReVancedSettings();
|
||||
SponsorBlockSettingsPreference.init(getActivity());
|
||||
|
||||
// Import/export
|
||||
setBackupRestorePreference();
|
||||
|
||||
// Store all preferences and their dependencies
|
||||
storeAllPreferences(getPreferenceScreen());
|
||||
|
||||
// Load and set initial preferences states
|
||||
for (Setting<?> setting : Setting.allLoadedSettings()) {
|
||||
final Preference preference = mPreferenceManager.findPreference(setting.key);
|
||||
if (preference != null && isSDKAbove(26)) {
|
||||
preference.setSingleLineTitle(false);
|
||||
}
|
||||
|
||||
if (preference instanceof SwitchPreference switchPreference) {
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
switchPreference.setChecked(boolSetting.get());
|
||||
} else if (preference instanceof EditTextPreference editTextPreference) {
|
||||
editTextPreference.setText(setting.get().toString());
|
||||
} else if (preference instanceof ListPreference listPreference) {
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
||||
}
|
||||
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
||||
updateListPreferenceSummary(listPreference, setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register preference change listener
|
||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
|
||||
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
|
||||
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
|
||||
} catch (Exception th) {
|
||||
Logger.printException(() -> "Error during onCreate()", th);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyPreferences(PreferenceScreen source, PreferenceScreen destination) {
|
||||
for (Preference preference : getAllPreferencesBy(source)) {
|
||||
destination.addPreference(preference);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup.
|
||||
*
|
||||
* @param preferenceGroup The preference group to scan.
|
||||
*/
|
||||
private void storeAllPreferences(PreferenceGroup preferenceGroup) {
|
||||
// Check if this is the root PreferenceScreen
|
||||
boolean isRootScreen = preferenceGroup == getPreferenceScreen();
|
||||
|
||||
// Use the special top-level group only for the root PreferenceScreen
|
||||
PreferenceGroup groupKey = isRootScreen
|
||||
? new PreferenceCategory(preferenceGroup.getContext())
|
||||
: preferenceGroup;
|
||||
|
||||
if (isRootScreen) {
|
||||
groupKey.setTitle(ResourceUtils.getString("revanced_extended_settings_title"));
|
||||
}
|
||||
|
||||
// Initialize a list to hold preferences of the current group
|
||||
List<Preference> currentGroupPreferences = groupedPreferences.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
||||
|
||||
for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
|
||||
Preference preference = preferenceGroup.getPreference(i);
|
||||
|
||||
// Add preference to the current group if not already added
|
||||
if (!currentGroupPreferences.contains(preference)) {
|
||||
currentGroupPreferences.add(preference);
|
||||
}
|
||||
|
||||
// Store dependencies
|
||||
if (preference.getDependency() != null) {
|
||||
String dependencyKey = preference.getDependency();
|
||||
dependencyMap.computeIfAbsent(dependencyKey, k -> new ArrayList<>()).add(preference);
|
||||
}
|
||||
|
||||
// Recursively handle nested PreferenceGroups
|
||||
if (preference instanceof PreferenceGroup nestedGroup) {
|
||||
storeAllPreferences(nestedGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters preferences based on the search query, displaying grouped results with group titles.
|
||||
*
|
||||
* @param query The search query.
|
||||
*/
|
||||
public void filterPreferences(String query) {
|
||||
// If the query is null or empty, reset preferences to their default state
|
||||
if (query == null || query.isEmpty()) {
|
||||
resetPreferences();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the query to lowercase for case-insensitive search
|
||||
query = query.toLowerCase();
|
||||
|
||||
// Get the preference screen to modify
|
||||
PreferenceScreen preferenceScreen = getPreferenceScreen();
|
||||
// Remove all current preferences from the screen
|
||||
preferenceScreen.removeAll();
|
||||
// Clear the list of added preferences to start fresh
|
||||
addedPreferences.clear();
|
||||
|
||||
// Create a map to store matched preferences for each group
|
||||
Map<PreferenceGroup, List<Preference>> matchedGroupPreferences = new LinkedHashMap<>();
|
||||
|
||||
// Create a set to store all keys that should be included
|
||||
Set<String> keysToInclude = new HashSet<>();
|
||||
|
||||
// First pass: identify all preferences that match the query and their dependencies
|
||||
for (Map.Entry<PreferenceGroup, List<Preference>> entry : groupedPreferences.entrySet()) {
|
||||
List<Preference> preferences = entry.getValue();
|
||||
for (Preference preference : preferences) {
|
||||
if (preferenceMatches(preference, query)) {
|
||||
addPreferenceAndDependencies(preference, keysToInclude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: add all identified preferences to matchedGroupPreferences
|
||||
for (Map.Entry<PreferenceGroup, List<Preference>> entry : groupedPreferences.entrySet()) {
|
||||
PreferenceGroup group = entry.getKey();
|
||||
List<Preference> preferences = entry.getValue();
|
||||
List<Preference> matchedPreferences = new ArrayList<>();
|
||||
|
||||
for (Preference preference : preferences) {
|
||||
if (keysToInclude.contains(preference.getKey())) {
|
||||
matchedPreferences.add(preference);
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedPreferences.isEmpty()) {
|
||||
matchedGroupPreferences.put(group, matchedPreferences);
|
||||
}
|
||||
}
|
||||
|
||||
// Add matched preferences to the screen, maintaining the original order
|
||||
for (Map.Entry<PreferenceGroup, List<Preference>> entry : matchedGroupPreferences.entrySet()) {
|
||||
PreferenceGroup group = entry.getKey();
|
||||
List<Preference> matchedPreferences = entry.getValue();
|
||||
|
||||
// Add the category for this group
|
||||
PreferenceCategory category = new PreferenceCategory(preferenceScreen.getContext());
|
||||
category.setTitle(group.getTitle());
|
||||
preferenceScreen.addPreference(category);
|
||||
|
||||
// Add matched preferences for this group
|
||||
for (Preference preference : matchedPreferences) {
|
||||
if (preference.isSelectable()) {
|
||||
addPreferenceWithDependencies(category, preference);
|
||||
} else {
|
||||
// For non-selectable preferences, just add them directly
|
||||
category.addPreference(preference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a preference matches the given query.
|
||||
*
|
||||
* @param preference The preference to check.
|
||||
* @param query The search query.
|
||||
* @return True if the preference matches the query, false otherwise.
|
||||
*/
|
||||
private boolean preferenceMatches(Preference preference, String query) {
|
||||
// Check if the title contains the query string
|
||||
if (preference.getTitle().toString().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the summary contains the query string
|
||||
if (preference.getSummary() != null && preference.getSummary().toString().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Additional checks for SwitchPreference
|
||||
if (preference instanceof SwitchPreference switchPreference) {
|
||||
CharSequence summaryOn = switchPreference.getSummaryOn();
|
||||
CharSequence summaryOff = switchPreference.getSummaryOff();
|
||||
|
||||
if ((summaryOn != null && summaryOn.toString().toLowerCase().contains(query)) ||
|
||||
(summaryOff != null && summaryOff.toString().toLowerCase().contains(query))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional checks for ListPreference
|
||||
if (preference instanceof ListPreference listPreference) {
|
||||
CharSequence[] entries = listPreference.getEntries();
|
||||
if (entries != null) {
|
||||
for (CharSequence entry : entries) {
|
||||
if (entry.toString().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CharSequence[] entryValues = listPreference.getEntryValues();
|
||||
if (entryValues != null) {
|
||||
for (CharSequence entryValue : entryValues) {
|
||||
if (entryValue.toString().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds a preference and its dependencies to the set of keys to include.
|
||||
*
|
||||
* @param preference The preference to add.
|
||||
* @param keysToInclude The set of keys to include.
|
||||
*/
|
||||
private void addPreferenceAndDependencies(Preference preference, Set<String> keysToInclude) {
|
||||
String key = preference.getKey();
|
||||
if (key != null && !keysToInclude.contains(key)) {
|
||||
keysToInclude.add(key);
|
||||
|
||||
// Add the preference this one depends on
|
||||
String dependencyKey = preference.getDependency();
|
||||
if (dependencyKey != null) {
|
||||
Preference dependency = findPreferenceInAllGroups(dependencyKey);
|
||||
if (dependency != null) {
|
||||
addPreferenceAndDependencies(dependency, keysToInclude);
|
||||
}
|
||||
}
|
||||
|
||||
// Add preferences that depend on this one
|
||||
if (dependencyMap.containsKey(key)) {
|
||||
for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) {
|
||||
addPreferenceAndDependencies(dependentPreference, keysToInclude);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds a preference along with its dependencies
|
||||
* (android:dependency attribute in XML).
|
||||
*
|
||||
* @param preferenceGroup The preference group to add to.
|
||||
* @param preference The preference to add.
|
||||
*/
|
||||
private void addPreferenceWithDependencies(PreferenceGroup preferenceGroup, Preference preference) {
|
||||
String key = preference.getKey();
|
||||
|
||||
// Instead of just using preference keys, we combine the category and key to ensure uniqueness
|
||||
if (key != null && !addedPreferences.contains(preferenceGroup.getTitle() + ":" + key)) {
|
||||
// Add dependencies first
|
||||
if (preference.getDependency() != null) {
|
||||
String dependencyKey = preference.getDependency();
|
||||
Preference dependency = findPreferenceInAllGroups(dependencyKey);
|
||||
if (dependency != null) {
|
||||
addPreferenceWithDependencies(preferenceGroup, dependency);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the preference using a combination of the category and the key
|
||||
preferenceGroup.addPreference(preference);
|
||||
addedPreferences.add(preferenceGroup.getTitle() + ":" + key); // Track based on both category and key
|
||||
|
||||
// Handle dependent preferences
|
||||
if (dependencyMap.containsKey(key)) {
|
||||
for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) {
|
||||
addPreferenceWithDependencies(preferenceGroup, dependentPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a preference in all groups based on its key.
|
||||
*
|
||||
* @param key The key of the preference to find.
|
||||
* @return The found preference, or null if not found.
|
||||
*/
|
||||
private Preference findPreferenceInAllGroups(String key) {
|
||||
for (List<Preference> preferences : groupedPreferences.values()) {
|
||||
for (Preference preference : preferences) {
|
||||
if (preference.getKey() != null && preference.getKey().equals(key)) {
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the preference screen to its original state.
|
||||
*/
|
||||
private void resetPreferences() {
|
||||
PreferenceScreen preferenceScreen = getPreferenceScreen();
|
||||
preferenceScreen.removeAll();
|
||||
for (Preference preference : getAllPreferencesBy(originalPreferenceScreen))
|
||||
preferenceScreen.addPreference(preference);
|
||||
}
|
||||
|
||||
private List<Preference> getAllPreferencesBy(PreferenceGroup preferenceGroup) {
|
||||
List<Preference> preferences = new ArrayList<>();
|
||||
for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++)
|
||||
preferences.add(preferenceGroup.getPreference(i));
|
||||
return preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Preference to Import/Export settings submenu
|
||||
*/
|
||||
private void setBackupRestorePreference() {
|
||||
findPreference("revanced_extended_settings_import").setOnPreferenceClickListener(pref -> {
|
||||
importActivity();
|
||||
return false;
|
||||
});
|
||||
|
||||
findPreference("revanced_extended_settings_export").setOnPreferenceClickListener(pref -> {
|
||||
exportActivity();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the SAF(Storage Access Framework) to export settings
|
||||
*/
|
||||
private void exportActivity() {
|
||||
@SuppressLint("SimpleDateFormat") final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
final String appName = ExtendedUtils.getApplicationLabel();
|
||||
final String versionName = ExtendedUtils.getVersionName();
|
||||
final String formatDate = dateFormat.format(new Date(System.currentTimeMillis()));
|
||||
final String fileName = String.format("%s_v%s_%s.txt", appName, versionName, formatDate);
|
||||
|
||||
final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TITLE, fileName);
|
||||
startActivityForResult(intent, WRITE_REQUEST_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the SAF(Storage Access Framework) to import settings
|
||||
*/
|
||||
private void importActivity() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType(isSDKAbove(29) ? "text/plain" : "*/*");
|
||||
startActivityForResult(intent, READ_REQUEST_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity should be done within the lifecycle of PreferenceFragment
|
||||
*/
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
exportText(data.getData());
|
||||
} else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
importText(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
private void exportText(Uri uri) {
|
||||
final Context context = this.getActivity();
|
||||
|
||||
try {
|
||||
@SuppressLint("Recycle")
|
||||
FileWriter jsonFileWriter =
|
||||
new FileWriter(
|
||||
Objects.requireNonNull(context.getApplicationContext()
|
||||
.getContentResolver()
|
||||
.openFileDescriptor(uri, "w"))
|
||||
.getFileDescriptor()
|
||||
);
|
||||
PrintWriter printWriter = new PrintWriter(jsonFileWriter);
|
||||
printWriter.write(Setting.exportToJson(context));
|
||||
printWriter.close();
|
||||
jsonFileWriter.close();
|
||||
|
||||
showToastShort(str("revanced_extended_settings_export_success"));
|
||||
} catch (IOException e) {
|
||||
showToastShort(str("revanced_extended_settings_export_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void importText(Uri uri) {
|
||||
final Context context = this.getActivity();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
|
||||
try {
|
||||
settingImportInProgress = true;
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
FileReader fileReader =
|
||||
new FileReader(
|
||||
Objects.requireNonNull(context.getApplicationContext()
|
||||
.getContentResolver()
|
||||
.openFileDescriptor(uri, "r"))
|
||||
.getFileDescriptor()
|
||||
);
|
||||
BufferedReader bufferedReader = new BufferedReader(fileReader);
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
bufferedReader.close();
|
||||
fileReader.close();
|
||||
|
||||
final boolean restartNeeded = Setting.importFromJSON(sb.toString(), true);
|
||||
if (restartNeeded) {
|
||||
showRestartDialog(getActivity());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
showToastShort(str("revanced_extended_settings_import_failed"));
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1;
|
||||
import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
||||
|
||||
import android.preference.Preference;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch;
|
||||
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
||||
|
||||
private static void enableDisablePreferences() {
|
||||
for (Setting<?> setting : Setting.allLoadedSettings()) {
|
||||
final Preference preference = mPreferenceManager.findPreference(setting.key);
|
||||
if (preference != null) {
|
||||
preference.setEnabled(setting.isAvailable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void enableDisablePreferences(final boolean isAvailable, final Setting<?>... unavailableEnum) {
|
||||
if (!isAvailable) {
|
||||
return;
|
||||
}
|
||||
for (Setting<?> setting : unavailableEnum) {
|
||||
final Preference preference = mPreferenceManager.findPreference(setting.key);
|
||||
if (preference != null) {
|
||||
preference.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void initializeReVancedSettings() {
|
||||
enableDisablePreferences();
|
||||
|
||||
AmbientModePreferenceLinks();
|
||||
ChangeHeaderPreferenceLinks();
|
||||
ExternalDownloaderPreferenceLinks();
|
||||
FullScreenPanelPreferenceLinks();
|
||||
LayoutOverrideLinks();
|
||||
MiniPlayerPreferenceLinks();
|
||||
NavigationPreferenceLinks();
|
||||
RYDPreferenceLinks();
|
||||
SeekBarPreferenceLinks();
|
||||
SpeedOverlayPreferenceLinks();
|
||||
QuickActionsPreferenceLinks();
|
||||
TabletLayoutLinks();
|
||||
WhitelistPreferenceLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Ambient Mode
|
||||
*/
|
||||
private static void AmbientModePreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
Settings.DISABLE_AMBIENT_MODE.get(),
|
||||
Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS,
|
||||
Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Change header
|
||||
*/
|
||||
private static void ChangeHeaderPreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
PatchStatus.MinimalHeader(),
|
||||
Settings.CHANGE_YOUTUBE_HEADER
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference for External downloader settings
|
||||
*/
|
||||
private static void ExternalDownloaderPreferenceLinks() {
|
||||
// Override download button will not work if spoofed with YouTube 18.24.xx or earlier.
|
||||
enableDisablePreferences(
|
||||
isSpoofingToLessThan("18.24.00"),
|
||||
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON,
|
||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Layout Override Preference
|
||||
*/
|
||||
private static void LayoutOverrideLinks() {
|
||||
enableDisablePreferences(
|
||||
ExtendedUtils.isTablet(),
|
||||
Settings.FORCE_FULLSCREEN
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preferences not working in tablet layout
|
||||
*/
|
||||
private static void TabletLayoutLinks() {
|
||||
final boolean isTablet = ExtendedUtils.isTablet() &&
|
||||
!LayoutSwitchPatch.phoneLayoutEnabled();
|
||||
|
||||
enableDisablePreferences(
|
||||
isTablet,
|
||||
Settings.DISABLE_ENGAGEMENT_PANEL,
|
||||
Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS,
|
||||
Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS,
|
||||
Settings.HIDE_MIX_PLAYLISTS,
|
||||
Settings.HIDE_RELATED_VIDEO_OVERLAY,
|
||||
Settings.SHOW_VIDEO_TITLE_SECTION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Fullscreen Panel
|
||||
*/
|
||||
private static void FullScreenPanelPreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
Settings.DISABLE_ENGAGEMENT_PANEL.get(),
|
||||
Settings.HIDE_RELATED_VIDEO_OVERLAY,
|
||||
Settings.HIDE_QUICK_ACTIONS,
|
||||
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON
|
||||
);
|
||||
|
||||
enableDisablePreferences(
|
||||
Settings.DISABLE_LANDSCAPE_MODE.get(),
|
||||
Settings.FORCE_FULLSCREEN
|
||||
);
|
||||
|
||||
enableDisablePreferences(
|
||||
Settings.FORCE_FULLSCREEN.get(),
|
||||
Settings.DISABLE_LANDSCAPE_MODE
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Hide Quick Actions
|
||||
*/
|
||||
private static void QuickActionsPreferenceLinks() {
|
||||
final boolean isEnabled =
|
||||
Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get();
|
||||
|
||||
enableDisablePreferences(
|
||||
isEnabled,
|
||||
Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON,
|
||||
Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Miniplayer settings
|
||||
*/
|
||||
private static void MiniPlayerPreferenceLinks() {
|
||||
final MiniplayerPatch.MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
|
||||
final boolean available =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) &&
|
||||
!Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() &&
|
||||
!Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
||||
|
||||
enableDisablePreferences(
|
||||
!available,
|
||||
Settings.MINIPLAYER_HIDE_EXPAND_CLOSE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Navigation settings
|
||||
*/
|
||||
private static void NavigationPreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(),
|
||||
Settings.HIDE_NAVIGATION_CREATE_BUTTON
|
||||
);
|
||||
enableDisablePreferences(
|
||||
!Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(),
|
||||
Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON,
|
||||
Settings.REPLACE_TOOLBAR_CREATE_BUTTON,
|
||||
Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE
|
||||
);
|
||||
enableDisablePreferences(
|
||||
!isSDKAbove(31),
|
||||
Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to RYD settings
|
||||
*/
|
||||
private static void RYDPreferenceLinks() {
|
||||
if (!(mPreferenceManager.findPreference(Settings.RYD_ENABLED.key) instanceof SwitchPreference enabledPreference)) {
|
||||
return;
|
||||
}
|
||||
if (!(mPreferenceManager.findPreference(Settings.RYD_SHORTS.key) instanceof SwitchPreference shortsPreference)) {
|
||||
return;
|
||||
}
|
||||
if (!(mPreferenceManager.findPreference(Settings.RYD_DISLIKE_PERCENTAGE.key) instanceof SwitchPreference percentagePreference)) {
|
||||
return;
|
||||
}
|
||||
if (!(mPreferenceManager.findPreference(Settings.RYD_COMPACT_LAYOUT.key) instanceof SwitchPreference compactLayoutPreference)) {
|
||||
return;
|
||||
}
|
||||
final Preference.OnPreferenceChangeListener clearAllUICaches = (pref, newValue) -> {
|
||||
ReturnYouTubeDislike.clearAllUICaches();
|
||||
|
||||
return true;
|
||||
};
|
||||
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
ReturnYouTubeDislikePatch.onRYDStatusChange();
|
||||
|
||||
return true;
|
||||
});
|
||||
String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
? str("revanced_ryd_shorts_summary_on")
|
||||
: str("revanced_ryd_shorts_summary_on_disclaimer");
|
||||
shortsPreference.setSummaryOn(shortsSummary);
|
||||
percentagePreference.setOnPreferenceChangeListener(clearAllUICaches);
|
||||
compactLayoutPreference.setOnPreferenceChangeListener(clearAllUICaches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Seek bar settings
|
||||
*/
|
||||
private static void SeekBarPreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(),
|
||||
Settings.ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to Speed overlay settings
|
||||
*/
|
||||
private static void SpeedOverlayPreferenceLinks() {
|
||||
enableDisablePreferences(
|
||||
Settings.DISABLE_SPEED_OVERLAY.get(),
|
||||
Settings.SPEED_OVERLAY_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
private static void WhitelistPreferenceLinks() {
|
||||
final boolean enabled = PatchStatus.RememberPlaybackSpeed() || PatchStatus.SponsorBlock();
|
||||
final String[] whitelistKey = {Settings.OVERLAY_BUTTON_WHITELIST.key, "revanced_whitelist_settings"};
|
||||
|
||||
for (String key : whitelistKey) {
|
||||
final Preference preference = mPreferenceManager.findPreference(key);
|
||||
if (preference != null) {
|
||||
preference.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.preference.ListPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SegmentCategoryListPreference extends ListPreference {
|
||||
private SegmentCategory mCategory;
|
||||
private EditText mEditText;
|
||||
private int mClickedDialogEntryIndex;
|
||||
|
||||
private void init() {
|
||||
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
|
||||
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
|
||||
mCategory = Objects.requireNonNull(segmentCategory);
|
||||
// Edit: Using preferences to sync together multiple pieces
|
||||
// of code together is messy and should be rethought.
|
||||
setKey(segmentCategory.behaviorSetting.key);
|
||||
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
|
||||
|
||||
setEntries(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorDescriptions());
|
||||
setEntryValues(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorKeyValues());
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public SegmentCategoryListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public SegmentCategoryListPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
|
||||
Context context = builder.getContext();
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(70, 0, 150, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
|
||||
TextView colorTextLabel = new TextView(context);
|
||||
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
|
||||
row.addView(colorTextLabel);
|
||||
|
||||
TextView colorDotView = new TextView(context);
|
||||
colorDotView.setText(mCategory.getCategoryColorDot());
|
||||
colorDotView.setPadding(30, 0, 30, 0);
|
||||
row.addView(colorDotView);
|
||||
|
||||
mEditText = new EditText(context);
|
||||
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||
mEditText.setText(mCategory.colorString());
|
||||
mEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
try {
|
||||
String colorString = s.toString();
|
||||
if (!colorString.startsWith("#")) {
|
||||
s.insert(0, "#"); // recursively calls back into this method
|
||||
return;
|
||||
}
|
||||
if (colorString.length() > 7) {
|
||||
s.delete(7, colorString.length());
|
||||
return;
|
||||
}
|
||||
final int color = Color.parseColor(colorString);
|
||||
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(mEditText);
|
||||
|
||||
table.addView(row);
|
||||
builder.setView(table);
|
||||
builder.setTitle(mCategory.title.toString());
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
||||
try {
|
||||
mCategory.resetColor();
|
||||
updateTitle();
|
||||
Utils.showToastShort(str("revanced_sb_color_reset"));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setNeutralButton failure", ex);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
mClickedDialogEntryIndex = findIndexOfValue(getValue());
|
||||
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
try {
|
||||
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
}
|
||||
String colorString = mEditText.getText().toString();
|
||||
try {
|
||||
if (!colorString.equals(mCategory.colorString())) {
|
||||
mCategory.setColor(colorString);
|
||||
Utils.showToastShort(str("revanced_sb_color_changed"));
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
||||
}
|
||||
updateTitle();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
setTitle(mCategory.getTitleWithColorDot());
|
||||
setEnabled(Settings.SB_ENABLED.get());
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.EditText;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SponsorBlockImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private String existingSettings;
|
||||
|
||||
@TargetApi(26)
|
||||
private void init() {
|
||||
setSelectable(true);
|
||||
|
||||
EditText editText = getEditText();
|
||||
editText.setTextIsSelectable(true);
|
||||
editText.setAutofillHints((String) null);
|
||||
editText.setInputType(editText.getInputType()
|
||||
| InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap.
|
||||
|
||||
// If the user has a private user id, then include a subtext that mentions not to share it.
|
||||
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
||||
? str("revanced_sb_settings_ie_sum_warning")
|
||||
: str("revanced_sb_settings_ie_sum");
|
||||
setSummary(importExportSummary);
|
||||
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public SponsorBlockImportExportPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public SponsorBlockImportExportPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
|
||||
existingSettings = SponsorBlockSettings.exportDesktopSettings();
|
||||
getEditText().setText(existingSettings);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setTitle(getTitle());
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) ->
|
||||
Utils.setClipboard(getEditText().getText().toString(), str("revanced_sb_share_copy_settings_success")))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) ->
|
||||
importSettings(getEditText().getText().toString()));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importSettings(String replacementSettings) {
|
||||
try {
|
||||
if (replacementSettings.equals(existingSettings)) {
|
||||
return;
|
||||
}
|
||||
SponsorBlockSettings.importDesktopSettings(replacementSettings);
|
||||
SponsorBlockSettingsPreference.updateSegmentCategories();
|
||||
SponsorBlockSettingsPreference.fetchAndDisplayStats();
|
||||
SponsorBlockSettingsPreference.updateUI();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,432 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static android.text.Html.fromHtml;
|
||||
import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.text.InputType;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
|
||||
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
|
||||
|
||||
private static PreferenceCategory statsCategory;
|
||||
|
||||
private static final int preferencesCategoryLayout = getLayoutIdentifier("revanced_settings_preferences_category");
|
||||
|
||||
private static final Preference.OnPreferenceChangeListener updateUI = (pref, newValue) -> {
|
||||
updateUI();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@NonNull
|
||||
private static SwitchPreference findSwitchPreference(BooleanSetting setting) {
|
||||
final String key = setting.key;
|
||||
if (mPreferenceManager.findPreference(key) instanceof SwitchPreference switchPreference) {
|
||||
switchPreference.setOnPreferenceChangeListener(updateUI);
|
||||
return switchPreference;
|
||||
} else {
|
||||
throw new IllegalStateException("SwitchPreference is null: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static ResettableEditTextPreference findResettableEditTextPreference(Setting<?> setting) {
|
||||
final String key = setting.key;
|
||||
if (mPreferenceManager.findPreference(key) instanceof ResettableEditTextPreference switchPreference) {
|
||||
switchPreference.setOnPreferenceChangeListener(updateUI);
|
||||
return switchPreference;
|
||||
} else {
|
||||
throw new IllegalStateException("ResettableEditTextPreference is null: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateUI() {
|
||||
if (!Settings.SB_ENABLED.get()) {
|
||||
SponsorBlockViewController.hideAll();
|
||||
SegmentPlaybackController.clearData();
|
||||
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
||||
SponsorBlockViewController.hideNewSegmentLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
public static void init(Activity mActivity) {
|
||||
if (!PatchStatus.SponsorBlock()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SwitchPreference sbEnabled = findSwitchPreference(Settings.SB_ENABLED);
|
||||
sbEnabled.setOnPreferenceClickListener(preference -> {
|
||||
updateUI();
|
||||
fetchAndDisplayStats();
|
||||
updateSegmentCategories();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!(sbEnabled.getParent() instanceof PreferenceScreen mPreferenceScreen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SwitchPreference votingEnabled = findSwitchPreference(Settings.SB_VOTING_BUTTON);
|
||||
final SwitchPreference compactSkipButton = findSwitchPreference(Settings.SB_COMPACT_SKIP_BUTTON);
|
||||
final SwitchPreference autoHideSkipSegmentButton = findSwitchPreference(Settings.SB_AUTO_HIDE_SKIP_BUTTON);
|
||||
final SwitchPreference showSkipToast = findSwitchPreference(Settings.SB_TOAST_ON_SKIP);
|
||||
showSkipToast.setOnPreferenceClickListener(preference -> {
|
||||
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
||||
return false;
|
||||
});
|
||||
|
||||
final SwitchPreference showTimeWithoutSegments = findSwitchPreference(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS);
|
||||
|
||||
final SwitchPreference addNewSegment = findSwitchPreference(Settings.SB_CREATE_NEW_SEGMENT);
|
||||
addNewSegment.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if ((Boolean) newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
||||
Context context = preference.getContext();
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
||||
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
||||
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
||||
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines(context))
|
||||
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
|
||||
final ResettableEditTextPreference newSegmentStep = findResettableEditTextPreference(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
||||
newSegmentStep.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
try {
|
||||
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
||||
if (newAdjustmentValue != 0) {
|
||||
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
Logger.printInfo(() -> "Invalid new segment step", ex);
|
||||
}
|
||||
|
||||
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
||||
updateUI();
|
||||
return false;
|
||||
});
|
||||
final Preference guidelinePreferences = Objects.requireNonNull(mPreferenceManager.findPreference("revanced_sb_guidelines_preference"));
|
||||
guidelinePreferences.setDependency(Settings.SB_ENABLED.key);
|
||||
guidelinePreferences.setOnPreferenceClickListener(preference -> {
|
||||
openGuidelines(preference.getContext());
|
||||
return true;
|
||||
});
|
||||
|
||||
final SwitchPreference toastOnConnectionError = findSwitchPreference(Settings.SB_TOAST_ON_CONNECTION_ERROR);
|
||||
final SwitchPreference trackSkips = findSwitchPreference(Settings.SB_TRACK_SKIP_COUNT);
|
||||
final ResettableEditTextPreference minSegmentDuration = findResettableEditTextPreference(Settings.SB_SEGMENT_MIN_DURATION);
|
||||
minSegmentDuration.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
try {
|
||||
Float minTimeDuration = Float.valueOf(newValue.toString());
|
||||
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
||||
return true;
|
||||
} catch (NumberFormatException ex) {
|
||||
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
||||
}
|
||||
|
||||
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
||||
updateUI();
|
||||
return false;
|
||||
});
|
||||
final ResettableEditTextPreference privateUserId = findResettableEditTextPreference(Settings.SB_PRIVATE_USER_ID);
|
||||
privateUserId.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
String newUUID = newValue.toString();
|
||||
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
||||
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
||||
return false;
|
||||
}
|
||||
|
||||
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
||||
try {
|
||||
updateUI();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
fetchAndDisplayStats();
|
||||
return true;
|
||||
});
|
||||
final Preference apiUrl = mPreferenceManager.findPreference(Settings.SB_API_URL.key);
|
||||
if (apiUrl != null) {
|
||||
apiUrl.setOnPreferenceClickListener(preference -> {
|
||||
Context context = preference.getContext();
|
||||
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(15, 0, 15, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
|
||||
EditText editText = new EditText(context);
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
editText.setText(Settings.SB_API_URL.get());
|
||||
editText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(editText);
|
||||
table.addView(row);
|
||||
|
||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||
Settings.SB_API_URL.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||
String serverAddress = editText.getText().toString();
|
||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
||||
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
||||
Settings.SB_API_URL.save(serverAddress);
|
||||
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
Utils.getEditTextDialogBuilder(context)
|
||||
.setView(table)
|
||||
.setTitle(apiUrl.getTitle())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
|
||||
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
statsCategory = new PreferenceCategory(mActivity);
|
||||
statsCategory.setLayoutResource(preferencesCategoryLayout);
|
||||
statsCategory.setTitle(str("revanced_sb_stats"));
|
||||
mPreferenceScreen.addPreference(statsCategory);
|
||||
fetchAndDisplayStats();
|
||||
|
||||
final PreferenceCategory aboutCategory = new PreferenceCategory(mActivity);
|
||||
aboutCategory.setLayoutResource(preferencesCategoryLayout);
|
||||
aboutCategory.setTitle(str("revanced_sb_about"));
|
||||
mPreferenceScreen.addPreference(aboutCategory);
|
||||
|
||||
Preference aboutPreference = new Preference(mActivity);
|
||||
aboutCategory.addPreference(aboutPreference);
|
||||
aboutPreference.setTitle(str("revanced_sb_about_api"));
|
||||
aboutPreference.setSummary(str("revanced_sb_about_api_sum"));
|
||||
aboutPreference.setOnPreferenceClickListener(preference -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sponsor.ajay.app"));
|
||||
preference.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
public static void updateSegmentCategories() {
|
||||
try {
|
||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||
final String key = category.keyValue;
|
||||
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
|
||||
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
|
||||
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "updateSegmentCategories failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void openGuidelines(Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static void fetchAndDisplayStats() {
|
||||
try {
|
||||
if (statsCategory == null) {
|
||||
return;
|
||||
}
|
||||
statsCategory.removeAll();
|
||||
if (!SponsorBlockSettings.userHasSBPrivateId()) {
|
||||
// User has never voted or created any segments. No stats to show.
|
||||
addLocalUserStats();
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = statsCategory.getContext();
|
||||
|
||||
Preference loadingPlaceholderPreference = new Preference(context);
|
||||
loadingPlaceholderPreference.setEnabled(false);
|
||||
statsCategory.addPreference(loadingPlaceholderPreference);
|
||||
if (Settings.SB_ENABLED.get()) {
|
||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
UserStats stats = SBRequester.retrieveUserStats();
|
||||
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
||||
addUserStats(loadingPlaceholderPreference, stats);
|
||||
addLocalUserStats();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "fetchAndDisplayStats failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
|
||||
Utils.verifyOnMainThread();
|
||||
try {
|
||||
if (stats == null) {
|
||||
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
|
||||
return;
|
||||
}
|
||||
statsCategory.removeAll();
|
||||
Context context = statsCategory.getContext();
|
||||
|
||||
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
||||
// If user has not created any segments, there's no reason to set a username.
|
||||
ResettableEditTextPreference preference = new ResettableEditTextPreference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
String userName = stats.userName;
|
||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
||||
preference.setSummary(str("revanced_sb_stats_username_change"));
|
||||
preference.setText(userName);
|
||||
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
String newUserName = (String) value;
|
||||
String errorMessage = SBRequester.setUsername(newUserName);
|
||||
Utils.runOnMainThread(() -> {
|
||||
if (errorMessage == null) {
|
||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
||||
preference.setText(newUserName);
|
||||
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
||||
} else {
|
||||
preference.setText(userName); // revert to previous
|
||||
Utils.showToastLong(errorMessage);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// number of segment submissions (does not include ignored segments)
|
||||
Preference preference = new Preference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
||||
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
||||
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||
preference.setSelectable(false);
|
||||
} else {
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
||||
preference1.getContext().startActivity(i);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// "user reputation". Usually not useful, since it appears most users have zero reputation.
|
||||
// But if there is a reputation, then show it here
|
||||
Preference preference = new Preference(context);
|
||||
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
||||
preference.setSelectable(false);
|
||||
if (stats.reputation != 0) {
|
||||
statsCategory.addPreference(preference);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// time saved for other users
|
||||
Preference preference = new Preference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
|
||||
String stats_saved;
|
||||
String stats_saved_sum;
|
||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||
stats_saved = str("revanced_sb_stats_saved_zero");
|
||||
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
|
||||
} else {
|
||||
stats_saved = str("revanced_sb_stats_saved",
|
||||
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
|
||||
stats_saved_sum = str("revanced_sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
||||
}
|
||||
preference.setTitle(fromHtml(stats_saved));
|
||||
preference.setSummary(fromHtml(stats_saved_sum));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||
preference1.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "addUserStats failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addLocalUserStats() {
|
||||
// time the user saved by using SB
|
||||
Preference preference = new Preference(statsCategory.getContext());
|
||||
statsCategory.addPreference(preference);
|
||||
|
||||
Runnable updateStatsSelfSaved = () -> {
|
||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
||||
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
||||
String formattedSaved = SponsorBlockUtils.getTimeSavedString(Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
||||
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
||||
};
|
||||
updateStatsSelfSaved.run();
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
new AlertDialog.Builder(preference1.getContext())
|
||||
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
||||
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
||||
updateStatsSelfSaved.run();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null).show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
// Because this listener may run before the ReVanced settings fragment updates Settings,
|
||||
// this could show the prior config and not the current.
|
||||
//
|
||||
// Push this call to the end of the main run queue,
|
||||
// so all other listeners are done and Settings is up to date.
|
||||
Utils.runOnMainThread(this::updateUI);
|
||||
};
|
||||
|
||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SpoofStreamingDataSideEffectsPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private void addChangeListener() {
|
||||
Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
private void removeChangeListener() {
|
||||
Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
|
||||
super.onAttachedToHierarchy(preferenceManager);
|
||||
updateUI();
|
||||
addChangeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareForRemoval() {
|
||||
super.onPrepareForRemoval();
|
||||
removeChangeListener();
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
|
||||
|
||||
final String summaryTextKey;
|
||||
if (clientType == ClientType.IOS && Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK.get()) {
|
||||
summaryTextKey = "revanced_spoof_streaming_data_side_effects_ios_skip_livestream_playback";
|
||||
} else {
|
||||
summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
|
||||
}
|
||||
|
||||
setSummary(str(summaryTextKey));
|
||||
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user