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
77 Commits
v0.122.1-d
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56cf001964 | ||
|
|
69c5028661 | ||
|
|
30f2c4f4c9 | ||
|
|
10a1e168d0 | ||
|
|
e79ebb26ec | ||
|
|
792dc0c522 | ||
|
|
8870f7ab0c | ||
|
|
1c9c51ca5f | ||
|
|
6f7a7b825e | ||
|
|
c4ee6ca4dd | ||
|
|
46dbbf5f86 | ||
|
|
0ce92c284d | ||
|
|
fb56e9a362 | ||
|
|
fa5713b4a0 | ||
|
|
92e8619cd7 | ||
|
|
9c635e5f91 | ||
|
|
f9102fa83b | ||
|
|
e7758a7ac4 | ||
|
|
fb433da6ad | ||
|
|
aa6f591141 | ||
|
|
0bb86694e2 | ||
|
|
d484f35127 | ||
|
|
f4e2d56b18 | ||
|
|
9394c512ba | ||
|
|
5d4c8b0a1b | ||
|
|
3a56431a28 | ||
|
|
fba7181e70 | ||
|
|
3b4f0206ad | ||
|
|
3d660e1b5e | ||
|
|
dd045ad985 | ||
|
|
0a15245f41 | ||
|
|
a18c134aaa | ||
|
|
0ea4e720ed | ||
|
|
c6a1b9fc59 | ||
|
|
d6ed0c061c | ||
|
|
3ba3d5108d | ||
|
|
c2112e066a | ||
|
|
6520499a27 | ||
|
|
b2d9dcd6c0 | ||
|
|
11abdf162f | ||
|
|
b514bdd7d0 | ||
|
|
eede028c96 | ||
|
|
9eb0471989 | ||
|
|
3dbfbf9e6f | ||
|
|
373820eb99 | ||
|
|
f936f240a7 | ||
|
|
b9d50fd3d8 | ||
|
|
11e7c04b70 | ||
|
|
2613e102cc | ||
|
|
2a582f9424 | ||
|
|
bc56aa5f07 | ||
|
|
1ebf8e04a6 | ||
|
|
c13504a0b3 | ||
|
|
0124f41cd6 | ||
|
|
31777e027e | ||
|
|
dedd2f658a | ||
|
|
7f0c2c507c | ||
|
|
17e78824b7 | ||
|
|
36fe5718cf | ||
|
|
355acc7023 | ||
|
|
dcca2e5e8d | ||
|
|
c4d01b2334 | ||
|
|
ce2ad04f60 | ||
|
|
1230d77f94 | ||
|
|
2735f99c0a | ||
|
|
64463dea5b | ||
|
|
4932a71088 | ||
|
|
f6e936812e | ||
|
|
c8510dbb4d | ||
|
|
d3021bcf0b | ||
|
|
ffcee71f79 | ||
|
|
8cbe2b5a92 | ||
|
|
abfbb0d7d5 | ||
|
|
ce0bedc5d3 | ||
|
|
5cf5d86913 | ||
|
|
7dd826ad71 | ||
|
|
58afcec641 |
59
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
59
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a very clearly broken issue.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced bug report
|
||||
|
||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-integrations/labels/bug).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: How did you find the bug? Any additional details that might help?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Add the steps to reproduce this bug including your environment.
|
||||
placeholder: Step 1. Download some files. Step 2. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
109
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: 'bug: '
|
||||
labels: ['Bug report']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Integrations bug report
|
||||
|
||||
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 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:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
- List used patches if applicable
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Error logs
|
||||
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution to the bug.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
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.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
46
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
46
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced feature request
|
||||
|
||||
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
|
||||
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-integrations/labels/feature-request).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Functionality
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
description: What is the current problem. Why does it require a feature request?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe your feature in detail. How does it solve the issue?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why should your feature should be considered?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
105
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
105
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: 'feat: '
|
||||
labels: ['Feature request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-integrations/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Integrations feature request
|
||||
|
||||
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 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:
|
||||
label: Feature description
|
||||
description: |
|
||||
- Describe your feature in detail
|
||||
- Add images, videos, links, examples, references, etc. if possible
|
||||
- Add the target application name in case you request a new patch
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
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.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
2
.github/config.yml
vendored
2
.github/config.yml
vendored
@@ -1,2 +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.
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||
|
||||
1
.github/workflows/pull_request.yml
vendored
1
.github/workflows/pull_request.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -23,23 +23,28 @@ jobs:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Cache
|
||||
|
||||
- name: Cache Node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ runner.home }}/.gradle/caches
|
||||
${{ runner.home }}/.gradle/wrapper
|
||||
.gradle
|
||||
node_modules
|
||||
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
|
||||
- name: Setup Java
|
||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
||||
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build clean
|
||||
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@
|
||||
/.idea
|
||||
/.vscode
|
||||
/*.log
|
||||
node_modules
|
||||
259
CHANGELOG.md
259
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(mapOf("path" to ":dummy")))
|
||||
compileOnly("androidx.annotation:annotation:1.6.0")
|
||||
compileOnly("androidx.annotation:annotation:1.7.0")
|
||||
compileOnly("androidx.appcompat:appcompat:1.7.0-alpha03")
|
||||
compileOnly("com.squareup.okhttp3:okhttp:5.0.0-alpha.11")
|
||||
compileOnly("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.content.Intent;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ChangeStartPagePatch {
|
||||
public static void changeIntent(Intent intent) {
|
||||
final var startPage = SettingsEnum.START_PAGE.getString();
|
||||
if (startPage.isEmpty()) return;
|
||||
|
||||
LogHelper.printDebug(() -> "Changing start page to " + startPage);
|
||||
intent.setAction("com.google.android.youtube.action." + startPage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class DisableRollingNumberAnimationsPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableRollingNumberAnimations() {
|
||||
return SettingsEnum.DISABLE_ROLLING_NUMBER_ANIMATIONS.getBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class GmsCoreSupport {
|
||||
private static final String GMS_CORE_PACKAGE_NAME
|
||||
= getGmsCoreVendor() + ".android.gms";
|
||||
private static final String DONT_KILL_MY_APP_LINK
|
||||
= "https://dontkillmyapp.com";
|
||||
private static final Uri GMS_CORE_PROVIDER
|
||||
= Uri.parse("content://" + getGmsCoreVendor() + ".android.gsf.gservices/prefix");
|
||||
|
||||
private static void search(Context context, String uriString, String message) {
|
||||
ReVancedUtils.showToastLong(message);
|
||||
|
||||
var intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(SearchManager.QUERY, uriString);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static void checkAvailability() {
|
||||
var context = Objects.requireNonNull(ReVancedUtils.getContext());
|
||||
|
||||
try {
|
||||
context.getPackageManager().getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
LogHelper.printInfo(() -> "GmsCore was not found", exception);
|
||||
search(context, getGmsCoreDownloadLink(), str("gms_core_not_installed_warning"));
|
||||
|
||||
// Gracefully exit the app, so it does not crash.
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||
if (client != null) return;
|
||||
LogHelper.printInfo(() -> "GmsCore is not running in the background");
|
||||
search(context, DONT_KILL_MY_APP_LINK, str("gms_core_not_running_warning"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String getGmsCoreDownloadLink() {
|
||||
final var vendor = getGmsCoreVendor();
|
||||
switch (vendor) {
|
||||
case "com.mgoogle":
|
||||
return "https://github.com/TeamVanced/VancedMicroG/releases/latest";
|
||||
case "app.revanced":
|
||||
return "https://github.com/revanced/gmscore/releases/latest";
|
||||
default:
|
||||
return vendor + ".android.gms";
|
||||
}
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
private static String getGmsCoreVendor() {
|
||||
return "app.revanced";
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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 java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class MicroGSupport {
|
||||
private static final String MICROG_VENDOR = "com.mgoogle";
|
||||
private static final String MICROG_PACKAGE_NAME = MICROG_VENDOR + ".android.gms";
|
||||
private static final String VANCED_MICROG_DOWNLOAD_LINK = "https://github.com/TeamVanced/VancedMicroG/releases/latest";
|
||||
private static final String DONT_KILL_MY_APP_LINK = "https://dontkillmyapp.com";
|
||||
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) {
|
||||
ReVancedUtils.showToastLong(message);
|
||||
|
||||
var intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setData(Uri.parse(uriString));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
public static void checkAvailability() {
|
||||
var context = Objects.requireNonNull(ReVancedUtils.getContext());
|
||||
|
||||
try {
|
||||
context.getPackageManager().getPackageInfo(MICROG_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
LogHelper.printInfo(() -> "Vanced MicroG was not found", exception);
|
||||
startIntent(context, VANCED_MICROG_DOWNLOAD_LINK, str("microg_not_installed_warning"));
|
||||
|
||||
// Gracefully exit the app, so it does not crash.
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,39 @@ package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class MinimizedPlaybackPatch {
|
||||
|
||||
public static boolean isPlaybackNotShort() {
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean playbackIsNotShort() {
|
||||
// Steps to verify most edge cases:
|
||||
// 1. Open a regular video
|
||||
// 2. Minimize app (PIP should appear)
|
||||
// 3. Reopen app
|
||||
// 4. Open a Short (without closing the regular video)
|
||||
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
|
||||
// 5. Minimize the app (PIP should not appear)
|
||||
// 6. Reopen app
|
||||
// 7. Close the Short
|
||||
// 8. Resume playing the regular video
|
||||
// 9. Minimize the app (PIP should appear)
|
||||
|
||||
if (!VideoInformation.lastVideoIdIsShort()) {
|
||||
return true; // Definitely is not a Short.
|
||||
}
|
||||
|
||||
// Might be a Short, or might be a prior regular video on screen again after a Short was closed.
|
||||
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Short,
|
||||
// But there's no way around this unless an additional hook is added to definitively detect
|
||||
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
@@ -27,19 +28,25 @@ import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislik
|
||||
* Handles all interaction of UI patch components.
|
||||
*
|
||||
* Known limitation:
|
||||
* Litho based Shorts player can experience temporarily frozen video playback if the RYD fetch takes too long.
|
||||
* The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
|
||||
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
|
||||
* not completed yet then the UI will be temporarily frozen.
|
||||
*
|
||||
* Temporary work around:
|
||||
* Enable app spoofing to version 18.33.40 or older, as that uses a non litho Shorts player.
|
||||
*
|
||||
* Permanent fix (yet to be implemented), either of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree
|
||||
* (and use that hook to force the shorts dislikes to update after the fetch is completed).
|
||||
* A (yet to be implemented) solution that fixes this problem. Any one of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree,
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
|
||||
|
||||
/**
|
||||
* RYD data for the current video on screen.
|
||||
*/
|
||||
@@ -318,8 +325,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
try {
|
||||
if (SettingsEnum.RYD_ENABLED.getBoolean() && !SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean()) {
|
||||
if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) {
|
||||
// +1 pixel is needed for some foreign languages that measure
|
||||
// the text different from what is used for layout (Greek in particular).
|
||||
// Probably a bug in Android, but who knows.
|
||||
// Single line mode is also used as an additional fix for this issue.
|
||||
return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right
|
||||
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
|
||||
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@@ -335,9 +346,17 @@ public class ReturnYouTubeDislikePatch {
|
||||
// YouTube Rolling Numbers do not use compound drawables or drawable padding.
|
||||
if (view.getCompoundDrawablePadding() == 0) {
|
||||
LogHelper.printDebug(() -> "Adding rolling number TextView changes");
|
||||
ShapeDrawable leftSeparator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||
view.setCompoundDrawables(leftSeparator, null, null, null);
|
||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||
if (ReVancedUtils.isRightToLeftTextLayout()) {
|
||||
view.setCompoundDrawables(null, null, separator, null);
|
||||
} else {
|
||||
view.setCompoundDrawables(separator, null, null, null);
|
||||
}
|
||||
// Single line mode does not clip words if the span is larger than the view bounds.
|
||||
// The styled span applied to the view should always have the same bounds,
|
||||
// but use this feature just in case the measurements are somehow off by a few pixels.
|
||||
view.setSingleLine(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +369,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
LogHelper.printDebug(() -> "Removing rolling number TextView changes");
|
||||
view.setCompoundDrawablePadding(0);
|
||||
view.setCompoundDrawables(null, null, null, null);
|
||||
view.setSingleLine(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,26 +556,47 @@ public class ReturnYouTubeDislikePatch {
|
||||
// Video Id and voting hooks (all players).
|
||||
//
|
||||
|
||||
private static volatile boolean lastPlayerResponseWasShort;
|
||||
|
||||
/**
|
||||
* Injection point. Uses 'playback response' video id hook to preload RYD.
|
||||
*/
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized()) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
lastPrefetchedVideoId = videoId;
|
||||
|
||||
final boolean videoIdIsShort = VideoInformation.lastVideoIdIsShort();
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
// and the 'is opening/playing' parameter will be false.
|
||||
// This hook will be called again when the Short is actually opened.
|
||||
if (videoIdIsShort && (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean())) {
|
||||
return;
|
||||
}
|
||||
final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
|
||||
&& videoIdIsShort && !lastPlayerResponseWasShort;
|
||||
|
||||
LogHelper.printDebug(() -> "Prefetching RYD for video: " + videoId);
|
||||
ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
if (waitForFetchToComplete && !fetch.fetchCompleted()) {
|
||||
// This call is off the main thread, so wait until the RYD fetch completely finishes,
|
||||
// otherwise if this returns before the fetch completes then the UI can
|
||||
// become frozen when the main thread tries to modify the litho Shorts dislikes and
|
||||
// it must wait for the fetch.
|
||||
// Only need to do this for the first Short opened, as the next Short to swipe to
|
||||
// are preloaded in the background.
|
||||
//
|
||||
// If an asynchronous litho Shorts solution is found, then this blocking call should be removed.
|
||||
LogHelper.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
|
||||
fetch.getFetchData(20000); // Any arbitrarily large max wait time.
|
||||
}
|
||||
// Set the fields after the fetch completes, so any concurrent calls will also wait.
|
||||
lastPlayerResponseWasShort = videoIdIsShort;
|
||||
lastPrefetchedVideoId = videoId;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "preloadVideoId failure", ex);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,15 @@ import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Hooking class for the current playing video.
|
||||
* @noinspection unused
|
||||
*/
|
||||
public final class VideoInformation {
|
||||
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;
|
||||
@@ -27,6 +32,7 @@ public final class VideoInformation {
|
||||
|
||||
@NonNull
|
||||
private static volatile String playerResponseVideoId = "";
|
||||
private static volatile boolean videoIdIsShort;
|
||||
|
||||
/**
|
||||
* The current playback speed
|
||||
@@ -64,12 +70,33 @@ public final class VideoInformation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the player parameters are for a Short.
|
||||
*/
|
||||
public static boolean playerParametersAreShort(@NonNull String parameters) {
|
||||
return parameters.startsWith(SHORTS_PLAYER_PARAMETERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String newPlayerResponseSignature(@NonNull String signature, boolean isShortAndOpeningOrPlaying) {
|
||||
final boolean isShort = playerParametersAreShort(signature);
|
||||
if (!isShort || isShortAndOpeningOrPlaying) {
|
||||
if (videoIdIsShort != isShort) {
|
||||
videoIdIsShort = isShort;
|
||||
LogHelper.printDebug(() -> "videoIdIsShort: " + isShort);
|
||||
}
|
||||
}
|
||||
return signature; // Return the original value since we are observing and not modifying.
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
*
|
||||
* @param videoId The id of the last video loaded.
|
||||
*/
|
||||
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
if (!playerResponseVideoId.equals(videoId)) {
|
||||
LogHelper.printDebug(() -> "New player response video id: " + videoId);
|
||||
playerResponseVideoId = videoId;
|
||||
@@ -89,7 +116,7 @@ public final class VideoInformation {
|
||||
|
||||
/**
|
||||
* Overrides the current playback speed.
|
||||
*
|
||||
* <p>
|
||||
* <b> Used exclusively by {@link RememberPlaybackSpeedPatch} </b>
|
||||
*/
|
||||
public static void overridePlaybackSpeed(float speedOverride) {
|
||||
@@ -124,32 +151,39 @@ public final class VideoInformation {
|
||||
/**
|
||||
* Seek on the current video.
|
||||
* Does not function for playback of Shorts.
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
* @return true if the seek was successful.
|
||||
*/
|
||||
public static boolean seekTo(final long millisecond) {
|
||||
final long videoLength = getVideoLength();
|
||||
|
||||
// Prevent issues such as play/ pause button or autoplay not working.
|
||||
final long seekToMilliseconds = Math.min(millisecond, VideoInformation.getVideoLength() - 250);
|
||||
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Seeking to " + millisecond);
|
||||
return (Boolean) seekMethod.invoke(playerControllerRef.get(), millisecond);
|
||||
LogHelper.printDebug(() -> "Seeking to " + seekToMilliseconds);
|
||||
//noinspection DataFlowIssue
|
||||
return (Boolean) seekMethod.invoke(playerControllerRef.get(), seekToMilliseconds);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to seek", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection UnusedReturnValue*/
|
||||
public static boolean seekToRelative(long millisecondsRelative) {
|
||||
return seekTo(videoTime + millisecondsRelative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of the current video playing. Includes Shorts.
|
||||
* Id of the last video opened. Includes Shorts.
|
||||
*
|
||||
* @return The id of the video. Empty string if not set yet.
|
||||
* @return The id of the video, or an empty string if no videos have been opened yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getVideoId() {
|
||||
@@ -158,20 +192,30 @@ public final class VideoInformation {
|
||||
|
||||
/**
|
||||
* Differs from {@link #videoId} as this is the video id for the
|
||||
* last player response received, which may not be the current video playing.
|
||||
*
|
||||
* last player response received, which may not be the last video opened.
|
||||
* <p>
|
||||
* If Shorts are loading the background, this commonly will be
|
||||
* different from the Short that is currently on screen.
|
||||
*
|
||||
* <p>
|
||||
* For most use cases, you should instead use {@link #getVideoId()}.
|
||||
*
|
||||
* @return The id of the last video loaded. Empty string if not set yet.
|
||||
* @return The id of the last video loaded, or an empty string if no videos have been loaded yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getPlayerResponseVideoId() {
|
||||
return playerResponseVideoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the last player response video id _that was opened_ was a Short.
|
||||
* <p>
|
||||
* Note: This value returned may not match the status of {@link #getPlayerResponseVideoId()}
|
||||
* since that includes player responses for videos not opened.
|
||||
*/
|
||||
public static boolean lastVideoIdIsShort() {
|
||||
return videoIdIsShort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current playback speed.
|
||||
*/
|
||||
@@ -192,9 +236,9 @@ public final class VideoInformation {
|
||||
|
||||
/**
|
||||
* Playback time of the current video playing. Includes Shorts.
|
||||
*
|
||||
* <p>
|
||||
* Value will lag behind the actual playback time by a variable amount based on the playback speed.
|
||||
*
|
||||
* <p>
|
||||
* If playback speed is 2.0x, this value may be up to 2000ms behind the actual playback time.
|
||||
* If playback speed is 1.0x, this value may be up to 1000ms behind the actual playback time.
|
||||
* If playback speed is 0.5x, this value may be up to 500ms behind the actual playback time.
|
||||
@@ -208,12 +252,12 @@ public final class VideoInformation {
|
||||
|
||||
/**
|
||||
* @return If the playback is at the end of the video.
|
||||
*
|
||||
* <p>
|
||||
* If video is playing in the background with no video visible,
|
||||
* this always returns false (even if the video is actually at the end).
|
||||
*
|
||||
* <p>
|
||||
* This is equivalent to checking for {@link VideoState#ENDED},
|
||||
* but can give a more up to date result for code calling from some hooks.
|
||||
* but can give a more up-to-date result for code calling from some hooks.
|
||||
*
|
||||
* @see VideoState
|
||||
*/
|
||||
|
||||
@@ -32,6 +32,9 @@ public final class AnnouncementsPatch {
|
||||
public static void showAnnouncement(final Activity context) {
|
||||
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
|
||||
|
||||
// Check if there is internet connection
|
||||
if (!ReVancedUtils.isNetworkConnected()) return;
|
||||
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
|
||||
@@ -43,7 +46,7 @@ public final class AnnouncementsPatch {
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return;
|
||||
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
ReVancedUtils.showToastLong("Failed to get announcement");
|
||||
|
||||
return;
|
||||
@@ -118,7 +121,7 @@ public final class AnnouncementsPatch {
|
||||
*/
|
||||
private static boolean emptyLastAnnouncementHash() {
|
||||
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true;
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
|
||||
SettingsEnum.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import app.revanced.integrations.utils.StringTrieSearch;
|
||||
|
||||
public final class AdsFilter extends Filter {
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
private final StringFilterGroup shoppingLinks;
|
||||
|
||||
public AdsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@@ -71,6 +72,11 @@ public final class AdsFilter extends Filter {
|
||||
"products_in_video"
|
||||
);
|
||||
|
||||
shoppingLinks = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHOPPING_LINKS,
|
||||
"expandable_list"
|
||||
);
|
||||
|
||||
final var webLinkPanel = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_WEB_SEARCH_RESULTS,
|
||||
"web_link_panel"
|
||||
@@ -93,6 +99,7 @@ public final class AdsFilter extends Filter {
|
||||
viewProducts,
|
||||
selfSponsor,
|
||||
webLinkPanel,
|
||||
shoppingLinks,
|
||||
movieAds
|
||||
);
|
||||
this.identifierFilterGroupList.addAll(carouselAd);
|
||||
@@ -104,6 +111,10 @@ public final class AdsFilter extends Filter {
|
||||
if (exceptions.matches(path))
|
||||
return false;
|
||||
|
||||
// Check for the index because of likelihood of false positives.
|
||||
if (matchedGroup == shoppingLinks && matchedIndex != 0)
|
||||
return false;
|
||||
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ import app.revanced.integrations.utils.StringTrieSearch;
|
||||
public final class LayoutComponentsFilter extends Filter {
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch();
|
||||
private static ByteArrayAsStringFilterGroup mixPlaylistsExceptions2;
|
||||
|
||||
private static final ByteArrayAsStringFilterGroup mixPlaylistsExceptions2 = new ByteArrayAsStringFilterGroup(
|
||||
null,
|
||||
"cell_description_body"
|
||||
);
|
||||
private final CustomFilterGroup custom;
|
||||
|
||||
private static final ByteArrayAsStringFilterGroup mixPlaylists = new ByteArrayAsStringFilterGroup(
|
||||
@@ -25,6 +27,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
private final StringFilterGroup notifyMe;
|
||||
private final StringFilterGroup expandableMetadata;
|
||||
|
||||
static {
|
||||
mixPlaylistsExceptions.addPatterns(
|
||||
"V.ED", // Playlist browse id.
|
||||
"java.lang.ref.WeakReference"
|
||||
);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@@ -35,16 +44,6 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"library_recent_shelf"
|
||||
);
|
||||
|
||||
mixPlaylistsExceptions.addPatterns(
|
||||
"V.ED", // Playlist browse id.
|
||||
"java.lang.ref.WeakReference"
|
||||
);
|
||||
|
||||
mixPlaylistsExceptions2 = new ByteArrayAsStringFilterGroup(
|
||||
null,
|
||||
"cell_description_body"
|
||||
);
|
||||
|
||||
custom = new CustomFilterGroup(
|
||||
SettingsEnum.CUSTOM_FILTER,
|
||||
SettingsEnum.CUSTOM_FILTER_STRINGS
|
||||
@@ -192,6 +191,11 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"featured_channel_watermark_overlay"
|
||||
);
|
||||
|
||||
final var forYouShelf = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_FOR_YOU_SHELF,
|
||||
"mixed_content_shelf"
|
||||
);
|
||||
|
||||
this.pathFilterGroupList.addAll(
|
||||
channelBar,
|
||||
communityPosts,
|
||||
@@ -216,6 +220,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
timedReactions,
|
||||
imageShelf,
|
||||
channelMemberShelf,
|
||||
forYouShelf,
|
||||
custom
|
||||
);
|
||||
|
||||
@@ -247,19 +252,25 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
* Injection point.
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(final Object conversionContext, final byte[] bytes) {
|
||||
public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
LogHelper.printDebug(() -> "bytes is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent playlist items being hidden, if a mix playlist is present in it.
|
||||
if (mixPlaylistsExceptions.matches(conversionContext.toString()))
|
||||
return false;
|
||||
|
||||
if (!mixPlaylists.check(bytes).isFiltered()) return false;
|
||||
if (!mixPlaylists.check(bytes).isFiltered())
|
||||
return false;
|
||||
|
||||
// Prevent hiding the description of some videos accidentally.
|
||||
if (mixPlaylistsExceptions2.check(bytes).isFiltered()) return false;
|
||||
if (mixPlaylistsExceptions2.check(bytes).isFiltered())
|
||||
return false;
|
||||
|
||||
LogHelper.printDebug(() -> "Filtered mix playlist");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public static boolean showWatermark() {
|
||||
|
||||
@@ -133,7 +133,7 @@ final class CustomFilterGroup extends StringFilterGroup {
|
||||
for (String pattern : patterns) {
|
||||
if (!StringTrieSearch.isValidPattern(pattern)) {
|
||||
ReVancedUtils.showToastLong("Invalid custom filter, resetting to default");
|
||||
setting.saveValue(setting.defaultValue);
|
||||
setting.resetToDefault();
|
||||
return getFilterPatterns(setting);
|
||||
}
|
||||
}
|
||||
@@ -489,8 +489,13 @@ public final class LithoFilterPatch {
|
||||
return false;
|
||||
|
||||
ByteBuffer protobufBuffer = bufferThreadLocal.get();
|
||||
if (protobufBuffer == null || !protobufBuffer.hasArray()) {
|
||||
LogHelper.printException(() -> "Proto buffer is null or has no array"); // Should never happen.
|
||||
if (protobufBuffer == null) {
|
||||
LogHelper.printException(() -> "Proto buffer is null"); // Should never happen.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!protobufBuffer.hasArray()) {
|
||||
LogHelper.printDebug(() -> "Proto buffer does not have an array");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean videoIsOpeningOrPlaying) {
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
if (!videoIsOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
synchronized (lastVideoIds) {
|
||||
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
|
||||
LogHelper.printDebug(() -> "New video id: " + videoId);
|
||||
LogHelper.printDebug(() -> "New Short video id: " + videoId);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
||||
@@ -15,15 +15,23 @@ import java.util.Arrays;
|
||||
public class CustomPlaybackSpeedPatch {
|
||||
/**
|
||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
||||
* Limit is required otherwise double digit speeds show up out of order in the UI selector.
|
||||
*
|
||||
* Going over 8x does not increase the actual playback speed any higher,
|
||||
* and the UI selector starts flickering and acting weird.
|
||||
* Over 10x and the speeds show up out of order in the UI selector.
|
||||
*/
|
||||
public static final float MAXIMUM_PLAYBACK_SPEED = 10;
|
||||
public static final float MAXIMUM_PLAYBACK_SPEED = 8;
|
||||
|
||||
/**
|
||||
* Custom playback speeds.
|
||||
*/
|
||||
public static float[] customPlaybackSpeeds;
|
||||
|
||||
/**
|
||||
* The last time the old playback menu was forcefully called.
|
||||
*/
|
||||
private static long lastTimeOldPlaybackMenuInvoked;
|
||||
|
||||
/**
|
||||
* PreferenceList entries and values, of all available playback speeds.
|
||||
*/
|
||||
@@ -35,7 +43,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
|
||||
private static void resetCustomSpeeds(@NonNull String toastMessage) {
|
||||
ReVancedUtils.showToastLong(toastMessage);
|
||||
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue);
|
||||
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||
}
|
||||
|
||||
private static void loadCustomSpeeds() {
|
||||
@@ -101,36 +109,48 @@ public class CustomPlaybackSpeedPatch {
|
||||
// For some reason, the custom playback speed flyout panel is activated when the user opens the share panel. (A/B tests)
|
||||
// Check the child count of playback speed flyout panel to prevent this issue.
|
||||
// Child count of playback speed flyout panel is always 8.
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible
|
||||
&& ((ViewGroup) recyclerView.getChildAt(0)).getChildCount() == 8) {
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false;
|
||||
ViewGroup parentView3rd = (ViewGroup) recyclerView.getParent().getParent().getParent();
|
||||
ViewGroup parentView4th = (ViewGroup) parentView3rd.getParent();
|
||||
|
||||
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
|
||||
// This only shows in phone layout.
|
||||
|
||||
final var touchInsidedView = parentView4th.getChildAt(0);
|
||||
touchInsidedView.setSoundEffectsEnabled(false);
|
||||
touchInsidedView.performClick();
|
||||
|
||||
// In tablet layout there is no Dismiss View, instead we just hide all two parent views.
|
||||
parentView3rd.setVisibility(View.GONE);
|
||||
parentView4th.setVisibility(View.GONE);
|
||||
|
||||
// This works without issues for both tablet and phone layouts,
|
||||
// So no code is needed to check whether the current device is a tablet or phone.
|
||||
|
||||
// Close the new Playback speed menu and show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
if (!PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible || recyclerView.getChildCount() == 0) {
|
||||
return;
|
||||
}
|
||||
ViewGroup PlaybackSpeedParentView = (ViewGroup) recyclerView.getChildAt(0);
|
||||
if (PlaybackSpeedParentView == null || PlaybackSpeedParentView.getChildCount() != 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false;
|
||||
ViewGroup parentView3rd = (ViewGroup) recyclerView.getParent().getParent().getParent();
|
||||
ViewGroup parentView4th = (ViewGroup) parentView3rd.getParent();
|
||||
|
||||
// Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
|
||||
// This only shows in phone layout.
|
||||
final var touchInsidedView = parentView4th.getChildAt(0);
|
||||
touchInsidedView.setSoundEffectsEnabled(false);
|
||||
touchInsidedView.performClick();
|
||||
|
||||
// In tablet layout there is no Dismiss View, instead we just hide all two parent views.
|
||||
parentView3rd.setVisibility(View.GONE);
|
||||
parentView4th.setVisibility(View.GONE);
|
||||
|
||||
// This works without issues for both tablet and phone layouts,
|
||||
// So no code is needed to check whether the current device is a tablet or phone.
|
||||
|
||||
// Close the new Playback speed menu and show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showOldPlaybackSpeedMenu() {
|
||||
private static void showOldPlaybackSpeedMenu() {
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
|
||||
LogHelper.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu");
|
||||
return;
|
||||
}
|
||||
lastTimeOldPlaybackMenuInvoked = now;
|
||||
LogHelper.printDebug(() -> "Old video quality menu shown");
|
||||
|
||||
// Rest of the implementation added by patch.
|
||||
|
||||
@@ -29,6 +29,11 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
|
||||
|
||||
/**
|
||||
* Parameters used when playing clips.
|
||||
*/
|
||||
private static final String CLIPS_PARAMETERS = "kAIB";
|
||||
|
||||
/**
|
||||
* Parameters causing playback issues.
|
||||
*/
|
||||
@@ -43,16 +48,13 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
private static final String SCRIM_PARAMETER = "SAFgAXgB";
|
||||
|
||||
/**
|
||||
* Parameters used in YouTube Shorts.
|
||||
*/
|
||||
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
|
||||
|
||||
/**
|
||||
* Last video id loaded. Used to prevent reloading the same spec multiple times.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile String lastPlayerResponseVideoId;
|
||||
|
||||
@Nullable
|
||||
private static volatile Future<StoryboardRenderer> rendererFuture;
|
||||
|
||||
private static volatile boolean useOriginalStoryboardRenderer;
|
||||
@@ -60,10 +62,13 @@ public class SpoofSignaturePatch {
|
||||
private static volatile boolean isPlayingShorts;
|
||||
|
||||
@Nullable
|
||||
private static StoryboardRenderer getRenderer() {
|
||||
if (rendererFuture != null) {
|
||||
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
|
||||
Future<StoryboardRenderer> future = rendererFuture;
|
||||
if (future != null) {
|
||||
try {
|
||||
return rendererFuture.get(2000, TimeUnit.MILLISECONDS);
|
||||
if (waitForCompletion || future.isDone()) {
|
||||
return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
|
||||
} // else, return null.
|
||||
} catch (TimeoutException ex) {
|
||||
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
@@ -81,37 +86,48 @@ public class SpoofSignaturePatch {
|
||||
*
|
||||
* @param parameters Original protobuf parameter value.
|
||||
*/
|
||||
public static String spoofParameter(String parameters) {
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||
public static String spoofParameter(String parameters, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
|
||||
|
||||
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||
// Clips are 60 seconds or less in length, so no spoofing.
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
|
||||
|
||||
// Shorts do not need to be spoofed.
|
||||
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
|
||||
isPlayingShorts = true;
|
||||
return parameters;
|
||||
}
|
||||
isPlayingShorts = false;
|
||||
|
||||
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
|
||||
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
|
||||
if (isPlayingFeed) {
|
||||
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
|
||||
// Don't spoof the feed video playback. This will cause video playback issues,
|
||||
// but only if user continues watching for more than 1 minute.
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
return parameters;
|
||||
}
|
||||
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
|
||||
fetchStoryboardRenderer();
|
||||
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
|
||||
}
|
||||
|
||||
fetchStoryboardRenderer();
|
||||
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||
// Clips are 60 seconds or less in length, so no spoofing.
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150 || containsAny(parameters, CLIPS_PARAMETERS)) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// Shorts do not need to be spoofed.
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
|
||||
isPlayingShorts = true;
|
||||
return parameters;
|
||||
}
|
||||
isPlayingShorts = false;
|
||||
|
||||
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
|
||||
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
|
||||
if (isPlayingFeed) {
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
|
||||
// Don't spoof the feed video playback. This will cause video playback issues,
|
||||
// but only if user continues watching for more than 1 minute.
|
||||
return parameters;
|
||||
}
|
||||
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
|
||||
fetchStoryboardRenderer();
|
||||
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
|
||||
}
|
||||
|
||||
fetchStoryboardRenderer();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "spoofParameter failure", ex);
|
||||
}
|
||||
return INCOGNITO_PARAMETERS;
|
||||
}
|
||||
|
||||
@@ -126,21 +142,24 @@ public class SpoofSignaturePatch {
|
||||
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
||||
lastPlayerResponseVideoId = videoId;
|
||||
}
|
||||
// Block until the fetch is completed. Without this, occasionally when a new video is opened
|
||||
// the video will be frozen a few seconds while the audio plays.
|
||||
// This is because the main thread is calling to get the storyboard but the fetch is not completed.
|
||||
// To prevent this, call get() here and block until the fetch is completed.
|
||||
// So later when the main thread calls to get the renderer it will never block as the future is done.
|
||||
getRenderer();
|
||||
// Block until the renderer fetch completes.
|
||||
// This is desired because if this returns without finishing the fetch
|
||||
// then video will start playback but the storyboard is not ready yet.
|
||||
getRenderer(true);
|
||||
}
|
||||
|
||||
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
|
||||
boolean returnNullIfLiveStream) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||
StoryboardRenderer renderer = getRenderer();
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer != null) {
|
||||
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
|
||||
return renderer.getSpec();
|
||||
if (returnNullIfLiveStream && renderer.isLiveStream()) {
|
||||
return null;
|
||||
}
|
||||
String spec = renderer.getSpec();
|
||||
if (spec != null) {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +190,7 @@ public class SpoofSignaturePatch {
|
||||
*/
|
||||
public static int getRecommendedLevel(int originalLevel) {
|
||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||
StoryboardRenderer renderer = getRenderer();
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer != null) {
|
||||
Integer recommendedLevel = renderer.getRecommendedLevel();
|
||||
if (recommendedLevel != null) return recommendedLevel;
|
||||
@@ -186,7 +205,17 @@ public class SpoofSignaturePatch {
|
||||
* if {@link SettingsEnum#SPOOF_STORYBOARD_RENDERER} is not enabled.
|
||||
*/
|
||||
public static boolean getSeekbarThumbnailOverrideValue() {
|
||||
return SettingsEnum.SPOOF_SIGNATURE.getBoolean();
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) {
|
||||
return false;
|
||||
}
|
||||
StoryboardRenderer renderer = getRenderer(false);
|
||||
if (renderer == null) {
|
||||
// Spoof storyboard renderer is turned off,
|
||||
// video is paid, or the storyboard fetch timed out.
|
||||
// Show empty thumbnails so the seek time and chapters still show up.
|
||||
return true;
|
||||
}
|
||||
return renderer.getSpec() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,15 +224,19 @@ public class SpoofSignaturePatch {
|
||||
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
|
||||
*/
|
||||
public static void seekbarImageViewCreated(ImageView view) {
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|
||||
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (isPlayingShorts) return;
|
||||
try {
|
||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|
||||
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (isPlayingShorts) return;
|
||||
|
||||
view.setVisibility(View.GONE);
|
||||
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
|
||||
ViewGroup parentLayout = (ViewGroup) view.getParent();
|
||||
parentLayout.setPadding(0, 0, 0, 0);
|
||||
view.setVisibility(View.GONE);
|
||||
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
|
||||
ViewGroup parentLayout = (ViewGroup) view.getParent();
|
||||
parentLayout.setPadding(0, 0, 0, 0);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "seekbarImageViewCreated failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package app.revanced.integrations.patches.spoof;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class StoryboardRenderer {
|
||||
@Nullable
|
||||
private final String spec;
|
||||
private final boolean isLiveStream;
|
||||
@Nullable
|
||||
private final Integer recommendedLevel;
|
||||
|
||||
public StoryboardRenderer(String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
|
||||
public StoryboardRenderer(@Nullable String spec, boolean isLiveStream, @Nullable Integer recommendedLevel) {
|
||||
this.spec = spec;
|
||||
this.isLiveStream = isLiveStream;
|
||||
this.recommendedLevel = recommendedLevel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Nullable
|
||||
public String getSpec() {
|
||||
return spec;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ final class PlayerRoutes {
|
||||
static final String ANDROID_INNER_TUBE_BODY;
|
||||
static final String TV_EMBED_INNER_TUBE_BODY;
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout
|
||||
*/
|
||||
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds.
|
||||
|
||||
static {
|
||||
JSONObject innerTubeBody = new JSONObject();
|
||||
|
||||
@@ -88,8 +93,8 @@ final class PlayerRoutes {
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,56 @@
|
||||
package app.revanced.integrations.patches.spoof.requests;
|
||||
|
||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.ANDROID_INNER_TUBE_BODY;
|
||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.GET_STORYBOARD_SPEC_RENDERER;
|
||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.TV_EMBED_INNER_TUBE_BODY;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.*;
|
||||
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class StoryboardRendererRequester {
|
||||
|
||||
/**
|
||||
* For videos that have no storyboard.
|
||||
* Usually for low resolution videos as old as YouTube itself.
|
||||
* Does not include paid videos where the renderer fetch fails.
|
||||
*/
|
||||
private static final StoryboardRenderer emptyStoryboard
|
||||
= new StoryboardRenderer(null, false, null);
|
||||
|
||||
private StoryboardRendererRequester() {
|
||||
}
|
||||
|
||||
private static void randomlyWaitIfLocallyDebugging() {
|
||||
final boolean randomlyWait = false; // Enable to simulate slow connection responses.
|
||||
if (randomlyWait) {
|
||||
final long maximumTimeToRandomlyWait = 10000;
|
||||
ReVancedUtils.doNothingForDuration(maximumTimeToRandomlyWait);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex,
|
||||
boolean showToastOnIOException) {
|
||||
if (showToastOnIOException) ReVancedUtils.showToastShort(toastMessage);
|
||||
LogHelper.printInfo(() -> toastMessage, ex);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
|
||||
private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
@@ -33,14 +62,21 @@ public class StoryboardRendererRequester {
|
||||
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
randomlyWaitIfLocallyDebugging();
|
||||
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||
|
||||
LogHelper.printException(() -> "API not available: " + responseCode);
|
||||
// Always show a toast for this, as a non 200 response means something is broken.
|
||||
handleConnectionError("Spoof storyboard not available: " + responseCode,
|
||||
null, showToastOnIOException || SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean());
|
||||
connection.disconnect();
|
||||
} catch (SocketTimeoutException ex) {
|
||||
LogHelper.printException(() -> "API timed out", ex);
|
||||
handleConnectionError("Spoof storyboard temporarily not available (API timed out)",
|
||||
ex, showToastOnIOException);
|
||||
} catch (IOException ex) {
|
||||
handleConnectionError("Spoof storyboard temporarily not available: " + ex.getMessage(),
|
||||
ex, showToastOnIOException);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||
LogHelper.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
|
||||
} finally {
|
||||
LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
|
||||
}
|
||||
@@ -64,8 +100,9 @@ public class StoryboardRendererRequester {
|
||||
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
||||
*/
|
||||
@Nullable
|
||||
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody) {
|
||||
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody);
|
||||
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody,
|
||||
boolean showToastOnIOException) {
|
||||
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
|
||||
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
||||
return getStoryboardRendererUsingResponse(playerResponse);
|
||||
|
||||
@@ -76,6 +113,10 @@ public class StoryboardRendererRequester {
|
||||
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Parsing response: " + playerResponse);
|
||||
if (!playerResponse.has("storyboards")) {
|
||||
LogHelper.printDebug(() -> "Using empty storyboard");
|
||||
return emptyStoryboard;
|
||||
}
|
||||
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
||||
final boolean isLiveStream = storyboards.has("playerLiveStoryboardSpecRenderer");
|
||||
final String storyboardsRendererTag = isLiveStream
|
||||
@@ -103,23 +144,19 @@ public class StoryboardRendererRequester {
|
||||
|
||||
@Nullable
|
||||
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId));
|
||||
var renderer = getStoryboardRendererUsingBody(
|
||||
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
|
||||
if (renderer == null) {
|
||||
LogHelper.printDebug(() -> videoId + " not available using Android client");
|
||||
renderer = getStoryboardRendererUsingBody(
|
||||
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
|
||||
if (renderer == null) {
|
||||
LogHelper.printDebug(() -> videoId + " not available using Android client");
|
||||
renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId));
|
||||
if (renderer == null) {
|
||||
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
|
||||
}
|
||||
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
|
||||
}
|
||||
|
||||
return renderer;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public final class SeekbarColorPatch {
|
||||
Color.colorToHSV(seekbarColor, customSeekbarColorHSV);
|
||||
} catch (Exception ex) {
|
||||
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value.");
|
||||
SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.saveValue(SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.defaultValue);
|
||||
SettingsEnum.SEEKBAR_CUSTOM_COLOR_VALUE.resetToDefault();
|
||||
loadCustomSeekbarColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +62,12 @@ public class ReturnYouTubeDislikeApi {
|
||||
* How long to wait until API calls are resumed, if the API requested a back off.
|
||||
* No clear guideline of how long to wait until resuming.
|
||||
*/
|
||||
private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes.
|
||||
private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||
|
||||
/**
|
||||
* How long to wait until API calls are resumed, if any connection error occurs.
|
||||
*/
|
||||
private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 60 * 1000; // 60 Seconds.
|
||||
private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes.
|
||||
|
||||
/**
|
||||
* If non zero, then the system time of when API calls can resume.
|
||||
@@ -139,25 +139,13 @@ public class ReturnYouTubeDislikeApi {
|
||||
* Simulates a slow response by doing meaningless calculations.
|
||||
* Used to debug the app UI and verify UI timeout logic works
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
private static long randomlyWaitIfLocallyDebugging() {
|
||||
private static void randomlyWaitIfLocallyDebugging() {
|
||||
final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI
|
||||
if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
|
||||
final long amountOfTimeToWaste = (long) (Math.random()
|
||||
* (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS));
|
||||
final long timeCalculationStarted = System.currentTimeMillis();
|
||||
LogHelper.printDebug(() -> "Artificially creating network delay of: " + amountOfTimeToWaste + "ms");
|
||||
|
||||
long meaninglessValue = 0;
|
||||
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
||||
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
|
||||
meaninglessValue += Long.numberOfLeadingZeros((long)Math.exp(Math.random()));
|
||||
}
|
||||
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
||||
// leaving an empty loop that hammers on the System.currentTimeMillis native call
|
||||
return meaninglessValue;
|
||||
ReVancedUtils.doNothingForDuration(amountOfTimeToWaste);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,18 +50,24 @@ public enum SettingsEnum {
|
||||
HIDE_MERCHANDISE_BANNERS("revanced_hide_merchandise_banners", BOOLEAN, TRUE),
|
||||
HIDE_PAID_CONTENT("revanced_hide_paid_content_ads", BOOLEAN, TRUE),
|
||||
HIDE_PRODUCTS_BANNER("revanced_hide_products_banner", BOOLEAN, TRUE),
|
||||
HIDE_SHOPPING_LINKS("revanced_hide_shopping_links", BOOLEAN, TRUE),
|
||||
HIDE_SELF_SPONSOR("revanced_hide_self_sponsor_ads", BOOLEAN, TRUE),
|
||||
HIDE_VIDEO_ADS("revanced_hide_video_ads", BOOLEAN, TRUE, true),
|
||||
HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE),
|
||||
|
||||
// Layout
|
||||
ALT_THUMBNAIL("revanced_alt_thumbnail", BOOLEAN, FALSE),
|
||||
ALT_THUMBNAIL_TYPE("revanced_alt_thumbnail_type", INTEGER, 2, parents(ALT_THUMBNAIL)),
|
||||
ALT_THUMBNAIL_FAST_QUALITY("revanced_alt_thumbnail_fast_quality", BOOLEAN, FALSE, parents(ALT_THUMBNAIL)),
|
||||
ALT_THUMBNAIL_STILLS("revanced_alt_thumbnail_stills", BOOLEAN, FALSE),
|
||||
ALT_THUMBNAIL_STILLS_TIME("revanced_alt_thumbnail_stills_time", INTEGER, 2, parents(ALT_THUMBNAIL_STILLS)),
|
||||
ALT_THUMBNAIL_STILLS_FAST("revanced_alt_thumbnail_stills_fast", BOOLEAN, FALSE, parents(ALT_THUMBNAIL_STILLS)),
|
||||
ALT_THUMBNAIL_DEARROW("revanced_alt_thumbnail_dearrow", BOOLEAN, false),
|
||||
ALT_THUMBNAIL_DEARROW_API_URL("revanced_alt_thumbnail_dearrow_api_url", STRING,
|
||||
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parents(ALT_THUMBNAIL_DEARROW)),
|
||||
ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST("revanced_alt_thumbnail_dearrow_connection_toast", BOOLEAN, TRUE, parents(ALT_THUMBNAIL_DEARROW)),
|
||||
CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE),
|
||||
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
|
||||
DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true),
|
||||
DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE),
|
||||
DISABLE_ROLLING_NUMBER_ANIMATIONS("revanced_disable_rolling_number_animations", BOOLEAN, FALSE),
|
||||
DISABLE_SUGGESTED_VIDEO_END_SCREEN("revanced_disable_suggested_video_end_screen", BOOLEAN, TRUE),
|
||||
GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE),
|
||||
HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true),
|
||||
@@ -113,6 +119,7 @@ public enum SettingsEnum {
|
||||
HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE),
|
||||
@Deprecated HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE),
|
||||
HIDE_VIDEO_CHANNEL_WATERMARK("revanced_hide_channel_watermark", BOOLEAN, TRUE),
|
||||
HIDE_FOR_YOU_SHELF("revanced_hide_for_you_shelf", BOOLEAN, TRUE),
|
||||
HIDE_VIDEO_QUALITY_MENU_FOOTER("revanced_hide_video_quality_menu_footer", BOOLEAN, TRUE),
|
||||
PLAYER_OVERLAY_OPACITY("revanced_player_overlay_opacity", INTEGER, 100, true),
|
||||
PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE),
|
||||
@@ -122,6 +129,8 @@ public enum SettingsEnum {
|
||||
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
|
||||
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
|
||||
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
|
||||
START_PAGE("revanced_start_page", STRING, ""),
|
||||
|
||||
// Description
|
||||
HIDE_CHAPTERS("revanced_hide_chapters", BOOLEAN, TRUE),
|
||||
HIDE_INFO_CARDS_SECTION("revanced_hide_info_cards_section", BOOLEAN, TRUE),
|
||||
@@ -427,7 +436,7 @@ public enum SettingsEnum {
|
||||
LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value
|
||||
+ "' from: " + oldSetting + " into replacement setting: " + newSetting);
|
||||
newSetting.saveValue(oldSetting.value);
|
||||
oldSetting.saveValue(oldSetting.defaultValue); // reset old value
|
||||
oldSetting.resetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +528,13 @@ public enum SettingsEnum {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identical to calling {@link #saveValue(Object)} using {@link #defaultValue}.
|
||||
*/
|
||||
public void resetToDefault() {
|
||||
saveValue(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if this setting can be configured and used.
|
||||
* <p>
|
||||
@@ -691,7 +707,7 @@ public enum SettingsEnum {
|
||||
} else if (setting.includeWithImportExport() && !setting.isSetToDefault()) {
|
||||
LogHelper.printDebug(() -> "Resetting to default: " + setting);
|
||||
rebootSettingChanged |= setting.rebootApp;
|
||||
setting.saveValue(setting.defaultValue);
|
||||
setting.resetToDefault();
|
||||
}
|
||||
}
|
||||
numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.revanced.integrations.settingsmenu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://dearrow.ajay.app"));
|
||||
pref.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user