You've already forked revanced-integrations
mirror of
https://github.com/revanced/revanced-integrations
synced 2025-11-19 03:23:27 +01:00
Compare commits
115 Commits
v0.100.0-d
...
v0.107.0-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ba636a910 | ||
|
|
68f42fc980 | ||
|
|
8255909b2f | ||
|
|
a91b0363a8 | ||
|
|
e34e75133c | ||
|
|
1185ceedf7 | ||
|
|
2ee18fce69 | ||
|
|
e774be0f05 | ||
|
|
8a7098cce4 | ||
|
|
a93a704c22 | ||
|
|
061ebc6546 | ||
|
|
0c6b22e929 | ||
|
|
7fc73368f1 | ||
|
|
8923fa9fd6 | ||
|
|
15f9c90941 | ||
|
|
c8784a5966 | ||
|
|
d3f8fb9739 | ||
|
|
ff0d64287c | ||
|
|
36fd2844c4 | ||
|
|
0de83fff0e | ||
|
|
24a609288f | ||
|
|
78d56d4fe1 | ||
|
|
27fdcfff08 | ||
|
|
f6f6c93c57 | ||
|
|
a661dac623 | ||
|
|
8cc1b6ea4a | ||
|
|
829895874b | ||
|
|
6b15514885 | ||
|
|
5b53e02613 | ||
|
|
2deacc5035 | ||
|
|
46d70a3e00 | ||
|
|
91ce39378a | ||
|
|
df4b03fed5 | ||
|
|
9f8063880c | ||
|
|
75dad2f307 | ||
|
|
80dc8f0421 | ||
|
|
6f2ae313cf | ||
|
|
cb7063b2b3 | ||
|
|
3b37a3b41f | ||
|
|
13e4be8567 | ||
|
|
be7b6f2a20 | ||
|
|
b9f6c62060 | ||
|
|
8493f57879 | ||
|
|
436a84ee07 | ||
|
|
f2603d3d79 | ||
|
|
246deb1602 | ||
|
|
050766de1d | ||
|
|
6952c50595 | ||
|
|
7fc8e882d8 | ||
|
|
a61148cac2 | ||
|
|
c2ff0c45ab | ||
|
|
587689ed7b | ||
|
|
a2af2c0c9f | ||
|
|
f7c3543d4f | ||
|
|
86c27890ad | ||
|
|
2ea55af9ce | ||
|
|
41c07f77f4 | ||
|
|
48050c1c50 | ||
|
|
8797765efa | ||
|
|
fb8442823e | ||
|
|
da7b669c97 | ||
|
|
4d9b41ca3a | ||
|
|
03f09cf7bc | ||
|
|
76a01d1b7c | ||
|
|
14223f40b5 | ||
|
|
5ba4cbd4e0 | ||
|
|
212e4f2ce4 | ||
|
|
b959c8ef98 | ||
|
|
6265a91841 | ||
|
|
6dbccfd472 | ||
|
|
e3d923d564 | ||
|
|
80ae9ebbd2 | ||
|
|
584de16236 | ||
|
|
a0ad968aaa | ||
|
|
d4de3f6819 | ||
|
|
22e453706d | ||
|
|
bbb07ec9c8 | ||
|
|
3025103014 | ||
|
|
e3529cfcec | ||
|
|
6528d444b4 | ||
|
|
f8184905bd | ||
|
|
5981e99e56 | ||
|
|
214f2c89c2 | ||
|
|
a7ae215bf7 | ||
|
|
eeddb59b08 | ||
|
|
1a0a6ee90b | ||
|
|
86bedb2183 | ||
|
|
5b9682522e | ||
|
|
6d32dff400 | ||
|
|
5cb5656324 | ||
|
|
7627e5d057 | ||
|
|
0810f84c4c | ||
|
|
8301fa07fd | ||
|
|
d16980ef2f | ||
|
|
83510e51b3 | ||
|
|
68d09305b9 | ||
|
|
dc5c1b45ba | ||
|
|
46e0272f9e | ||
|
|
69ccb5fc05 | ||
|
|
2dd14313a6 | ||
|
|
5e518855d1 | ||
|
|
52ac4acff3 | ||
|
|
a08bc53828 | ||
|
|
5d7dc94d8d | ||
|
|
1e1504d118 | ||
|
|
393d6e62f2 | ||
|
|
1361595076 | ||
|
|
2f5c839613 | ||
|
|
7e64e05709 | ||
|
|
d5919a8a2c | ||
|
|
844bc3b24f | ||
|
|
67fa87051f | ||
|
|
1f90f7b9cc | ||
|
|
0c725218fd | ||
|
|
416c695837 |
2
.github/config.yml
vendored
Normal file
2
.github/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
firstPRMergeComment: >
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||
9
.github/workflows/pull_request.yml
vendored
9
.github/workflows/pull_request.yml
vendored
@@ -21,5 +21,10 @@ jobs:
|
||||
with:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||
pr_draft: true
|
||||
pr_body: |
|
||||
This pull request will ${{ env.MESSAGE }}.
|
||||
|
||||
## Dependencies before merge
|
||||
|
||||
- [ ] https://github.com/revanced/revanced-patches
|
||||
pr_draft: true
|
||||
|
||||
400
CHANGELOG.md
400
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -43,8 +43,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(mapOf("path" to ":dummy")))
|
||||
compileOnly("androidx.annotation:annotation:1.5.0")
|
||||
compileOnly("androidx.appcompat:appcompat:1.6.1")
|
||||
compileOnly("androidx.annotation:annotation:1.6.0")
|
||||
compileOnly("androidx.appcompat:appcompat:1.7.0-alpha02")
|
||||
compileOnly("com.squareup.okhttp3:okhttp:5.0.0-alpha.11")
|
||||
compileOnly("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
}
|
||||
|
||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -20,6 +20,8 @@
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
-keepattributes * # https://www.guardsquare.com/manual/configuration/attributes
|
||||
-keep class app.revanced.** {
|
||||
*;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,31 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
final class ButtonsPatch extends Filter {
|
||||
private final BlockRule actionButtonsRule;
|
||||
private final BlockRule dislikeRule;
|
||||
private final BlockRule actionBarRule;
|
||||
|
||||
private final BlockRule[] rules;
|
||||
|
||||
public ButtonsPatch() {
|
||||
BlockRule like = new BlockRule(SettingsEnum.HIDE_LIKE_BUTTON, "|like_button");
|
||||
dislikeRule = new BlockRule(SettingsEnum.HIDE_DISLIKE_BUTTON, "dislike_button");
|
||||
BlockRule download = new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button");
|
||||
actionButtonsRule = new BlockRule(SettingsEnum.HIDE_ACTION_BUTTON, "ContainerType|video_action_button");
|
||||
BlockRule playlist = new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button");
|
||||
rules = new BlockRule[]{like, dislikeRule, download, actionButtonsRule, playlist};
|
||||
|
||||
actionBarRule = new BlockRule(null, "video_action_bar");
|
||||
|
||||
this.pathRegister.registerAll(
|
||||
like,
|
||||
dislikeRule,
|
||||
download,
|
||||
playlist
|
||||
pathRegister.registerAll(
|
||||
new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_CLIP_BUTTON, "|clip_button.eml|"),
|
||||
new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button", "|CellType|CollectionType|CellType|ContainerType|button.eml|")
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hideActionBar() {
|
||||
for (BlockRule rule : rules) if (!rule.isEnabled()) return false;
|
||||
private boolean canHideActionBar() {
|
||||
for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filter(final String path, final String identifier) {
|
||||
if (hideActionBar() && actionBarRule.check(identifier).isBlocked()) return true;
|
||||
// If everything is hidden, then also hide the video bar itself.
|
||||
if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true;
|
||||
|
||||
var currentIsActionButton = actionButtonsRule.check(path).isBlocked();
|
||||
|
||||
if (dislikeRule.check(path).isBlocked()) ActionButton.doNotBlockCounter = 4;
|
||||
|
||||
if (currentIsActionButton && ActionButton.doNotBlockCounter-- > 0) {
|
||||
if (SettingsEnum.HIDE_SHARE_BUTTON.getBoolean()) {
|
||||
LogHelper.printDebug(() -> "Hiding share button");
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
if ((currentIsActionButton && ActionButton.doNotBlockCounter <= 0 && actionButtonsRule.isEnabled()) || pathRegister.contains(path)) {
|
||||
LogHelper.printDebug(() -> "Blocked: " + path);
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
static class ActionButton {
|
||||
public static int doNotBlockCounter = 4;
|
||||
return pathRegister.contains(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.StringRef;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class CopyVideoUrlPatch {
|
||||
public static void copyUrl(Boolean withTimestamp) {
|
||||
try {
|
||||
String url = String.format("https://youtu.be/%s", VideoInformation.getCurrentVideoId());
|
||||
String url = String.format("https://youtu.be/%s", VideoInformation.getVideoId());
|
||||
if (withTimestamp) {
|
||||
long seconds = VideoInformation.getVideoTime() / 1000;
|
||||
url += String.format("?t=%s", seconds);
|
||||
}
|
||||
|
||||
Context context = ReVancedUtils.getContext();
|
||||
|
||||
ReVancedUtils.setClipboard(url);
|
||||
if (context != null) Toast.makeText(context, StringRef.str("share_copy_url_success"), Toast.LENGTH_SHORT).show();
|
||||
ReVancedUtils.showToastShort(str("share_copy_url_success"));
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Failed to generate video url", e);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public final class GeneralAdsPatch extends Filter {
|
||||
var infoPanel = new BlockRule(SettingsEnum.ADREMOVER_INFO_PANEL_REMOVAL, "publisher_transparency_panel", "single_item_information_panel");
|
||||
var latestPosts = new BlockRule(SettingsEnum.ADREMOVER_HIDE_LATEST_POSTS, "post_shelf");
|
||||
var channelGuidelines = new BlockRule(SettingsEnum.ADREMOVER_HIDE_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner");
|
||||
var audioTrackButton = new BlockRule(SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, "multi_feed_icon_button");
|
||||
var artistCard = new BlockRule(SettingsEnum.HIDE_ARTIST_CARDS, "official_card");
|
||||
var selfSponsor = new BlockRule(SettingsEnum.ADREMOVER_SELF_SPONSOR_REMOVAL, "cta_shelf_card");
|
||||
var chapterTeaser = new BlockRule(SettingsEnum.ADREMOVER_CHAPTER_TEASER_REMOVAL, "expandable_metadata", "macro_markers_carousel");
|
||||
@@ -49,6 +50,7 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"_buttoned_layout",
|
||||
"full_width_square_image_layout",
|
||||
"_ad_with",
|
||||
"video_display_button_group_layout",
|
||||
"landscape_image_wide_button_layout"
|
||||
);
|
||||
var generalAds = new BlockRule(
|
||||
@@ -59,7 +61,13 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"watch_metadata_app_promo",
|
||||
"video_display_full_layout",
|
||||
"hero_promo_image",
|
||||
"statement_banner"
|
||||
"statement_banner",
|
||||
"carousel_footered_layout",
|
||||
"text_image_button_layout",
|
||||
"primetime_promo",
|
||||
"product_details",
|
||||
"full_width_portrait_image_layout",
|
||||
"brand_video_shelf"
|
||||
);
|
||||
var movieAds = new BlockRule(
|
||||
SettingsEnum.ADREMOVER_MOVIE_REMOVAL,
|
||||
@@ -67,7 +75,8 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"compact_movie",
|
||||
"horizontal_movie_shelf",
|
||||
"movie_and_show_upsell_card",
|
||||
"compact_tvfilm_item"
|
||||
"compact_tvfilm_item",
|
||||
"offer_module_root"
|
||||
);
|
||||
|
||||
this.pathRegister.registerAll(
|
||||
@@ -89,6 +98,7 @@ public final class GeneralAdsPatch extends Filter {
|
||||
merchandise,
|
||||
infoPanel,
|
||||
channelGuidelines,
|
||||
audioTrackButton,
|
||||
artistCard,
|
||||
selfSponsor,
|
||||
webLinkPanel,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class HideCreateButtonPatch {
|
||||
|
||||
//Used by app.revanced.patches.youtube.layout.createbutton.patch.CreateButtonRemoverPatch
|
||||
public static void hideCreateButton(View view) {
|
||||
boolean hidden = SettingsEnum.HIDE_CREATE_BUTTON.getBoolean();
|
||||
LogHelper.printDebug(() -> "Create button: " + (hidden ? "hidden" : "shown"));
|
||||
view.setVisibility(hidden ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class HideGetPremiumPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideGetPremiumView() {
|
||||
return SettingsEnum.HIDE_GET_PREMIUM.getBoolean();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.adremover.AdRemoverAPI;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class HideMixPlaylistsPatch {
|
||||
|
||||
public static void hideMixPlaylists(View view) {
|
||||
if (!SettingsEnum.HIDE_MIX_PLAYLISTS.getBoolean()) return;
|
||||
AdRemoverAPI.HideViewWithLayout1dp(view);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.adremover.AdRemoverAPI;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class HideReelsPatch {
|
||||
|
||||
/**
|
||||
* Used by app.revanced.patches.youtube.layout.reels.patch.HideReelsPatch
|
||||
*
|
||||
* @param view
|
||||
*/
|
||||
public static void HideReel(View view) {
|
||||
if (SettingsEnum.HIDE_REEL_BUTTON.getBoolean()) {
|
||||
AdRemoverAPI.HideViewWithLayout1dp(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class HideShortsButtonPatch {
|
||||
|
||||
// Used by app.revanced.patches.youtube.layout.shorts.button.patch.ShortsButtonRemoverPatch
|
||||
public static void hideShortsButton(View view) {
|
||||
if (lastPivotTab != null && lastPivotTab.name() == "TAB_SHORTS") {
|
||||
boolean hide = SettingsEnum.HIDE_SHORTS_BUTTON.getBoolean();
|
||||
LogHelper.printDebug(() -> hide ? "Shorts button: hidden" : "Shorts button: shown");
|
||||
if (hide) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Needed for the ShortsButtonRemoverPatch
|
||||
public static Enum lastPivotTab;
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class MicroGSupport {
|
||||
private static final String MICROG_VENDOR = "com.mgoogle";
|
||||
@@ -20,7 +21,7 @@ public class MicroGSupport {
|
||||
private static final Uri VANCED_MICROG_PROVIDER = Uri.parse("content://" + MICROG_VENDOR + ".android.gsf.gservices/prefix");
|
||||
|
||||
private static void startIntent(Context context, String uriString, String message) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
|
||||
ReVancedUtils.showToastLong(message);
|
||||
|
||||
var intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -28,6 +29,7 @@ public class MicroGSupport {
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
public static void checkAvailability() {
|
||||
var context = Objects.requireNonNull(ReVancedUtils.getContext());
|
||||
|
||||
@@ -41,10 +43,11 @@ public class MicroGSupport {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
|
||||
try (var client = context.getContentResolver().acquireContentProviderClient(VANCED_MICROG_PROVIDER)) {
|
||||
if (client != null) return;
|
||||
LogHelper.printInfo(() -> "Vanced MicroG is not running in the background");
|
||||
startIntent(context, DONT_KILL_MY_APP_LINK, str("microg_not_running_warning"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
|
||||
public class MinimizedPlaybackPatch {
|
||||
|
||||
public static boolean isNotPlayingShorts(boolean isPipEnabled) {
|
||||
return !PlayerType.getCurrent().isNoneOrHidden() && isPipEnabled;
|
||||
public static boolean isPlaybackNotShort() {
|
||||
return !PlayerType.getCurrent().isNoneOrHidden();
|
||||
}
|
||||
|
||||
public static boolean isMinimizedPlaybackEnabled() {
|
||||
return SettingsEnum.ENABLE_MINIMIZED_PLAYBACK.getBoolean();
|
||||
public static boolean overrideMinimizedPlaybackAvailable() {
|
||||
// This could be done entirely in the patch,
|
||||
// but having a unique method to search for makes manually inspecting the patched apk much easier.
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
|
||||
import android.view.View;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class NavigationButtonsPatch {
|
||||
public static Enum lastNavigationButton;
|
||||
|
||||
public static void hideCreateButton(final View view) {
|
||||
view.setVisibility(SettingsEnum.HIDE_CREATE_BUTTON.getBoolean() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
public static boolean switchCreateWithNotificationButton() {
|
||||
return SettingsEnum.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.getBoolean();
|
||||
}
|
||||
|
||||
public static void hideButton(final View buttonView) {
|
||||
if (lastNavigationButton == null) return;
|
||||
|
||||
for (NavigationButton button : NavigationButton.values())
|
||||
if (button.name.equals(lastNavigationButton.name()))
|
||||
if (button.enabled) buttonView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private enum NavigationButton {
|
||||
HOME("PIVOT_HOME", SettingsEnum.HIDE_HOME_BUTTON.getBoolean()),
|
||||
SHORTS("TAB_SHORTS", SettingsEnum.HIDE_SHORTS_BUTTON.getBoolean()),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", SettingsEnum.HIDE_SUBSCRIPTIONS_BUTTON.getBoolean());
|
||||
private final boolean enabled;
|
||||
private final String name;
|
||||
|
||||
NavigationButton(final String name, final boolean enabled) {
|
||||
this.name = name;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class NewActionbarPatch {
|
||||
|
||||
//Used by app.revanced.patches.youtube.layout.widesearchbar.patch.WideSearchbarPatch
|
||||
public static boolean getNewActionBar() {
|
||||
return SettingsEnum.WIDE_SEARCHBAR.getBoolean(); // TODO: maybe this has to be inverted
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class PlayerTypeHookPatch {
|
||||
LogHelper.printException(() -> "Unknown PlayerType encountered: " + type);
|
||||
} else {
|
||||
PlayerType.setCurrent(newType);
|
||||
LogHelper.printDebug(() -> "YouTubePlayerOverlaysLayout player type was updated to " + newType);
|
||||
LogHelper.printDebug(() -> "PlayerType was updated to: " + newType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,202 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.text.Spanned;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Used by app.revanced.patches.youtube.layout.returnyoutubedislike.patch.ReturnYouTubeDislikePatch
|
||||
*/
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Resource identifier of old UI dislike button.
|
||||
*/
|
||||
public static void newVideoLoaded(String videoId) {
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
private static final int OLD_UI_DISLIKE_BUTTON_RESOURCE_ID
|
||||
= ReVancedUtils.getResourceIdentifier("dislike_button", "id");
|
||||
|
||||
/**
|
||||
* Dislikes text label used by old UI.
|
||||
*/
|
||||
@NonNull
|
||||
private static WeakReference<TextView> oldUITextViewRef = new WeakReference<>(null);
|
||||
|
||||
/**
|
||||
* Original old UI 'Dislikes' text before patch modifications.
|
||||
* Required to reset the dislikes when changing videos and RYD is not available.
|
||||
* Set only once during the first load.
|
||||
*/
|
||||
private static Spanned oldUIOriginalSpan;
|
||||
|
||||
/**
|
||||
* Replacement span that contains dislike value. Used by {@link #oldUiTextWatcher}.
|
||||
*/
|
||||
@Nullable
|
||||
private static Spanned oldUIReplacementSpan;
|
||||
|
||||
/**
|
||||
* Old UI dislikes can be set multiple times by YouTube.
|
||||
* To prevent it from reverting changes made here, this listener overrides any future changes YouTube makes.
|
||||
*/
|
||||
private static final TextWatcher oldUiTextWatcher = 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) {
|
||||
if (oldUIReplacementSpan == null || oldUIReplacementSpan.toString().equals(s.toString())) {
|
||||
return;
|
||||
}
|
||||
s.replace(0, s.length(), oldUIReplacementSpan);
|
||||
}
|
||||
};
|
||||
|
||||
private static void updateOldUIDislikesTextView() {
|
||||
TextView oldUITextView = oldUITextViewRef.get();
|
||||
if (oldUITextView == null) {
|
||||
return;
|
||||
}
|
||||
oldUIReplacementSpan = ReturnYouTubeDislike.getDislikesSpanForRegularVideo(oldUIOriginalSpan, false);
|
||||
if (!oldUIReplacementSpan.equals(oldUITextView.getText())) {
|
||||
oldUITextView.setText(oldUIReplacementSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point. Called on main thread.
|
||||
*
|
||||
* Called when a litho text component is created
|
||||
* Used when spoofing the older app versions of {@link SpoofAppVersionPatch}.
|
||||
*/
|
||||
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
||||
ReturnYouTubeDislike.onComponentCreated(conversionContext, textRef);
|
||||
public static void setOldUILayoutDislikes(int buttonViewResourceId, @NonNull TextView textView) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()
|
||||
|| buttonViewResourceId != OLD_UI_DISLIKE_BUTTON_RESOURCE_ID) {
|
||||
return;
|
||||
}
|
||||
if (oldUIOriginalSpan == null) {
|
||||
// Use value of the first instance, as it appears TextViews can be recycled
|
||||
// and might contain dislikes previously added by the patch.
|
||||
oldUIOriginalSpan = (Spanned) textView.getText();
|
||||
}
|
||||
oldUITextViewRef = new WeakReference<>(textView);
|
||||
// No way to check if a listener is already attached, so remove and add again.
|
||||
textView.removeTextChangedListener(oldUiTextWatcher);
|
||||
textView.addTextChangedListener(oldUiTextWatcher);
|
||||
|
||||
updateOldUIDislikesTextView();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setOldUILayoutDislikes failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*
|
||||
* Called when a Shorts dislike Spannable is created
|
||||
* Injection point.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned dislike) {
|
||||
return ReturnYouTubeDislike.onShortsComponentCreated(dislike);
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when the like/dislike button is clicked
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* @param vote -1 (dislike), 0 (none) or 1 (like)
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param textRef Cache reference to the like/dislike char sequence,
|
||||
* which may or may not be the same as the original span parameter.
|
||||
* If dislikes are added, the atomic reference must be set to the replacement span.
|
||||
* @param original Original span that was created or reused by Litho.
|
||||
* @return The original span (if nothing should change), or a replacement span that contains dislikes.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean() || PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
final boolean isSegmentedButton;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
isSegmentedButton = true;
|
||||
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||
isSegmentedButton = false;
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
|
||||
Spanned replacement = ReturnYouTubeDislike.getDislikesSpanForRegularVideo((Spannable) original, isSegmentedButton);
|
||||
textRef.set(replacement);
|
||||
return replacement;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onLithoTextLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a Shorts dislike Spanned is created.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
return ReturnYouTubeDislike.getDislikeSpanForShort(original);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote int that matches {@link ReturnYouTubeDislike.Vote#value}
|
||||
*/
|
||||
public static void sendVote(int vote) {
|
||||
ReturnYouTubeDislike.sendVote(vote);
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
ReturnYouTubeDislike.sendVote(v);
|
||||
updateOldUIDislikesTextView();
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogHelper.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import app.revanced.integrations.settings.SettingsEnum;
|
||||
public class SpoofAppVersionPatch {
|
||||
|
||||
public static String getYouTubeVersionOverride(String version) {
|
||||
if (SettingsEnum.SPOOF_APP_VERSION.getBoolean()){
|
||||
// Override with the most recent version that does not show the new UI player layout.
|
||||
// If the new UI shows up for some users, then change this to an older version (such as 17.29.34).
|
||||
return "17.30.34";
|
||||
if (SettingsEnum.SPOOF_APP_VERSION.getBoolean()) {
|
||||
return SettingsEnum.SPOOF_APP_VERSION_TARGET.getString();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SpoofSignatureVerificationPatch {
|
||||
/**
|
||||
* Protobuf parameters used for autoplay in scrim.
|
||||
* Prepend this parameter to mute video playback (for autoplay in feed)
|
||||
*/
|
||||
private static final String PROTOBUF_PARAMETER_SCRIM = "SAFgAXgB";
|
||||
|
||||
/**
|
||||
* Protobuf parameter of shorts and YouTube stories.
|
||||
* Known issue: captions are positioned on upper area in the player.
|
||||
*/
|
||||
private static final String PROTOBUF_PARAMETER_SHORTS = "8AEB"; // "8AEByAMTuAQP"
|
||||
|
||||
/**
|
||||
* Target Protobuf parameters.
|
||||
*/
|
||||
private static final String[] PROTOBUF_PARAMETER_TARGETS = {
|
||||
"YAHI", // Autoplay in feed
|
||||
"SAFg" // Autoplay in scrim
|
||||
};
|
||||
|
||||
/**
|
||||
* On app first start, the first video played usually contains a single non-default window setting value
|
||||
* and all other subtitle settings for the video are (incorrect) default shorts window settings.
|
||||
* For this situation, the shorts settings must be replaced.
|
||||
*
|
||||
* But some videos use multiple text positions on screen (such as https://youtu.be/3hW1rMNC89o),
|
||||
* and by chance many of the subtitles uses window positions that match a default shorts position.
|
||||
* To handle these videos, selectively allowing the shorts specific window settings to 'pass thru' unchanged,
|
||||
* but only if the video contains multiple non-default subtitle window positions.
|
||||
*
|
||||
* Do not enable 'pass thru mode' until this many non default subtitle settings are observed for a single video.
|
||||
*/
|
||||
private static final int NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU = 2;
|
||||
|
||||
/**
|
||||
* The number of non default subtitle settings encountered for the current video.
|
||||
*/
|
||||
private static int numberOfNonDefaultSettingsObserved;
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param originalValue originalValue protobuf parameter
|
||||
*/
|
||||
public static String overrideProtobufParameter(String originalValue) {
|
||||
try {
|
||||
if (!SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + originalValue);
|
||||
|
||||
// Video is Short or Story.
|
||||
var isPlayingShorts = originalValue.contains(PROTOBUF_PARAMETER_SHORTS);
|
||||
if (isPlayingShorts) return originalValue;
|
||||
|
||||
boolean isPlayingFeed = containsAny(originalValue, PROTOBUF_PARAMETER_TARGETS) && PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL;
|
||||
if (isPlayingFeed) {
|
||||
// Videos in feed won't autoplay with sound.
|
||||
return PROTOBUF_PARAMETER_SCRIM + PROTOBUF_PARAMETER_SHORTS;
|
||||
} else {
|
||||
// Spoof the parameter to prevent playback issues.
|
||||
return PROTOBUF_PARAMETER_SHORTS;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "overrideProtobufParameter failure", ex);
|
||||
}
|
||||
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point. Runs off the main thread.
|
||||
* <p>
|
||||
* Used to check the response code of video playback requests made by YouTube.
|
||||
* Response code of interest is 403 that indicate a signature verification failure for the current request
|
||||
*
|
||||
* @param responseCode HTTP status code of the completed YouTube connection
|
||||
*/
|
||||
public static void onResponse(int responseCode) {
|
||||
try {
|
||||
if (responseCode < 400 || responseCode >= 500) {
|
||||
return; // everything normal
|
||||
}
|
||||
LogHelper.printDebug(() -> "YouTube HTTP status code: " + responseCode);
|
||||
|
||||
if (SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) {
|
||||
return; // already enabled
|
||||
}
|
||||
|
||||
SettingsEnum.SIGNATURE_SPOOFING.saveValue(true);
|
||||
ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues");
|
||||
// it would be great if the video could be forcefully reloaded, but currently there is no code to do this
|
||||
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onResponse failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Last WindowsSetting constructor values. Values are checked for changes to reduce log spam.
|
||||
*/
|
||||
private static int lastAp, lastAh, lastAv;
|
||||
private static boolean lastVs, lastSd;
|
||||
|
||||
/**
|
||||
* Injection point. Overrides values passed into SubtitleWindowSettings constructor.
|
||||
*
|
||||
* @param ap anchor position. A bitmask with 6 bit fields, that appears to indicate the layout position on screen
|
||||
* @param ah anchor horizontal. A percentage [0, 100], that appears to be a horizontal text anchor point
|
||||
* @param av anchor vertical. A percentage [0, 100], that appears to be a vertical text anchor point
|
||||
* @param vs appears to indicate if subtitles exist, and the value is always true.
|
||||
* @param sd function is not entirely clear
|
||||
*/
|
||||
public static int[] getSubtitleWindowSettingsOverride(int ap, int ah, int av, boolean vs, boolean sd) {
|
||||
final boolean signatureSpoofing = SettingsEnum.SIGNATURE_SPOOFING.getBoolean();
|
||||
if (SettingsEnum.DEBUG.getBoolean()) {
|
||||
if (ap != lastAp || ah != lastAh || av != lastAv || vs != lastVs || sd != lastSd) {
|
||||
LogHelper.printDebug(() -> "video: " + VideoInformation.getVideoId() + " spoof: " + signatureSpoofing
|
||||
+ " ap:" + ap + " ah:" + ah + " av:" + av + " vs:" + vs + " sd:" + sd);
|
||||
lastAp = ap;
|
||||
lastAh = ah;
|
||||
lastAv = av;
|
||||
lastVs = vs;
|
||||
lastSd = sd;
|
||||
}
|
||||
}
|
||||
|
||||
// Videos with custom captions that specify screen positions appear to always have correct screen positions (even with spoofing).
|
||||
// But for auto generated and most other captions, the spoof incorrectly gives various default Shorts caption settings.
|
||||
// Check for these known default shorts captions parameters, and replace with the known correct values.
|
||||
//
|
||||
// If a regular video uses a custom subtitle setting that match a default short setting,
|
||||
// then this will incorrectly replace the setting.
|
||||
// But, if the video uses multiple subtitles in different screen locations, then detect the non-default values
|
||||
// and do not replace any window settings for the video (regardless if they match a shorts default).
|
||||
if (signatureSpoofing && !PlayerType.getCurrent().isNoneOrHidden()
|
||||
&& numberOfNonDefaultSettingsObserved < NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU) {
|
||||
for (SubtitleWindowReplacementSettings setting : SubtitleWindowReplacementSettings.values()) {
|
||||
if (setting.match(ap, ah, av, vs, sd)) {
|
||||
return setting.replacementSetting();
|
||||
}
|
||||
}
|
||||
|
||||
numberOfNonDefaultSettingsObserved++;
|
||||
LogHelper.printDebug(() ->
|
||||
numberOfNonDefaultSettingsObserved < NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU
|
||||
? "Non default subtitle found."
|
||||
: "Multiple non default subtitles found. Allowing all subtitles for this video to pass thru unchanged.");
|
||||
}
|
||||
|
||||
return new int[]{ap, ah, av};
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setCurrentVideoId(@NonNull String videoId) {
|
||||
try {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return;
|
||||
}
|
||||
currentVideoId = videoId;
|
||||
numberOfNonDefaultSettingsObserved = 0;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setCurrentVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Known incorrect default Shorts subtitle parameters, and the corresponding correct (non-Shorts) values.
|
||||
*/
|
||||
private enum SubtitleWindowReplacementSettings {
|
||||
DEFAULT_SHORTS_PARAMETERS_1(10, 50, 0, true, false,
|
||||
34, 50, 95),
|
||||
DEFAULT_SHORTS_PARAMETERS_2(9, 20, 0, true, false,
|
||||
34, 50, 90),
|
||||
DEFAULT_SHORTS_PARAMETERS_3(9, 20, 0, true, true,
|
||||
33, 20, 100);
|
||||
|
||||
// original values
|
||||
final int ap, ah, av;
|
||||
final boolean vs, sd;
|
||||
|
||||
// replacement int values
|
||||
final int[] replacement;
|
||||
|
||||
SubtitleWindowReplacementSettings(int ap, int ah, int av, boolean vs, boolean sd,
|
||||
int replacementAp, int replacementAh, int replacementAv) {
|
||||
this.ap = ap;
|
||||
this.ah = ah;
|
||||
this.av = av;
|
||||
this.vs = vs;
|
||||
this.sd = sd;
|
||||
this.replacement = new int[]{replacementAp, replacementAh, replacementAv};
|
||||
}
|
||||
|
||||
boolean match(int ap, int ah, int av, boolean vs, boolean sd) {
|
||||
return this.ap == ap && this.ah == ah && this.av == av && this.vs == vs && this.sd == sd;
|
||||
}
|
||||
|
||||
int[] replacementSetting() {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class VideoBufferPatch {
|
||||
|
||||
public static int getMaxBuffer() {
|
||||
int confVal = SettingsEnum.MAX_BUFFER.getInt();
|
||||
if (confVal < 1) confVal = 1;
|
||||
return confVal;
|
||||
}
|
||||
|
||||
public static int getPlaybackBuffer() {
|
||||
int confVal = SettingsEnum.PLAYBACK_MAX_BUFFER.getInt();
|
||||
if (confVal < 1) confVal = 1;
|
||||
return confVal;
|
||||
}
|
||||
|
||||
public static int getReBuffer() {
|
||||
int confVal = SettingsEnum.MAX_PLAYBACK_BUFFER_AFTER_REBUFFER.getInt();
|
||||
if (confVal < 1) confVal = 1;
|
||||
return confVal;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,121 +1,192 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/**
|
||||
* Hooking class for the current playing video.
|
||||
*/
|
||||
public final class VideoInformation {
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
private static final String SEEK_METHOD_NAME = "seekTo";
|
||||
|
||||
private static WeakReference<Object> playerController;
|
||||
private static Method seekMethod;
|
||||
|
||||
@NonNull
|
||||
private static String videoId = "";
|
||||
private static long videoLength = 1;
|
||||
private static long videoTime = -1;
|
||||
|
||||
private static long videoLength = 0;
|
||||
private static volatile long videoTime = -1; // must be volatile. Value is set off main thread from high precision patch hook
|
||||
/**
|
||||
* The current playback speed
|
||||
*/
|
||||
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
|
||||
/**
|
||||
* Hook into PlayerController.onCreate() method.
|
||||
* Injection point.
|
||||
* Sets a reference to the YouTube playback controller.
|
||||
*
|
||||
* @param thisRef Reference to the player controller object.
|
||||
*/
|
||||
public static void playerController_onCreateHook(final Object thisRef) {
|
||||
playerController = new WeakReference<>(thisRef);
|
||||
videoLength = 1;
|
||||
videoLength = 0;
|
||||
videoTime = -1;
|
||||
|
||||
try {
|
||||
seekMethod = thisRef.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
|
||||
seekMethod.setAccessible(true);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to initialize", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video id.
|
||||
* Injection point.
|
||||
*
|
||||
* @param videoId The id of the video.
|
||||
* @param newlyLoadedVideoId id of the current video
|
||||
*/
|
||||
public static void setVideoId(String videoId) {
|
||||
LogHelper.printDebug(() -> "Setting current video id to: " + videoId);
|
||||
|
||||
VideoInformation.videoId = videoId;
|
||||
public static void setVideoId(@NonNull String newlyLoadedVideoId) {
|
||||
if (!videoId.equals(newlyLoadedVideoId)) {
|
||||
LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId);
|
||||
videoId = newlyLoadedVideoId;
|
||||
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video length.
|
||||
* Injection point.
|
||||
* Called when user selects a playback speed.
|
||||
*
|
||||
* @param userSelectedPlaybackSpeed The playback speed the user selected
|
||||
*/
|
||||
public static void userSelectedPlaybackSpeed(float userSelectedPlaybackSpeed) {
|
||||
LogHelper.printDebug(() -> "User selected playback speed: " + userSelectedPlaybackSpeed);
|
||||
playbackSpeed = userSelectedPlaybackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the current playback speed.
|
||||
*
|
||||
* <b> Used exclusively by {@link RememberPlaybackSpeedPatch} </b>
|
||||
*/
|
||||
public static void overridePlaybackSpeed(float speedOverride) {
|
||||
if (playbackSpeed != speedOverride) {
|
||||
LogHelper.printDebug(() -> "Overriding playback speed to: " + speedOverride);
|
||||
playbackSpeed = speedOverride;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param length The length of the video in milliseconds.
|
||||
*/
|
||||
public static void setVideoLength(final long length) {
|
||||
LogHelper.printDebug(() -> "Setting current video length to " + length);
|
||||
videoLength = length;
|
||||
if (videoLength != length) {
|
||||
LogHelper.printDebug(() -> "Current video length: " + length);
|
||||
videoLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video time.
|
||||
* Injection point.
|
||||
* Called off the main thread approximately every 50ms to 100ms
|
||||
*
|
||||
* @param time The time of the video in milliseconds.
|
||||
* @param currentPlaybackTime The current playback time of the video in milliseconds.
|
||||
*/
|
||||
public static void setVideoTime(final long time) {
|
||||
LogHelper.printDebug(() -> "Current video time " + time);
|
||||
videoTime = time;
|
||||
public static void setVideoTimeHighPrecision(final long currentPlaybackTime) {
|
||||
videoTime = currentPlaybackTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek on the current video.
|
||||
* Does not function for playback of Shorts or Stories.
|
||||
*
|
||||
* Caution: If called from a videoTimeHook() callback,
|
||||
* this will cause a recursive call into the same videoTimeHook() callback.
|
||||
*
|
||||
* @param millisecond The millisecond to seek the video to.
|
||||
* @return if the seek was successful
|
||||
*/
|
||||
public static void seekTo(final long millisecond) {
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
if (seekMethod == null) {
|
||||
LogHelper.printDebug(() -> "seekMethod was null");
|
||||
return;
|
||||
}
|
||||
public static boolean seekTo(final long millisecond) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
if (seekMethod == null) {
|
||||
LogHelper.printException(() -> "seekMethod was null");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Seeking to " + millisecond);
|
||||
seekMethod.invoke(playerController.get(), millisecond);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to seek", ex);
|
||||
}
|
||||
});
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Seeking to " + millisecond);
|
||||
return (Boolean) seekMethod.invoke(playerController.get(), millisecond);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to seek", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean seekToRelative(long millisecondsRelative) {
|
||||
return seekTo(videoTime + millisecondsRelative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the current video playing.
|
||||
* Id of the current video playing. Includes Shorts and YouTube Stories.
|
||||
*
|
||||
* @return The id of the video. Empty string if not set yet.
|
||||
*/
|
||||
public static String getCurrentVideoId() {
|
||||
@NonNull
|
||||
public static String getVideoId() {
|
||||
return videoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the current video playing.
|
||||
*
|
||||
* @return The length of the video in milliseconds. 1 if not set yet.
|
||||
* @return The current playback speed.
|
||||
*/
|
||||
public static long getCurrentVideoLength() {
|
||||
public static float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the current video playing. Includes Shorts and YouTube Stories.
|
||||
*
|
||||
* @return The length of the video in milliseconds.
|
||||
* If the video is not yet loaded, or if the video is playing in the background with no video visible,
|
||||
* then this returns zero.
|
||||
*/
|
||||
public static long getVideoLength() {
|
||||
return videoLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the current video playing.
|
||||
* Playback time of the current video playing.
|
||||
* Value can lag up to approximately 100ms behind the actual current video playback time.
|
||||
*
|
||||
* Note: Code inside a videoTimeHook patch callback
|
||||
* should use the callback video time and avoid using this method
|
||||
* (in situations of recursive hook callbacks, the value returned here may be outdated).
|
||||
*
|
||||
* Includes Shorts and YouTube Stories.
|
||||
*
|
||||
* @return The time of the video in milliseconds. -1 if not set yet.
|
||||
*/
|
||||
public static long getVideoTime() {
|
||||
return videoTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the playback is at the end of the video.
|
||||
*
|
||||
* If video is playing in the background with no video visible,
|
||||
* this always returns false (even if the video is actually at the end)
|
||||
*/
|
||||
public static boolean isAtEndOfVideo() {
|
||||
return videoTime > 0 && videoLength > 0 && videoTime >= videoLength;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class WideSearchbarPatch {
|
||||
public static boolean enableWideSearchbar() {
|
||||
return SettingsEnum.WIDE_SEARCHBAR.getBoolean();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package app.revanced.integrations.patches.downloads.views
|
||||
|
||||
class DownloadOptions {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,12 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
public class CustomVideoSpeedPatch {
|
||||
// Values are useless as they are being overridden by the respective patch.
|
||||
// This generates a .array segment in Dalvik bytecode
|
||||
// which the patch utilizes to store the video speeds in, only
|
||||
// if it has two or more default values.
|
||||
public static final float[] videoSpeeds = { 0, 0 };
|
||||
/**
|
||||
* Default playback speeds offered by YouTube.
|
||||
* Values are also used by {@link RememberPlaybackSpeedPatch}.
|
||||
*
|
||||
* If custom video speed is applied,
|
||||
* then this array is overwritten by the patch with custom speeds
|
||||
*/
|
||||
public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
import static app.revanced.integrations.utils.SharedPrefHelper.SharedPrefNames.REVANCED_PREFS;
|
||||
import static app.revanced.integrations.utils.SharedPrefHelper.getFloat;
|
||||
import static app.revanced.integrations.utils.SharedPrefHelper.saveFloat;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
|
||||
public final class RememberPlaybackRatePatch {
|
||||
private static final String REMEMBERED_PLAYBACK_RATE_PREFERENCE_KEY = "revanced_remember_playback_rate_last_value";
|
||||
|
||||
public static void rememberPlaybackRate(final float selectedPlaybackRate) {
|
||||
if (!SettingsEnum.REMEMBER_PLAYBACK_RATE_SELECTED.getBoolean()) return;
|
||||
|
||||
Toast.makeText(ReVancedUtils.getContext(), "Playback rate will be remembered", Toast.LENGTH_SHORT).show();
|
||||
|
||||
LogHelper.printDebug(() -> "Remembering playback rate: " + selectedPlaybackRate);
|
||||
saveFloat(REVANCED_PREFS, REMEMBERED_PLAYBACK_RATE_PREFERENCE_KEY, selectedPlaybackRate);
|
||||
}
|
||||
|
||||
public static float getRememberedPlaybackRate() {
|
||||
final var playbackRateOverride = getFloat(REVANCED_PREFS, REMEMBERED_PLAYBACK_RATE_PREFERENCE_KEY, -2f);
|
||||
|
||||
LogHelper.printDebug(() -> "Overriding playback rate: " + playbackRateOverride);
|
||||
return playbackRateOverride;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
import android.preference.ListPreference;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public final class RememberPlaybackSpeedPatch {
|
||||
|
||||
/**
|
||||
* PreferenceList entries and values, of all available playback speeds.
|
||||
*/
|
||||
private static String[] preferenceListEntries, preferenceListEntryValues;
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when a new video loads.
|
||||
*/
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return;
|
||||
}
|
||||
currentVideoId = videoId;
|
||||
VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when user selects a playback speed.
|
||||
*
|
||||
* @param playbackSpeed The playback speed the user selected
|
||||
*/
|
||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||
if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) {
|
||||
SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed);
|
||||
ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed
|
||||
*/
|
||||
public static float getPlaybackSpeedOverride() {
|
||||
return VideoInformation.getPlaybackSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a settings preference list.
|
||||
*
|
||||
* Normally this is done during patching by creating a static xml preference list,
|
||||
* but the available playback speeds differ depending if {@link CustomVideoSpeedPatch} is applied or not.
|
||||
*/
|
||||
public static void initializeListPreference(ListPreference preference) {
|
||||
if (preferenceListEntries == null) {
|
||||
float[] videoSpeeds = CustomVideoSpeedPatch.videoSpeeds;
|
||||
preferenceListEntries = new String[videoSpeeds.length];
|
||||
preferenceListEntryValues = new String[videoSpeeds.length];
|
||||
int i = 0;
|
||||
for (float speed : videoSpeeds) {
|
||||
String speedString = String.valueOf(speed);
|
||||
preferenceListEntries[i] = speedString + "x";
|
||||
preferenceListEntryValues[i] = speedString;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
preference.setEntries(preferenceListEntries);
|
||||
preference.setEntryValues(preferenceListEntryValues);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getContext;
|
||||
|
||||
import android.content.Context;
|
||||
package app.revanced.integrations.patches.theme;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
public class LithoThemePatch {
|
||||
public class ThemeLithoComponentsPatch {
|
||||
// color constants used in relation with litho components
|
||||
private static final int[] WHITE_VALUES = {
|
||||
-1, // comments chip background
|
||||
@@ -34,7 +31,7 @@ public class LithoThemePatch {
|
||||
* @param originalValue The original color value.
|
||||
* @return The new or original color value
|
||||
*/
|
||||
public static int applyLithoTheme(int originalValue) {
|
||||
public static int getValue(int originalValue) {
|
||||
if (ThemeHelper.isDarkTheme()) {
|
||||
if (anyEquals(originalValue, DARK_VALUES)) return getBlackColor();
|
||||
} else {
|
||||
@@ -44,29 +41,15 @@ public class LithoThemePatch {
|
||||
}
|
||||
|
||||
private static int getBlackColor() {
|
||||
if (blackColor == 0) blackColor = getColor("yt_black1");
|
||||
if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1");
|
||||
return blackColor;
|
||||
}
|
||||
|
||||
private static int getWhiteColor() {
|
||||
if (whiteColor == 0) whiteColor = getColor("yt_white1");
|
||||
if (whiteColor == 0) whiteColor = ReVancedUtils.getResourceColor("yt_white1");
|
||||
return whiteColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color for a color resource.
|
||||
*
|
||||
* @param name The color resource name.
|
||||
* @return The value of the color.
|
||||
*/
|
||||
private static int getColor(String name) {
|
||||
Context context = getContext();
|
||||
|
||||
return context != null ? context.getColor(context.getResources()
|
||||
.getIdentifier(name, "color", context.getPackageName())
|
||||
) : 0;
|
||||
}
|
||||
|
||||
private static boolean anyEquals(int value, int... of) {
|
||||
for (int v : of) if (value == v) return true;
|
||||
return false;
|
||||
@@ -0,0 +1,30 @@
|
||||
package app.revanced.integrations.patches.theme;
|
||||
|
||||
import android.graphics.Color;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public final class ThemePatch {
|
||||
public static final int DEFAULT_SEEKBAR_COLOR = 0xffff0000;
|
||||
|
||||
public static final int ORIGINAL_SEEKBAR_CLICKED_COLOR = -65536;
|
||||
|
||||
private static void resetSeekbarColor() {
|
||||
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value.");
|
||||
SettingsEnum.SEEKBAR_COLOR.saveValue("#" + Integer.toHexString(DEFAULT_SEEKBAR_COLOR));
|
||||
}
|
||||
|
||||
public static int getSeekbarClickedColorValue(final int colorValue) {
|
||||
// YouTube uses a specific color when the seekbar is clicked. Override in that case.
|
||||
return colorValue == ORIGINAL_SEEKBAR_CLICKED_COLOR ? getSeekbarColorValue() : colorValue;
|
||||
}
|
||||
|
||||
public static int getSeekbarColorValue() {
|
||||
try {
|
||||
return Color.parseColor(SettingsEnum.SEEKBAR_COLOR.getString());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
resetSeekbarColor();
|
||||
return DEFAULT_SEEKBAR_COLOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,7 @@ public class Requester {
|
||||
String url = apiUrl + route.compile(params).getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
// TODO: change the user agent string
|
||||
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
|
||||
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced");
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user