You've already forked revanced-integrations
mirror of
https://github.com/revanced/revanced-integrations
synced 2025-11-21 18:35:37 +01:00
Compare commits
84 Commits
v1.9.2-dev
...
v1.12.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f3d7a0f7 | ||
|
|
34c02aeb2a | ||
|
|
d57a64b659 | ||
|
|
86b25ea468 | ||
|
|
e879e40e56 | ||
|
|
2f2eeea5a7 | ||
|
|
3945a37944 | ||
|
|
fbf629fd62 | ||
|
|
ab0093ff83 | ||
|
|
4ac698fd4b | ||
|
|
848ed6e878 | ||
|
|
e71955d5bb | ||
|
|
7bf43c6896 | ||
|
|
0345a00d60 | ||
|
|
d996d3832a | ||
|
|
396ba77c20 | ||
|
|
67ff3172bb | ||
|
|
7af763f4b1 | ||
|
|
2fabdb245f | ||
|
|
3368023ff9 | ||
|
|
1fa59a62a1 | ||
|
|
66cf6c4263 | ||
|
|
7cdaf8df14 | ||
|
|
34ef27de79 | ||
|
|
6fe85a21e9 | ||
|
|
120188d643 | ||
|
|
d8d2a852d3 | ||
|
|
9469239264 | ||
|
|
cda1f3160c | ||
|
|
34a224e5de | ||
|
|
1aba976b28 | ||
|
|
6fef1b28a4 | ||
|
|
5ce16eedc6 | ||
|
|
7aec04647a | ||
|
|
ff2637cb4c | ||
|
|
77533cf3d6 | ||
|
|
d9f7679020 | ||
|
|
c237e3c02c | ||
|
|
3dda3de280 | ||
|
|
3a3ceec4b5 | ||
|
|
8fe73b25d9 | ||
|
|
fac49c7c10 | ||
|
|
e018746ceb | ||
|
|
f82dfce887 | ||
|
|
a1e358bc18 | ||
|
|
064d8e99a9 | ||
|
|
84d2484ace | ||
|
|
240e805489 | ||
|
|
376eb46f10 | ||
|
|
58f8172b2d | ||
|
|
758dfade7f | ||
|
|
0c9ad35fc9 | ||
|
|
5da0913d1d | ||
|
|
84c50c080c | ||
|
|
925f8bb297 | ||
|
|
f483af6d3a | ||
|
|
7a7b2db6f7 | ||
|
|
9adbc66197 | ||
|
|
7736ca4ef8 | ||
|
|
5f79196692 | ||
|
|
ecd687100c | ||
|
|
f74fb17a12 | ||
|
|
4f11b1d2eb | ||
|
|
5f0852c0c2 | ||
|
|
240e19966d | ||
|
|
59220d6e25 | ||
|
|
571ce75c84 | ||
|
|
2b2a70e6ea | ||
|
|
caa94fa6a4 | ||
|
|
4dce73a6fb | ||
|
|
a7e9390479 | ||
|
|
b826865ef4 | ||
|
|
b1109350fa | ||
|
|
777ffb1360 | ||
|
|
3fe0f3fa06 | ||
|
|
60ae48bdbf | ||
|
|
1f36aae81b | ||
|
|
3a978ecc92 | ||
|
|
e28edbadbf | ||
|
|
b2eac9099f | ||
|
|
2227b45020 | ||
|
|
22ed627a5d | ||
|
|
2ac29c2579 | ||
|
|
0e7bf05a0e |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -70,7 +70,7 @@ body:
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-integrations/labels/Bug%20report).
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-integrations/issues?q=label%3A%22Bug+report%22).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -101,7 +101,7 @@ body:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing bug report.
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -70,7 +70,7 @@ body:
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-integrations/labels/Feature%20request).
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-integrations/issues?q=label%3A%22Feature+request%22).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -97,7 +97,7 @@ body:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing feature request.
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ env.GPG_FINGERPRINT }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
[
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
|
||||
285
CHANGELOG.md
285
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin)
|
||||
publishing
|
||||
signing
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -53,28 +54,27 @@ dependencies {
|
||||
compileOnly(project(":stub"))
|
||||
}
|
||||
|
||||
tasks {
|
||||
// Because the signing plugin doesn't support signing APKs, do it manually.
|
||||
register("sign") {
|
||||
group = "signing"
|
||||
|
||||
dependsOn(build)
|
||||
tasks {
|
||||
val assembleReleaseSignApk by registering {
|
||||
dependsOn("assembleRelease")
|
||||
|
||||
val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk")
|
||||
|
||||
inputs.file(apk).withPropertyName("input")
|
||||
outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") })
|
||||
|
||||
doLast {
|
||||
val outputDirectory = layout.buildDirectory.dir("outputs/apk/release").get().asFile
|
||||
val integrationsApk = outputDirectory.resolve("${rootProject.name}-$version.apk")
|
||||
|
||||
org.gradle.security.internal.gnupg.GnupgSignatoryFactory().createSignatory(project).sign(
|
||||
integrationsApk.inputStream(),
|
||||
outputDirectory.resolve("${integrationsApk.name}.asc").outputStream(),
|
||||
)
|
||||
signing {
|
||||
useGpgCmd()
|
||||
sign(*inputs.files.files.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needed by gradle-semantic-release-plugin.
|
||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
|
||||
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
|
||||
publish {
|
||||
dependsOn(build)
|
||||
dependsOn("sign")
|
||||
dependsOn(assembleReleaseSignApk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.revanced.integrations.boostforreddit;
|
||||
|
||||
import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
|
||||
|
||||
import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||
static {
|
||||
INSTANCE = new FixSLinksPatch();
|
||||
}
|
||||
|
||||
private FixSLinksPatch() {
|
||||
webViewActivityClass = WebViewActivity.class;
|
||||
}
|
||||
|
||||
public static boolean patchResolveSLink(String link) {
|
||||
return INSTANCE.resolveSLink(link);
|
||||
}
|
||||
|
||||
public static void patchSetAccessToken(String accessToken) {
|
||||
INSTANCE.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ public class Utils {
|
||||
*
|
||||
* @return The manifest 'Version' entry of the patches.jar used during patching.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
public static String getPatchesReleaseVersion() {
|
||||
return ""; // Value is replaced during patching.
|
||||
}
|
||||
@@ -89,6 +90,7 @@ public class Utils {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hide a view by setting its layout height and width to 1dp.
|
||||
*
|
||||
@@ -96,11 +98,24 @@ public class Utils {
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
|
||||
if (!condition.get()) return;
|
||||
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Hiding view with setting: " + condition);
|
||||
/**
|
||||
* Hide a view by setting its layout height and width to 1dp.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
|
||||
if (condition) {
|
||||
hideViewByLayoutParams(view);
|
||||
return true;
|
||||
}
|
||||
|
||||
hideViewByLayoutParams(view);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,20 +125,42 @@ public class Utils {
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
|
||||
if (!condition.get()) return;
|
||||
|
||||
Logger.printDebug(() -> "Hiding view with setting: " + condition);
|
||||
|
||||
view.setVisibility(View.GONE);
|
||||
if (hideViewUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeViewFromParentUnderConditions(BooleanSetting setting, View view) {
|
||||
if (setting.get()) {
|
||||
/**
|
||||
* Hide a view by setting its visibility to GONE.
|
||||
*
|
||||
* @param condition The setting to check for hiding the view.
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
public static boolean hideViewUnderCondition(boolean condition, View view) {
|
||||
if (condition) {
|
||||
view.setVisibility(View.GONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
|
||||
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
|
||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
||||
if (setting) {
|
||||
ViewParent parent = view.getParent();
|
||||
if (parent instanceof ViewGroup) {
|
||||
((ViewGroup) parent).removeView(view);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,6 +273,8 @@ public class Utils {
|
||||
@NonNull MatchFilter<View> filter) {
|
||||
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
||||
View childAt = viewGroup.getChildAt(i);
|
||||
Logger.printDebug(() -> "View id: " + childAt.getId() + " tag: " + childAt.getTag());
|
||||
|
||||
if (filter.matches(childAt)) {
|
||||
//noinspection unchecked
|
||||
return (T) childAt;
|
||||
@@ -249,6 +288,25 @@ public class Utils {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ViewParent getParentView(@NonNull View view, int nthParent) {
|
||||
ViewParent parent = view.getParent();
|
||||
|
||||
int currentDepth = 0;
|
||||
while (++currentDepth < nthParent && parent != null) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
if (currentDepth == nthParent) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
final int currentDepthLog = currentDepth;
|
||||
Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
|
||||
+ " and instead found at: " + currentDepthLog + " view: " + view);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void restartApp(@NonNull Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package app.revanced.integrations.shared.fixes.slink;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.shared.Utils.getContext;
|
||||
|
||||
|
||||
/**
|
||||
* Base class to implement /s/ link resolution in 3rd party Reddit apps.
|
||||
* <br>
|
||||
* <br>
|
||||
* Usage:
|
||||
* <br>
|
||||
* <br>
|
||||
* An implementation of this class must have two static methods that are called by the app:
|
||||
* <ul>
|
||||
* <li>public static boolean patchResolveSLink(String link)</li>
|
||||
* <li>public static void patchSetAccessToken(String accessToken)</li>
|
||||
* </ul>
|
||||
* The static methods must call the instance methods of the base class.
|
||||
* <br>
|
||||
* The singleton pattern can be used to access the instance of the class:
|
||||
* <pre>
|
||||
* {@code
|
||||
* {
|
||||
* INSTANCE = new FixSLinksPatch();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* Set the app's web view activity class as a fallback to open /s/ links if the resolution fails:
|
||||
* <pre>
|
||||
* {@code
|
||||
* private FixSLinksPatch() {
|
||||
* webViewActivityClass = WebViewActivity.class;
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* Hook the app's navigation handler to call this method before doing any of its own resolution:
|
||||
* <pre>
|
||||
* {@code
|
||||
* public static boolean patchResolveSLink(Context context, String link) {
|
||||
* return INSTANCE.resolveSLink(context, link);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* If this method returns true, the app should early return and not do any of its own resolution.
|
||||
* <br>
|
||||
* <br>
|
||||
* Hook the app's access token so that this class can use it to resolve /s/ links:
|
||||
* <pre>
|
||||
* {@code
|
||||
* public static void patchSetAccessToken(String accessToken) {
|
||||
* INSTANCE.setAccessToken(access_token);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public abstract class BaseFixSLinksPatch {
|
||||
/**
|
||||
* The class of the activity used to open links in a web view if resolving them fails.
|
||||
*/
|
||||
protected Class<? extends Activity> webViewActivityClass;
|
||||
|
||||
/**
|
||||
* The access token used to resolve the /s/ link.
|
||||
*/
|
||||
protected String accessToken;
|
||||
|
||||
/**
|
||||
* The URL that was trying to be resolved before the access token was set.
|
||||
* If this is not null, the URL will be resolved right after the access token is set.
|
||||
*/
|
||||
protected String pendingUrl;
|
||||
|
||||
/**
|
||||
* The singleton instance of the class.
|
||||
*/
|
||||
protected static BaseFixSLinksPatch INSTANCE;
|
||||
|
||||
public boolean resolveSLink(String link) {
|
||||
switch (resolveLink(link)) {
|
||||
case ACCESS_TOKEN_START: {
|
||||
pendingUrl = link;
|
||||
return true;
|
||||
}
|
||||
case DO_NOTHING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ResolveResult resolveLink(String link) {
|
||||
Context context = getContext();
|
||||
if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
|
||||
// A link ends with #bypass if it failed to resolve below.
|
||||
// resolveLink is called with the same link again but this time with #bypass
|
||||
// so that the link is opened in the app browser instead of trying to resolve it again.
|
||||
if (link.endsWith("#bypass")) {
|
||||
openInAppBrowser(context, link);
|
||||
|
||||
return ResolveResult.DO_NOTHING;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Resolving " + link);
|
||||
|
||||
if (accessToken == null) {
|
||||
// This is not optimal.
|
||||
// However, an accessToken is necessary to make an authenticated request to Reddit.
|
||||
// in case Reddit has banned the IP - e.g. VPN.
|
||||
Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||
context.startActivity(startIntent);
|
||||
|
||||
return ResolveResult.ACCESS_TOKEN_START;
|
||||
}
|
||||
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
String bypassLink = link + "#bypass";
|
||||
|
||||
String finalLocation = bypassLink;
|
||||
try {
|
||||
HttpURLConnection connection = getHttpURLConnection(link, accessToken);
|
||||
connection.connect();
|
||||
String location = connection.getHeaderField("location");
|
||||
connection.disconnect();
|
||||
|
||||
Objects.requireNonNull(location, "Location is null");
|
||||
|
||||
finalLocation = location;
|
||||
Logger.printDebug(() -> "Resolved " + link + " to " + location);
|
||||
} catch (SocketTimeoutException e) {
|
||||
Logger.printException(() -> "Timeout when trying to resolve " + link, e);
|
||||
finalLocation = bypassLink;
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Failed to resolve " + link, e);
|
||||
finalLocation = bypassLink;
|
||||
} finally {
|
||||
Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation));
|
||||
startIntent.setPackage(context.getPackageName());
|
||||
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
return ResolveResult.DO_NOTHING;
|
||||
}
|
||||
|
||||
return ResolveResult.CONTINUE;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
Logger.printDebug(() -> "Setting access token");
|
||||
|
||||
this.accessToken = accessToken;
|
||||
|
||||
// In case a link was trying to be resolved before access token was set.
|
||||
// The link is resolved now, after the access token is set.
|
||||
if (pendingUrl != null) {
|
||||
String link = pendingUrl;
|
||||
pendingUrl = null;
|
||||
|
||||
Logger.printDebug(() -> "Opening pending URL");
|
||||
|
||||
resolveLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
private void openInAppBrowser(Context context, String link) {
|
||||
Intent intent = new Intent(context, webViewActivityClass);
|
||||
intent.putExtra("url", link);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException {
|
||||
URL url = new URL(link);
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod("HEAD");
|
||||
connection.setConnectTimeout(2000);
|
||||
connection.setReadTimeout(2000);
|
||||
|
||||
if (accessToken != null) {
|
||||
Logger.printDebug(() -> "Setting access token to make /s/ request");
|
||||
|
||||
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null");
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package app.revanced.integrations.shared.fixes.slink;
|
||||
|
||||
public enum ResolveResult {
|
||||
// Let app handle rest of stuff
|
||||
CONTINUE,
|
||||
// Start app, to make it cache its access_token
|
||||
ACCESS_TOKEN_START,
|
||||
// Don't do anything - we started resolving
|
||||
DO_NOTHING
|
||||
}
|
||||
@@ -2,13 +2,15 @@ package app.revanced.integrations.shared.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
|
||||
/**
|
||||
* If an Enum value is removed or changed, any saved or imported data using the
|
||||
* non-existent value will be reverted to the default value
|
||||
@@ -98,4 +100,18 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on if this setting is currently set to any of the provided types.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Setting.Availability availability(@NonNull T... types) {
|
||||
return () -> {
|
||||
T currentEnumType = get();
|
||||
for (T enumType : types) {
|
||||
if (currentEnumType == enumType) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,24 @@
|
||||
package app.revanced.integrations.syncforreddit;
|
||||
|
||||
import android.os.StrictMode;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch;
|
||||
|
||||
public final class FixSLinksPatch {
|
||||
public static String resolveSLink(String link) {
|
||||
if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
|
||||
Logger.printInfo(() -> "Resolving " + link);
|
||||
try {
|
||||
URL url = new URL(link);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
connection.setRequestMethod("HEAD");
|
||||
/** @noinspection unused*/
|
||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||
static {
|
||||
INSTANCE = new FixSLinksPatch();
|
||||
}
|
||||
|
||||
// Disable strict mode in order to allow network access on the main thread.
|
||||
// This is not ideal, but it's the easiest solution for now.
|
||||
final var currentPolicy = StrictMode.getThreadPolicy();
|
||||
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
||||
StrictMode.setThreadPolicy(policy);
|
||||
private FixSLinksPatch() {
|
||||
webViewActivityClass = WebViewActivity.class;
|
||||
}
|
||||
|
||||
connection.connect();
|
||||
String location = connection.getHeaderField("location");
|
||||
connection.disconnect();
|
||||
public static boolean patchResolveSLink(String link) {
|
||||
return INSTANCE.resolveSLink(link);
|
||||
}
|
||||
|
||||
// Restore the original strict mode policy.
|
||||
StrictMode.setThreadPolicy(currentPolicy);
|
||||
|
||||
Logger.printInfo(() -> "Resolved " + link + " -> " + location);
|
||||
|
||||
return location;
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Failed to resolve " + link, e);
|
||||
}
|
||||
}
|
||||
|
||||
return link;
|
||||
public static void patchSetAccessToken(String accessToken) {
|
||||
INSTANCE.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import app.revanced.integrations.tiktok.settings.Settings;
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofSimPatch {
|
||||
|
||||
private static final Boolean ENABLED = Settings.SIM_SPOOF.get();
|
||||
private static final boolean ENABLED = Settings.SIM_SPOOF.get();
|
||||
|
||||
public static String getCountryIso(String value) {
|
||||
if (ENABLED) {
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ThemeHelper {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String darkThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_black3";
|
||||
@@ -58,6 +59,7 @@ public class ThemeHelper {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String lightThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_white1";
|
||||
|
||||
@@ -380,7 +380,7 @@ public abstract class TrieSearch<T> {
|
||||
throw new IllegalArgumentException("endIndex: " + endIndex
|
||||
+ " is greater than texToSearchLength: " + textToSearchLength);
|
||||
}
|
||||
if (patterns.size() == 0) {
|
||||
if (patterns.isEmpty()) {
|
||||
return false; // No patterns were added.
|
||||
}
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
@@ -393,7 +393,7 @@ public abstract class TrieSearch<T> {
|
||||
* @return Estimated memory size (in kilobytes) of this instance.
|
||||
*/
|
||||
public int getEstimatedMemorySize() {
|
||||
if (patterns.size() == 0) {
|
||||
if (patterns.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
// Assume the device has less than 32GB of ram (and can use pointer compression),
|
||||
|
||||
@@ -190,16 +190,17 @@ public final class AlternativeThumbnailsPatch {
|
||||
* 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.
|
||||
* @return The alternative thumbnail url, or if not available NULL.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
|
||||
@Nullable
|
||||
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;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,14 +285,21 @@ public final class AlternativeThumbnailsPatch {
|
||||
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;
|
||||
String fallbackUrl = null;
|
||||
if (option.useStillImages) {
|
||||
fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
}
|
||||
if (fallbackUrl == null) {
|
||||
fallbackUrl = decodedUrl.sanitizedUrl;
|
||||
}
|
||||
|
||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
||||
} else if (option.useStillImages) {
|
||||
includeTracking = true; // Include view tracking parameters if present.
|
||||
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
if (sanitizedReplacementUrl == null) {
|
||||
return originalUrl; // Still capture is not available. Return the untouched original url.
|
||||
}
|
||||
} else {
|
||||
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
|
||||
}
|
||||
@@ -345,7 +353,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
return; // Not a thumbnail.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + url);
|
||||
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
||||
if (quality == null) {
|
||||
@@ -627,14 +635,17 @@ public final class AlternativeThumbnailsPatch {
|
||||
* 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";
|
||||
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
|
||||
|
||||
@Nullable
|
||||
static DecodedThumbnailUrl decodeImageUrl(String url) {
|
||||
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
|
||||
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
|
||||
if (urlPathStartIndex <= 0) return null;
|
||||
|
||||
final int urlPathEndIndex = url.indexOf('/', urlPathStartIndex);
|
||||
if (urlPathEndIndex < 0) return null;
|
||||
|
||||
final int videoIdStartIndex = url.indexOf('/', urlPathEndIndex) + 1;
|
||||
if (videoIdStartIndex <= 0) return null;
|
||||
|
||||
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
|
||||
@@ -647,15 +658,15 @@ public final class AlternativeThumbnailsPatch {
|
||||
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
||||
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
||||
|
||||
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
|
||||
return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, 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;
|
||||
/** Url path, such as 'vi' or 'vi_webp' */
|
||||
final String urlPath;
|
||||
final String videoId;
|
||||
/** Quality, such as hq720 or sddefault. */
|
||||
final String imageQuality;
|
||||
@@ -664,11 +675,11 @@ public final class AlternativeThumbnailsPatch {
|
||||
/** User view tracking parameters, only present on some images. */
|
||||
final String viewTrackingParameters;
|
||||
|
||||
DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
|
||||
DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
|
||||
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
||||
originalFullUrl = fullUrl;
|
||||
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
|
||||
urlPrefix = fullUrl.substring(0, videoIdStartIndex);
|
||||
urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex);
|
||||
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
|
||||
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
|
||||
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
||||
@@ -681,9 +692,12 @@ public final class AlternativeThumbnailsPatch {
|
||||
// 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).
|
||||
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
|
||||
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
|
||||
builder.append(urlPrefix);
|
||||
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
|
||||
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
|
||||
// So always use the primary domain for a higher success rate.
|
||||
builder.append(YOUTUBE_THUMBNAIL_DOMAIN).append(urlPath).append('/');
|
||||
builder.append(videoId).append('/');
|
||||
builder.append(qualityToUse.getAltImageNameToUse());
|
||||
builder.append('.').append(imageExtension);
|
||||
|
||||
@@ -3,7 +3,7 @@ package app.revanced.integrations.youtube.patches;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class MinimizedPlaybackPatch {
|
||||
public class BackgroundPlaybackPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
@@ -35,7 +35,7 @@ public class MinimizedPlaybackPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean overrideMinimizedPlaybackAvailable() {
|
||||
public static boolean overrideBackgroundPlaybackAvailable() {
|
||||
// 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,46 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class BypassImageRegionRestrictionsPatch {
|
||||
|
||||
private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get();
|
||||
|
||||
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
|
||||
|
||||
/**
|
||||
* YouTube static images domain. Includes user and channel avatar images and community post images.
|
||||
*/
|
||||
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all image urls loaded.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
|
||||
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
|
||||
|
||||
if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) {
|
||||
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CustomPlayerOverlayOpacityPatch {
|
||||
|
||||
public static void changeOpacity(ImageView imageView) {
|
||||
private static final int PLAYER_OVERLAY_OPACITY_LEVEL;
|
||||
|
||||
static {
|
||||
int opacity = Settings.PLAYER_OVERLAY_OPACITY.get();
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong("Player overlay opacity must be between 0-100");
|
||||
Utils.showToastLong(str("revanced_player_overlay_opacity_invalid_toast"));
|
||||
Settings.PLAYER_OVERLAY_OPACITY.resetToDefault();
|
||||
opacity = Settings.PLAYER_OVERLAY_OPACITY.defaultValue;
|
||||
}
|
||||
|
||||
imageView.setImageAlpha((opacity * 255) / 100);
|
||||
PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void changeOpacity(ImageView imageView) {
|
||||
imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableAutoCaptionsPatch {
|
||||
@@ -11,7 +12,9 @@ public class DisableAutoCaptionsPatch {
|
||||
public static boolean captionsButtonDisabled;
|
||||
|
||||
public static boolean autoCaptionsEnabled() {
|
||||
return Settings.AUTO_CAPTIONS.get();
|
||||
return Settings.AUTO_CAPTIONS.get()
|
||||
// Do not use auto captions for Shorts.
|
||||
&& !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
/** @noinspection unused*/
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class EnableTabletLayoutPatch {
|
||||
public static boolean enableTabletLayout() {
|
||||
return Settings.TABLET_LAYOUT.get();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,13 @@ import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideAutoplayButtonPatch {
|
||||
public static boolean isButtonShown() {
|
||||
return !Settings.HIDE_AUTOPLAY_BUTTON.get();
|
||||
|
||||
private static final boolean HIDE_AUTOPLAY_BUTTON_ENABLED = Settings.HIDE_AUTOPLAY_BUTTON.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideAutoPlayButton() {
|
||||
return HIDE_AUTOPLAY_BUTTON_ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*;
|
||||
|
||||
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.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.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
|
||||
= Utils.getResourceIdentifier("modern_miniplayer_subtitle_text", "id");
|
||||
|
||||
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
|
||||
|
||||
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && 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 {
|
||||
int opacity = Settings.MINIPLAYER_OPACITY.get();
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast"));
|
||||
Settings.MINIPLAYER_OPACITY.resetToDefault();
|
||||
opacity = Settings.MINIPLAYER_OPACITY.defaultValue;
|
||||
}
|
||||
|
||||
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 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 void 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
View subtitleText = Utils.getChildView((ViewGroup) group, true,
|
||||
view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT);
|
||||
|
||||
if (subtitleText != null) {
|
||||
subtitleText.setVisibility(View.GONE);
|
||||
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public final class NavigationButtonsPatch {
|
||||
}
|
||||
};
|
||||
|
||||
private static final Boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON
|
||||
private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON
|
||||
= Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get();
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,6 @@ package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.youtube.shared.PlayerOverlays;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -221,6 +221,10 @@ public class ReturnYouTubeDislikePatch {
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
|
||||
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml|")) {
|
||||
return original;
|
||||
}
|
||||
|
||||
final CharSequence replacement;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
// Regular video.
|
||||
@@ -289,6 +293,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
@NonNull String original) {
|
||||
try {
|
||||
CharSequence replacement = onLithoTextLoaded(conversionContext, original, true);
|
||||
|
||||
String replacementString = replacement.toString();
|
||||
if (!replacementString.equals(original)) {
|
||||
rollingNumberSpan = replacement;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class TabletLayoutPatch {
|
||||
|
||||
private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getTabletLayoutEnabled() {
|
||||
return TABLET_LAYOUT_ENABLED;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class TabletMiniPlayerOverridePatch {
|
||||
|
||||
public static boolean getTabletMiniPlayerOverride(boolean original) {
|
||||
if (Settings.USE_TABLET_MINIPLAYER.get())
|
||||
return true;
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -15,15 +14,21 @@ import java.util.Objects;
|
||||
* @noinspection unused
|
||||
*/
|
||||
public final class VideoInformation {
|
||||
|
||||
public interface PlaybackController {
|
||||
// Methods are added to YT classes during patching.
|
||||
boolean seekTo(long videoTime);
|
||||
boolean seekToRelative(long videoTimeOffset);
|
||||
}
|
||||
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
private static final String SEEK_METHOD_NAME = "seekTo";
|
||||
/**
|
||||
* Prefix present in all Short player parameters signature.
|
||||
*/
|
||||
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
|
||||
|
||||
private static WeakReference<Object> playerControllerRef;
|
||||
private static Method seekMethod;
|
||||
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
|
||||
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
|
||||
|
||||
@NonNull
|
||||
private static String videoId = "";
|
||||
@@ -45,20 +50,30 @@ public final class VideoInformation {
|
||||
*
|
||||
* @param playerController player controller object.
|
||||
*/
|
||||
public static void initialize(@NonNull Object playerController) {
|
||||
public static void initialize(@NonNull PlaybackController playerController) {
|
||||
try {
|
||||
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
|
||||
videoTime = -1;
|
||||
videoLength = 0;
|
||||
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
|
||||
seekMethod = playerController.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
|
||||
seekMethod.setAccessible(true);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to initialize", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param mdxPlayerDirector MDX player director object (casting mode).
|
||||
*/
|
||||
public static void initializeMdx(@NonNull PlaybackController mdxPlayerDirector) {
|
||||
try {
|
||||
mdxPlayerDirectorRef = new WeakReference<>(Objects.requireNonNull(mdxPlayerDirector));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to initialize MDX", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
@@ -177,18 +192,80 @@ public final class VideoInformation {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Seeking to " + adjustedSeekTime);
|
||||
//noinspection DataFlowIssue
|
||||
return (Boolean) seekMethod.invoke(playerControllerRef.get(), adjustedSeekTime);
|
||||
Logger.printDebug(() -> "Seeking to: " + adjustedSeekTime);
|
||||
|
||||
// Try regular playback controller first, and it will not succeed if casting.
|
||||
PlaybackController controller = playerControllerRef.get();
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
|
||||
} else {
|
||||
if (controller.seekTo(adjustedSeekTime)) return true;
|
||||
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
|
||||
// Else the video is loading or changing videos, or video is casting to a different device.
|
||||
}
|
||||
|
||||
// Try calling the seekTo method of the MDX player director (called when casting).
|
||||
// The difference has to be a different second mark in order to avoid infinite skip loops
|
||||
// as the Lounge API only supports seconds.
|
||||
if (adjustedSeekTime / 1000 == videoTime / 1000) {
|
||||
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
|
||||
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
|
||||
return false;
|
||||
}
|
||||
|
||||
controller = mdxPlayerDirectorRef.get();
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seekTo MXD because player controller is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
return controller.seekTo(adjustedSeekTime);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to seek", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection UnusedReturnValue*/
|
||||
public static boolean seekToRelative(long millisecondsRelative) {
|
||||
return seekTo(videoTime + millisecondsRelative);
|
||||
/**
|
||||
* Seeks a relative amount. Should always be used over {@link #seekTo(long)}
|
||||
* when the desired seek time is an offset of the current time.
|
||||
*
|
||||
* @noinspection UnusedReturnValue
|
||||
*/
|
||||
public static boolean seekToRelative(long seekTime) {
|
||||
Utils.verifyOnMainThread();
|
||||
try {
|
||||
Logger.printDebug(() -> "Seeking relative to: " + seekTime);
|
||||
|
||||
// Try regular playback controller first, and it will not succeed if casting.
|
||||
PlaybackController controller = playerControllerRef.get();
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
|
||||
} else {
|
||||
if (controller.seekToRelative(seekTime)) return true;
|
||||
Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD.");
|
||||
}
|
||||
|
||||
// Adjust the fine adjustment function so it's at least 1 second before/after.
|
||||
// Otherwise the fine adjustment will do nothing when casting.
|
||||
final long adjustedSeekTime;
|
||||
if (seekTime < 0) {
|
||||
adjustedSeekTime = Math.min(seekTime, -1000);
|
||||
} else {
|
||||
adjustedSeekTime = Math.max(seekTime, 1000);
|
||||
}
|
||||
|
||||
controller = mdxPlayerDirectorRef.get();
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
return controller.seekToRelative(adjustedSeekTime);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to seek relative", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,6 +355,7 @@ public final class VideoInformation {
|
||||
*
|
||||
* @see VideoState
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public static boolean isAtEndOfVideo() {
|
||||
return videoTime >= videoLength && videoLength > 0;
|
||||
}
|
||||
|
||||
@@ -56,10 +56,11 @@ public final class AdsFilter extends Filter {
|
||||
|
||||
final var buttonedAd = new StringFilterGroup(
|
||||
Settings.HIDE_BUTTONED_ADS,
|
||||
"_buttoned_layout",
|
||||
"full_width_square_image_layout",
|
||||
"_ad_with",
|
||||
"text_image_button_group_layout",
|
||||
"_buttoned_layout",
|
||||
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
|
||||
"image_button_group_layout",
|
||||
"full_width_square_image_layout",
|
||||
"video_display_button_group_layout",
|
||||
"landscape_image_wide_button_layout",
|
||||
"video_display_carousel_button_group_layout"
|
||||
|
||||
@@ -14,21 +14,37 @@ final class CommentsFilter extends Filter {
|
||||
private final ByteArrayFilterGroup emojiPickerBufferGroup;
|
||||
|
||||
public CommentsFilter() {
|
||||
var commentsByMembers = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_BY_MEMBERS_HEADER,
|
||||
"sponsorships_comments_header.eml",
|
||||
"sponsorships_comments_footer.eml"
|
||||
);
|
||||
|
||||
var comments = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_SECTION,
|
||||
"video_metadata_carousel",
|
||||
"_comments"
|
||||
);
|
||||
|
||||
var createAShort = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_CREATE_A_SHORT_BUTTON,
|
||||
"composer_short_creation_button.eml"
|
||||
);
|
||||
|
||||
var previewComment = new StringFilterGroup(
|
||||
Settings.HIDE_PREVIEW_COMMENT,
|
||||
Settings.HIDE_COMMENTS_PREVIEW_COMMENT,
|
||||
"|carousel_item",
|
||||
"comments_entry_point_teaser",
|
||||
"comments_entry_point_simplebox"
|
||||
);
|
||||
|
||||
var thanksButton = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_THANKS_BUTTON,
|
||||
"super_thanks_button.eml"
|
||||
);
|
||||
|
||||
commentComposer = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS,
|
||||
Settings.HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS,
|
||||
"comment_composer.eml"
|
||||
);
|
||||
|
||||
@@ -38,8 +54,11 @@ final class CommentsFilter extends Filter {
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
commentsByMembers,
|
||||
comments,
|
||||
createAShort,
|
||||
previewComment,
|
||||
thanksButton,
|
||||
commentComposer
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user