mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-10-31 08:42:58 +01:00
chore: Merge branch dev
to main
(#704)
This commit is contained in:
commit
14f767f943
4
.github/workflows/open_pull_request.yml
vendored
4
.github/workflows/open_pull_request.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
pr_body: |
|
||||
This pull request will ${{ env.MESSAGE }}.
|
||||
|
||||
## Dependencies before merge
|
||||
## Before merging this PR
|
||||
|
||||
- [ ] https://github.com/revanced/revanced-patches
|
||||
- [ ] Remember about https://github.com/revanced/revanced-patches
|
||||
pr_draft: true
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -10,6 +10,8 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -49,5 +51,5 @@ jobs:
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm exec semantic-release
|
||||
|
@ -23,7 +23,8 @@
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
|
79
CHANGELOG.md
79
CHANGELOG.md
@ -1,3 +1,82 @@
|
||||
# [1.16.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.10...v1.16.0-dev.11) (2024-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#717](https://github.com/ReVanced/revanced-integrations/issues/717)) ([1c9a966](https://github.com/ReVanced/revanced-integrations/commit/1c9a966354243dd1a106e1fc767227c1b025125e))
|
||||
|
||||
# [1.16.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.9...v1.16.0-dev.10) (2024-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube:** Support versions `19.25` and `19.34` ([#689](https://github.com/ReVanced/revanced-integrations/issues/689)) ([61569ba](https://github.com/ReVanced/revanced-integrations/commit/61569ba111af82aaff60d11863bc57221a295fe8))
|
||||
|
||||
# [1.16.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.8...v1.16.0-dev.9) (2024-10-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Hide new type of Playable ([1a58a40](https://github.com/ReVanced/revanced-integrations/commit/1a58a406db76e4deaea070d077a31714f270e479))
|
||||
|
||||
# [1.16.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.7...v1.16.0-dev.8) (2024-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Sync for Reddit:** Add `Fix video downloads` patch ([#710](https://github.com/ReVanced/revanced-integrations/issues/710)) ([888de49](https://github.com/ReVanced/revanced-integrations/commit/888de49edd39913116028ac1d173f2b6e0feab09))
|
||||
|
||||
# [1.16.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.6...v1.16.0-dev.7) (2024-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Twitter:** Add `Change link sharing domain` patch ([#715](https://github.com/ReVanced/revanced-integrations/issues/715)) ([c673951](https://github.com/ReVanced/revanced-integrations/commit/c6739517f179bf8e811e869640a24f433d729f42))
|
||||
|
||||
# [1.16.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.5...v1.16.0-dev.6) (2024-10-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Fix playback for Android VR by removing invalid body as well ([#716](https://github.com/ReVanced/revanced-integrations/issues/716)) ([8ad3f78](https://github.com/ReVanced/revanced-integrations/commit/8ad3f78865836fbe38a832ef6395c6eb8d0edbf2))
|
||||
|
||||
# [1.16.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.4...v1.16.0-dev.5) (2024-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Add options to hide `Use template`, `Upcoming`, `Green screen` buttons ([#714](https://github.com/ReVanced/revanced-integrations/issues/714)) ([faad754](https://github.com/ReVanced/revanced-integrations/commit/faad7548df2091c24d41dad98a589745ce8a6b73))
|
||||
|
||||
# [1.16.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.3...v1.16.0-dev.4) (2024-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Adjust settings text ([#713](https://github.com/ReVanced/revanced-integrations/issues/713)) ([119c416](https://github.com/ReVanced/revanced-integrations/commit/119c416bc5c24f2455bfe70adccd51234b165d25))
|
||||
|
||||
# [1.16.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.2...v1.16.0-dev.3) (2024-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide layout components:** Add option to hide Yoodles (YouTube Doodles) ([#712](https://github.com/ReVanced/revanced-integrations/issues/712)) ([4b5f3de](https://github.com/ReVanced/revanced-integrations/commit/4b5f3deef9c3a8c700e23a0f4d9ce999013ec9d4))
|
||||
|
||||
# [1.16.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.16.0-dev.1...v1.16.0-dev.2) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Spoof video streams:** Handle app left open for a long time ([#709](https://github.com/ReVanced/revanced-integrations/issues/709)) ([ea4b073](https://github.com/ReVanced/revanced-integrations/commit/ea4b073f5c21b0fea4e3922488e8bbf69cfcb421))
|
||||
|
||||
# [1.16.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.15.1-dev.1...v1.16.0-dev.1) (2024-10-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Add option to hide like fountain ([#708](https://github.com/ReVanced/revanced-integrations/issues/708)) ([16c3ef7](https://github.com/ReVanced/revanced-integrations/commit/16c3ef7ee5a32ec22db6da876dcf19fc02bc9aac))
|
||||
|
||||
## [1.15.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.15.0...v1.15.1-dev.1) (2024-10-01)
|
||||
|
||||
# [1.15.0](https://github.com/ReVanced/revanced-integrations/compare/v1.14.2...v1.15.0) (2024-09-30)
|
||||
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
package app.revanced.integrations.shared;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.*;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
@ -268,6 +265,20 @@ public class Utils {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes sub children.
|
||||
*
|
||||
* @noinspection unchecked
|
||||
*/
|
||||
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
|
||||
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
||||
if (child != null) {
|
||||
return (R) child;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchRecursively If children ViewGroups should also be
|
||||
* recursively searched using depth first search.
|
||||
@ -710,4 +721,21 @@ public class Utils {
|
||||
pref.setOrder(order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
|
||||
* the Dialog theme corresponding to [Android library] should be used.
|
||||
* <p>
|
||||
* If not, the following issues will occur:
|
||||
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
|
||||
* <p>
|
||||
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
|
||||
*/
|
||||
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
|
||||
final int editTextDialogStyle = getResourceIdentifier(
|
||||
"revanced_edit_text_dialog_style", "style");
|
||||
if (editTextDialogStyle != 0) {
|
||||
builder.getContext().setTheme(editTextDialogStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
import app.revanced.integrations.shared.settings.BooleanSetting;
|
||||
import app.revanced.integrations.shared.settings.Setting;
|
||||
|
||||
@ -141,8 +142,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
} else if (pref.hasKey()) {
|
||||
String key = pref.getKey();
|
||||
Setting<?> setting = Setting.getSettingFromPath(key);
|
||||
|
||||
if (setting != null) {
|
||||
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
|
||||
} else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
|
||||
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
|
||||
// Probably a typo in the patches preference declaration.
|
||||
Logger.printException(() -> "Preference key has no setting: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
|
@ -7,6 +7,8 @@ import android.preference.EditTextPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.shared.settings.Setting;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
|
||||
@ -33,6 +35,8 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
Setting<?> setting = Setting.getSettingFromPath(getKey());
|
||||
if (setting != null) {
|
||||
builder.setNeutralButton(str("revanced_settings_reset"), null);
|
||||
|
@ -0,0 +1,77 @@
|
||||
package app.revanced.integrations.syncforreddit;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class FixRedditVideoDownloadPatch {
|
||||
private static @Nullable Pair<Integer, String> getBestMpEntry(Element element) {
|
||||
var representations = element.getElementsByTagName("Representation");
|
||||
var entries = new ArrayList<Pair<Integer, String>>();
|
||||
|
||||
for (int i = 0; i < representations.getLength(); i++) {
|
||||
Element representation = (Element) representations.item(i);
|
||||
var bandwidthStr = representation.getAttribute("bandwidth");
|
||||
try {
|
||||
var bandwidth = Integer.parseInt(bandwidthStr);
|
||||
var baseUrl = representation.getElementsByTagName("BaseURL").item(0);
|
||||
if (baseUrl != null) {
|
||||
entries.add(new Pair<>(bandwidth, baseUrl.getTextContent()));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collections.sort(entries, (e1, e2) -> e2.first - e1.first);
|
||||
return entries.get(0);
|
||||
}
|
||||
|
||||
private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException {
|
||||
var adaptionSets = DocumentBuilderFactory
|
||||
.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(data))
|
||||
.getElementsByTagName("AdaptationSet");
|
||||
|
||||
String videoUrl = null;
|
||||
String audioUrl = null;
|
||||
|
||||
for (int i = 0; i < adaptionSets.getLength(); i++) {
|
||||
Element element = (Element) adaptionSets.item(i);
|
||||
var contentType = element.getAttribute("contentType");
|
||||
var bestEntry = getBestMpEntry(element);
|
||||
if (bestEntry == null) continue;
|
||||
|
||||
if (contentType.equalsIgnoreCase("video")) {
|
||||
videoUrl = bestEntry.second;
|
||||
} else if (contentType.equalsIgnoreCase("audio")) {
|
||||
audioUrl = bestEntry.second;
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{videoUrl, audioUrl};
|
||||
}
|
||||
|
||||
public static String[] getLinks(byte[] data) {
|
||||
try {
|
||||
return parse(data);
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
return new String[]{null, null};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package app.revanced.integrations.twitter.patches.links;
|
||||
|
||||
public final class ChangeLinkSharingDomainPatch {
|
||||
private static final String DOMAIN_NAME = "https://fxtwitter.com";
|
||||
private static final String LINK_FORMAT = "%s/%s/status/%s";
|
||||
|
||||
public static String formatResourceLink(Object... formatArgs) {
|
||||
String username = (String) formatArgs[0];
|
||||
String tweetId = (String) formatArgs[1];
|
||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
||||
}
|
||||
|
||||
public static String formatLink(long tweetId, String username) {
|
||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
||||
}
|
||||
}
|
@ -8,7 +8,9 @@ public class BackgroundPlaybackPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean playbackIsNotShort() {
|
||||
public static boolean allowBackgroundPlayback(boolean original) {
|
||||
if (original) return true;
|
||||
|
||||
// Steps to verify most edge cases:
|
||||
// 1. Open a regular video
|
||||
// 2. Minimize app (PIP should appear)
|
||||
|
@ -1,21 +1,129 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ChangeStartPagePatch {
|
||||
public static void changeIntent(final Intent intent) {
|
||||
final var startPage = Settings.START_PAGE.get();
|
||||
if (startPage.isEmpty()) return;
|
||||
|
||||
Logger.printDebug(() -> "Changing start page to " + startPage);
|
||||
public enum StartPage {
|
||||
/**
|
||||
* Unmodified type, and same as un-patched.
|
||||
*/
|
||||
ORIGINAL("", null),
|
||||
|
||||
if (startPage.startsWith("www"))
|
||||
intent.setData(Uri.parse(startPage));
|
||||
else
|
||||
intent.setAction("com.google.android.youtube.action." + startPage);
|
||||
/**
|
||||
* Browse id.
|
||||
*/
|
||||
BROWSE("FEguide_builder", TRUE),
|
||||
EXPLORE("FEexplore", TRUE),
|
||||
HISTORY("FEhistory", TRUE),
|
||||
LIBRARY("FElibrary", TRUE),
|
||||
MOVIE("FEstorefront", TRUE),
|
||||
SUBSCRIPTIONS("FEsubscriptions", TRUE),
|
||||
TRENDING("FEtrending", TRUE),
|
||||
|
||||
/**
|
||||
* Channel id, this can be used as a browseId.
|
||||
*/
|
||||
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
|
||||
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
|
||||
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
|
||||
SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE),
|
||||
|
||||
/**
|
||||
* Playlist id, this can be used as a browseId.
|
||||
*/
|
||||
LIKED_VIDEO("VLLL", TRUE),
|
||||
WATCH_LATER("VLWL", TRUE),
|
||||
|
||||
/**
|
||||
* Intent action.
|
||||
*/
|
||||
SEARCH("com.google.android.youtube.action.open.search", FALSE),
|
||||
SHORTS("com.google.android.youtube.action.open.shorts", FALSE);
|
||||
|
||||
@Nullable
|
||||
final Boolean isBrowseId;
|
||||
|
||||
@NonNull
|
||||
final String id;
|
||||
|
||||
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
|
||||
this.id = id;
|
||||
this.isBrowseId = isBrowseId;
|
||||
}
|
||||
|
||||
private boolean isBrowseId() {
|
||||
return TRUE.equals(isBrowseId);
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
private boolean isIntentAction() {
|
||||
return FALSE.equals(isBrowseId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent action when YouTube is cold started from the launcher.
|
||||
* <p>
|
||||
* If you don't check this, the hooking will also apply in the following cases:
|
||||
* Case 1. The user clicked Shorts button on the YouTube shortcut.
|
||||
* Case 2. The user clicked Shorts button on the YouTube widget.
|
||||
* In this case, instead of opening Shorts, the start page specified by the user is opened.
|
||||
*/
|
||||
private static final String ACTION_MAIN = "android.intent.action.MAIN";
|
||||
|
||||
private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get();
|
||||
|
||||
/**
|
||||
* There is an issue where the back button on the toolbar doesn't work properly.
|
||||
* As a workaround for this issue, instead of overriding the browserId multiple times, just override it once.
|
||||
*/
|
||||
private static boolean appLaunched = false;
|
||||
|
||||
public static String overrideBrowseId(@NonNull String original) {
|
||||
if (!START_PAGE.isBrowseId()) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (appLaunched) {
|
||||
Logger.printDebug(() -> "Ignore override browseId as the app already launched");
|
||||
return original;
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id);
|
||||
return START_PAGE.id;
|
||||
}
|
||||
|
||||
public static void overrideIntentAction(@NonNull Intent intent) {
|
||||
if (!START_PAGE.isIntentAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ACTION_MAIN.equals(intent.getAction())) {
|
||||
Logger.printDebug(() -> "Ignore override intent action" +
|
||||
" as the current activity is not the entry point of the application");
|
||||
return;
|
||||
}
|
||||
|
||||
if (appLaunched) {
|
||||
Logger.printDebug(() -> "Ignore override intent action as the app already launched");
|
||||
return;
|
||||
}
|
||||
appLaunched = true;
|
||||
|
||||
final String intentAction = START_PAGE.id;
|
||||
Logger.printDebug(() -> "Changing intent action to " + intentAction);
|
||||
intent.setAction(intentAction);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,47 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HidePlayerButtonsPatch {
|
||||
|
||||
private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get();
|
||||
|
||||
private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID =
|
||||
Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id");
|
||||
|
||||
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID =
|
||||
Utils.getResourceIdentifier("player_control_next_button_touch_area", "id");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean previousOrNextButtonIsVisible(boolean previousOrNextButtonVisible) {
|
||||
if (Settings.HIDE_PLAYER_BUTTONS.get()) {
|
||||
return false;
|
||||
public static void hidePreviousNextButtons(View parentView) {
|
||||
if (!HIDE_PLAYER_BUTTONS_ENABLED) {
|
||||
return;
|
||||
}
|
||||
return previousOrNextButtonVisible;
|
||||
|
||||
// Must use a deferred call to main thread to hide the button.
|
||||
// Otherwise the layout crashes if set to hidden now.
|
||||
Utils.runOnMainThread(() -> {
|
||||
hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID);
|
||||
hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID);
|
||||
});
|
||||
}
|
||||
|
||||
private static void hideView(View parentView, int resourceId) {
|
||||
View nextPreviousButton = parentView.findViewById(resourceId);
|
||||
|
||||
if (nextPreviousButton == null) {
|
||||
Logger.printException(() -> "Could not find player previous/next button");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Hiding previous/next button");
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,22 @@ package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*;
|
||||
import static app.revanced.integrations.youtube.patches.VersionCheckPatch.*;
|
||||
|
||||
import android.util.DisplayMetrics;
|
||||
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.shared.settings.Setting;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused", "SpellCheckingInspection"})
|
||||
public final class MiniplayerPatch {
|
||||
|
||||
/**
|
||||
@ -28,7 +30,12 @@ public final class MiniplayerPatch {
|
||||
TABLET(true, null),
|
||||
MODERN_1(null, 1),
|
||||
MODERN_2(null, 2),
|
||||
MODERN_3(null, 3);
|
||||
MODERN_3(null, 3),
|
||||
/**
|
||||
* Half broken miniplayer, that might be work in progress or left over abandoned code.
|
||||
* Can force this type by editing the import/export settings.
|
||||
*/
|
||||
MODERN_4(null, 4);
|
||||
|
||||
/**
|
||||
* Legacy tablet hook value.
|
||||
@ -52,6 +59,35 @@ public final class MiniplayerPatch {
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MINIPLAYER_SIZE;
|
||||
|
||||
static {
|
||||
// YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size.
|
||||
DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics();
|
||||
final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density);
|
||||
|
||||
// YT seems to use a minimum height to calculate the minimum miniplayer width based on the video.
|
||||
// 170 seems to be the smallest that can be used and using less makes no difference.
|
||||
final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works.
|
||||
final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding.
|
||||
// Round down to the nearest 5 pixels, to keep any error toasts easier to read.
|
||||
final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
|
||||
Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX);
|
||||
|
||||
int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get();
|
||||
|
||||
if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) {
|
||||
Utils.showToastLong(str("revanced_miniplayer_width_dip_invalid_toast",
|
||||
WIDTH_DIP_MIN, WIDTH_DIP_MAX));
|
||||
|
||||
// Instead of resetting, clamp the size at the bounds.
|
||||
dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX));
|
||||
Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth);
|
||||
}
|
||||
|
||||
MINIPLAYER_SIZE = dipWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern subtitle overlay for {@link MiniplayerType#MODERN_2}.
|
||||
* Resource is not present in older targets, and this field will be zero.
|
||||
@ -61,8 +97,21 @@ public final class MiniplayerPatch {
|
||||
|
||||
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
|
||||
|
||||
/**
|
||||
* Cannot turn off double tap with modern 2 or 3 with later targets,
|
||||
* as forcing it off breakings tapping the miniplayer.
|
||||
*/
|
||||
private static final boolean DOUBLE_TAP_ACTION_ENABLED =
|
||||
// 19.29+ is very broken if double tap is not enabled.
|
||||
IS_19_29_OR_GREATER ||
|
||||
(CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get());
|
||||
|
||||
private static final boolean DRAG_AND_DROP_ENABLED =
|
||||
CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
||||
|
||||
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get();
|
||||
Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get()
|
||||
&& Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable();
|
||||
|
||||
private static final boolean HIDE_SUBTEXT_ENABLED =
|
||||
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
|
||||
@ -70,8 +119,29 @@ public final class MiniplayerPatch {
|
||||
private static final boolean HIDE_REWIND_FORWARD_ENABLED =
|
||||
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get();
|
||||
|
||||
private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED =
|
||||
Settings.MINIPLAYER_ROUNDED_CORNERS.get();
|
||||
|
||||
/**
|
||||
* Remove a broken and always present subtitle text that is only
|
||||
* present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21.
|
||||
*/
|
||||
private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE =
|
||||
CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER;
|
||||
|
||||
private static final int OPACITY_LEVEL;
|
||||
|
||||
public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability {
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
|
||||
return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3))
|
||||
|| (!IS_19_26_OR_GREATER && type == MODERN_1
|
||||
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|
||||
|| (IS_19_29_OR_GREATER && type == MODERN_3);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
int opacity = Settings.MINIPLAYER_OPACITY.get();
|
||||
|
||||
@ -122,6 +192,90 @@ public final class MiniplayerPatch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean getModernFeatureFlagsActiveOverride(boolean original) {
|
||||
if (original) Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original);
|
||||
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return CURRENT_TYPE.isModern();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDoubleTapAction(boolean original) {
|
||||
if (original) Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + true);
|
||||
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return DOUBLE_TAP_ACTION_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean enableMiniplayerDragAndDrop(boolean original) {
|
||||
if (original) Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + true);
|
||||
|
||||
if (CURRENT_TYPE == ORIGINAL) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return DRAG_AND_DROP_ENABLED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean setRoundedCorners(boolean original) {
|
||||
if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true);
|
||||
|
||||
if (CURRENT_TYPE.isModern()) {
|
||||
return MINIPLAYER_ROUNDED_CORNERS_ENABLED;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int setMiniplayerDefaultSize(int original) {
|
||||
if (CURRENT_TYPE.isModern()) {
|
||||
return MINIPLAYER_SIZE;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static float setMovementBoundFactor(float original) {
|
||||
// Not clear if customizing this is useful or not.
|
||||
// So for now just log this and use the original value.
|
||||
if (original != 1.0) Logger.printDebug(() -> "setMovementBoundFactor original: " + original);
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean setDropShadow(boolean original) {
|
||||
if (original) Logger.printDebug(() -> "setViewElevation original: " + true);
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@ -140,27 +294,35 @@ public final class MiniplayerPatch {
|
||||
* 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);
|
||||
try {
|
||||
// Different subviews are passed in, but only TextView is of interest here.
|
||||
if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) {
|
||||
Logger.printDebug(() -> "Hiding subtext view");
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(true, view);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "hideMiniplayerSubTexts failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
try {
|
||||
if (HIDE_BROKEN_MODERN_2_SUBTITLE && 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");
|
||||
if (subtitleText != null) {
|
||||
subtitleText.setVisibility(View.GONE);
|
||||
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -699,10 +699,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (!Settings.RYD_ENABLED.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printDebug(() -> "Cannot send vote, as current video data is null");
|
||||
@ -723,6 +725,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sendVote failure", ex);
|
||||
|
@ -4,7 +4,11 @@ import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SlideToSeekPatch {
|
||||
public static boolean isSlideToSeekDisabled() {
|
||||
return !Settings.SLIDE_TO_SEEK.get();
|
||||
private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get();
|
||||
|
||||
public static boolean isSlideToSeekDisabled(boolean isDisabled) {
|
||||
if (!isDisabled) return isDisabled;
|
||||
|
||||
return SLIDE_TO_SEEK_DISABLED;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
|
||||
public class VersionCheckPatch {
|
||||
public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0;
|
||||
public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0;
|
||||
public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0;
|
||||
public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0;
|
||||
}
|
@ -18,7 +18,7 @@ public final class VideoInformation {
|
||||
public interface PlaybackController {
|
||||
// Methods are added to YT classes during patching.
|
||||
boolean seekTo(long videoTime);
|
||||
boolean seekToRelative(long videoTimeOffset);
|
||||
void seekToRelative(long videoTimeOffset);
|
||||
}
|
||||
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
@ -229,21 +229,19 @@ public final class VideoInformation {
|
||||
/**
|
||||
* 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) {
|
||||
public static void seekToRelative(long seekTime) {
|
||||
Utils.verifyOnMainThread();
|
||||
try {
|
||||
Logger.printDebug(() -> "Seeking relative to: " + seekTime);
|
||||
|
||||
// Try regular playback controller first, and it will not succeed if casting.
|
||||
// 19.39+ does not have a boolean return type for relative seek.
|
||||
// But can call both methods and it works correctly for both situations.
|
||||
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.");
|
||||
controller.seekToRelative(seekTime);
|
||||
}
|
||||
|
||||
// Adjust the fine adjustment function so it's at least 1 second before/after.
|
||||
@ -258,13 +256,11 @@ public final class VideoInformation {
|
||||
controller = mdxPlayerDirectorRef.get();
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
|
||||
return false;
|
||||
} else {
|
||||
controller.seekToRelative(adjustedSeekTime);
|
||||
}
|
||||
|
||||
return controller.seekToRelative(adjustedSeekTime);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to seek relative", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,90 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
|
||||
/**
|
||||
* Filters litho based components.
|
||||
*
|
||||
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
||||
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*
|
||||
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
|
||||
* either an identifier or a path.
|
||||
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
||||
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||
*
|
||||
* All callbacks must be registered before the constructor completes.
|
||||
*/
|
||||
abstract class Filter {
|
||||
|
||||
public enum FilterContentType {
|
||||
IDENTIFIER,
|
||||
PATH,
|
||||
PROTOBUFFER
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier callbacks. Do not add to this instance,
|
||||
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
|
||||
*/
|
||||
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
||||
/**
|
||||
* Path callbacks. Do not add to this instance,
|
||||
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*/
|
||||
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
||||
identifierCallbacks.addAll(Arrays.asList(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
||||
pathCallbacks.addAll(Arrays.asList(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after an enabled filter has been matched.
|
||||
* Default implementation is to always filter the matched component and log the action.
|
||||
* Subclasses can perform additional or different checks if needed.
|
||||
* <p>
|
||||
* If the content is to be filtered, subclasses should always
|
||||
* call this method (and never return a plain 'true').
|
||||
* That way the logs will always show when a component was filtered and which filter hide it.
|
||||
* <p>
|
||||
* Method is called off the main thread.
|
||||
*
|
||||
* @param matchedGroup The actual filter that matched.
|
||||
* @param contentType The type of content matched.
|
||||
* @param contentIndex Matched index of the identifier or path.
|
||||
* @return True if the litho component should be filtered out.
|
||||
*/
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
String filterSimpleName = getClass().getSimpleName();
|
||||
if (contentType == FilterContentType.IDENTIFIER) {
|
||||
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
|
||||
} else {
|
||||
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,214 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.BooleanSetting;
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
|
||||
abstract class FilterGroup<T> {
|
||||
final static class FilterGroupResult {
|
||||
private BooleanSetting setting;
|
||||
private int matchedIndex;
|
||||
private int matchedLength;
|
||||
// In the future it might be useful to include which pattern matched,
|
||||
// but for now that is not needed.
|
||||
|
||||
FilterGroupResult() {
|
||||
this(null, -1, 0);
|
||||
}
|
||||
|
||||
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
setValues(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
|
||||
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
this.setting = setting;
|
||||
this.matchedIndex = matchedIndex;
|
||||
this.matchedLength = matchedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* A null value if the group has no setting,
|
||||
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
||||
*/
|
||||
public BooleanSetting getSetting() {
|
||||
return setting;
|
||||
}
|
||||
|
||||
public boolean isFiltered() {
|
||||
return matchedIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matched index of first pattern that matched, or -1 if nothing matched.
|
||||
*/
|
||||
public int getMatchedIndex() {
|
||||
return matchedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the matched filter pattern.
|
||||
*/
|
||||
public int getMatchedLength() {
|
||||
return matchedLength;
|
||||
}
|
||||
}
|
||||
|
||||
protected final BooleanSetting setting;
|
||||
protected final T[] filters;
|
||||
|
||||
/**
|
||||
* Initialize a new filter group.
|
||||
*
|
||||
* @param setting The associated setting.
|
||||
* @param filters The filters.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public FilterGroup(final BooleanSetting setting, final T... filters) {
|
||||
this.setting = setting;
|
||||
this.filters = filters;
|
||||
if (filters.length == 0) {
|
||||
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return setting == null || setting.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If {@link FilterGroupList} should include this group when searching.
|
||||
* By default, all filters are included except non enabled settings that require reboot.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean includeInSearch() {
|
||||
return isEnabled() || !setting.rebootApp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
|
||||
}
|
||||
|
||||
public abstract FilterGroupResult check(final T stack);
|
||||
}
|
||||
|
||||
class StringFilterGroup extends FilterGroup<String> {
|
||||
|
||||
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final String string) {
|
||||
int matchedIndex = -1;
|
||||
int matchedLength = 0;
|
||||
if (isEnabled()) {
|
||||
for (String pattern : filters) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = string.indexOf(pattern);
|
||||
if (indexOf >= 0) {
|
||||
matchedIndex = indexOf;
|
||||
matchedLength = pattern.length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If you have more than 1 filter patterns, then all instances of
|
||||
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
|
||||
* which uses a prefix tree to give better performance.
|
||||
*/
|
||||
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
|
||||
private volatile int[][] failurePatterns;
|
||||
|
||||
// Modified implementation from https://stackoverflow.com/a/1507813
|
||||
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
|
||||
// Finds the first occurrence of the pattern in the byte array using
|
||||
// KMP matching algorithm.
|
||||
int patternLength = pattern.length;
|
||||
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
|
||||
while (j > 0 && pattern[j] != data[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == data[i]) {
|
||||
j++;
|
||||
}
|
||||
if (j == patternLength) {
|
||||
return i - patternLength + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int[] createFailurePattern(byte[] pattern) {
|
||||
// Computes the failure function using a boot-strapping process,
|
||||
// where the pattern is matched against itself.
|
||||
final int patternLength = pattern.length;
|
||||
final int[] failure = new int[patternLength];
|
||||
|
||||
for (int i = 1, j = 0; i < patternLength; i++) {
|
||||
while (j > 0 && pattern[j] != pattern[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == pattern[i]) {
|
||||
j++;
|
||||
}
|
||||
failure[i] = j;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Strings into byte arrays. Used to search for text in binary data.
|
||||
*/
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
|
||||
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
|
||||
}
|
||||
|
||||
private synchronized void buildFailurePatterns() {
|
||||
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
|
||||
Logger.printDebug(() -> "Building failure array for: " + this);
|
||||
int[][] failurePatterns = new int[filters.length][];
|
||||
int i = 0;
|
||||
for (byte[] pattern : filters) {
|
||||
failurePatterns[i++] = createFailurePattern(pattern);
|
||||
}
|
||||
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final byte[] bytes) {
|
||||
int matchedLength = 0;
|
||||
int matchedIndex = -1;
|
||||
if (isEnabled()) {
|
||||
int[][] failures = failurePatterns;
|
||||
if (failures == null) {
|
||||
buildFailurePatterns(); // Lazy load.
|
||||
failures = failurePatterns;
|
||||
}
|
||||
for (int i = 0, length = filters.length; i < length; i++) {
|
||||
byte[] filter = filters[i];
|
||||
matchedIndex = indexOf(bytes, filter, failures[i]);
|
||||
if (matchedIndex >= 0) {
|
||||
matchedLength = filter.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
import app.revanced.integrations.youtube.StringTrieSearch;
|
||||
import app.revanced.integrations.youtube.TrieSearch;
|
||||
|
||||
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
|
||||
private final List<T> filterGroups = new ArrayList<>();
|
||||
private final TrieSearch<V> search = createSearchGraph();
|
||||
|
||||
@SafeVarargs
|
||||
protected final void addAll(final T... groups) {
|
||||
filterGroups.addAll(Arrays.asList(groups));
|
||||
|
||||
for (T group : groups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
for (V pattern : group.filters) {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
if (group.isEnabled()) {
|
||||
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
||||
result.setValues(group.setting, matchedStartIndex, matchedLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return filterGroups.iterator();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void forEach(@NonNull Consumer<? super T> action) {
|
||||
filterGroups.forEach(action);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@NonNull
|
||||
@Override
|
||||
public Spliterator<T> spliterator() {
|
||||
return filterGroups.spliterator();
|
||||
}
|
||||
|
||||
protected FilterGroup.FilterGroupResult check(V stack) {
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
}
|
||||
|
||||
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
||||
protected StringTrieSearch createSearchGraph() {
|
||||
return new StringTrieSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If searching for a single byte pattern, then it is slightly better to use
|
||||
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
||||
* than a prefix tree to search for only 1 pattern.
|
||||
*/
|
||||
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||
protected ByteTrieSearch createSearchGraph() {
|
||||
return new ByteTrieSearch();
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
@ -177,7 +178,8 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
final var playables = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYABLES,
|
||||
"horizontal_gaming_shelf.eml"
|
||||
"horizontal_gaming_shelf.eml",
|
||||
"mini_game_card.eml"
|
||||
);
|
||||
|
||||
final var quickActions = new StringFilterGroup(
|
||||
@ -380,6 +382,21 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get();
|
||||
}
|
||||
|
||||
|
||||
private static final boolean HIDE_DOODLES_ENABLED = Settings.HIDE_DOODLES.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@Nullable
|
||||
public static Drawable hideYoodles(Drawable animatedYoodle) {
|
||||
if (HIDE_DOODLES_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return animatedYoodle;
|
||||
}
|
||||
|
||||
private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get();
|
||||
|
||||
/**
|
||||
@ -406,7 +423,6 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
// Check navigation button last.
|
||||
// Only filter if the library tab is not selected.
|
||||
// This check is important as the shelf layout is used for the library tab playlists.
|
||||
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
|
||||
return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab();
|
||||
return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
|
||||
}
|
||||
}
|
||||
|
@ -1,389 +1,15 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Spliterator;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.BooleanSetting;
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
import app.revanced.integrations.youtube.StringTrieSearch;
|
||||
import app.revanced.integrations.youtube.TrieSearch;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
abstract class FilterGroup<T> {
|
||||
final static class FilterGroupResult {
|
||||
private BooleanSetting setting;
|
||||
private int matchedIndex;
|
||||
private int matchedLength;
|
||||
// In the future it might be useful to include which pattern matched,
|
||||
// but for now that is not needed.
|
||||
|
||||
FilterGroupResult() {
|
||||
this(null, -1, 0);
|
||||
}
|
||||
|
||||
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
setValues(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
|
||||
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
||||
this.setting = setting;
|
||||
this.matchedIndex = matchedIndex;
|
||||
this.matchedLength = matchedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* A null value if the group has no setting,
|
||||
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
||||
*/
|
||||
public BooleanSetting getSetting() {
|
||||
return setting;
|
||||
}
|
||||
|
||||
public boolean isFiltered() {
|
||||
return matchedIndex >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matched index of first pattern that matched, or -1 if nothing matched.
|
||||
*/
|
||||
public int getMatchedIndex() {
|
||||
return matchedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the matched filter pattern.
|
||||
*/
|
||||
public int getMatchedLength() {
|
||||
return matchedLength;
|
||||
}
|
||||
}
|
||||
|
||||
protected final BooleanSetting setting;
|
||||
protected final T[] filters;
|
||||
|
||||
/**
|
||||
* Initialize a new filter group.
|
||||
*
|
||||
* @param setting The associated setting.
|
||||
* @param filters The filters.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public FilterGroup(final BooleanSetting setting, final T... filters) {
|
||||
this.setting = setting;
|
||||
this.filters = filters;
|
||||
if (filters.length == 0) {
|
||||
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return setting == null || setting.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If {@link FilterGroupList} should include this group when searching.
|
||||
* By default, all filters are included except non enabled settings that require reboot.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean includeInSearch() {
|
||||
return isEnabled() || !setting.rebootApp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
|
||||
}
|
||||
|
||||
public abstract FilterGroupResult check(final T stack);
|
||||
}
|
||||
|
||||
class StringFilterGroup extends FilterGroup<String> {
|
||||
|
||||
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final String string) {
|
||||
int matchedIndex = -1;
|
||||
int matchedLength = 0;
|
||||
if (isEnabled()) {
|
||||
for (String pattern : filters) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = string.indexOf(pattern);
|
||||
if (indexOf >= 0) {
|
||||
matchedIndex = indexOf;
|
||||
matchedLength = pattern.length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If you have more than 1 filter patterns, then all instances of
|
||||
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
|
||||
* which uses a prefix tree to give better performance.
|
||||
*/
|
||||
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
|
||||
private volatile int[][] failurePatterns;
|
||||
|
||||
// Modified implementation from https://stackoverflow.com/a/1507813
|
||||
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
|
||||
// Finds the first occurrence of the pattern in the byte array using
|
||||
// KMP matching algorithm.
|
||||
int patternLength = pattern.length;
|
||||
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
|
||||
while (j > 0 && pattern[j] != data[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == data[i]) {
|
||||
j++;
|
||||
}
|
||||
if (j == patternLength) {
|
||||
return i - patternLength + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int[] createFailurePattern(byte[] pattern) {
|
||||
// Computes the failure function using a boot-strapping process,
|
||||
// where the pattern is matched against itself.
|
||||
final int patternLength = pattern.length;
|
||||
final int[] failure = new int[patternLength];
|
||||
|
||||
for (int i = 1, j = 0; i < patternLength; i++) {
|
||||
while (j > 0 && pattern[j] != pattern[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == pattern[i]) {
|
||||
j++;
|
||||
}
|
||||
failure[i] = j;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Strings into byte arrays. Used to search for text in binary data.
|
||||
*/
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
|
||||
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
|
||||
}
|
||||
|
||||
private synchronized void buildFailurePatterns() {
|
||||
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
|
||||
Logger.printDebug(() -> "Building failure array for: " + this);
|
||||
int[][] failurePatterns = new int[filters.length][];
|
||||
int i = 0;
|
||||
for (byte[] pattern : filters) {
|
||||
failurePatterns[i++] = createFailurePattern(pattern);
|
||||
}
|
||||
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final byte[] bytes) {
|
||||
int matchedLength = 0;
|
||||
int matchedIndex = -1;
|
||||
if (isEnabled()) {
|
||||
int[][] failures = failurePatterns;
|
||||
if (failures == null) {
|
||||
buildFailurePatterns(); // Lazy load.
|
||||
failures = failurePatterns;
|
||||
}
|
||||
for (int i = 0, length = filters.length; i < length; i++) {
|
||||
byte[] filter = filters[i];
|
||||
matchedIndex = indexOf(bytes, filter, failures[i]);
|
||||
if (matchedIndex >= 0) {
|
||||
matchedLength = filter.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new FilterGroupResult(setting, matchedIndex, matchedLength);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
|
||||
private final List<T> filterGroups = new ArrayList<>();
|
||||
private final TrieSearch<V> search = createSearchGraph();
|
||||
|
||||
@SafeVarargs
|
||||
protected final void addAll(final T... groups) {
|
||||
filterGroups.addAll(Arrays.asList(groups));
|
||||
|
||||
for (T group : groups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
for (V pattern : group.filters) {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
if (group.isEnabled()) {
|
||||
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
||||
result.setValues(group.setting, matchedStartIndex, matchedLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return filterGroups.iterator();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void forEach(@NonNull Consumer<? super T> action) {
|
||||
filterGroups.forEach(action);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@NonNull
|
||||
@Override
|
||||
public Spliterator<T> spliterator() {
|
||||
return filterGroups.spliterator();
|
||||
}
|
||||
|
||||
protected FilterGroup.FilterGroupResult check(V stack) {
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
}
|
||||
|
||||
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
||||
protected StringTrieSearch createSearchGraph() {
|
||||
return new StringTrieSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If searching for a single byte pattern, then it is slightly better to use
|
||||
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
||||
* than a prefix tree to search for only 1 pattern.
|
||||
*/
|
||||
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||
protected ByteTrieSearch createSearchGraph() {
|
||||
return new ByteTrieSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters litho based components.
|
||||
*
|
||||
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
||||
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*
|
||||
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
|
||||
* either an identifier or a path.
|
||||
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
||||
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||
*
|
||||
* All callbacks must be registered before the constructor completes.
|
||||
*/
|
||||
abstract class Filter {
|
||||
|
||||
public enum FilterContentType {
|
||||
IDENTIFIER,
|
||||
PATH,
|
||||
PROTOBUFFER
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier callbacks. Do not add to this instance,
|
||||
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
|
||||
*/
|
||||
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
||||
/**
|
||||
* Path callbacks. Do not add to this instance,
|
||||
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*/
|
||||
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
||||
identifierCallbacks.addAll(Arrays.asList(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||
* if any of the groups are found.
|
||||
*/
|
||||
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
||||
pathCallbacks.addAll(Arrays.asList(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after an enabled filter has been matched.
|
||||
* Default implementation is to always filter the matched component and log the action.
|
||||
* Subclasses can perform additional or different checks if needed.
|
||||
* <p>
|
||||
* If the content is to be filtered, subclasses should always
|
||||
* call this method (and never return a plain 'true').
|
||||
* That way the logs will always show when a component was filtered and which filter hide it.
|
||||
* <p>
|
||||
* Method is called off the main thread.
|
||||
*
|
||||
* @param matchedGroup The actual filter that matched.
|
||||
* @param contentType The type of content matched.
|
||||
* @param contentIndex Matched index of the identifier or path.
|
||||
* @return True if the litho component should be filtered out.
|
||||
*/
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
String filterSimpleName = getClass().getSimpleName();
|
||||
if (contentType == FilterContentType.IDENTIFIER) {
|
||||
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
|
||||
} else {
|
||||
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
final class DummyFilter extends Filter { }
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LithoFilterPatch {
|
||||
/**
|
||||
@ -520,9 +146,9 @@ public final class LithoFilterPatch {
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
|
||||
try {
|
||||
// It is assumed that protobufBuffer is empty as well in this case.
|
||||
if (pathBuilder.length() == 0)
|
||||
if (pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer protobufBuffer = bufferThreadLocal.get();
|
||||
final byte[] bufferArray;
|
||||
@ -542,14 +168,22 @@ public final class LithoFilterPatch {
|
||||
pathBuilder.toString(), bufferArray);
|
||||
Logger.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
if (parameter.identifier != null) {
|
||||
if (identifierSearchTree.matches(parameter.identifier, parameter)) return true;
|
||||
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathSearchTree.matches(parameter.path, parameter)) {
|
||||
return true;
|
||||
}
|
||||
if (pathSearchTree.matches(parameter.path, parameter)) return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Litho filter failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
final class DummyFilter extends Filter { }
|
@ -9,6 +9,11 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.NavigationBar;
|
||||
@ -16,14 +21,26 @@ import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class ShortsFilter extends Filter {
|
||||
public static PivotBar pivotBar; // Set by patch.
|
||||
|
||||
public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
|
||||
private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
|
||||
|
||||
/**
|
||||
* For paid promotion label and subscribe button that appears in the channel bar.
|
||||
*/
|
||||
private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml";
|
||||
|
||||
/**
|
||||
* Tags that appears when opening the Shorts player.
|
||||
*/
|
||||
private static final List<String> REEL_WATCH_FRAGMENT_INIT_PLAYBACK = Arrays.asList("r_fs", "r_ts");
|
||||
|
||||
/**
|
||||
* Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden.
|
||||
*/
|
||||
public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 100;
|
||||
|
||||
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
|
||||
|
||||
private final StringFilterGroup shortsCompactFeedVideoPath;
|
||||
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
||||
|
||||
@ -33,7 +50,7 @@ public final class ShortsFilter extends Filter {
|
||||
private final StringFilterGroup shelfHeader;
|
||||
|
||||
private final StringFilterGroup suggestedAction;
|
||||
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
|
||||
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup actionBar;
|
||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
||||
@ -117,6 +134,11 @@ public final class ShortsFilter extends Filter {
|
||||
"stickers_layer.eml"
|
||||
);
|
||||
|
||||
StringFilterGroup likeFountain = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_LIKE_FOUNTAIN,
|
||||
"like_fountain.eml"
|
||||
);
|
||||
|
||||
joinButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_JOIN_BUTTON,
|
||||
"sponsor_button"
|
||||
@ -145,7 +167,7 @@ public final class ShortsFilter extends Filter {
|
||||
addPathCallbacks(
|
||||
shortsCompactFeedVideoPath, suggestedAction, actionBar, joinButton, subscribeButton,
|
||||
paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel, videoTitle,
|
||||
reelSoundMetadata, soundButton, infoPanel, stickers
|
||||
reelSoundMetadata, soundButton, infoPanel, stickers, likeFountain
|
||||
);
|
||||
|
||||
//
|
||||
@ -213,10 +235,36 @@ public final class ShortsFilter extends Filter {
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
|
||||
"yt_outline_dollar_sign_heart_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON,
|
||||
"yt_outline_template_add_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_UPCOMING_BUTTON,
|
||||
"yt_outline_bell_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
|
||||
"greenscreen_temp"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_HASHTAG_BUTTON,
|
||||
"yt_outline_hashtag_"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isEverySuggestedActionFilterEnabled() {
|
||||
for (ByteArrayFilterGroup group : suggestedActionsGroupList) {
|
||||
if (!group.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
@ -224,9 +272,7 @@ public final class ShortsFilter extends Filter {
|
||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
||||
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
||||
if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
|
||||
return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
|
||||
);
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -241,19 +287,21 @@ public final class ShortsFilter extends Filter {
|
||||
// Video action buttons (like, dislike, comment, share, remix) have the same path.
|
||||
if (matchedGroup == actionBar) {
|
||||
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
|
||||
);
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
// Suggested actions can be at the start or in the middle of a path.
|
||||
// Skip searching the buffer if all suggested actions are set to hidden.
|
||||
// This has a secondary effect of hiding all new un-identified actions
|
||||
// under the assumption that the user wants all actions hidden.
|
||||
if (isEverySuggestedActionFilterEnabled()) {
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
|
||||
);
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -326,6 +374,14 @@ public final class ShortsFilter extends Filter {
|
||||
}
|
||||
}
|
||||
|
||||
public static int getSoundButtonSize(int original) {
|
||||
if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
// region Hide the buttons in older versions of YouTube. New versions use Litho.
|
||||
|
||||
public static void hideLikeButton(final View likeButtonView) {
|
||||
@ -357,17 +413,30 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
// endregion
|
||||
|
||||
public static void hideNavigationBar() {
|
||||
if (!Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) return;
|
||||
if (pivotBar == null) return;
|
||||
|
||||
pivotBar.setVisibility(View.GONE);
|
||||
public static void setNavigationBar(PivotBar view) {
|
||||
Logger.printDebug(() -> "Setting navigation bar");
|
||||
pivotBarRef = new WeakReference<>(view);
|
||||
}
|
||||
|
||||
public static View hideNavigationBar(final View navigationBarView) {
|
||||
if (Settings.HIDE_SHORTS_NAVIGATION_BAR.get())
|
||||
return null; // Hides the navigation bar.
|
||||
public static void hideNavigationBar(String tag) {
|
||||
if (HIDE_SHORTS_NAVIGATION_BAR) {
|
||||
if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.contains(tag)) {
|
||||
var pivotBar = pivotBarRef.get();
|
||||
if (pivotBar == null) return;
|
||||
|
||||
return navigationBarView;
|
||||
Logger.printDebug(() -> "Hiding navbar by setting to GONE");
|
||||
pivotBar.setVisibility(View.GONE);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Ignoring tag: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int getNavigationBarHeight(int original) {
|
||||
if (HIDE_SHORTS_NAVIGATION_BAR) {
|
||||
return HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ public class SpoofVideoStreamsPatch {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
|
||||
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "getStreamingData failure", ex);
|
||||
}
|
||||
@ -154,13 +154,11 @@ public class SpoofVideoStreamsPatch {
|
||||
final int methodPost = 2;
|
||||
if (method == methodPost) {
|
||||
String path = uri.getPath();
|
||||
String clientNameQueryKey = "c";
|
||||
final boolean iosClient = "IOS".equals(uri.getQueryParameter(clientNameQueryKey));
|
||||
if (iosClient && path != null && path.contains("videoplayback")) {
|
||||
if (path != null && path.contains("videoplayback")) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,12 @@ public class StreamingDataRequest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] REQUEST_HEADER_KEYS = {
|
||||
"Authorization", // Available only to logged in users.
|
||||
"X-GOOG-API-FORMAT-VERSION",
|
||||
"X-Goog-Visitor-Id"
|
||||
};
|
||||
|
||||
/**
|
||||
* TCP connection and HTTP read timeout.
|
||||
*/
|
||||
@ -112,10 +118,12 @@ public class StreamingDataRequest {
|
||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||
|
||||
String authHeader = playerHeaders.get("Authorization");
|
||||
String visitorId = playerHeaders.get("X-Goog-Visitor-Id");
|
||||
connection.setRequestProperty("Authorization", authHeader);
|
||||
connection.setRequestProperty("X-Goog-Visitor-Id", visitorId);
|
||||
for (String key : REQUEST_HEADER_KEYS) {
|
||||
String value = playerHeaders.get(key);
|
||||
if (value != null) {
|
||||
connection.setRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
|
||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||
|
@ -4,20 +4,32 @@ import static app.revanced.integrations.shared.StringRef.str;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class SeekbarColorPatch {
|
||||
|
||||
private static final boolean USE_SEEKBAR_CUSTOM_COLOR = Settings.SEEKBAR_CUSTOM_COLOR.get();
|
||||
private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get();
|
||||
|
||||
/**
|
||||
* Default color of the seekbar.
|
||||
*/
|
||||
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
|
||||
|
||||
/**
|
||||
* Default colors of the gradient seekbar.
|
||||
*/
|
||||
private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 };
|
||||
|
||||
/**
|
||||
* Default positions of the gradient seekbar.
|
||||
*/
|
||||
private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f };
|
||||
|
||||
/**
|
||||
* Default YouTube seekbar color brightness.
|
||||
*/
|
||||
@ -40,7 +52,7 @@ public final class SeekbarColorPatch {
|
||||
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
|
||||
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
|
||||
|
||||
if (USE_SEEKBAR_CUSTOM_COLOR) {
|
||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
loadCustomSeekbarColor();
|
||||
}
|
||||
}
|
||||
@ -60,6 +72,14 @@ public final class SeekbarColorPatch {
|
||||
return seekbarColor;
|
||||
}
|
||||
|
||||
public static boolean playerSeekbarGradientEnabled(boolean original) {
|
||||
if (original) {
|
||||
Logger.printDebug(() -> "playerSeekbarGradientEnabled original: " + true);
|
||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false;
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
@ -74,17 +94,42 @@ public final class SeekbarColorPatch {
|
||||
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
||||
return 0x00000000;
|
||||
}
|
||||
|
||||
return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR);
|
||||
}
|
||||
return colorValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setLinearGradient(int[] colors, float[] positions) {
|
||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
// Most litho usage of linear gradients is hooked here,
|
||||
// so must only change if the values are those for the seekbar.
|
||||
if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors)
|
||||
&& Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) {
|
||||
Arrays.fill(colors, Settings.HIDE_SEEKBAR_THUMBNAIL.get()
|
||||
? 0x00000000
|
||||
: seekbarColor);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors)
|
||||
+ " positions: " + Arrays.toString(positions));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Overrides color when video player seekbar is clicked.
|
||||
*/
|
||||
public static int getVideoPlayerSeekbarClickedColor(int colorValue) {
|
||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
return colorValue;
|
||||
}
|
||||
|
||||
return colorValue == ORIGINAL_SEEKBAR_COLOR
|
||||
? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR)
|
||||
: colorValue;
|
||||
@ -96,6 +141,10 @@ public final class SeekbarColorPatch {
|
||||
* Overrides color used for the video player seekbar.
|
||||
*/
|
||||
public static int getVideoPlayerSeekbarColor(int originalColor) {
|
||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
return originalColor;
|
||||
}
|
||||
|
||||
return getSeekbarColorValue(originalColor);
|
||||
}
|
||||
|
||||
@ -105,9 +154,10 @@ public final class SeekbarColorPatch {
|
||||
*/
|
||||
private static int getSeekbarColorValue(int originalColor) {
|
||||
try {
|
||||
if (!USE_SEEKBAR_CUSTOM_COLOR || originalColor == seekbarColor) {
|
||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) {
|
||||
return originalColor; // nothing to do
|
||||
}
|
||||
|
||||
final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR);
|
||||
|
||||
// The seekbar uses the same color but different brightness for different situations.
|
||||
@ -131,11 +181,13 @@ public final class SeekbarColorPatch {
|
||||
}
|
||||
}
|
||||
|
||||
static int clamp(int value, int lower, int upper) {
|
||||
/** @noinspection SameParameterValue */
|
||||
private static int clamp(int value, int lower, int upper) {
|
||||
return Math.max(lower, Math.min(value, upper));
|
||||
}
|
||||
|
||||
static float clamp(float value, float lower, float upper) {
|
||||
/** @noinspection SameParameterValue */
|
||||
private static float clamp(float value, float lower, float upper) {
|
||||
return Math.max(lower, Math.min(value, upper));
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ public class Requester {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
jsonBuilder.append(line);
|
||||
jsonBuilder.append("\n");
|
||||
jsonBuilder.append('\n');
|
||||
}
|
||||
return jsonBuilder.toString();
|
||||
}
|
||||
|
@ -585,8 +585,13 @@ public class ReturnYouTubeDislike {
|
||||
public void sendVote(@NonNull Vote vote) {
|
||||
Utils.verifyOnMainThread();
|
||||
Objects.requireNonNull(vote);
|
||||
|
||||
try {
|
||||
if (isShort != PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
PlayerType currentType = PlayerType.getCurrent();
|
||||
if (isShort != currentType.isNoneHiddenOrMinimized()) {
|
||||
Logger.printDebug(() -> "Cannot vote for video: " + videoId
|
||||
+ " as current player type does not match: " + currentType);
|
||||
|
||||
// Shorts was loaded with regular video present, then Shorts was closed.
|
||||
// and then user voted on the now visible original video.
|
||||
// Cannot send a vote, because this instance is for the wrong video.
|
||||
|
@ -1,5 +1,18 @@
|
||||
package app.revanced.integrations.youtube.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.integrations.shared.settings.Setting.*;
|
||||
import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.*;
|
||||
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
|
||||
@ -12,18 +25,6 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch;
|
||||
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static app.revanced.integrations.shared.settings.Setting.*;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
|
||||
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class Settings extends BaseSettings {
|
||||
// Video
|
||||
@ -57,6 +58,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true);
|
||||
public static final BooleanSetting HIDE_ARTIST_CARDS = new BooleanSetting("revanced_hide_artist_cards", FALSE);
|
||||
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
|
||||
public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message");
|
||||
|
||||
// Alternative thumbnails
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||
@ -129,16 +131,21 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE);
|
||||
public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
|
||||
public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE);
|
||||
public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE);
|
||||
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
|
||||
|
||||
// Miniplayer
|
||||
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true);
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
|
||||
private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4);
|
||||
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability());
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
|
||||
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
|
||||
// External downloader
|
||||
@ -187,7 +194,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_VIDEO_QUALITY_MENU_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE);
|
||||
|
||||
// General layout
|
||||
public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", "");
|
||||
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true);
|
||||
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
|
||||
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION));
|
||||
public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message");
|
||||
@ -220,9 +227,14 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_STICKERS = new BooleanSetting("revanced_hide_shorts_stickers", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LIKE_FOUNTAIN = new BooleanSetting("revanced_hide_shorts_like_fountain", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
|
||||
@ -234,12 +246,12 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", TRUE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true);
|
||||
|
||||
// Seekbar
|
||||
public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE);
|
||||
public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", TRUE);
|
||||
public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE);
|
||||
public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true);
|
||||
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE);
|
||||
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE);
|
||||
|
@ -381,6 +381,8 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
importExport = new EditTextPreference(context) {
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
});
|
||||
|
@ -8,6 +8,8 @@ import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -156,11 +158,11 @@ public final class NavigationBar {
|
||||
try {
|
||||
String lastEnumName = lastYTNavigationEnumName;
|
||||
|
||||
for (NavigationButton button : NavigationButton.values()) {
|
||||
if (button.ytEnumName.equals(lastEnumName)) {
|
||||
for (NavigationButton buttonType : NavigationButton.values()) {
|
||||
if (buttonType.ytEnumNames.contains(lastEnumName)) {
|
||||
Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName);
|
||||
viewToButtonMap.put(navigationButtonGroup, button);
|
||||
navigationTabCreatedCallback(button, navigationButtonGroup);
|
||||
viewToButtonMap.put(navigationButtonGroup, buttonType);
|
||||
navigationTabCreatedCallback(buttonType, navigationButtonGroup);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -184,10 +186,10 @@ public final class NavigationBar {
|
||||
public static void navigationImageResourceTabLoaded(View view) {
|
||||
// 'You' tab has no YT enum name and the enum hook is not called for it.
|
||||
// Compare the last enum to figure out which tab this actually is.
|
||||
if (CREATE.ytEnumName.equals(lastYTNavigationEnumName)) {
|
||||
if (CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) {
|
||||
navigationTabLoaded(view);
|
||||
} else {
|
||||
lastYTNavigationEnumName = NavigationButton.LIBRARY_YOU.ytEnumName;
|
||||
lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0);
|
||||
navigationTabLoaded(view);
|
||||
}
|
||||
}
|
||||
@ -237,44 +239,39 @@ public final class NavigationBar {
|
||||
}
|
||||
|
||||
public enum NavigationButton {
|
||||
HOME("PIVOT_HOME"),
|
||||
SHORTS("TAB_SHORTS"),
|
||||
HOME("PIVOT_HOME", "TAB_HOME_CAIRO"),
|
||||
SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"),
|
||||
/**
|
||||
* Create new video tab.
|
||||
* This tab will never be in a selected state, even if the create video UI is on screen.
|
||||
*/
|
||||
CREATE("CREATION_TAB_LARGE"),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"),
|
||||
CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
|
||||
/**
|
||||
* Notifications tab. Only present when
|
||||
* {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active.
|
||||
*/
|
||||
NOTIFICATIONS("TAB_ACTIVITY"),
|
||||
NOTIFICATIONS("TAB_ACTIVITY", "TAB_ACTIVITY_CAIRO"),
|
||||
/**
|
||||
* Library tab when the user is not logged in.
|
||||
* Library tab, including if the user is in incognito mode or when logged out.
|
||||
*/
|
||||
LIBRARY_LOGGED_OUT("ACCOUNT_CIRCLE"),
|
||||
/**
|
||||
* User is logged in with incognito mode enabled.
|
||||
*/
|
||||
LIBRARY_INCOGNITO("INCOGNITO_CIRCLE"),
|
||||
/**
|
||||
* Old library tab (pre 'You' layout), only present when version spoofing.
|
||||
*/
|
||||
LIBRARY_OLD_UI("VIDEO_LIBRARY_WHITE"),
|
||||
/**
|
||||
* 'You' library tab that is sometimes momentarily loaded.
|
||||
* When this is loaded, {@link #LIBRARY_YOU} is also present.
|
||||
*
|
||||
* This might be a temporary tab while the user profile photo is loading,
|
||||
* but its exact purpose is not entirely clear.
|
||||
*/
|
||||
LIBRARY_PIVOT_UNKNOWN("PIVOT_LIBRARY"),
|
||||
/**
|
||||
* Modern library tab with 'You' layout.
|
||||
*/
|
||||
// The hooked YT code does not use an enum, and a dummy name is used here.
|
||||
LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME");
|
||||
LIBRARY(
|
||||
// Modern library tab with 'You' layout.
|
||||
// The hooked YT code does not use an enum, and a dummy name is used here.
|
||||
"YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME",
|
||||
// User is logged out.
|
||||
"ACCOUNT_CIRCLE",
|
||||
"ACCOUNT_CIRCLE_CAIRO",
|
||||
// User is logged in with incognito mode enabled.
|
||||
"INCOGNITO_CIRCLE",
|
||||
"INCOGNITO_CAIRO",
|
||||
// Old library tab (pre 'You' layout), only present when version spoofing.
|
||||
"VIDEO_LIBRARY_WHITE",
|
||||
// 'You' library tab that is sometimes momentarily loaded.
|
||||
// This might be a temporary tab while the user profile photo is loading,
|
||||
// but its exact purpose is not entirely clear.
|
||||
"PIVOT_LIBRARY"
|
||||
);
|
||||
|
||||
@Nullable
|
||||
private static volatile NavigationButton selectedNavigationButton;
|
||||
@ -303,16 +300,10 @@ public final class NavigationBar {
|
||||
/**
|
||||
* YouTube enum name for this tab.
|
||||
*/
|
||||
private final String ytEnumName;
|
||||
private final List<String> ytEnumNames;
|
||||
|
||||
NavigationButton(String ytEnumName) {
|
||||
this.ytEnumName = ytEnumName;
|
||||
}
|
||||
|
||||
public boolean isLibraryOrYouTab() {
|
||||
return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN
|
||||
|| this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO
|
||||
|| this == LIBRARY_LOGGED_OUT;
|
||||
NavigationButton(String... ytEnumNames) {
|
||||
this.ytEnumNames = Arrays.asList(ytEnumNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import java.util.Objects;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SegmentCategoryListPreference extends ListPreference {
|
||||
private final SegmentCategory category;
|
||||
private EditText mEditText;
|
||||
@ -45,6 +46,8 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
Context context = builder.getContext();
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
@ -25,8 +25,8 @@ public class CreateSegmentButtonController {
|
||||
public static void initialize(View youtubeControlsLayout) {
|
||||
try {
|
||||
Logger.printDebug(() -> "initializing new segment button");
|
||||
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
|
||||
getResourceIdentifier("revanced_sb_create_segment_button", "id")));
|
||||
ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName(
|
||||
youtubeControlsLayout, "revanced_sb_create_segment_button"));
|
||||
imageView.setVisibility(View.GONE);
|
||||
imageView.setOnClickListener(v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility());
|
||||
|
||||
|
@ -27,8 +27,8 @@ public class VotingButtonController {
|
||||
public static void initialize(View youtubeControlsLayout) {
|
||||
try {
|
||||
Logger.printDebug(() -> "initializing voting button");
|
||||
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
|
||||
getResourceIdentifier("revanced_sb_voting_button", "id")));
|
||||
ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName(
|
||||
youtubeControlsLayout, "revanced_sb_voting_button"));
|
||||
imageView.setVisibility(View.GONE);
|
||||
imageView.setOnClickListener(v -> SponsorBlockUtils.onVotingClicked(v.getContext()));
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
android.useAndroidX = true
|
||||
version = 1.15.0
|
||||
version = 1.16.0-dev.11
|
||||
|
2387
package-lock.json
generated
2387
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.9.1",
|
||||
"semantic-release": "^23.0.8"
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.1.2"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user