chore: merge branch dev to main (#385)

This commit is contained in:
oSumAtrIX 2023-05-24 21:17:10 +02:00 committed by GitHub
commit 7dc71e6861
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 3427 additions and 1275 deletions

View File

@ -1,3 +1,199 @@
# [0.108.0-dev.24](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.23...v0.108.0-dev.24) (2023-05-22)
### Bug Fixes
* **youtube/return-youtube-dislike:** fix dislikes not showing for video opened from feed autoplay ([#408](https://github.com/revanced/revanced-integrations/issues/408)) ([307315c](https://github.com/revanced/revanced-integrations/commit/307315c43c68a47c983384351a617f5c5f508b4f))
# [0.108.0-dev.23](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.22...v0.108.0-dev.23) (2023-05-21)
### Features
* **reddit:** add `sanitize-sharing-links` patch ([#407](https://github.com/revanced/revanced-integrations/issues/407)) ([191cc71](https://github.com/revanced/revanced-integrations/commit/191cc711de1ecbf6632fc27d32ee4f0c81413c57))
# [0.108.0-dev.22](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.21...v0.108.0-dev.22) (2023-05-20)
### Bug Fixes
* **twitter:** correctly resolve to integrations methods ([cd93917](https://github.com/revanced/revanced-integrations/commit/cd93917148e2f7695effb15183f53b84ddb9800a))
### Features
* **twitter/hide-recommended-users:** hide "Who to follow" ([c7cabc0](https://github.com/revanced/revanced-integrations/commit/c7cabc0b5799464ed75d290dfae5fcd2faa4fc94))
# [0.108.0-dev.21](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.20...v0.108.0-dev.21) (2023-05-19)
### Bug Fixes
* **youtube/settings:** fix non functional back button in settings ([#404](https://github.com/revanced/revanced-integrations/issues/404)) ([0c55d70](https://github.com/revanced/revanced-integrations/commit/0c55d70370dad9275dfb5bc3817f71d4290f5a13))
# [0.108.0-dev.20](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.19...v0.108.0-dev.20) (2023-05-19)
### Features
* **youtube/copy-video-url:** add tap and hold functionality to copy video url buttons ([#403](https://github.com/revanced/revanced-integrations/issues/403)) ([80689ef](https://github.com/revanced/revanced-integrations/commit/80689eff5b2deb971feb1fc59e987ef835506bae))
# [0.108.0-dev.19](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.18...v0.108.0-dev.19) (2023-05-19)
### Features
* **youtube:** support version `18.19.35` ([b47a781](https://github.com/revanced/revanced-integrations/commit/b47a781ba710e6fb66e144ef95cdd51af358e4de))
# [0.108.0-dev.18](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.17...v0.108.0-dev.18) (2023-05-18)
### Features
* add capability to filter from protobuf buffer ([5652c32](https://github.com/revanced/revanced-integrations/commit/5652c323455b58f6760d4938c79d704c22fd546c))
* **youtube/hide-shorts-components:** hide navigation bar ([ac13d10](https://github.com/revanced/revanced-integrations/commit/ac13d1030561905a81059ad0db31a749833a31cd))
* **youtube:** add `hide-shorts-components` patch ([5ec90db](https://github.com/revanced/revanced-integrations/commit/5ec90db28a46e8f5d79f4793c141a7411a2da05d))
# [0.108.0-dev.17](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.16...v0.108.0-dev.17) (2023-05-16)
### Bug Fixes
* **youtube/sponsorblock:** fix toast shown when scrubbing thru a paused video ([#401](https://github.com/revanced/revanced-integrations/issues/401)) ([7da5673](https://github.com/revanced/revanced-integrations/commit/7da56738a14a36fbf66f05d28fd886baaafbee3f))
# [0.108.0-dev.16](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.15...v0.108.0-dev.16) (2023-05-16)
### Features
* **youtube:** add options to disable toasts on connection errors ([#402](https://github.com/revanced/revanced-integrations/issues/402)) ([ae18edd](https://github.com/revanced/revanced-integrations/commit/ae18edd047d7979307bc28f28db17bae2c5cc226))
# [0.108.0-dev.15](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.14...v0.108.0-dev.15) (2023-05-15)
### Features
* **youtube:** import / export of revanced settings ([#388](https://github.com/revanced/revanced-integrations/issues/388)) ([c3f08d8](https://github.com/revanced/revanced-integrations/commit/c3f08d8d7e8116496611b85508fbd54bb3a71992))
# [0.108.0-dev.14](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.13...v0.108.0-dev.14) (2023-05-14)
### Bug Fixes
* **youtube/return-youtube-dislikes:** fix temporarily frozen video after opening a shorts ([#396](https://github.com/revanced/revanced-integrations/issues/396)) ([6a94bd2](https://github.com/revanced/revanced-integrations/commit/6a94bd2237be9cde6256c83fcec72b3f0de83496))
# [0.108.0-dev.13](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.12...v0.108.0-dev.13) (2023-05-13)
### Bug Fixes
* **youtube/remember-video-quality:** do not show 'auto' in video resolution picker if a default quality is set ([#400](https://github.com/revanced/revanced-integrations/issues/400)) ([e30d120](https://github.com/revanced/revanced-integrations/commit/e30d1201c992f4896a0b7106230377d78506cd6f))
# [0.108.0-dev.12](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.11...v0.108.0-dev.12) (2023-05-13)
### Bug Fixes
* **youtube/swipe-controls:** restart when "press to swipe" preference is changed ([#399](https://github.com/revanced/revanced-integrations/issues/399)) ([a3d754c](https://github.com/revanced/revanced-integrations/commit/a3d754c209e443135759850c7634708b23330a7c))
# [0.108.0-dev.11](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.10...v0.108.0-dev.11) (2023-05-12)
### Features
* **twitch:** add `auto-claim-channel-points` patch ([#398](https://github.com/revanced/revanced-integrations/issues/398)) ([d7f050b](https://github.com/revanced/revanced-integrations/commit/d7f050ba2ff513c91cccbf0095fc7756dbb47400))
# [0.108.0-dev.10](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.9...v0.108.0-dev.10) (2023-05-12)
### Features
* **youtube:** add `hide-filter-bar` patch ([9649c3d](https://github.com/revanced/revanced-integrations/commit/9649c3dbc8406c3639c4fff9dd179d6d29886e60))
# [0.108.0-dev.9](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.8...v0.108.0-dev.9) (2023-05-11)
### Features
* **youtube/video-speed:** change custom video speeds inside app settings ([#393](https://github.com/revanced/revanced-integrations/issues/393)) ([b42790f](https://github.com/revanced/revanced-integrations/commit/b42790fbca0f6c854d41871834fd6266dd2ea106))
# [0.108.0-dev.8](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.7...v0.108.0-dev.8) (2023-05-11)
### Bug Fixes
* **youtube/theme:** apply custom seekbar color to video thumbnails ([#391](https://github.com/revanced/revanced-integrations/issues/391)) ([ae99408](https://github.com/revanced/revanced-integrations/commit/ae994086360b45340ed1ed896c35917d785bb4f9))
# [0.108.0-dev.7](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.6...v0.108.0-dev.7) (2023-05-10)
### Bug Fixes
* **youtube/hide-ads:** don't filter for `reels_player_overlay` ([415c194](https://github.com/revanced/revanced-integrations/commit/415c1948fccdc8eb27b76b043996017c5c56eac3))
# [0.108.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.5...v0.108.0-dev.6) (2023-05-09)
### Bug Fixes
* **youtube/spoof-app-version:** restore watch history preview ([#394](https://github.com/revanced/revanced-integrations/issues/394)) ([4c7f737](https://github.com/revanced/revanced-integrations/commit/4c7f737913a0c3690f8230c51f6dd217e8b04c7a))
# [0.108.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.4...v0.108.0-dev.5) (2023-05-09)
### Bug Fixes
* **youtube/remember-video-quality:** fix default video quality/speed being applied when resuming app ([#392](https://github.com/revanced/revanced-integrations/issues/392)) ([c97d1b7](https://github.com/revanced/revanced-integrations/commit/c97d1b7ee5be6a0f097f2995321608bc74f5822c))
# [0.108.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.3...v0.108.0-dev.4) (2023-05-07)
### Features
* **youtube/hide-player-overlay:** make it toggleable in settings ([#382](https://github.com/revanced/revanced-integrations/issues/382)) ([1b4aa0f](https://github.com/revanced/revanced-integrations/commit/1b4aa0fcc6b89acd4156e93685b1da7519aa7148))
# [0.108.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.2...v0.108.0-dev.3) (2023-05-07)
### Features
* **youtube:** `hide-load-more-button` patch ([#389](https://github.com/revanced/revanced-integrations/issues/389)) ([7da9d44](https://github.com/revanced/revanced-integrations/commit/7da9d440eedfc895b49aac40498f0279156ad117))
# [0.108.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.108.0-dev.1...v0.108.0-dev.2) (2023-05-05)
### Bug Fixes
* **youtube/theme:** fix app crash if user clears seekbar color ([#390](https://github.com/revanced/revanced-integrations/issues/390)) ([e2f5290](https://github.com/revanced/revanced-integrations/commit/e2f52905dc445f881666c06877c3a69306335dcb))
# [0.108.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.3...v0.108.0-dev.1) (2023-05-03)
### Features
* **youtube/settings:** add reset button to edit preference dialog ([#383](https://github.com/revanced/revanced-integrations/issues/383)) ([cb5a4d0](https://github.com/revanced/revanced-integrations/commit/cb5a4d0c9b3b340928695fcb1d10b164a6dcef27))
## [0.107.1-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.2...v0.107.1-dev.3) (2023-05-03)
### Bug Fixes
* **youtube/theme:** fix toast shown on fresh app install ([#381](https://github.com/revanced/revanced-integrations/issues/381)) ([2dc431f](https://github.com/revanced/revanced-integrations/commit/2dc431f1bf54c12dfc45c4511a0b0792e214be4f))
## [0.107.1-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.107.1-dev.1...v0.107.1-dev.2) (2023-05-03)
### Bug Fixes
* **youtube/sponsorblock:** fix skip button in wrong location when full screen and comments visible ([#387](https://github.com/revanced/revanced-integrations/issues/387)) ([486b79b](https://github.com/revanced/revanced-integrations/commit/486b79b4e4927d4c05cfb4d5222a1d74fe60e327))
## [0.107.1-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.107.0...v0.107.1-dev.1) (2023-05-02)
### Bug Fixes
* **youtube/return-youtube-dislike:** fix potential error toast when using old UI layout ([#384](https://github.com/revanced/revanced-integrations/issues/384)) ([6c36bee](https://github.com/revanced/revanced-integrations/commit/6c36beeda139156bfbb5a17bc89aa63c25afa83c))
# [0.107.0](https://github.com/revanced/revanced-integrations/compare/v0.106.0...v0.107.0) (2023-05-02)

View File

@ -1,8 +1,10 @@
# ReVanced Integrations
# 🔩 ReVanced Integrations
The official ReVanced Integrations containing classes to be merged by ReVanced Patcher.
## ❓ How to use debugging:
# How to use debugging:
- Usage on Windows: ```adb logcat | findstr "revanced" > log.txt```
- Usage on Linux: ```adb logcat | grep --line-buffered "revanced" > log.txt```
This will write the log to a file called log.txt which you can view then.

View File

@ -1,42 +0,0 @@
package app.revanced.integrations.adremover;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toolbar;
import app.revanced.integrations.utils.LogHelper;
public class AdRemoverAPI {
/**
* Removes Reels and Home ads
*
* @param view
*/
//ToDo: refactor this
public static void HideViewWithLayout1dp(View view) {
if (view instanceof LinearLayout) {
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams);
} else if (view instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams2);
} else if (view instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams3);
} else if (view instanceof Toolbar) {
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(1, 1);
view.setLayoutParams(layoutParams4);
} else if (view instanceof ViewGroup) {
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1);
view.setLayoutParams(layoutParams5);
} else {
LogHelper.printDebug(() -> "HideViewWithLayout1dp - Id: " + view.getId() + " Type: " + view.getClass().getName());
}
}
}

View File

@ -5,6 +5,6 @@ import app.revanced.integrations.settings.SettingsEnum;
public class AutoRepeatPatch {
//Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch
public static boolean shouldAutoRepeat() {
return SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean();
return SettingsEnum.AUTO_REPEAT.getBoolean();
}
}

View File

@ -1,31 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
final class ButtonsPatch extends Filter {
private final BlockRule actionBarRule;
public ButtonsPatch() {
actionBarRule = new BlockRule(null, "video_action_bar");
pathRegister.registerAll(
new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"),
new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"),
new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"),
new BlockRule(SettingsEnum.HIDE_CLIP_BUTTON, "|clip_button.eml|"),
new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button", "|CellType|CollectionType|CellType|ContainerType|button.eml|")
);
}
private boolean canHideActionBar() {
for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false;
return true;
}
@Override
public boolean filter(final String path, final String identifier) {
// If everything is hidden, then also hide the video bar itself.
if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true;
return pathRegister.contains(path);
}
}

View File

@ -1,31 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
final class CommentsPatch extends Filter {
public CommentsPatch() {
var comments = new BlockRule(SettingsEnum.HIDE_COMMENTS_SECTION, "video_metadata_carousel", "_comments");
var previewComment = new BlockRule(
SettingsEnum.HIDE_PREVIEW_COMMENT,
"|carousel_item",
"comments_entry_point_teaser",
"comments_entry_point_simplebox"
);
this.pathRegister.registerAll(
comments,
previewComment
);
}
@Override
boolean filter(String path, String _identifier) {
if (!pathRegister.contains(path)) return false;
LogHelper.printDebug(() -> "Blocked: " + path);
return true;
}
}

View File

@ -2,22 +2,46 @@ package app.revanced.integrations.patches;
import static app.revanced.integrations.utils.StringRef.str;
import android.os.Build;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class CopyVideoUrlPatch {
public static void copyUrl(Boolean withTimestamp) {
public static void copyUrl(boolean withTimestamp) {
try {
String url = String.format("https://youtu.be/%s", VideoInformation.getVideoId());
if (withTimestamp) {
long seconds = VideoInformation.getVideoTime() / 1000;
url += String.format("?t=%s", seconds);
StringBuilder builder = new StringBuilder("https://youtu.be/");
builder.append(VideoInformation.getVideoId());
final long currentVideoTimeInSeconds = VideoInformation.getVideoTime() / 1000;
if (withTimestamp && currentVideoTimeInSeconds > 0) {
final long hour = currentVideoTimeInSeconds / (60 * 60);
final long minute = (currentVideoTimeInSeconds / 60) % 60;
final long second = currentVideoTimeInSeconds % 60;
builder.append("?t=");
if (hour > 0) {
builder.append(hour).append("h");
}
if (minute > 0) {
builder.append(minute).append("m");
}
if (second > 0) {
builder.append(second).append("s");
}
}
ReVancedUtils.setClipboard(url);
ReVancedUtils.showToastShort(str("share_copy_url_success"));
ReVancedUtils.setClipboard(builder.toString());
// Do not show a toast if using Android 13+ as it shows it's own toast.
// But if the user copied with a timestamp then show a toast.
// Unfortunately this will show 2 toasts on Android 13+, but no way around this.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) {
ReVancedUtils.showToastShort(withTimestamp && currentVideoTimeInSeconds > 0
? str("revanced_share_copy_url_timestamp_success")
: str("revanced_share_copy_url_success"));
}
} catch (Exception e) {
LogHelper.printException(() -> "Failed to generate video url", e);
}
}
}

View File

@ -4,10 +4,13 @@ import app.revanced.integrations.settings.SettingsEnum;
public class DisableAutoCaptionsPatch {
/**
* Used by injected code. Do not delete.
*/
public static boolean captionsButtonDisabled;
public static boolean autoCaptionsEnabled() {
return SettingsEnum.CAPTIONS_ENABLED.getBoolean();
return SettingsEnum.AUTO_CAPTIONS.getBoolean();
}
}

View File

@ -5,6 +5,6 @@ import app.revanced.integrations.settings.SettingsEnum;
public class DisableStartupShortsPlayerPatch {
//Used by app.revanced.patches.youtube.layout.startupshortsreset.patch.DisableShortsOnStartupPatch
public static boolean disableStartupShortsPlayer() {
return SettingsEnum.DISABLE_STARTUP_SHORTS_PLAYER.getBoolean();
return SettingsEnum.DISABLE_RESUMING_SHORTS_PLAYER.getBoolean();
}
}

View File

@ -1,191 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.adremover.AdRemoverAPI;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public final class GeneralAdsPatch extends Filter {
private final String[] IGNORE = {
"home_video_with_context",
"related_video_with_context",
"comment_thread", // skip blocking anything in the comments
"|comment.", // skip blocking anything in the comments replies
"library_recent_shelf",
};
private final BlockRule custom = new CustomBlockRule(
SettingsEnum.ADREMOVER_CUSTOM_ENABLED,
SettingsEnum.ADREMOVER_CUSTOM_REMOVAL
);
public GeneralAdsPatch() {
var communityPosts = new BlockRule(SettingsEnum.ADREMOVER_COMMUNITY_POSTS_REMOVAL, "post_base_wrapper");
var communityGuidelines = new BlockRule(SettingsEnum.ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL, "community_guidelines");
var subscribersCommunityGuidelines = new BlockRule(SettingsEnum.ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL, "sponsorships_comments_upsell");
var channelMemberShelf = new BlockRule(SettingsEnum.ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL, "member_recognition_shelf");
var compactBanner = new BlockRule(SettingsEnum.ADREMOVER_COMPACT_BANNER_REMOVAL, "compact_banner");
var inFeedSurvey = new BlockRule(SettingsEnum.ADREMOVER_FEED_SURVEY_REMOVAL, "in_feed_survey", "slimline_survey");
var medicalPanel = new BlockRule(SettingsEnum.ADREMOVER_MEDICAL_PANEL_REMOVAL, "medical_panel");
var paidContent = new BlockRule(SettingsEnum.ADREMOVER_PAID_CONTENT_REMOVAL, "paid_content_overlay");
var merchandise = new BlockRule(SettingsEnum.ADREMOVER_MERCHANDISE_REMOVAL, "product_carousel");
var infoPanel = new BlockRule(SettingsEnum.ADREMOVER_INFO_PANEL_REMOVAL, "publisher_transparency_panel", "single_item_information_panel");
var latestPosts = new BlockRule(SettingsEnum.ADREMOVER_HIDE_LATEST_POSTS, "post_shelf");
var channelGuidelines = new BlockRule(SettingsEnum.ADREMOVER_HIDE_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner");
var audioTrackButton = new BlockRule(SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, "multi_feed_icon_button");
var artistCard = new BlockRule(SettingsEnum.HIDE_ARTIST_CARDS, "official_card");
var selfSponsor = new BlockRule(SettingsEnum.ADREMOVER_SELF_SPONSOR_REMOVAL, "cta_shelf_card");
var chapterTeaser = new BlockRule(SettingsEnum.ADREMOVER_CHAPTER_TEASER_REMOVAL, "expandable_metadata", "macro_markers_carousel");
var viewProducts = new BlockRule(SettingsEnum.ADREMOVER_VIEW_PRODUCTS, "product_item", "products_in_video");
var webLinkPanel = new BlockRule(SettingsEnum.ADREMOVER_WEB_SEARCH_RESULTS, "web_link_panel");
var channelBar = new BlockRule(SettingsEnum.ADREMOVER_CHANNEL_BAR, "channel_bar");
var relatedVideos = new BlockRule(SettingsEnum.ADREMOVER_RELATED_VIDEOS, "fullscreen_related_videos");
var quickActions = new BlockRule(SettingsEnum.ADREMOVER_QUICK_ACTIONS, "quick_actions");
var imageShelf = new BlockRule(SettingsEnum.ADREMOVER_IMAGE_SHELF, "image_shelf");
var graySeparator = new BlockRule(SettingsEnum.ADREMOVER_GRAY_SEPARATOR,
"cell_divider" // layout residue (gray line above the buttoned ad),
);
var buttonedAd = new BlockRule(SettingsEnum.ADREMOVER_BUTTONED_REMOVAL,
"_buttoned_layout",
"full_width_square_image_layout",
"_ad_with",
"video_display_button_group_layout",
"landscape_image_wide_button_layout"
);
var generalAds = new BlockRule(
SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL,
"ads_video_with_context",
"banner_text_icon",
"square_image_layout",
"watch_metadata_app_promo",
"video_display_full_layout",
"hero_promo_image",
"statement_banner",
"carousel_footered_layout",
"text_image_button_layout",
"primetime_promo",
"product_details",
"full_width_portrait_image_layout",
"brand_video_shelf"
);
var movieAds = new BlockRule(
SettingsEnum.ADREMOVER_MOVIE_REMOVAL,
"browsy_bar",
"compact_movie",
"horizontal_movie_shelf",
"movie_and_show_upsell_card",
"compact_tvfilm_item",
"offer_module_root"
);
this.pathRegister.registerAll(
generalAds,
buttonedAd,
channelBar,
communityPosts,
paidContent,
latestPosts,
movieAds,
chapterTeaser,
communityGuidelines,
quickActions,
relatedVideos,
compactBanner,
inFeedSurvey,
viewProducts,
medicalPanel,
merchandise,
infoPanel,
channelGuidelines,
audioTrackButton,
artistCard,
selfSponsor,
webLinkPanel,
imageShelf,
subscribersCommunityGuidelines,
channelMemberShelf
);
var carouselAd = new BlockRule(SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL,
"carousel_ad"
);
var shorts = new BlockRule(SettingsEnum.ADREMOVER_SHORTS_REMOVAL,
"reels_player_overlay",
"shorts_shelf",
"inline_shorts",
"shorts_grid"
);
this.identifierRegister.registerAll(
shorts,
graySeparator,
carouselAd
);
}
public boolean filter(final String path, final String identifier) {
BlockResult result;
if (custom.isEnabled() && custom.check(path).isBlocked())
result = BlockResult.CUSTOM;
else if (ReVancedUtils.containsAny(path, IGNORE))
result = BlockResult.IGNORED;
else if (pathRegister.contains(path) || identifierRegister.contains(identifier))
result = BlockResult.DEFINED;
else
result = BlockResult.UNBLOCKED;
LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path));
return result.filter;
}
private enum BlockResult {
UNBLOCKED(false, "Unblocked"),
IGNORED(false, "Ignored"),
DEFINED(true, "Blocked"),
CUSTOM(true, "Custom");
final Boolean filter;
final String message;
BlockResult(boolean filter, String message) {
this.filter = filter;
this.message = message;
}
}
/**
* Hide a view.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
private static void hideView(SettingsEnum condition, View view) {
if (!condition.getBoolean()) return;
LogHelper.printDebug(() -> "Hiding view with setting: " + condition);
AdRemoverAPI.HideViewWithLayout1dp(view);
}
/**
* Hide the view, which shows ads in the homepage.
*
* @param view The view, which shows ads.
*/
public static void hideAdAttributionView(View view) {
hideView(SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, view);
}
/**
* Hide the view, which shows reels in the homepage.
*
* @param view The view, which shows reels.
*/
public static void hideReelView(View view) {
hideView(SettingsEnum.ADREMOVER_SHORTS_REMOVAL, view);
}
}

View File

@ -21,7 +21,7 @@ public class HDRAutoBrightnessPatch {
*/
public static float getHDRBrightness(float original) {
// do nothing if disabled
if (!SettingsEnum.USE_HDR_AUTO_BRIGHTNESS.getBoolean()) {
if (!SettingsEnum.HDR_AUTO_BRIGHTNESS.getBoolean()) {
return original;
}

View File

@ -2,13 +2,12 @@ package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.adremover.AdRemoverAPI;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideAlbumCardsPatch {
//Used by app.revanced.patches.youtube.layout.hidealbumcards.patch.HideAlbumCardsPatch
public static void hideAlbumCards(View view) {
public static void hideAlbumCard(View view) {
if (!SettingsEnum.HIDE_ALBUM_CARDS.getBoolean()) return;
AdRemoverAPI.HideViewWithLayout1dp(view);
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -2,13 +2,28 @@ package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.adremover.AdRemoverAPI;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideBreakingNewsPatch {
//Used by app.revanced.patches.youtube.layout.homepage.breakingnews.patch.BreakingNewsPatch
/**
* When spoofing to app versions older than 17.30.35, the watch history preview bar uses
* the same layout components as the breaking news shelf.
*
* Breaking news does not appear to be present in these older versions anyways.
*/
private static boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory() {
return SettingsEnum.SPOOF_APP_VERSION.getBoolean()
&& SettingsEnum.SPOOF_APP_VERSION_TARGET.getString().compareTo("17.30.35") < 0;
}
/**
* Injection point.
*/
public static void hideBreakingNews(View view) {
if (!SettingsEnum.HIDE_BREAKING_NEWS.getBoolean()) return;
AdRemoverAPI.HideViewWithLayout1dp(view);
if (!SettingsEnum.HIDE_BREAKING_NEWS.getBoolean()
|| isSpoofingOldVersionWithHorizontalCardListWatchHistory()) return;
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -2,13 +2,13 @@ package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.adremover.AdRemoverAPI;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideCrowdfundingBoxPatch {
//Used by app.revanced.patches.youtube.layout.hidecrowdfundingbox.patch.HideCrowdfundingBoxPatch
public static void hideCrowdfundingBox(View view) {
if (!SettingsEnum.HIDE_CROWDFUNDING_BOX.getBoolean()) return;
AdRemoverAPI.HideViewWithLayout1dp(view);
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public final class HideFilterBarPatch {
public static int hideInFeed(final int height) {
if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_FEED.getBoolean()) return 0;
return height;
}
public static void hideInRelatedVideos(final View chipView) {
if (!SettingsEnum.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(chipView);
}
public static int hideInSearch(final int height) {
if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_SEARCH.getBoolean()) return 0;
return height;
}
}

View File

@ -0,0 +1,13 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideLoadMoreButtonPatch {
public static void hideLoadMoreButton(View view){
if(!SettingsEnum.HIDE_LOAD_MORE_BUTTON.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.integrations.patches;
import android.widget.ImageView;
import app.revanced.integrations.settings.SettingsEnum;
public class HidePlayerOverlayPatch {
public static void hidePlayerOverlay(ImageView view) {
if (!SettingsEnum.HIDE_PLAYER_OVERLAY.getBoolean()) return;
view.setImageResource(android.R.color.transparent);
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
public class HideShortsCommentsButtonPatch {
//Used by app.revanced.patches.youtube.layout.comments.patch.CommentsPatch
public static void hideShortsCommentsButton(View view) {
if (!SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON.getBoolean()) return;
view.setVisibility(View.GONE);
}
}

View File

@ -1,140 +0,0 @@
package app.revanced.integrations.patches;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
class BlockRule {
final static class BlockResult {
private final boolean blocked;
private final SettingsEnum setting;
public BlockResult(final SettingsEnum setting, final boolean blocked) {
this.setting = setting;
this.blocked = blocked;
}
public SettingsEnum getSetting() {
return setting;
}
public boolean isBlocked() {
return blocked;
}
}
protected final SettingsEnum setting;
private final String[] blocks;
/**
* Initialize a new rule for components.
*
* @param setting The setting which controls the blocking of this component.
* @param blocks The rules to block the component on.
*/
public BlockRule(final SettingsEnum setting, final String... blocks) {
this.setting = setting;
this.blocks = blocks;
}
public boolean isEnabled() {
return setting.getBoolean();
}
public BlockResult check(final String string) {
return new BlockResult(setting, string != null && ReVancedUtils.containsAny(string, blocks));
}
}
final class CustomBlockRule extends BlockRule {
/**
* Initialize a new rule for components.
*
* @param setting The setting which controls the blocking of the components.
* @param filter The setting which contains the list of component names.
*/
public CustomBlockRule(final SettingsEnum setting, final SettingsEnum filter) {
super(setting, filter.getString().split(","));
}
}
abstract class Filter {
final protected LithoBlockRegister pathRegister = new LithoBlockRegister();
final protected LithoBlockRegister identifierRegister = new LithoBlockRegister();
abstract boolean filter(final String path, final String identifier);
}
final class LithoBlockRegister implements Iterable<BlockRule> {
private final ArrayList<BlockRule> blocks = new ArrayList<>();
public void registerAll(BlockRule... blocks) {
this.blocks.addAll(Arrays.asList(blocks));
}
@NonNull
@Override
public Iterator<BlockRule> iterator() {
return blocks.iterator();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void forEach(@NonNull Consumer<? super BlockRule> action) {
blocks.forEach(action);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
@Override
public Spliterator<BlockRule> spliterator() {
return blocks.spliterator();
}
public boolean contains(String path) {
for (var rule : this) {
if (!rule.isEnabled()) continue;
var result = rule.check(path);
if (result.isBlocked()) {
return true;
}
}
return false;
}
}
public final class LithoFilterPatch {
private static final Filter[] filters = new Filter[]{
new GeneralAdsPatch(),
new ButtonsPatch(),
new CommentsPatch(),
};
public static boolean filter(final StringBuilder pathBuilder, final String identifier) {
var path = pathBuilder.toString();
if (path.isEmpty()) return false;
LogHelper.printDebug(() -> String.format("Searching (ID: %s): %s", identifier, path));
for (var filter : filters) {
if (filter.filter(path, identifier)) return true;
}
return false;
}
}

View File

@ -13,7 +13,7 @@ public class OpenLinksExternallyPatch {
* @return The new, default service to open links with or the original service.
*/
public static String enableExternalBrowser(String original) {
if (SettingsEnum.ENABLE_EXTERNAL_BROWSER.getBoolean()) original = "";
if (SettingsEnum.EXTERNAL_BROWSER.getBoolean()) original = "";
return original;
}
}

View File

@ -15,7 +15,7 @@ import app.revanced.integrations.shared.PlayerOverlays;
@SuppressWarnings("unused")
public class PlayerOverlaysHookPatch {
/**
* Hook into YouTubePlayerOverlaysLayout.onFinishInflate() method
* Injection point.
*
* @param thisRef reference to the view
* @smali YouTubePlayerOverlaysLayout_onFinishInflateHook(Ljava / lang / Object ;)V

View File

@ -3,32 +3,25 @@ package app.revanced.integrations.patches;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.shared.VideoState;
/**
* Hook receiver class for 'player-type-hook' patch
*
* @usedBy app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch
* @smali Lapp/revanced/integrations/patches/PlayerTypeHookPatch;
*/
@SuppressWarnings("unused")
public class PlayerTypeHookPatch {
/**
* Hook into YouTubePlayerOverlaysLayout.updatePlayerLayout() method
*
* @param type the new player type
* @smali YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V
* Injection point.
*/
public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(@Nullable Object type) {
if (type == null) return;
public static void setPlayerType(@Nullable Enum<?> youTubePlayerType) {
if (youTubePlayerType == null) return;
// update current player type
final PlayerType newType = PlayerType.safeParseFromString(type.toString());
if (newType == null) {
LogHelper.printException(() -> "Unknown PlayerType encountered: " + type);
} else {
PlayerType.setCurrent(newType);
LogHelper.printDebug(() -> "PlayerType was updated to: " + newType);
}
PlayerType.setFromString(youTubePlayerType.name());
}
/**
* Injection point.
*/
public static void setVideoState(@Nullable Enum<?> youTubeVideoState) {
if (youTubeVideoState == null) return;
VideoState.setFromString(youTubeVideoState.name());
}
}

View File

@ -2,26 +2,42 @@ package app.revanced.integrations.patches;
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import android.graphics.Rect;
import android.os.Build;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextWatcher;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/**
* Handles all interaction of UI patch components.
*
* Does not handle creating dislike spans or anything to do with {@link ReturnYouTubeDislikeApi}.
*/
public class ReturnYouTubeDislikePatch {
@Nullable
private static String currentVideoId;
/**
* Resource identifier of old UI dislike button.
*/
@ -60,7 +76,7 @@ public class ReturnYouTubeDislikePatch {
if (oldUIReplacementSpan == null || oldUIReplacementSpan.toString().equals(s.toString())) {
return;
}
s.replace(0, s.length(), oldUIReplacementSpan);
s.replace(0, s.length(), oldUIReplacementSpan); // Causes a recursive call back into this listener
}
};
@ -80,12 +96,15 @@ public class ReturnYouTubeDislikePatch {
*
* Used when spoofing the older app versions of {@link SpoofAppVersionPatch}.
*/
public static void setOldUILayoutDislikes(int buttonViewResourceId, @NonNull TextView textView) {
public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable TextView textView) {
try {
if (!SettingsEnum.RYD_ENABLED.getBoolean()
|| buttonViewResourceId != OLD_UI_DISLIKE_BUTTON_RESOURCE_ID) {
|| buttonViewResourceId != OLD_UI_DISLIKE_BUTTON_RESOURCE_ID
|| textView == null) {
return;
}
LogHelper.printDebug(() -> "setOldUILayoutDislikes");
if (oldUIOriginalSpan == null) {
// Use value of the first instance, as it appears TextViews can be recycled
// and might contain dislikes previously added by the patch.
@ -96,23 +115,19 @@ public class ReturnYouTubeDislikePatch {
textView.removeTextChangedListener(oldUiTextWatcher);
textView.addTextChangedListener(oldUiTextWatcher);
/**
* If the patch is changed to include the dislikes button as a parameter to this method,
* then if the button is already selected the dislikes could be adjusted using
* {@link ReturnYouTubeDislike#setUserVote(Vote)}
*/
updateOldUIDislikesTextView();
} catch (Exception ex) {
LogHelper.printException(() -> "setOldUILayoutDislikes failure", ex);
}
}
/**
* Injection point.
*/
public static void newVideoLoaded(@NonNull String videoId) {
try {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
ReturnYouTubeDislike.newVideoLoaded(videoId);
} catch (Exception ex) {
LogHelper.printException(() -> "newVideoLoaded failure", ex);
}
}
/**
* Injection point.
@ -157,21 +172,153 @@ public class ReturnYouTubeDislikePatch {
return original;
}
/**
* Replacement text to use for "Dislikes" while RYD is fetching.
*/
private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-");
/**
* Dislikes TextViews used by Shorts.
*
* Multiple TextViews are loaded at once (for the prior and next videos to swipe to).
* Keep track of all of them, and later pick out the correct one based on their on screen position.
*/
private static final List<WeakReference<TextView>> shortsTextViewRefs = new ArrayList<>();
private static void clearRemovedShortsTextViews() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
shortsTextViewRefs.removeIf(ref -> ref.get() == null);
return;
}
throw new IllegalStateException(); // YouTube requires Android N or greater
}
/**
* Injection point. Called when a Shorts dislike is updated.
* Handles update asynchronously, otherwise Shorts video will be frozen while the UI thread is blocked.
*
* @return if RYD is enabled and the TextView was updated
*/
public static boolean setShortsDislikes(@NonNull View likeDislikeView) {
try {
if (!SettingsEnum.RYD_ENABLED.getBoolean() || !SettingsEnum.RYD_SHORTS.getBoolean()) {
return false;
}
LogHelper.printDebug(() -> "setShortsDislikes");
TextView textView = (TextView) likeDislikeView;
textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text
shortsTextViewRefs.add(new WeakReference<>(textView));
if (likeDislikeView.isSelected() && isShortTextViewOnScreen(textView)) {
LogHelper.printDebug(() -> "Shorts dislike is already selected");
ReturnYouTubeDislike.setUserVote(Vote.DISLIKE);
}
// For the first short played, the shorts dislike hook is called after the video id hook.
// But for most other times this hook is called before the video id (which is not ideal).
// Must update the TextViews here, and also after the videoId changes.
updateOnScreenShortsTextViews(false);
return true;
} catch (Exception ex) {
LogHelper.printException(() -> "setShortsDislikes failure", ex);
return false;
}
}
/**
* @param forceUpdate if false, then only update the 'loading text views.
* If true, update all on screen text views.
*/
private static void updateOnScreenShortsTextViews(boolean forceUpdate) {
try {
clearRemovedShortsTextViews();
if (shortsTextViewRefs.isEmpty()) {
return;
}
LogHelper.printDebug(() -> "updateShortsTextViews");
String videoId = VideoInformation.getVideoId();
Runnable update = () -> {
Spanned shortsDislikesSpan = ReturnYouTubeDislike.getDislikeSpanForShort(SHORTS_LOADING_SPAN);
ReVancedUtils.runOnMainThreadNowOrLater(() -> {
if (!videoId.equals(VideoInformation.getVideoId())) {
// User swiped to new video before fetch completed
LogHelper.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId);
return;
}
// Update text views that appear to be visible on screen.
// Only 1 will be the actual textview for the current Short,
// but discarded and not yet garbage collected views can remain.
// So must set the dislike span on all views that match.
for (WeakReference<TextView> textViewRef : shortsTextViewRefs) {
TextView textView = textViewRef.get();
if (textView == null) {
continue;
}
if (isShortTextViewOnScreen(textView)
&& (forceUpdate || textView.getText().toString().equals(SHORTS_LOADING_SPAN.toString()))) {
LogHelper.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan);
textView.setText(shortsDislikesSpan);
}
}
});
};
if (ReturnYouTubeDislike.fetchCompleted()) {
update.run(); // Network call is completed, no need to wait on background thread.
} else {
ReVancedUtils.runOnBackgroundThread(update);
}
} catch (Exception ex) {
LogHelper.printException(() -> "updateVisibleShortsTextViews failure", ex);
}
}
/**
* Check if a view is within the screen bounds.
*/
private static boolean isShortTextViewOnScreen(@NonNull View view) {
final int[] location = new int[2];
view.getLocationInWindow(location);
if (location[0] <= 0 && location[1] <= 0) { // Lower bound
return false;
}
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect); // Upper bound
return location[0] < windowRect.width() && location[1] < windowRect.height();
}
/**
* Injection point.
*
* Called when a Shorts dislike Spanned is created.
*/
public static Spanned onShortsComponentCreated(Spanned original) {
public static void newVideoLoaded(@NonNull String videoId) {
try {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
return original;
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
if (!videoId.equals(currentVideoId)) {
currentVideoId = videoId;
final boolean noneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
if (noneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) {
ReturnYouTubeDislike.setCurrentVideoId(null);
return;
}
ReturnYouTubeDislike.newVideoLoaded(videoId);
if (noneHiddenOrMinimized) {
// Shorts TextView hook can be called out of order with the video id hook.
// Must manually update again here.
updateOnScreenShortsTextViews(true);
}
}
return ReturnYouTubeDislike.getDislikeSpanForShort(original);
} catch (Exception ex) {
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
LogHelper.printException(() -> "newVideoLoaded failure", ex);
}
return original;
}
/**
@ -186,10 +333,14 @@ public class ReturnYouTubeDislikePatch {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
return;
}
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrMinimized()) {
return;
}
for (Vote v : Vote.values()) {
if (v.value == vote) {
ReturnYouTubeDislike.sendVote(v);
updateOldUIDislikesTextView();
return;
}

View File

@ -2,11 +2,8 @@ package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class SeekbarTappingPatch {
//Used by app.revanced.patches.youtube.interaction.seekbar.patch.EnableSeekbarTappingPatch
public static boolean isTapSeekingEnabled() {
return SettingsEnum.TAP_SEEKING_ENABLED.getBoolean();
public final class SeekbarTappingPatch {
public static boolean seekbarTappingEnabled() {
return SettingsEnum.SEEKBAR_TAPPING.getBoolean();
}
}

View File

@ -60,7 +60,7 @@ public class SpoofSignatureVerificationPatch {
*/
public static String overrideProtobufParameter(String originalValue) {
try {
if (!SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) {
if (!SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) {
return originalValue;
}
@ -101,11 +101,11 @@ public class SpoofSignatureVerificationPatch {
}
LogHelper.printDebug(() -> "YouTube HTTP status code: " + responseCode);
if (SettingsEnum.SIGNATURE_SPOOFING.getBoolean()) {
if (SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean()) {
return; // already enabled
}
SettingsEnum.SIGNATURE_SPOOFING.saveValue(true);
SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.saveValue(true);
ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues");
// it would be great if the video could be forcefully reloaded, but currently there is no code to do this
@ -130,7 +130,7 @@ public class SpoofSignatureVerificationPatch {
* @param sd function is not entirely clear
*/
public static int[] getSubtitleWindowSettingsOverride(int ap, int ah, int av, boolean vs, boolean sd) {
final boolean signatureSpoofing = SettingsEnum.SIGNATURE_SPOOFING.getBoolean();
final boolean signatureSpoofing = SettingsEnum.SPOOF_SIGNATURE_VERIFICATION.getBoolean();
if (SettingsEnum.DEBUG.getBoolean()) {
if (ap != lastAp || ah != lastAh || av != lastAv || vs != lastVs || sd != lastSd) {
LogHelper.printDebug(() -> "video: " + VideoInformation.getVideoId() + " spoof: " + signatureSpoofing

View File

@ -7,8 +7,7 @@ public class VideoAdsPatch {
// Used by app.revanced.patches.youtube.ad.general.video.patch.VideoAdsPatch
// depends on Whitelist patch (still needs to be written)
public static boolean shouldShowAds() {
return !SettingsEnum.VIDEO_ADS_REMOVAL.getBoolean(); // TODO && Whitelist.shouldShowAds();
return !SettingsEnum.HIDE_VIDEO_ADS.getBoolean(); // TODO && Whitelist.shouldShowAds();
}
}

View File

@ -4,8 +4,10 @@ import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Objects;
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
import app.revanced.integrations.shared.VideoState;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
@ -16,7 +18,7 @@ public final class VideoInformation {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
private static final String SEEK_METHOD_NAME = "seekTo";
private static WeakReference<Object> playerController;
private static WeakReference<Object> playerControllerRef;
private static Method seekMethod;
@NonNull
@ -30,17 +32,17 @@ public final class VideoInformation {
/**
* Injection point.
* Sets a reference to the YouTube playback controller.
*
* @param thisRef Reference to the player controller object.
* @param playerController player controller object.
*/
public static void playerController_onCreateHook(final Object thisRef) {
playerController = new WeakReference<>(thisRef);
videoLength = 0;
videoTime = -1;
public static void initialize(@NonNull Object playerController) {
try {
seekMethod = thisRef.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
videoTime = -1;
videoLength = 0;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
seekMethod = playerController.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
seekMethod.setAccessible(true);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to initialize", ex);
@ -56,7 +58,6 @@ public final class VideoInformation {
if (!videoId.equals(newlyLoadedVideoId)) {
LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId);
videoId = newlyLoadedVideoId;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
}
}
@ -124,7 +125,7 @@ public final class VideoInformation {
try {
LogHelper.printDebug(() -> "Seeking to " + millisecond);
return (Boolean) seekMethod.invoke(playerController.get(), millisecond);
return (Boolean) seekMethod.invoke(playerControllerRef.get(), millisecond);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to seek", ex);
return false;
@ -183,7 +184,12 @@ public final class VideoInformation {
* @return If the playback is at the end of the video.
*
* If video is playing in the background with no video visible,
* this always returns false (even if the video is actually at the end)
* this always returns false (even if the video is actually at the end).
*
* This is equivalent to checking for {@link VideoState#ENDED},
* but can give a more up to date result for code calling from some hooks.
*
* @see VideoState
*/
public static boolean isAtEndOfVideo() {
return videoTime > 0 && videoLength > 0 && videoTime >= videoLength;

View File

@ -0,0 +1,265 @@
package app.revanced.integrations.patches.components;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public final class AdsFilter extends Filter {
private final String[] exceptions;
private final CustomFilterGroup custom;
public AdsFilter() {
exceptions = new String[]{
"home_video_with_context",
"related_video_with_context",
"comment_thread", // skip filtering anything in the comments
"|comment.", // skip filtering anything in the comments replies
"library_recent_shelf",
};
custom = new CustomFilterGroup(
SettingsEnum.CUSTOM_FILTER,
SettingsEnum.CUSTOM_FILTER_STRINGS
);
final var communityPosts = new StringFilterGroup(
SettingsEnum.HIDE_COMMUNITY_POSTS,
"post_base_wrapper"
);
final var communityGuidelines = new StringFilterGroup(
SettingsEnum.HIDE_COMMUNITY_GUIDELINES,
"community_guidelines"
);
final var subscribersCommunityGuidelines = new StringFilterGroup(
SettingsEnum.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES,
"sponsorships_comments_upsell"
);
final var channelMemberShelf = new StringFilterGroup(
SettingsEnum.HIDE_CHANNEL_MEMBER_SHELF,
"member_recognition_shelf"
);
final var compactBanner = new StringFilterGroup(
SettingsEnum.HIDE_COMPACT_BANNER,
"compact_banner"
);
final var inFeedSurvey = new StringFilterGroup(
SettingsEnum.HIDE_FEED_SURVEY,
"in_feed_survey",
"slimline_survey"
);
final var medicalPanel = new StringFilterGroup(
SettingsEnum.HIDE_MEDICAL_PANELS,
"medical_panel"
);
final var paidContent = new StringFilterGroup(
SettingsEnum.HIDE_PAID_CONTENT,
"paid_content_overlay"
);
final var merchandise = new StringFilterGroup(
SettingsEnum.HIDE_MERCHANDISE_BANNERS,
"product_carousel"
);
final var infoPanel = new StringFilterGroup(
SettingsEnum.HIDE_HIDE_INFO_PANELS,
"publisher_transparency_panel",
"single_item_information_panel"
);
final var latestPosts = new StringFilterGroup(
SettingsEnum.HIDE_HIDE_LATEST_POSTS,
"post_shelf"
);
final var channelGuidelines = new StringFilterGroup(
SettingsEnum.HIDE_HIDE_CHANNEL_GUIDELINES,
"channel_guidelines_entry_banner"
);
final var audioTrackButton = new StringFilterGroup(
SettingsEnum.HIDE_AUDIO_TRACK_BUTTON,
"multi_feed_icon_button"
);
final var artistCard = new StringFilterGroup(
SettingsEnum.HIDE_ARTIST_CARDS,
"official_card"
);
final var selfSponsor = new StringFilterGroup(
SettingsEnum.HIDE_SELF_SPONSOR,
"cta_shelf_card"
);
final var chapterTeaser = new StringFilterGroup(
SettingsEnum.HIDE_CHAPTER_TEASER,
"expandable_metadata",
"macro_markers_carousel"
);
final var viewProducts = new StringFilterGroup(
SettingsEnum.HIDE_PRODUCTS_BANNER,
"product_item",
"products_in_video"
);
final var webLinkPanel = new StringFilterGroup(
SettingsEnum.HIDE_WEB_SEARCH_RESULTS,
"web_link_panel"
);
final var channelBar = new StringFilterGroup(
SettingsEnum.HIDE_CHANNEL_BAR,
"channel_bar"
);
final var relatedVideos = new StringFilterGroup(
SettingsEnum.HIDE_RELATED_VIDEOS,
"fullscreen_related_videos"
);
final var quickActions = new StringFilterGroup(
SettingsEnum.HIDE_QUICK_ACTIONS,
"quick_actions"
);
final var imageShelf = new StringFilterGroup(
SettingsEnum.HIDE_IMAGE_SHELF,
"image_shelf"
);
final var graySeparator = new StringFilterGroup(
SettingsEnum.HIDE_GRAY_SEPARATOR,
"cell_divider" // layout residue (gray line above the buttoned ad),
);
final var buttonedAd = new StringFilterGroup(
SettingsEnum.HIDE_BUTTONED_ADS,
"_buttoned_layout",
"full_width_square_image_layout",
"_ad_with",
"video_display_button_group_layout",
"landscape_image_wide_button_layout"
);
final var generalAds = new StringFilterGroup(
SettingsEnum.HIDE_GENERAL_ADS,
"ads_video_with_context",
"banner_text_icon",
"square_image_layout",
"watch_metadata_app_promo",
"video_display_full_layout",
"hero_promo_image",
"statement_banner",
"carousel_footered_layout",
"text_image_button_layout",
"primetime_promo",
"product_details",
"full_width_portrait_image_layout",
"brand_video_shelf"
);
final var movieAds = new StringFilterGroup(
SettingsEnum.HIDE_MOVIES_SECTION,
"browsy_bar",
"compact_movie",
"horizontal_movie_shelf",
"movie_and_show_upsell_card",
"compact_tvfilm_item",
"offer_module_root"
);
this.pathFilterGroups.addAll(
generalAds,
buttonedAd,
channelBar,
communityPosts,
paidContent,
latestPosts,
movieAds,
chapterTeaser,
communityGuidelines,
quickActions,
relatedVideos,
compactBanner,
inFeedSurvey,
viewProducts,
medicalPanel,
merchandise,
infoPanel,
channelGuidelines,
audioTrackButton,
artistCard,
selfSponsor,
webLinkPanel,
imageShelf,
subscribersCommunityGuidelines,
channelMemberShelf
);
final var carouselAd = new StringFilterGroup(
SettingsEnum.HIDE_GENERAL_ADS,
"carousel_ad"
);
this.identifierFilterGroups.addAll(
graySeparator,
carouselAd
);
}
@Override
public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) {
FilterResult result;
if (custom.isEnabled() && custom.check(path).isFiltered())
result = FilterResult.CUSTOM;
else if (ReVancedUtils.containsAny(path, exceptions))
result = FilterResult.EXCEPTION;
else if (pathFilterGroups.contains(path) || identifierFilterGroups.contains(identifier))
result = FilterResult.FILTERED;
else
result = FilterResult.UNFILTERED;
LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path));
return result.filter;
}
private enum FilterResult {
UNFILTERED(false, "Unfiltered"),
EXCEPTION(false, "Exception"),
FILTERED(true, "Filtered"),
CUSTOM(true, "Custom");
final Boolean filter;
final String message;
FilterResult(boolean filter, String message) {
this.filter = filter;
this.message = message;
}
}
/**
* Hide the view, which shows ads in the homepage.
*
* @param view The view, which shows ads.
*/
public static void hideAdAttributionView(View view) {
ReVancedUtils.hideViewBy1dpUnderCondition(SettingsEnum.HIDE_GENERAL_ADS, view);
}
}

View File

@ -0,0 +1,54 @@
package app.revanced.integrations.patches.components;
import app.revanced.integrations.settings.SettingsEnum;
final class ButtonsFilter extends Filter {
private final StringFilterGroup actionBarRule;
public ButtonsFilter() {
actionBarRule = new StringFilterGroup(
null,
"video_action_bar"
);
pathFilterGroups.addAll(
new StringFilterGroup(
SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON,
"|like_button",
"dislike_button"
),
new StringFilterGroup(
SettingsEnum.HIDE_DOWNLOAD_BUTTON,
"download_button"
),
new StringFilterGroup(
SettingsEnum.HIDE_PLAYLIST_BUTTON,
"save_to_playlist_button"
),
new StringFilterGroup(
SettingsEnum.HIDE_CLIP_BUTTON,
"|clip_button.eml|"
),
new StringFilterGroup(
SettingsEnum.HIDE_ACTION_BUTTONS,
"ContainerType|video_action_button",
"|CellType|CollectionType|CellType|ContainerType|button.eml|"
)
);
}
private boolean isEveryFilterGroupEnabled() {
for (StringFilterGroup rule : pathFilterGroups)
if (!rule.isEnabled()) return false;
return true;
}
@Override
public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) {
if (isEveryFilterGroupEnabled())
if (actionBarRule.check(identifier).isFiltered()) return true;
return super.isFiltered(path, identifier, _protobufBufferArray);
}
}

View File

@ -0,0 +1,26 @@
package app.revanced.integrations.patches.components;
import app.revanced.integrations.settings.SettingsEnum;
final class CommentsFilter extends Filter {
public CommentsFilter() {
var comments = new StringFilterGroup(
SettingsEnum.HIDE_COMMENTS_SECTION,
"video_metadata_carousel",
"_comments"
);
var previewComment = new StringFilterGroup(
SettingsEnum.HIDE_PREVIEW_COMMENT,
"|carousel_item",
"comments_entry_point_teaser",
"comments_entry_point_simplebox"
);
this.pathFilterGroups.addAll(
comments,
previewComment
);
}
}

View File

@ -0,0 +1,263 @@
package app.revanced.integrations.patches.components;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
abstract class FilterGroup<T> {
final static class FilterGroupResult {
private final boolean filtered;
private final SettingsEnum setting;
public FilterGroupResult(final SettingsEnum setting, final boolean filtered) {
this.setting = setting;
this.filtered = filtered;
}
public SettingsEnum getSetting() {
return setting;
}
public boolean isFiltered() {
return filtered;
}
}
protected final SettingsEnum setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final SettingsEnum setting, final T... filters) {
this.setting = setting;
this.filters = filters;
}
public boolean isEnabled() {
return setting.getBoolean();
}
public abstract FilterGroupResult check(final T stack);
}
class StringFilterGroup extends FilterGroup<String> {
/**
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
*/
public StringFilterGroup(final SettingsEnum setting, final String... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final String string) {
return new FilterGroupResult(setting, string != null && ReVancedUtils.containsAny(string, filters));
}
}
final class CustomFilterGroup extends StringFilterGroup {
/**
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
*/
public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) {
super(setting, filter.getString().split(","));
}
}
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
// Modified implementation from https://stackoverflow.com/a/1507813
private int indexOf(final byte[] data, final byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int[] failure = new int[pattern.length];
int j = 0;
for (int i = 1; i < pattern.length; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
j = 0;
if (data.length == 0) return -1;
for (int i = 0; i < data.length; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == pattern.length) {
return i - pattern.length + 1;
}
}
return -1;
}
/**
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
*/
public ByteArrayFilterGroup(final SettingsEnum setting, final byte[]... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final byte[] bytes) {
var matched = false;
for (byte[] filter : filters) {
if (indexOf(bytes, filter) == -1) continue;
matched = true;
break;
}
final var filtered = matched;
return new FilterGroupResult(setting, filtered);
}
}
final class ByteArrayAsStringFilterGroup extends ByteArrayFilterGroup {
/**
* {@link ByteArrayFilterGroup#ByteArrayFilterGroup(SettingsEnum, byte[]...)}
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public ByteArrayAsStringFilterGroup(SettingsEnum setting, String... filters) {
super(setting, Arrays.stream(filters).map(String::getBytes).toArray(byte[][]::new));
}
}
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
private final ArrayList<T> filterGroups = new ArrayList<>();
@SafeVarargs
protected final void addAll(final T... filterGroups) {
this.filterGroups.addAll(Arrays.asList(filterGroups));
}
@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 boolean contains(final V stack) {
for (T filterGroup : this) {
if (!filterGroup.isEnabled()) continue;
var result = filterGroup.check(stack);
if (result.isFiltered()) {
return true;
}
}
return false;
}
}
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
}
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
}
abstract class Filter {
final protected StringFilterGroupList pathFilterGroups = new StringFilterGroupList();
final protected StringFilterGroupList identifierFilterGroups = new StringFilterGroupList();
final protected ByteArrayFilterGroupList protobufBufferFilterGroups = new ByteArrayFilterGroupList();
/**
* Check if the given path, identifier or protobuf buffer is filtered by any {@link FilterGroup}.
*
* @return True if filtered, false otherwise.
*/
boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) {
if (pathFilterGroups.contains(path)) {
LogHelper.printDebug(() -> String.format("Filtered path: %s", path));
return true;
}
if (identifierFilterGroups.contains(identifier)) {
LogHelper.printDebug(() -> String.format("Filtered identifier: %s", identifier));
return true;
}
if (protobufBufferFilterGroups.contains(protobufBufferArray)) {
LogHelper.printDebug(() -> "Filtered from protobuf-buffer");
return true;
}
return false;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("unused")
public final class LithoFilterPatch {
private static final Filter[] filters = new Filter[]{
new AdsFilter(),
new ButtonsFilter(),
new CommentsFilter(),
new ShortsFilter()
};
@SuppressWarnings("unused")
public static boolean filter(final StringBuilder pathBuilder, final String identifier, final ByteBuffer protobufBuffer) {
var path = pathBuilder.toString();
// It is assumed that protobufBuffer is empty as well in this case.
if (path.isEmpty()) return false;
LogHelper.printDebug(() -> String.format(
"Searching (ID: %s, Buffer-size: %s): %s",
identifier, protobufBuffer.remaining(), path
));
var protobufBufferArray = protobufBuffer.array();
// check if any filter-group
for (var filter : filters)
if (filter.isFiltered(path, identifier, protobufBufferArray)) return true;
return false;
}
}

View File

@ -0,0 +1,92 @@
package app.revanced.integrations.patches.components;
import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition;
import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition;
import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
import app.revanced.integrations.settings.SettingsEnum;
public final class ShortsFilter extends Filter {
public static PivotBar pivotBar;
@SuppressLint("StaticFieldLeak")
private final StringFilterGroup reelChannelBar = new StringFilterGroup(
null,
"reel_channel_bar"
);
public ShortsFilter() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
final var thanksButton = new StringFilterGroup(
SettingsEnum.HIDE_SHORTS_THANKS_BUTTON,
"suggested_action"
);
final var subscribeButton = new StringFilterGroup(
SettingsEnum.HIDE_SHORTS_SUBSCRIBE_BUTTON,
"subscribe_button"
);
final var joinButton = new StringFilterGroup(
SettingsEnum.HIDE_SHORTS_JOIN_BUTTON,
"sponsor_button"
);
final var shorts = new StringFilterGroup(
SettingsEnum.HIDE_SHORTS,
"shorts_shelf",
"inline_shorts",
"shorts_grid"
);
this.pathFilterGroups.addAll(joinButton, subscribeButton);
this.identifierFilterGroups.addAll(shorts, thanksButton);
}
@Override
boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) {
// Filter the path only when reelChannelBar is visible.
if (reelChannelBar.check(path).isFiltered())
if (this.pathFilterGroups.contains(path)) return true;
return this.identifierFilterGroups.contains(identifier);
}
public static void hideShortsShelf(final View shortsShelfView) {
hideViewBy1dpUnderCondition(SettingsEnum.HIDE_SHORTS, shortsShelfView);
}
// Additional components that have to be hidden by setting their visibility
public static void hideShortsCommentsButton(final View commentsButtonView) {
hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_COMMENTS_BUTTON, commentsButtonView);
}
public static void hideShortsRemixButton(final View remixButtonView) {
hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_REMIX_BUTTON, remixButtonView);
}
public static void hideShortsShareButton(final View shareButtonView) {
hideViewUnderCondition(SettingsEnum.HIDE_SHORTS_SHARE_BUTTON, shareButtonView);
}
public static void hideNavigationBar() {
if (!SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean()) return;
if (pivotBar == null) return;
pivotBar.setVisibility(View.GONE);
}
public static View hideNavigationBar(final View navigationBarView) {
if (SettingsEnum.HIDE_SHORTS_NAVIGATION_BAR.getBoolean())
return null; // Hides the navigation bar.
return navigationBarView;
}
}

View File

@ -10,7 +10,7 @@ import app.revanced.integrations.utils.LogHelper;
public class OldQualityLayoutPatch {
public static void showOldQualityMenu(ListView listView)
{
if (!SettingsEnum.OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS.getBoolean()) return;
if (!SettingsEnum.SHOW_OLD_VIDEO_MENU.getBoolean()) return;
listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@Override

View File

@ -2,7 +2,6 @@ package app.revanced.integrations.patches.playback.quality;
import static app.revanced.integrations.utils.ReVancedUtils.NetworkType;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
@ -20,12 +19,10 @@ public class RememberVideoQualityPatch {
private static final SettingsEnum mobileQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_MOBILE;
private static boolean qualityNeedsUpdating;
@Nullable
private static String currentVideoId;
/**
* If the user selected a new quality from the flyout menu,
* and {@link SettingsEnum#VIDEO_QUALITY_REMEMBER_LAST_SELECTED} is enabled.
* and {@link SettingsEnum#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled.
*/
private static boolean userChangedDefaultQuality;
@ -91,7 +88,7 @@ public class RememberVideoQualityPatch {
}
}
}
LogHelper.printDebug(() -> "VideoId: " + currentVideoId + " videoQualities: " + videoQualities);
LogHelper.printDebug(() -> "videoQualities: " + videoQualities);
}
if (userChangedDefaultQuality) {
@ -113,15 +110,25 @@ public class RememberVideoQualityPatch {
}
i++;
}
// If the desired quality index is equal to the original index,
// then the video is already set to the desired default quality.
//
// The method could return here, but the UI video quality flyout will still
// show 'Auto' (ie: Auto (480p))
// It appears that "Auto" picks the resolution on video load,
// and it does not appear to change the resolution during playback.
//
// To prevent confusion, set the video index anyways (even if it matches the existing index)
// As that will force the UI picker to not display "Auto" which may confuse the user.
if (qualityIndexToUse == originalQualityIndex) {
LogHelper.printDebug(() -> "Video is already preferred quality: " + preferredQuality);
return originalQualityIndex;
} else {
final int qualityToUseLog = qualityToUse;
LogHelper.printDebug(() -> "Quality changed from: "
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog);
}
final int qualityToUseLog = qualityToUse;
LogHelper.printDebug(() -> "Quality changed from: "
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog);
Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
m.invoke(qInterface, qualityToUse);
return qualityIndexToUse;
@ -135,7 +142,7 @@ public class RememberVideoQualityPatch {
* Injection point.
*/
public static void userChangedQuality(int selectedQuality) {
if (!SettingsEnum.VIDEO_QUALITY_REMEMBER_LAST_SELECTED.getBoolean()) return;
if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return;
userSelectedQualityIndex = selectedQuality;
userChangedDefaultQuality = true;
@ -144,25 +151,9 @@ public class RememberVideoQualityPatch {
/**
* Injection point.
*/
public static void newVideoStarted(@NonNull String videoId) {
// The same videoId can be passed in multiple times for a single video playback.
// Such as closing and opening the app, and sometimes when turning off/on the device screen.
//
// Known limitation, if:
// 1. a default video quality exists, and remember quality is turned off
// 2. user opens a video
// 3. user changes the video quality
// 4. user turns off then on the device screen (or does anything else that triggers the video id hook)
// result: the video quality of the current video will revert back to the saved default
//
// qualityNeedsUpdating could be set only when the videoId changes
// but then if the user closes and re-opens the same video the default video quality will not be applied.
LogHelper.printDebug(() -> "newVideoStarted: " + videoId);
public static void newVideoStarted(Object ignoredPlayerController) {
LogHelper.printDebug(() -> "newVideoStarted");
qualityNeedsUpdating = true;
if (!videoId.equals(currentVideoId)) {
currentVideoId = videoId;
videoQualities = null;
}
videoQualities = null;
}
}

View File

@ -1,12 +1,103 @@
package app.revanced.integrations.patches.playback.speed;
import android.preference.ListPreference;
import androidx.annotation.NonNull;
import java.util.Arrays;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class CustomVideoSpeedPatch {
/**
* Default playback speeds offered by YouTube.
* Values are also used by {@link RememberPlaybackSpeedPatch}.
*
* If custom video speed is applied,
* then this array is overwritten by the patch with custom speeds
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
*/
public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
public static final float MAXIMUM_PLAYBACK_SPEED = 10;
/**
* Custom playback speeds.
*/
public static float[] customVideoSpeeds;
/**
* Minimum value of {@link #customVideoSpeeds}
*/
public static float minVideoSpeed;
/**
* Maxium value of {@link #customVideoSpeeds}
*/
public static float maxVideoSpeed;
/**
* PreferenceList entries and values, of all available playback speeds.
*/
private static String[] preferenceListEntries, preferenceListEntryValues;
static {
loadSpeeds();
}
private static void resetCustomSpeeds(@NonNull String toastMessage) {
ReVancedUtils.showToastLong(toastMessage);
SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.saveValue(SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.defaultValue);
}
private static void loadSpeeds() {
try {
String[] speedStrings = SettingsEnum.CUSTOM_PLAYBACK_SPEEDS.getString().split("\\s+");
Arrays.sort(speedStrings);
if (speedStrings.length == 0) {
throw new IllegalArgumentException();
}
customVideoSpeeds = new float[speedStrings.length];
for (int i = 0, length = speedStrings.length; i < length; i++) {
final float speed = Float.parseFloat(speedStrings[i]);
if (speed <= 0 || arrayContains(customVideoSpeeds, speed)) {
throw new IllegalArgumentException();
}
if (speed >= MAXIMUM_PLAYBACK_SPEED) {
resetCustomSpeeds("Custom speeds must be less than " + MAXIMUM_PLAYBACK_SPEED
+ ". Using default values.");
loadSpeeds();
return;
}
minVideoSpeed = Math.min(minVideoSpeed, speed);
maxVideoSpeed = Math.max(maxVideoSpeed, speed);
customVideoSpeeds[i] = speed;
}
} catch (Exception ex) {
LogHelper.printInfo(() -> "parse error", ex);
resetCustomSpeeds("Invalid custom video speeds. Using default values.");
loadSpeeds();
}
}
private static boolean arrayContains(float[] array, float value) {
for (float arrayValue : array) {
if (arrayValue == value) return true;
}
return false;
}
/**
* Initialize a settings preference list with the available playback speeds.
*/
public static void initializeListPreference(ListPreference preference) {
if (preferenceListEntries == null) {
preferenceListEntries = new String[customVideoSpeeds.length];
preferenceListEntryValues = new String[customVideoSpeeds.length];
int i = 0;
for (float speed : customVideoSpeeds) {
String speedString = String.valueOf(speed);
preferenceListEntries[i] = speedString + "x";
preferenceListEntryValues[i] = speedString;
i++;
}
}
preference.setEntries(preferenceListEntries);
preference.setEntryValues(preferenceListEntryValues);
}
}

View File

@ -1,33 +1,17 @@
package app.revanced.integrations.patches.playback.speed;
import android.preference.ListPreference;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public final class RememberPlaybackSpeedPatch {
/**
* PreferenceList entries and values, of all available playback speeds.
*/
private static String[] preferenceListEntries, preferenceListEntryValues;
@Nullable
private static String currentVideoId;
/**
* Injection point.
* Called when a new video loads.
*/
public static void newVideoLoaded(@NonNull String videoId) {
if (videoId.equals(currentVideoId)) {
return;
}
currentVideoId = videoId;
public static void newVideoStarted(Object ignoredPlayerController) {
LogHelper.printDebug(() -> "newVideoStarted");
VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat());
}
@ -38,7 +22,7 @@ public final class RememberPlaybackSpeedPatch {
* @param playbackSpeed The playback speed the user selected
*/
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) {
if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) {
SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed);
ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x");
}
@ -52,26 +36,4 @@ public final class RememberPlaybackSpeedPatch {
return VideoInformation.getPlaybackSpeed();
}
/**
* Initialize a settings preference list.
*
* Normally this is done during patching by creating a static xml preference list,
* but the available playback speeds differ depending if {@link CustomVideoSpeedPatch} is applied or not.
*/
public static void initializeListPreference(ListPreference preference) {
if (preferenceListEntries == null) {
float[] videoSpeeds = CustomVideoSpeedPatch.videoSpeeds;
preferenceListEntries = new String[videoSpeeds.length];
preferenceListEntryValues = new String[videoSpeeds.length];
int i = 0;
for (float speed : videoSpeeds) {
String speedString = String.valueOf(speed);
preferenceListEntries[i] = speedString + "x";
preferenceListEntryValues[i] = speedString;
i++;
}
}
preference.setEntries(preferenceListEntries);
preference.setEntryValues(preferenceListEntryValues);
}
}

View File

@ -0,0 +1,47 @@
package app.revanced.integrations.patches.theme;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.patches.HideSeekbarPatch;
import app.revanced.integrations.settings.SettingsEnum;
/**
* Used by {@link SeekbarColorPatch} change the color of the seekbar.
* and {@link HideSeekbarPatch} to hide the seekbar of the feed and watch history.
*/
public class ProgressBarDrawable extends Drawable {
private final Paint paint = new Paint();
@Override
public void draw(@NonNull Canvas canvas) {
if (SettingsEnum.HIDE_SEEKBAR.getBoolean()) {
return;
}
paint.setColor(SeekbarColorPatch.getCustomSeekbarColor());
canvas.drawRect(getBounds(), paint);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View File

@ -0,0 +1,111 @@
package app.revanced.integrations.patches.theme;
import android.graphics.Color;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public final class SeekbarColorPatch {
/**
* Default color of seekbar.
*/
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
/**
* Default YouTube seekbar color brightness.
*/
private static final float ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS;
/**
* Color value of {@link SettingsEnum#SEEKBAR_COLOR}
*/
private static int customSeekbarColor;
/**
* Custom seekbar hue, saturation, and brightness values.
*/
private static final float[] customSeekbarColorHSV = new float[3];
static {
float[] hsv = new float[3];
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
loadCustomSeekbarColorHSV();
}
private static void loadCustomSeekbarColorHSV() {
try {
customSeekbarColor = Color.parseColor(SettingsEnum.SEEKBAR_COLOR.getString());
Color.colorToHSV(customSeekbarColor, customSeekbarColorHSV);
} catch (Exception ex) {
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value.");
SettingsEnum.SEEKBAR_COLOR.saveValue(SettingsEnum.SEEKBAR_COLOR.defaultValue);
loadCustomSeekbarColorHSV();
}
}
public static int getCustomSeekbarColor() {
return customSeekbarColor;
}
/**
* Injection point.
*
* Overrides color when seekbar is clicked, and all Litho components that use the YouTube seekbar color.
*/
public static int getSeekbarColorOverride(int colorValue) {
return colorValue == ORIGINAL_SEEKBAR_COLOR
? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR)
: colorValue;
}
/**
* Injection point.
*
* If {@link SettingsEnum#HIDE_SEEKBAR} is enabled, this returns a fully transparent color.
*
* Otherwise the original color is changed to the custom seekbar color, while retaining
* the brightness and alpha changes of the parameter value compared to the original seekbar color.
*/
public static int getSeekbarColorValue(int originalColor) {
try {
if (SettingsEnum.HIDE_SEEKBAR.getBoolean()) {
return 0x00000000;
}
if (customSeekbarColor == ORIGINAL_SEEKBAR_COLOR) {
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.
float[] hsv = new float[3];
Color.colorToHSV(originalColor, hsv);
final float brightnessDifference = hsv[2] - ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS;
// Apply the brightness difference to the custom seekbar color.
hsv[0] = customSeekbarColorHSV[0];
hsv[1] = customSeekbarColorHSV[1];
hsv[2] = clamp(customSeekbarColorHSV[2] + brightnessDifference, 0, 1);
final int replacementAlpha = clamp(Color.alpha(customSeekbarColor) + alphaDifference, 0, 255);
final int replacementColor = Color.HSVToColor(replacementAlpha, hsv);
LogHelper.printDebug(() -> String.format("Original color: #%08X replacement color: #%08X",
originalColor, replacementColor));
return replacementColor;
} catch (Exception ex) {
LogHelper.printException(() -> "getSeekbarColorValue failure", ex);
return originalColor;
}
}
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) {
return Math.max(lower, Math.min(value, upper));
}
}

View File

@ -1,30 +0,0 @@
package app.revanced.integrations.patches.theme;
import android.graphics.Color;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public final class ThemePatch {
public static final int DEFAULT_SEEKBAR_COLOR = 0xffff0000;
public static final int ORIGINAL_SEEKBAR_CLICKED_COLOR = -65536;
private static void resetSeekbarColor() {
ReVancedUtils.showToastShort("Invalid seekbar color value. Using default value.");
SettingsEnum.SEEKBAR_COLOR.saveValue("#" + Integer.toHexString(DEFAULT_SEEKBAR_COLOR));
}
public static int getSeekbarClickedColorValue(final int colorValue) {
// YouTube uses a specific color when the seekbar is clicked. Override in that case.
return colorValue == ORIGINAL_SEEKBAR_CLICKED_COLOR ? getSeekbarColorValue() : colorValue;
}
public static int getSeekbarColorValue() {
try {
return Color.parseColor(SettingsEnum.SEEKBAR_COLOR.getString());
} catch (IllegalArgumentException exception) {
resetSeekbarColor();
return DEFAULT_SEEKBAR_COLOR;
}
}
}

View File

@ -25,8 +25,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -45,13 +48,47 @@ import app.revanced.integrations.utils.ThemeHelper;
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
*/
public class ReturnYouTubeDislike {
/**
* Simple wrapper to cache a Future.
*/
private static class RYDCachedFetch {
/**
* How long to retain cached RYD fetches.
*/
static final long CACHE_TIMEOUT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes
@NonNull
final Future<RYDVoteData> future;
final String videoId;
final long timeFetched;
RYDCachedFetch(@NonNull Future<RYDVoteData> future, @NonNull String videoId) {
this.future = Objects.requireNonNull(future);
this.videoId = Objects.requireNonNull(videoId);
this.timeFetched = System.currentTimeMillis();
}
boolean isExpired(long now) {
return (now - timeFetched) > CACHE_TIMEOUT_MILLISECONDS;
}
boolean futureInProgressOrFinishedSuccessfully() {
try {
return !future.isDone() || future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS) != null;
} catch (ExecutionException | InterruptedException | TimeoutException ex) {
LogHelper.printInfo(() -> "failed to lookup cache", ex); // will never happen
}
return false;
}
}
/**
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
*
* Must be less than 5 seconds, as per:
* https://developer.android.com/topic/performance/vitals/anr
*/
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE = 4000;
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
/**
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
@ -59,6 +96,12 @@ public class ReturnYouTubeDislike {
*/
private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character
/**
* Cached lookup of RYD fetches.
*/
@GuardedBy("videoIdLockObject")
private static final Map<String, RYDCachedFetch> futureCache = new HashMap<>();
/**
* Used to send votes, one by one, in the same order the user created them.
*/
@ -85,6 +128,13 @@ public class ReturnYouTubeDislike {
@GuardedBy("videoIdLockObject")
private static Future<RYDVoteData> voteFetchFuture;
/**
* Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing.
*/
@Nullable
@GuardedBy("videoIdLockObject")
private static Vote userVote;
/**
* Original dislike span, before modifications.
*/
@ -135,13 +185,25 @@ public class ReturnYouTubeDislike {
}
}
private static void setCurrentVideoId(@Nullable String videoId) {
public static void setCurrentVideoId(@Nullable String videoId) {
synchronized (videoIdLockObject) {
if (videoId == null && currentVideoId != null) {
LogHelper.printDebug(() -> "Clearing data");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final long now = System.currentTimeMillis();
futureCache.values().removeIf(value -> {
final boolean expired = value.isExpired(now);
if (expired) LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId);
return expired;
});
} else {
throw new IllegalStateException(); // YouTube requires Android N or greater
}
currentVideoId = videoId;
dislikeDataIsShort = false;
userVote = null;
voteFetchFuture = null;
originalDislikeSpan = null;
replacementLikeDislikeSpan = null;
@ -154,7 +216,7 @@ public class ReturnYouTubeDislike {
public static void clearCache() {
synchronized (videoIdLockObject) {
if (replacementLikeDislikeSpan != null) {
LogHelper.printDebug(() -> "Clearing cache");
LogHelper.printDebug(() -> "Clearing replacement spans");
}
replacementLikeDislikeSpan = null;
}
@ -177,12 +239,6 @@ public class ReturnYouTubeDislike {
public static void newVideoLoaded(@NonNull String videoId) {
Objects.requireNonNull(videoId);
PlayerType currentPlayerType = PlayerType.getCurrent();
if (currentPlayerType == PlayerType.INLINE_MINIMAL) {
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId);
setCurrentVideoId(null);
return;
}
synchronized (videoIdLockObject) {
if (videoId.equals(currentVideoId)) {
return; // already loaded
@ -192,17 +248,23 @@ public class ReturnYouTubeDislike {
setCurrentVideoId(null);
return;
}
PlayerType currentPlayerType = PlayerType.getCurrent();
LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
setCurrentVideoId(videoId);
// If a Short is opened while a regular video is on screen, this will incorrectly set this as false.
// But this check is needed to fix unusual situations of opening/closing the app
// while both a regular video and a short are on screen.
dislikeDataIsShort = PlayerType.getCurrent().isNoneOrHidden();
dislikeDataIsShort = currentPlayerType.isNoneHiddenOrMinimized();
// No need to wrap the call in a try/catch,
// as any exceptions are propagated out in the later Future#Get call.
RYDCachedFetch entry = futureCache.get(videoId);
if (entry != null && entry.futureInProgressOrFinishedSuccessfully()) {
LogHelper.printDebug(() -> "Using cached RYD fetch: "+ entry.videoId);
voteFetchFuture = entry.future;
return;
}
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
futureCache.put(videoId, new RYDCachedFetch(voteFetchFuture, videoId));
}
}
@ -240,13 +302,28 @@ public class ReturnYouTubeDislike {
@NonNull
private static Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) {
try {
Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
if (fetchFuture == null) {
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
return oldSpannable;
}
// Absolutely cannot be holding any lock during get().
RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS);
if (votingData == null) {
LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
return oldSpannable;
}
// Must check against existing replacements, after the fetch,
// otherwise concurrent threads can create the same replacement same multiple times.
// Also do the replacement comparison and creation in a single synchronized block.
synchronized (videoIdLockObject) {
if (replacementLikeDislikeSpan != null) {
if (spansHaveEqualTextAndColor(replacementLikeDislikeSpan, oldSpannable)) {
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) {
if (spansHaveEqualTextAndColor(oldSpannable, replacementLikeDislikeSpan)) {
LogHelper.printDebug(() -> "Ignoring previously created dislikes span");
return oldSpannable;
}
if (spansHaveEqualTextAndColor(Objects.requireNonNull(originalDislikeSpan), oldSpannable)) {
if (spansHaveEqualTextAndColor(oldSpannable, originalDislikeSpan)) {
LogHelper.printDebug(() -> "Replacing span with previously created dislike span");
return replacementLikeDislikeSpan;
}
@ -258,31 +335,19 @@ public class ReturnYouTubeDislike {
return oldSpannable;
}
oldSpannable = originalDislikeSpan;
} else {
originalDislikeSpan = oldSpannable; // most up to date original
}
}
// Must block the current thread until fetching is done.
// There's no known way to edit the text after creation yet.
Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
if (fetchFuture == null) {
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
return oldSpannable;
}
RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
if (votingData == null) {
LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
return oldSpannable;
}
// No replacement span exist, create it now.
SpannableString replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
synchronized (videoIdLockObject) {
replacementLikeDislikeSpan = replacement;
if (userVote != null) {
votingData.updateUsingVote(userVote);
}
originalDislikeSpan = oldSpannable;
replacementLikeDislikeSpan = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
LogHelper.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + replacementLikeDislikeSpan + "'");
return replacementLikeDislikeSpan;
}
final Spanned oldSpannableLogging = oldSpannable;
LogHelper.printDebug(() -> "Replaced: '" + oldSpannableLogging + "' with: '" + replacement + "'");
return replacement;
} catch (TimeoutException e) {
LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast
} catch (Exception e) {
@ -291,13 +356,22 @@ public class ReturnYouTubeDislike {
return oldSpannable;
}
/**
* @return if the RYD fetch call has completed.
*/
public static boolean fetchCompleted() {
Future<RYDVoteData> future = getVoteFetchFuture();
return future != null && future.isDone();
}
public static void sendVote(@NonNull Vote vote) {
ReVancedUtils.verifyOnMainThread();
Objects.requireNonNull(vote);
try {
// Must make a local copy of videoId, since it may change between now and when the vote thread runs.
String videoIdToVoteFor = getCurrentVideoId();
if (videoIdToVoteFor == null || dislikeDataIsShort != PlayerType.getCurrent().isNoneOrHidden()) {
if (videoIdToVoteFor == null ||
(SettingsEnum.RYD_SHORTS.getBoolean() && dislikeDataIsShort != PlayerType.getCurrent().isNoneHiddenOrMinimized())) {
// User enabled RYD after starting playback of a video.
// Or shorts was loaded with regular video present, then shorts was closed,
// and then user voted on the now visible original video.
@ -317,27 +391,48 @@ public class ReturnYouTubeDislike {
}
});
clearCache(); // UI needs updating
// Update the downloaded vote data.
Future<RYDVoteData> future = getVoteFetchFuture();
if (future == null) {
LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null");
return;
}
// The future should always be completed before user can like/dislike, but use a timeout just in case.
RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
if (voteData == null) {
// RYD fetch failed
LogHelper.printDebug(() -> "Cannot update UI (vote data not available)");
return;
}
voteData.updateUsingVote(vote);
setUserVote(vote);
} catch (Exception ex) {
LogHelper.printException(() -> "Error trying to send vote", ex);
}
}
public static void setUserVote(@NonNull Vote vote) {
Objects.requireNonNull(vote);
try {
LogHelper.printDebug(() -> "setUserVote: " + vote);
// Update the downloaded vote data.
Future<RYDVoteData> future = getVoteFetchFuture();
if (future != null && future.isDone()) {
RYDVoteData voteData;
try {
voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (ExecutionException | InterruptedException | TimeoutException ex) {
// Should never happen
LogHelper.printInfo(() -> "Could not update vote data", ex);
return;
}
if (voteData == null) {
// RYD fetch failed
LogHelper.printDebug(() -> "Cannot update UI (vote data not available)");
return;
}
voteData.updateUsingVote(vote);
} // Else, vote will be applied after vote data is received
synchronized (videoIdLockObject) {
if (userVote != vote) {
userVote = vote;
clearCache(); // UI needs updating
}
}
} catch (Exception ex) {
LogHelper.printException(() -> "setUserVote failure", ex);
}
}
/**
* Must call off main thread, as this will make a network call if user is not yet registered.
*
@ -363,6 +458,7 @@ public class ReturnYouTubeDislike {
/**
* @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike.
*/
@NonNull
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
if (!isSegmentedButton) {
// Simple replacement of 'dislike' with a number/percentage.
@ -393,7 +489,7 @@ public class ReturnYouTubeDislike {
}
SpannableStringBuilder builder = new SpannableStringBuilder();
final boolean compactLayout = SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean();
final boolean compactLayout = SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean();
final int separatorColor = ThemeHelper.isDarkTheme()
? 0x29AAAAAA // transparent dark gray
: 0xFFD9D9D9; // light gray
@ -477,12 +573,12 @@ public class ReturnYouTubeDislike {
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
SettingsEnum.RYD_DISLIKE_PERCENTAGE.getBoolean()
? formatDislikePercentage(voteData.getDislikePercentage())
: formatDislikeCount(voteData.getDislikeCount()));
}
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) {
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
SpannableString destination = new SpannableString(newSpanText);
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
for (Object span : spans) {

View File

@ -82,15 +82,12 @@ public final class RYDVoteData {
public void updateUsingVote(Vote vote) {
if (vote == Vote.LIKE) {
LogHelper.printDebug(() -> "Increasing like count");
likeCount = fetchedLikeCount + 1;
dislikeCount = fetchedDislikeCount;
} else if (vote == Vote.DISLIKE) {
LogHelper.printDebug(() -> "Increasing dislike count");
likeCount = fetchedLikeCount;
dislikeCount = fetchedDislikeCount + 1;
} else if (vote == Vote.LIKE_REMOVE) {
LogHelper.printDebug(() -> "Resetting like/dislike to fetched values");
likeCount = fetchedLikeCount;
dislikeCount = fetchedDislikeCount;
} else {

View File

@ -5,11 +5,13 @@ import static app.revanced.integrations.utils.StringRef.str;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
@ -22,6 +24,7 @@ import java.util.Objects;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
@ -219,6 +222,15 @@ public class ReturnYouTubeDislikeApi {
}
}
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
if (SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()) {
ReVancedUtils.showToastShort(toastMessage);
}
if (ex != null) {
LogHelper.printInfo(() -> toastMessage, ex);
}
}
/**
* @return NULL if fetch failed, or if a rate limit is in effect.
*/
@ -272,12 +284,13 @@ public class ReturnYouTubeDislikeApi {
LogHelper.printDebug(() -> "Video has no like/dislikes (video is a YouTube Story?): " + videoId);
return null; // do not updated connection statistics
} else {
LogHelper.printException(() -> "Failed to fetch votes for video: " + videoId + " response code was: " + responseCode,
null, str("revanced_ryd_failure_connection_status_code", responseCode));
connection.disconnect(); // something went wrong, might as well disconnect
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
}
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error
LogHelper.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_connection_timeout"));
handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex);
} catch (IOException ex) {
handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex);
} catch (Exception ex) {
// should never happen
LogHelper.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_generic", ex.getMessage()));
@ -318,11 +331,14 @@ public class ReturnYouTubeDislikeApi {
String solution = solvePuzzle(challenge, difficulty);
return confirmRegistration(userId, solution);
}
LogHelper.printException(() -> "Failed to register new user: " + userId
+ " response code was: " + responseCode); // failed attempt, and ok to log userId
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
connection.disconnect();
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to register user", ex);
LogHelper.printException(() -> "Failed to register user", ex); // should never happen
}
return null;
}
@ -351,19 +367,23 @@ public class ReturnYouTubeDislikeApi {
connection.disconnect(); // disconnect, as no more connections will be made for a little while
return null;
}
String result = null;
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
String result = Requester.parseJson(connection);
result = Requester.parseJson(connection);
if (result.equalsIgnoreCase("true")) {
LogHelper.printDebug(() -> "Registration confirmation successful");
return userId;
}
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " response string was: " + result);
} else {
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " response code was: " + responseCode);
}
final String resultLog = result == null ? "(no response)" : result;
LogHelper.printInfo(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog);
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
+ "solution: " + solution, ex);
@ -405,10 +425,16 @@ public class ReturnYouTubeDislikeApi {
String solution = solvePuzzle(challenge, difficulty);
return confirmVote(videoId, userId, solution);
}
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
+ " vote: " + vote + " response code was: " + responseCode);
LogHelper.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote
+ " response code was: " + responseCode);
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex);
} catch (Exception ex) {
// should never happen
LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
}
return false;
@ -438,23 +464,26 @@ public class ReturnYouTubeDislikeApi {
connection.disconnect(); // disconnect, as no more connections will be made for a little while
return false;
}
String result = null;
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
String result = Requester.parseJson(connection);
result = Requester.parseJson(connection);
if (result.equalsIgnoreCase("true")) {
LogHelper.printDebug(() -> "Vote confirm successful for video: " + videoId);
return true;
}
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " response string was: " + result);
} else {
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " response code was: " + responseCode);
}
final String resultLog = result == null ? "(no response)" : result;
LogHelper.printInfo(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog);
handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null);
connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution, ex);
+ " solution: " + solution, ex); // should never happen
}
return false;
}
@ -503,7 +532,7 @@ public class ReturnYouTubeDislikeApi {
}
// should never be reached
throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " of difficulty: " + difficulty);
throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " difficulty: " + difficulty);
}
// https://stackoverflow.com/a/157202
@ -519,9 +548,8 @@ public class ReturnYouTubeDislikeApi {
private static int countLeadingZeroes(byte[] uInt8View) {
int zeroes = 0;
int value;
for (byte b : uInt8View) {
value = b & 0xFF;
int value = b & 0xFF;
if (value == 0) {
zeroes += 8;
} else {

View File

@ -1,69 +1,90 @@
package app.revanced.integrations.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.BOOLEAN;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.FLOAT;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.INTEGER;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.LONG;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.STRING;
import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE;
import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK;
import static app.revanced.integrations.utils.StringRef.str;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.utils.StringRef;
import app.revanced.integrations.patches.theme.ThemePatch;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.StringRef;
import app.revanced.integrations.utils.LogHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*;
import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE;
import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
public enum SettingsEnum {
//Download Settings
DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", BOOLEAN, TRUE),
DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe" /* NewPipe */, parents(DOWNLOADS_BUTTON_SHOWN)),
// External downloader
EXTERNAL_DOWNLOADER("revanced_external_downloader", BOOLEAN, TRUE),
EXTERNAL_DOWNLOADER_PACKAGE_NAME("revanced_external_downloader_name", STRING,
"org.schabi.newpipe" /* NewPipe */, parents(EXTERNAL_DOWNLOADER)),
// Copy video URL settings
COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", BOOLEAN, TRUE),
COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE),
// Copy video URL
COPY_VIDEO_URL("revanced_copy_video_url", BOOLEAN, FALSE),
COPY_VIDEO_URL_TIMESTAMP("revanced_copy_video_url_timestamp", BOOLEAN, TRUE),
// Video settings
OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", BOOLEAN, TRUE),
VIDEO_QUALITY_REMEMBER_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE),
VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2),
VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2),
PLAYBACK_SPEED_REMEMBER_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE),
PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f),
// Video
HDR_AUTO_BRIGHTNESS("revanced_hdr_auto_brightness", BOOLEAN, TRUE),
SHOW_OLD_VIDEO_MENU("revanced_show_old_video_menu", BOOLEAN, TRUE),
REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE),
VIDEO_QUALITY_DEFAULT_WIFI("revanced_video_quality_default_wifi", INTEGER, -2),
VIDEO_QUALITY_DEFAULT_MOBILE("revanced_video_quality_default_mobile", INTEGER, -2),
REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE),
PLAYBACK_SPEED_DEFAULT("revanced_playback_speed_default", FLOAT, 1.0f),
CUSTOM_PLAYBACK_SPEEDS("revanced_custom_playback_speeds", STRING,
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true),
// TODO: Unused currently
// Whitelist settings
//ENABLE_WHITELIST("revanced_whitelist_ads_enabled", BOOLEAN, FALSE),
// Whitelist
//WHITELIST("revanced_whitelist_ads", BOOLEAN, FALSE), // TODO: Unused currently
// Ad settings
ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", BOOLEAN, TRUE),
ADREMOVER_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE),
ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL("revanced_adremover_channel_member_shelf_removal", BOOLEAN, TRUE),
ADREMOVER_CHAPTER_TEASER_REMOVAL("revanced_adremover_chapter_teaser", BOOLEAN, TRUE),
ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_community_guidelines", BOOLEAN, TRUE),
ADREMOVER_COMMUNITY_POSTS_REMOVAL("revanced_adremover_community_posts_removal", BOOLEAN, FALSE),
ADREMOVER_COMPACT_BANNER_REMOVAL("revanced_adremover_compact_banner_removal", BOOLEAN, TRUE),
ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", BOOLEAN, FALSE),
ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", STRING, "", true, parents(ADREMOVER_CUSTOM_ENABLED)),
ADREMOVER_EMERGENCY_BOX_REMOVAL("revanced_adremover_emergency_box_removal", BOOLEAN, TRUE),
ADREMOVER_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", BOOLEAN, TRUE),
ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", BOOLEAN, TRUE),
ADREMOVER_GRAY_SEPARATOR("revanced_adremover_separator", BOOLEAN, TRUE),
ADREMOVER_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", BOOLEAN, TRUE),
ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", BOOLEAN, TRUE),
ADREMOVER_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE),
ADREMOVER_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", BOOLEAN, TRUE),
ADREMOVER_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", BOOLEAN, TRUE),
ADREMOVER_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", BOOLEAN, TRUE),
ADREMOVER_MOVIE_REMOVAL("revanced_adremover_movie", BOOLEAN, TRUE),
ADREMOVER_PAID_CONTENT_REMOVAL("revanced_adremover_paid_content", BOOLEAN, TRUE),
ADREMOVER_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE),
ADREMOVER_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE),
ADREMOVER_SELF_SPONSOR_REMOVAL("revanced_adremover_self_sponsor", BOOLEAN, TRUE),
ADREMOVER_SHORTS_REMOVAL("revanced_adremover_shorts", BOOLEAN, TRUE, true),
ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", BOOLEAN, TRUE),
ADREMOVER_VIEW_PRODUCTS("revanced_adremover_view_products", BOOLEAN, TRUE),
ADREMOVER_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", BOOLEAN, TRUE),
VIDEO_ADS_REMOVAL("revanced_video_ads_removal", BOOLEAN, TRUE, true),
// Ads
HIDE_BUTTONED_ADS("revanced_hide_buttoned_ads", BOOLEAN, TRUE),
HIDE_GENERAL_ADS("revanced_hide_general_ads", BOOLEAN, TRUE),
HIDE_HIDE_LATEST_POSTS("revanced_hide_latest_posts_ads", BOOLEAN, TRUE),
HIDE_PAID_CONTENT("revanced_hide_paid_content_ads", BOOLEAN, TRUE),
HIDE_SELF_SPONSOR("revanced_hide_self_sponsor_ads", BOOLEAN, TRUE),
HIDE_VIDEO_ADS("revanced_hide_video_ads", BOOLEAN, TRUE, true),
CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE),
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
// Layout
HIDE_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE),
HIDE_CHANNEL_MEMBER_SHELF("revanced_hide_channel_member_shelf", BOOLEAN, TRUE),
HIDE_CHAPTER_TEASER("revanced_hide_chapter_teaser", BOOLEAN, TRUE),
HIDE_COMMUNITY_GUIDELINES("revanced_hide_community_guidelines", BOOLEAN, TRUE),
HIDE_COMMUNITY_POSTS("revanced_hide_community_posts", BOOLEAN, FALSE),
HIDE_COMPACT_BANNER("revanced_hide_compact_banner", BOOLEAN, TRUE),
HIDE_EMERGENCY_BOX("revanced_hide_emergency_box", BOOLEAN, TRUE),
HIDE_FEED_SURVEY("revanced_hide_feed_survey", BOOLEAN, TRUE),
HIDE_GRAY_SEPARATOR("revanced_hide_gray_separator", BOOLEAN, TRUE),
HIDE_HIDE_CHANNEL_GUIDELINES("revanced_hide_channel_guidelines", BOOLEAN, TRUE),
HIDE_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE),
HIDE_HIDE_INFO_PANELS("revanced_hide_info_panels", BOOLEAN, TRUE),
HIDE_MEDICAL_PANELS("revanced_hide_medical_panels", BOOLEAN, TRUE),
HIDE_MERCHANDISE_BANNERS("revanced_hide_merchandise_banners", BOOLEAN, TRUE),
HIDE_MOVIES_SECTION("revanced_hide_movies_section", BOOLEAN, TRUE),
HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES("revanced_hide_subscribers_community_guidelines", BOOLEAN, TRUE),
HIDE_PRODUCTS_BANNER("revanced_hide_products_banner", BOOLEAN, TRUE),
HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE),
HIDE_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE),
HIDE_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE),
// Action buttons
HIDE_LIKE_DISLIKE_BUTTON("revanced_hide_like_dislike_button", BOOLEAN, FALSE),
@ -72,8 +93,8 @@ public enum SettingsEnum {
HIDE_CLIP_BUTTON("revanced_hide_clip_button", BOOLEAN, FALSE, "revanced_hide_clip_button_user_dialog_message"),
HIDE_ACTION_BUTTONS("revanced_hide_action_buttons", BOOLEAN, FALSE),
// Layout settings
DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", BOOLEAN, FALSE),
// Layout
DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE),
HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true),
HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE),
HIDE_AUDIO_TRACK_BUTTON("revanced_hide_audio_track_button", BOOLEAN, FALSE),
@ -83,87 +104,253 @@ public enum SettingsEnum {
HIDE_CAST_BUTTON("revanced_hide_cast_button", BOOLEAN, TRUE, true),
HIDE_COMMENTS_SECTION("revanced_hide_comments_section", BOOLEAN, FALSE, true),
HIDE_CREATE_BUTTON("revanced_hide_create_button", BOOLEAN, TRUE, true),
SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true),
HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", BOOLEAN, FALSE, true),
HIDE_EMAIL_ADDRESS("revanced_hide_email_address", BOOLEAN, FALSE),
HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", BOOLEAN, TRUE),
HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", BOOLEAN, TRUE, true),
HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", BOOLEAN, TRUE),
HIDE_GET_PREMIUM("revanced_hide_get_premium", BOOLEAN, TRUE),
HIDE_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE),
HIDE_INFO_CARDS("revanced_hide_info_cards", BOOLEAN, TRUE),
HIDE_LOAD_MORE_BUTTON("revanced_hide_load_more_button", BOOLEAN, TRUE, true),
HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", BOOLEAN, FALSE),
HIDE_PLAYER_OVERLAY("revanced_hide_player_overlay", BOOLEAN, FALSE, true),
HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", BOOLEAN, FALSE, true),
HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE),
HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE, true),
HIDE_HOME_BUTTON("revanced_hide_home_button", BOOLEAN, FALSE, true),
HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", BOOLEAN, TRUE, true),
HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true),
HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE),
HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE),
HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE),
HIDE_WATCH_IN_VR("revanced_hide_watch_in_vr", BOOLEAN, FALSE, true),
PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", BOOLEAN, FALSE),
PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE),
SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true),
SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"),
SPOOF_APP_VERSION_TARGET("revanced_spoof_app_version_target", STRING, "17.30.35", true, parents(SPOOF_APP_VERSION)),
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
SEEKBAR_COLOR("revanced_seekbar_color", STRING, Integer.toHexString(ThemePatch.DEFAULT_SEEKBAR_COLOR), true),
SEEKBAR_COLOR("revanced_seekbar_color", STRING, "#FF0000", true),
HIDE_FILTER_BAR_FEED_IN_FEED("revanced_hide_filter_bar_feed_in_feed", BOOLEAN, FALSE, true),
HIDE_FILTER_BAR_FEED_IN_SEARCH("revanced_hide_filter_bar_feed_in_search", BOOLEAN, FALSE, true),
HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS("revanced_hide_filter_bar_feed_in_related_videos", BOOLEAN, FALSE, true),
HIDE_SHORTS_JOIN_BUTTON("revanced_hide_shorts_join_button", BOOLEAN, FALSE),
HIDE_SHORTS_SUBSCRIBE_BUTTON("revanced_hide_shorts_subscribe_button", BOOLEAN, FALSE),
HIDE_SHORTS_THANKS_BUTTON("revanced_hide_shorts_thanks_button", BOOLEAN, FALSE),
HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE),
HIDE_SHORTS_REMIX_BUTTON("revanced_hide_shorts_remix_button", BOOLEAN, FALSE),
HIDE_SHORTS_SHARE_BUTTON("revanced_hide_shorts_share_button", BOOLEAN, FALSE),
HIDE_SHORTS_NAVIGATION_BAR("revanced_hide_shorts_navigation_bar", BOOLEAN, TRUE, true),
HIDE_SHORTS("revanced_hide_shorts", BOOLEAN, FALSE, true),
// Misc. Settings
SIGNATURE_SPOOFING("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"),
CAPTIONS_ENABLED("revanced_autocaptions_enabled", BOOLEAN, FALSE),
// Misc
AUTO_CAPTIONS("revanced_auto_captions", BOOLEAN, FALSE),
DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", BOOLEAN, TRUE),
ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE, true),
PREFERRED_AUTO_REPEAT("revanced_pref_auto_repeat", BOOLEAN, FALSE),
TAP_SEEKING_ENABLED("revanced_enable_tap_seeking", BOOLEAN, TRUE),
USE_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", BOOLEAN, TRUE),
EXTERNAL_BROWSER("revanced_external_browser", BOOLEAN, TRUE, true),
AUTO_REPEAT("revanced_auto_repeat", BOOLEAN, FALSE),
SEEKBAR_TAPPING("revanced_seekbar_tapping", BOOLEAN, TRUE),
SPOOF_SIGNATURE_VERIFICATION("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"),
// Swipe controls
ENABLE_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", BOOLEAN, TRUE),
ENABLE_SWIPE_VOLUME("revanced_enable_swipe_volume", BOOLEAN, TRUE),
ENABLE_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", BOOLEAN, FALSE,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
ENABLE_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", BOOLEAN, TRUE,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_magnitude_threshold", FLOAT, 30f,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE),
SWIPE_VOLUME("revanced_swipe_volume", BOOLEAN, TRUE),
SWIPE_PRESS_TO_ENGAGE("revanced_swipe_press_to_engage", BOOLEAN, FALSE, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_HAPTIC_FEEDBACK("revanced_swipe_haptic_feedback", BOOLEAN, TRUE,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_threshold", INTEGER, 30,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", INTEGER, 127,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_overlay_text_size", FLOAT, 22f,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_text_overlay_size", INTEGER, 22,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", LONG, 500L,
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
// Debug settings
DEBUG("revanced_debug_enabled", BOOLEAN, FALSE),
DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", BOOLEAN, FALSE, parents(DEBUG)),
DEBUG_SHOW_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"),
// Debugging
DEBUG("revanced_debug", BOOLEAN, FALSE),
DEBUG_STACKTRACE("revanced_debug_stacktrace", BOOLEAN, FALSE, parents(DEBUG)),
DEBUG_TOAST_ON_ERROR("revanced_debug_toast_on_error", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"),
// ReturnYoutubeDislike settings
// ReturnYoutubeDislike
RYD_ENABLED("ryd_enabled", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE),
RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE),
RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_USER_ID("ryd_user_id", STRING, "", RETURN_YOUTUBE_DISLIKE),
RYD_SHORTS("ryd_shorts", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_DISLIKE_PERCENTAGE("ryd_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_COMPACT_LAYOUT("ryd_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_TOAST_ON_CONNECTION_ERROR("ryd_toast_on_connection_error", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
// SponsorBlock settings
SB_ENABLED("sb-enabled", BOOLEAN, TRUE, SPONSOR_BLOCK),
SB_VOTING_ENABLED("sb-voting-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_USE_COMPACT_SKIP_BUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_AUTO_HIDE_SKIP_BUTTON("sb-auto-hide-skip-segment-button", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_SHOW_TOAST_ON_SKIP("show-toast", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TRACK_SKIP_COUNT("count-skips", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_UUID("uuid", STRING, "", SPONSOR_BLOCK),
SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_MIN_DURATION("sb-min-duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_SEEN_GUIDELINES("sb-seen-gl", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", INTEGER, 0, SPONSOR_BLOCK),
SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", LONG, 0L, SPONSOR_BLOCK),
SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_IS_VIP("sb-is-vip", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_LAST_VIP_CHECK("sb-last-vip-check", LONG, 0L, SPONSOR_BLOCK),
SB_API_URL("sb-api-host-url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK);
// SponsorBlock
SB_ENABLED("sb_enabled", BOOLEAN, TRUE, SPONSOR_BLOCK),
SB_PRIVATE_USER_ID("sb_private_user_id_Do_Not_Share", STRING, "", SPONSOR_BLOCK), /** Do not use directly, instead use {@link SponsorBlockSettings} */
DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING("uuid", STRING, "", SPONSOR_BLOCK), // Delete sometime in 2024
SB_CREATE_NEW_SEGMENT_STEP("sb_create_new_segment_step", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_VOTING_BUTTON("sb_voting_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_CREATE_NEW_SEGMENT("sb_create_new_segment", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_COMPACT_SKIP_BUTTON("sb_compact_skip_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_AUTO_HIDE_SKIP_BUTTON("sb_auto_hide_skip_button", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TOAST_ON_SKIP("sb_toast_on_skip", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TOAST_ON_CONNECTION_ERROR("sb_toast_on_connection_error", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TRACK_SKIP_COUNT("sb_track_skip_count", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_SEGMENT_MIN_DURATION("sb_min_segment_duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_VIDEO_LENGTH_WITHOUT_SEGMENTS("sb_video_length_without_segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_API_URL("sb_api_url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK),
SB_USER_IS_VIP("sb_user_is_vip", BOOLEAN, FALSE, SPONSOR_BLOCK),
// SB settings not exported
SB_LAST_VIP_CHECK("sb_last_vip_check", LONG, 0L, SPONSOR_BLOCK),
SB_HIDE_EXPORT_WARNING("sb_hide_export_warning", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_SEEN_GUIDELINES("sb_seen_guidelines", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS("sb_local_time_saved_number_segments", INTEGER, 0, SPONSOR_BLOCK),
SB_LOCAL_TIME_SAVED_MILLISECONDS("sb_local_time_saved_milliseconds", LONG, 0L, SPONSOR_BLOCK),
private static SettingsEnum[] parents(SettingsEnum ... parents) {
//
// TODO: eventually, delete these
//
@Deprecated
DEPRECATED_ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_ADREMOVER_PAID_CONTENT("revanced_adremover_paid_content", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_ADREMOVER_SELF_SPONSOR("revanced_adremover_self_sponsor", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", STRING, "", true),
@Deprecated
DEPRECATED_REMOVE_VIDEO_ADS("revanced_video_ads_removal", BOOLEAN, TRUE, true),
@Deprecated
DEPRECATED_HIDE_CHANNEL_MEMBER_SHELF("revanced_adremover_channel_member_shelf_removal", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_CHAPTER_TEASER("revanced_adremover_chapter_teaser", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_COMMUNITY_GUIDELINES("revanced_adremover_community_guidelines", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_COMMUNITY_POSTS("revanced_adremover_community_posts_removal", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_HIDE_COMPACT_BANNER("revanced_adremover_compact_banner_removal", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_EMERGENCY_BOX("revanced_adremover_emergency_box_removal", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_GRAY_SEPARATOR("revanced_adremover_separator", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_MOVIE_REMOVAL("revanced_adremover_movie", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_VIEW_PRODUCTS("revanced_adremover_view_products", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HIDE_SHORTS("revanced_adremover_shorts", BOOLEAN, TRUE, true),
@Deprecated
DEPRECATED_HIDE_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_startup_shorts_player", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_ETERNAL_DOWNLOADER("revanced_downloads_enabled", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_EXTERNAL_DOWNLOADER_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe"),
@Deprecated
DEPRECATED_SHOW_OLD_VIDEO_MENU("revanced_use_old_style_quality_settings", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2),
@Deprecated
DEPRECATED_VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2),
@Deprecated
DEPRECATED_PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f),
@Deprecated
DEPRECATED_COPY_VIDEO_URL("revanced_copy_video_url_enabled", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_COPY_VIDEO_URL_TIMESTAMP("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_AUTO_CAPTIONS("revanced_autocaptions_enabled", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_SWIPE_VOLUME("revanced_enable_swipe_volume", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_DEBUG("revanced_debug_enabled", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_DEBUG_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_AUTO_REPEAT("revanced_pref_auto_repeat", BOOLEAN, FALSE),
@Deprecated
DEPRECATED_TAP_SEEKING("revanced_enable_tap_seeking", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", BOOLEAN, TRUE),
@Deprecated
DEPRECATED_RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE),
@Deprecated
DEPRECATED_RYD_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE),
@Deprecated
DEPRECATED_RYD_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE),
@Deprecated
DEPRECATED_SB_ENABLED("sb-enabled", BOOLEAN, TRUE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_VOTING_BUTTON("sb-voting-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_CREATE_NEW_SEGMENT("sb-new-segment-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_COMPACT_SKIP_BUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_MIN_DURATION("sb-min-duration", FLOAT, 0F, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_VIDEO_LENGTH_WITHOUT_SEGMENTS("sb-length-without-segments", BOOLEAN, TRUE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_API_URL("sb-api-host-url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_TOAST_ON_SKIP("show-toast", BOOLEAN, TRUE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_AUTO_HIDE_SKIP_BUTTON("sb-auto-hide-skip-segment-button", BOOLEAN, TRUE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_TRACK_SKIP_COUNT("count-skips", BOOLEAN, TRUE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", INTEGER, 150, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_LAST_VIP_CHECK("sb-last-vip-check", LONG, 0L, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_IS_VIP("sb-is-vip", BOOLEAN, FALSE, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS("sb-skipped-segments", INTEGER, 0, SPONSOR_BLOCK),
@Deprecated
DEPRECATED_SB_LOCAL_TIME_SAVED_MILLISECONDS("sb-skipped-segments-time", LONG, 0L, SPONSOR_BLOCK);
//
// TODO END
//
private static SettingsEnum[] parents(SettingsEnum... parents) {
return parents;
}
@ -206,26 +393,32 @@ public enum SettingsEnum {
SettingsEnum(String path, ReturnType returnType, Object defaultValue) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null,null);
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
String userDialogMessage) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, String userDialogMessage) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, String userDialogMessage, SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, parents);
@ -234,20 +427,24 @@ public enum SettingsEnum {
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName) {
this(path, returnType, defaultValue, prefName, false, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
boolean rebootApp) {
this(path, returnType, defaultValue, prefName, rebootApp, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
String userDialogMessage) {
this(path, returnType, defaultValue, prefName, false, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
SettingsEnum[] parents) {
this(path, returnType, defaultValue, prefName, false, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) {
boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) {
this.path = Objects.requireNonNull(path);
this.returnType = Objects.requireNonNull(returnType);
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
@ -273,22 +470,126 @@ public enum SettingsEnum {
}
}
private static final Map<String, SettingsEnum> pathToSetting = new HashMap<>(2* values().length);
static {
loadAllSettings();
for (SettingsEnum setting : values()) {
pathToSetting.put(setting.path, setting);
}
}
@Nullable
public static SettingsEnum settingFromPath(@NonNull String str) {
for (SettingsEnum setting : values()) {
if (setting.path.equals(str)) return setting;
}
return null;
return pathToSetting.get(str);
}
private static void loadAllSettings() {
for (SettingsEnum setting : values()) {
setting.load();
}
//
// TODO: eventually delete this
// renamed settings with new path names, but otherwise the new and old settings are identical
//
SettingsEnum[][] renamedSettings = {
// TODO: do _not_ delete this SB private user id migration property until sometime in 2024.
// This is the only setting that cannot be reconfigured if lost,
// and more time should be given for users who rarely upgrade.
{DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID},
// TODO: delete the rest of these migration settings. When to delete? Anytime.
{DEPRECATED_ADREMOVER_BUTTONED_REMOVAL, HIDE_BUTTONED_ADS},
{DEPRECATED_ADREMOVER_GENERAL_ADS_REMOVAL, HIDE_GENERAL_ADS},
{DEPRECATED_ADREMOVER_HIDE_LATEST_POSTS, HIDE_HIDE_LATEST_POSTS},
{DEPRECATED_ADREMOVER_PAID_CONTENT, HIDE_PAID_CONTENT},
{DEPRECATED_ADREMOVER_SELF_SPONSOR, HIDE_SELF_SPONSOR},
{DEPRECATED_REMOVE_VIDEO_ADS, HIDE_VIDEO_ADS},
{DEPRECATED_ADREMOVER_CUSTOM_ENABLED, CUSTOM_FILTER},
{DEPRECATED_ADREMOVER_CUSTOM_REMOVAL, CUSTOM_FILTER_STRINGS},
{DEPRECATED_HIDE_CHANNEL_MEMBER_SHELF, HIDE_CHANNEL_MEMBER_SHELF},
{DEPRECATED_HIDE_CHAPTER_TEASER, HIDE_CHAPTER_TEASER},
{DEPRECATED_HIDE_COMMUNITY_GUIDELINES, HIDE_COMMUNITY_GUIDELINES},
{DEPRECATED_HIDE_COMMUNITY_POSTS, HIDE_COMMUNITY_POSTS},
{DEPRECATED_HIDE_COMPACT_BANNER, HIDE_COMPACT_BANNER},
{DEPRECATED_HIDE_EMERGENCY_BOX, HIDE_EMERGENCY_BOX},
{DEPRECATED_HIDE_FEED_SURVEY_REMOVAL, HIDE_FEED_SURVEY},
{DEPRECATED_HIDE_GRAY_SEPARATOR, HIDE_GRAY_SEPARATOR},
{DEPRECATED_HIDE_HIDE_CHANNEL_GUIDELINES, HIDE_HIDE_CHANNEL_GUIDELINES},
{DEPRECATED_HIDE_INFO_PANEL_REMOVAL, HIDE_HIDE_INFO_PANELS},
{DEPRECATED_HIDE_MEDICAL_PANEL_REMOVAL, HIDE_MEDICAL_PANELS},
{DEPRECATED_HIDE_MERCHANDISE_REMOVAL, HIDE_MERCHANDISE_BANNERS},
{DEPRECATED_HIDE_MOVIE_REMOVAL, HIDE_MOVIES_SECTION},
{DEPRECATED_HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL, HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES},
{DEPRECATED_HIDE_VIEW_PRODUCTS, HIDE_PRODUCTS_BANNER},
{DEPRECATED_HIDE_WEB_SEARCH_RESULTS, HIDE_WEB_SEARCH_RESULTS},
{DEPRECATED_HIDE_SHORTS, HIDE_SHORTS},
{DEPRECATED_DISABLE_RESUMING_SHORTS_PLAYER, DISABLE_RESUMING_SHORTS_PLAYER},
{DEPRECATED_HIDE_INFO_CARDS, HIDE_INFO_CARDS},
{DEPRECATED_ETERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER},
{DEPRECATED_EXTERNAL_DOWNLOADER_PACKAGE_NAME, EXTERNAL_DOWNLOADER_PACKAGE_NAME},
{DEPRECATED_COPY_VIDEO_URL, COPY_VIDEO_URL},
{DEPRECATED_COPY_VIDEO_URL_TIMESTAMP, COPY_VIDEO_URL_TIMESTAMP},
{DEPRECATED_SHOW_OLD_VIDEO_MENU, SHOW_OLD_VIDEO_MENU},
{DEPRECATED_VIDEO_QUALITY_DEFAULT_WIFI, VIDEO_QUALITY_DEFAULT_WIFI},
{DEPRECATED_VIDEO_QUALITY_DEFAULT_MOBILE, VIDEO_QUALITY_DEFAULT_MOBILE},
{DEPRECATED_PLAYBACK_SPEED_DEFAULT, PLAYBACK_SPEED_DEFAULT},
{DEPRECATED_AUTO_CAPTIONS, AUTO_CAPTIONS},
{DEPRECATED_PLAYER_POPUP_PANELS, PLAYER_POPUP_PANELS},
{DEPRECATED_SWIPE_BRIGHTNESS, SWIPE_BRIGHTNESS},
{DEPRECATED_SWIPE_VOLUME, SWIPE_VOLUME},
{DEPRECATED_PRESS_TO_SWIPE, SWIPE_PRESS_TO_ENGAGE},
{DEPRECATED_SWIPE_HAPTIC_FEEDBACK, SWIPE_HAPTIC_FEEDBACK},
{DEPRECATED_DEBUG, DEBUG},
{DEPRECATED_DEBUG_STACKTRACE, DEBUG_STACKTRACE},
{DEPRECATED_DEBUG_TOAST_ON_ERROR, DEBUG_TOAST_ON_ERROR},
{DEPRECATED_EXTERNAL_BROWSER, EXTERNAL_BROWSER},
{DEPRECATED_AUTO_REPEAT, AUTO_REPEAT},
{DEPRECATED_TAP_SEEKING, SEEKBAR_TAPPING},
{DEPRECATED_HDR_AUTO_BRIGHTNESS, HDR_AUTO_BRIGHTNESS},
{DEPRECATED_RYD_USER_ID, RYD_USER_ID},
{DEPRECATED_RYD_DISLIKE_PERCENTAGE, RYD_DISLIKE_PERCENTAGE},
{DEPRECATED_RYD_COMPACT_LAYOUT, RYD_COMPACT_LAYOUT},
{DEPRECATED_SB_ENABLED, SB_ENABLED},
{DEPRECATED_SB_VOTING_BUTTON, SB_VOTING_BUTTON},
{DEPRECATED_SB_CREATE_NEW_SEGMENT, SB_CREATE_NEW_SEGMENT},
{DEPRECATED_SB_COMPACT_SKIP_BUTTON, SB_COMPACT_SKIP_BUTTON},
{DEPRECATED_SB_MIN_DURATION, SB_SEGMENT_MIN_DURATION},
{DEPRECATED_SB_VIDEO_LENGTH_WITHOUT_SEGMENTS, SB_VIDEO_LENGTH_WITHOUT_SEGMENTS},
{DEPRECATED_SB_API_URL, SB_API_URL},
{DEPRECATED_SB_TOAST_ON_SKIP, SB_TOAST_ON_SKIP},
{DEPRECATED_SB_AUTO_HIDE_SKIP_BUTTON, SB_AUTO_HIDE_SKIP_BUTTON},
{DEPRECATED_SB_TRACK_SKIP_COUNT, SB_TRACK_SKIP_COUNT},
{DEPRECATED_SB_ADJUST_NEW_SEGMENT_STEP, SB_CREATE_NEW_SEGMENT_STEP},
{DEPRECATED_SB_LAST_VIP_CHECK, SB_LAST_VIP_CHECK},
{DEPRECATED_SB_IS_VIP, SB_USER_IS_VIP},
{DEPRECATED_SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS, SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS},
{DEPRECATED_SB_LOCAL_TIME_SAVED_MILLISECONDS, SB_LOCAL_TIME_SAVED_MILLISECONDS},
};
for (SettingsEnum[] oldNewSetting : renamedSettings) {
SettingsEnum oldSetting = oldNewSetting[0];
SettingsEnum newSetting = oldNewSetting[1];
if (!oldSetting.isSetToDefault()) {
LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value
+ "' from: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.saveValue(oldSetting.value);
oldSetting.saveValue(oldSetting.defaultValue); // reset old value
}
}
//
// TODO end
//
}
private void load() {
@ -315,10 +616,10 @@ public enum SettingsEnum {
/**
* Sets, but does _not_ persistently save the value.
*
* <p>
* This intentionally is a static method, to deter accidental usage
* when {@link #saveValue(Object)} was intended.
*
* <p>
* This method is only to be used by the Settings preference code.
*/
public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) {
@ -343,11 +644,12 @@ public enum SettingsEnum {
throw new IllegalStateException(setting.name());
}
}
/**
* This method is only to be used by the Settings preference code.
*/
public static void setValue(@NonNull SettingsEnum setting, @NonNull Boolean newValue) {
Objects.requireNonNull(newValue);
setting.returnType.validate(newValue);
setting.value = newValue;
}
@ -355,7 +657,8 @@ public enum SettingsEnum {
* Sets the value, and persistently saves it.
*/
public void saveValue(@NonNull Object newValue) {
Objects.requireNonNull(newValue);
returnType.validate(newValue);
value = newValue; // Must set before saving to preferences (otherwise importing fails to update UI correctly).
switch (returnType) {
case BOOLEAN:
sharedPref.saveBoolean(path, (boolean) newValue);
@ -375,12 +678,11 @@ public enum SettingsEnum {
default:
throw new IllegalStateException(name());
}
value = newValue;
}
/**
* @return if this setting can be configured and used.
*
* <p>
* Not to be confused with {@link #getBoolean()}
*/
public boolean isAvailable() {
@ -393,6 +695,13 @@ public enum SettingsEnum {
return false;
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
}
public boolean getBoolean() {
return (Boolean) value;
}
@ -422,11 +731,174 @@ public enum SettingsEnum {
return value;
}
/**
* This could be yet another field,
* for now use a simple switch statement since this method is not used outside this class.
*/
private boolean includeWithImportExport() {
switch (this) {
case RYD_USER_ID: // Not useful to export, no reason to include it.
case SB_LAST_VIP_CHECK:
case SB_HIDE_EXPORT_WARNING:
case SB_SEEN_GUIDELINES:
case SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS:
case SB_LOCAL_TIME_SAVED_MILLISECONDS:
return false;
}
return true;
}
// Begin import / export
/**
* If a setting path has this prefix, then remove it before importing/exporting.
*/
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
/**
* The path, minus any 'revanced' prefix to keep json concise.
*/
private String getImportExportKey() {
if (path.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
return path.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
}
return path;
}
private static SettingsEnum[] valuesSortedForExport() {
SettingsEnum[] sorted = values();
Arrays.sort(sorted, (SettingsEnum o1, SettingsEnum o2) -> {
// Organize SponsorBlock settings last.
final boolean o1IsSb = o1.sharedPref == SPONSOR_BLOCK;
final boolean o2IsSb = o2.sharedPref == SPONSOR_BLOCK;
if (o1IsSb != o2IsSb) {
return o1IsSb ? 1 : -1;
}
return o1.path.compareTo(o2.path);
});
return sorted;
}
@NonNull
public static String exportJSON(@Nullable Context alertDialogContext) {
try {
JSONObject json = new JSONObject();
for (SettingsEnum setting : valuesSortedForExport()) {
String importExportKey = setting.getImportExportKey();
if (json.has(importExportKey)) {
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
}
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
if (setting.includeWithImportExport() && (!setting.isSetToDefault() | exportDefaultValues)) {
json.put(importExportKey, setting.getObjectValue());
}
}
SponsorBlockSettings.exportCategoriesToFlatJson(alertDialogContext, json);
if (json.length() == 0) {
return "";
}
String export = json.toString(0);
// Remove the outer JSON braces to make the output more compact,
// and leave less chance of the user forgetting to copy it
return export.substring(2, export.length() - 2);
} catch (JSONException e) {
LogHelper.printException(() -> "Export failure", e); // should never happen
return "";
}
}
/**
* @return if any settings that require a reboot were changed.
*/
public static boolean importJSON(@NonNull String settingsJsonString) {
try {
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
}
JSONObject json = new JSONObject(settingsJsonString);
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
for (SettingsEnum setting : values()) {
String key = setting.getImportExportKey();
if (json.has(key)) {
Object value;
switch (setting.returnType) {
case BOOLEAN:
value = json.getBoolean(key);
break;
case INTEGER:
value = json.getInt(key);
break;
case LONG:
value = json.getLong(key);
break;
case FLOAT:
value = (float) json.getDouble(key);
break;
case STRING:
value = json.getString(key);
break;
default:
throw new IllegalStateException();
}
if (!setting.getObjectValue().equals(value)) {
rebootSettingChanged |= setting.rebootApp;
setting.saveValue(value);
}
numberOfSettingsImported++;
} else if (setting.includeWithImportExport() && !setting.isSetToDefault()) {
LogHelper.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp;
setting.saveValue(setting.defaultValue);
}
}
numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json);
ReVancedUtils.showToastLong(numberOfSettingsImported == 0
? str("revanced_settings_import_reset")
: str("revanced_settings_import_success", numberOfSettingsImported));
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
ReVancedUtils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
LogHelper.printInfo(() -> "", ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
}
return false;
}
// End import / export
public enum ReturnType {
BOOLEAN,
INTEGER,
STRING,
LONG,
FLOAT,
STRING;
public void validate(@Nullable Object obj) throws IllegalArgumentException {
if (!matches(obj)) {
throw new IllegalArgumentException("'" + obj + "' does not match:" + this);
}
}
public boolean matches(@Nullable Object obj) {
switch (this) {
case BOOLEAN:
return obj instanceof Boolean;
case INTEGER:
return obj instanceof Integer;
case LONG:
return obj instanceof Long;
case FLOAT:
return obj instanceof Float;
case STRING:
return obj instanceof String;
}
return false;
}
}
}
}

View File

@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/**
@ -35,6 +36,11 @@ public enum SharedPrefCategory {
preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE);
}
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
LogHelper.printException(() -> "Found conflicting preference: " + key);
preferences.edit().remove(key).apply();
}
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
}
@ -91,7 +97,14 @@ public enum SharedPrefCategory {
}
return _default;
} catch (ClassCastException ex) {
return preferences.getInt(key, _default); // old data, previously stored as primitive
try {
// Old data previously stored as primitive.
return preferences.getInt(key, _default);
} catch (ClassCastException ex2) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
}
@ -104,7 +117,12 @@ public enum SharedPrefCategory {
}
return _default;
} catch (ClassCastException ex) {
return preferences.getLong(key, _default);
try {
return preferences.getLong(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
}
@ -117,7 +135,12 @@ public enum SharedPrefCategory {
}
return _default;
} catch (ClassCastException ex) {
return preferences.getFloat(key, _default);
try {
return preferences.getFloat(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
}

View File

@ -0,0 +1,97 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Build;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
private void init() {
setSelectable(true);
EditText editText = getEditText();
editText.setTextIsSelectable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
editText.setAutofillHints((String) null);
}
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
setOnPreferenceClickListener(this);
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ImportExportPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = SettingsEnum.exportJSON(getContext());
getEditText().setText(existingSettings);
} catch (Exception ex) {
LogHelper.printException(() -> "showDialog failure", ex);
}
return true;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
ReVancedUtils.setClipboard(getEditText().getText().toString());
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
importSettings(getEditText().getText().toString());
});
} catch (Exception ex) {
LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
private void importSettings(String replacementSettings) {
try {
if (replacementSettings.equals(existingSettings)) {
return;
}
ReVancedSettingsFragment.settingImportInProgress = true;
final boolean rebootNeeded = SettingsEnum.importJSON(replacementSettings);
if (rebootNeeded) {
ReVancedSettingsFragment.showRebootDialog(getContext());
}
} catch (Exception ex) {
LogHelper.printException(() -> "importSettings failure", ex);
} finally {
ReVancedSettingsFragment.settingImportInProgress = false;
}
}
}

View File

@ -1,17 +1,18 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.ReVancedUtils.getChildView;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.preference.PreferenceFragment;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.google.android.libraries.social.licenses.LicenseActivity;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.ThemeHelper;
public class ReVancedSettingActivity {
@ -19,76 +20,68 @@ public class ReVancedSettingActivity {
/**
* Injection point.
*/
public static void setTheme(LicenseActivity base) {
final var whiteTheme = "Theme.YouTube.Settings";
final var darkTheme = "Theme.YouTube.Settings.Dark";
final var theme = ThemeHelper.isDarkTheme() ? darkTheme : whiteTheme;
LogHelper.printDebug(() -> "Using theme: " + theme);
base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style"));
}
/**
* Injection point.
*/
public static void initializeSettings(LicenseActivity base) {
base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout"));
PreferenceFragment preferenceFragment;
String preferenceIdentifier;
String dataString = base.getIntent().getDataString();
if (dataString.equalsIgnoreCase("sponsorblock_settings")) {
preferenceIdentifier = "sb_settings";
preferenceFragment = new SponsorBlockSettingsFragment();
} else if (dataString.equalsIgnoreCase("ryd_settings")) {
preferenceIdentifier = "revanced_ryd_settings_title";
preferenceFragment = new ReturnYouTubeDislikeSettingsFragment();
} else {
preferenceIdentifier = "revanced_settings";
preferenceFragment = new ReVancedSettingsFragment();
}
public static void initializeSettings(Activity licenseActivity) {
try {
TextView toolbar = getTextView((ViewGroup) base.findViewById(ReVancedUtils.getResourceIdentifier("toolbar", "id")));
if (toolbar == null) {
// FIXME
// https://github.com/revanced/revanced-patches/issues/1384
LogHelper.printDebug(() -> "Could not find toolbar");
} else {
toolbar.setText(preferenceIdentifier);
ThemeHelper.setActivityTheme(licenseActivity);
licenseActivity.setContentView(
getResourceIdentifier("revanced_settings_with_toolbar", "layout"));
setBackButton(licenseActivity);
PreferenceFragment fragment;
String toolbarTitleResourceName;
String dataString = licenseActivity.getIntent().getDataString();
switch (dataString) {
case "sponsorblock_settings":
toolbarTitleResourceName = "revanced_sponsorblock_settings_title";
fragment = new SponsorBlockSettingsFragment();
break;
case "ryd_settings":
toolbarTitleResourceName = "revanced_ryd_settings_title";
fragment = new ReturnYouTubeDislikeSettingsFragment();
break;
case "revanced_settings":
toolbarTitleResourceName = "revanced_settings_title";
fragment = new ReVancedSettingsFragment();
break;
default:
LogHelper.printException(() -> "Unknown setting: " + dataString);
return;
}
} catch (Exception e) {
LogHelper.printException(() -> "Could not set Toolbar title", e);
setToolbarTitle(licenseActivity, toolbarTitleResourceName);
licenseActivity.getFragmentManager()
.beginTransaction()
.replace(getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
.commit();
} catch (Exception ex) {
LogHelper.printException(() -> "onCreate failure", ex);
}
base.getFragmentManager().beginTransaction().replace(ReVancedUtils.getResourceIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit();
}
private static void setToolbarTitle(Activity activity, String toolbarTitleResourceName) {
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof TextView));
toolbarTextView.setText(getResourceIdentifier(toolbarTitleResourceName, "string"));
}
@Nullable
public static <T extends View> T getView(Class<T> typeClass, ViewGroup viewGroup) {
if (viewGroup == null) {
return null;
@SuppressLint("UseCompatLoadingForDrawables")
private static void setBackButton(Activity activity) {
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof ImageButton));
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_arrow_left_white_24"
: "yt_outline_arrow_left_black_24",
"drawable");
imageButton.setImageDrawable(activity.getResources().getDrawable(backButtonResource));
imageButton.setOnClickListener(view -> activity.onBackPressed());
}
private static int getToolbarResourceId() {
final int toolbarResourceId = getResourceIdentifier("revanced_toolbar", "id");
if (toolbarResourceId == 0) {
throw new IllegalStateException("Could not find back button resource");
}
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
if (childAt.getClass() == typeClass) {
return (T) childAt;
}
}
return null;
return toolbarResourceId;
}
@Nullable
public static ImageButton getImageButton(ViewGroup viewGroup) {
return getView(ImageButton.class, viewGroup);
}
@Nullable
public static TextView getTextView(ViewGroup viewGroup) {
return getView(TextView.class, viewGroup);
}
}

View File

@ -24,13 +24,40 @@ import androidx.annotation.Nullable;
import com.google.android.apps.youtube.app.application.Shell_HomeActivity;
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
import app.revanced.integrations.patches.playback.speed.CustomVideoSpeedPatch;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.settings.SharedPrefCategory;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class ReVancedSettingsFragment extends PreferenceFragment {
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
*/
static boolean settingImportInProgress;
private static void reboot(@NonNull Context activity) {
final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent intent = PendingIntent.getActivity(activity, 0,
new Intent(activity, Shell_HomeActivity.class), intentFlags);
AlarmManager systemService = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE);
systemService.setExact(AlarmManager.ELAPSED_REALTIME, 1500L, intent);
Process.killProcess(Process.myPid());
}
static void showRebootDialog(@NonNull Context activity) {
String positiveButton = str("in_app_update_restart_button");
String negativeButton = str("sign_in_cancel");
new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config"))
.setPositiveButton(positiveButton, (dialog, id) -> {
reboot(activity);
})
.setNegativeButton(negativeButton, null)
.setCancelable(false)
.show();
}
/**
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
*/
@ -42,33 +69,53 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
if (setting == null) {
return;
}
Preference pref = this.findPreference(str);
LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref);
Preference pref = findPreference(str);
LogHelper.printDebug(() -> setting.name() + ": " + " setting value:" + setting.getObjectValue() + " pref:" + pref);
if (pref == null) {
return;
}
if (pref instanceof SwitchPreference) {
SwitchPreference switchPref = (SwitchPreference) pref;
SettingsEnum.setValue(setting, switchPref.isChecked());
if (settingImportInProgress) {
switchPref.setChecked(setting.getBoolean());
} else {
SettingsEnum.setValue(setting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference) {
String editText = ((EditTextPreference) pref).getText();
SettingsEnum.setValue(setting, editText);
EditTextPreference editPreference = (EditTextPreference) pref;
if (settingImportInProgress) {
editPreference.getEditText().setText(setting.getObjectValue().toString());
} else {
SettingsEnum.setValue(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
SettingsEnum.setValue(setting, listPref.getValue());
if (settingImportInProgress) {
listPref.setValue(setting.getObjectValue().toString());
} else {
SettingsEnum.setValue(setting, listPref.getValue());
}
updateListPreferenceSummary((ListPreference) pref, setting);
} else {
LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref);
return;
}
enableDisablePreferences();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting);
} else if (setting.rebootApp) {
rebootDialog(getActivity());
showRebootDialog(getActivity());
}
}
enableDisablePreferences();
} catch (Exception ex) {
LogHelper.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
@ -88,20 +135,24 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
// if the preference was included, then initialize it based on the available playback speed
Preference defaultSpeedPreference = findPreference(SettingsEnum.PLAYBACK_SPEED_DEFAULT.path);
if (defaultSpeedPreference instanceof ListPreference) {
RememberPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
CustomVideoSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
}
// set the summary text for any ListPreferences
// Set current value from SettingsEnum
for (SettingsEnum setting : SettingsEnum.values()) {
Preference preference = findPreference(setting.path);
if (preference instanceof ListPreference) {
if (preference instanceof SwitchPreference) {
((SwitchPreference) preference).setChecked(setting.getBoolean());
} else if (preference instanceof EditTextPreference) {
((EditTextPreference) preference).setText(setting.getObjectValue().toString());
} else if (preference instanceof ListPreference) {
updateListPreferenceSummary((ListPreference) preference, setting);
}
}
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
} catch (Exception ex) {
LogHelper.printException(() -> "onActivityCreated() error", ex);
LogHelper.printException(() -> "onActivityCreated() failure", ex);
}
}
@ -120,34 +171,18 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
}
}
/**
* Sets summary text to the currently selected list option.
*/
private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) {
final int entryIndex = listPreference.findIndexOfValue(setting.getObjectValue().toString());
String objectStringValue = setting.getObjectValue().toString();
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
if (entryIndex >= 0) {
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
listPreference.setValue(objectStringValue);
}
}
private void reboot(@NonNull Activity activity) {
final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent intent = PendingIntent.getActivity(activity, 0,
new Intent(activity, Shell_HomeActivity.class), intentFlags);
AlarmManager systemService = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE);
systemService.setExact(AlarmManager.ELAPSED_REALTIME, 1500L, intent);
Process.killProcess(Process.myPid());
}
private void rebootDialog(@NonNull Activity activity) {
String positiveButton = str("in_app_update_restart_button");
String negativeButton = str("sign_in_cancel");
new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config"))
.setPositiveButton(positiveButton, (dialog, id) -> {
reboot(activity);
})
.setNegativeButton(negativeButton, null)
.setCancelable(false)
.show();
}
private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) {
showingUserDialogMessage = true;
new AlertDialog.Builder(activity)
@ -155,7 +190,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
rebootDialog(activity);
showRebootDialog(activity);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {

View File

@ -0,0 +1,63 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
public class ResettableEditTextPreference extends EditTextPreference {
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResettableEditTextPreference(Context context) {
super(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
SettingsEnum setting = SettingsEnum.settingFromPath(getKey());
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// Override the button click listener to prevent dismissing the dialog.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
if (button == null) {
return;
}
button.setOnClickListener(v -> {
try {
SettingsEnum setting = Objects.requireNonNull(SettingsEnum.settingFromPath(getKey()));
String defaultStringValue = setting.defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
} catch (Exception ex) {
LogHelper.printException(() -> "reset failure", ex);
}
});
}
}

View File

@ -19,6 +19,11 @@ import app.revanced.integrations.settings.SharedPrefCategory;
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
/**
* If dislikes are shown on Shorts.
*/
private SwitchPreference shortsPreference;
/**
* If dislikes are shown as percentage.
*/
@ -29,9 +34,16 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
*/
private SwitchPreference compactLayoutPreference;
/**
* If segmented like/dislike button uses smaller compact layout.
*/
private SwitchPreference toastOnRYDNotAvailable;
private void updateUIState() {
percentagePreference.setEnabled(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.isAvailable());
compactLayoutPreference.setEnabled(SettingsEnum.RYD_USE_COMPACT_LAYOUT.isAvailable());
shortsPreference.setEnabled(SettingsEnum.RYD_SHORTS.isAvailable());
percentagePreference.setEnabled(SettingsEnum.RYD_DISLIKE_PERCENTAGE.isAvailable());
compactLayoutPreference.setEnabled(SettingsEnum.RYD_COMPACT_LAYOUT.isAvailable());
toastOnRYDNotAvailable.setEnabled(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable());
}
@Override
@ -58,13 +70,25 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
});
preferenceScreen.addPreference(enabledPreference);
shortsPreference = new SwitchPreference(context);
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
shortsPreference.setSummaryOn(str("revanced_ryd_shorts_summary_on"));
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_SHORTS.saveValue(newValue);
updateUIState();
return true;
});
preferenceScreen.addPreference(shortsPreference);
percentagePreference = new SwitchPreference(context);
percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean());
percentagePreference.setChecked(SettingsEnum.RYD_DISLIKE_PERCENTAGE.getBoolean());
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on"));
percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off"));
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue(newValue);
SettingsEnum.RYD_DISLIKE_PERCENTAGE.saveValue(newValue);
ReturnYouTubeDislike.clearCache();
updateUIState();
return true;
@ -72,18 +96,30 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
preferenceScreen.addPreference(percentagePreference);
compactLayoutPreference = new SwitchPreference(context);
compactLayoutPreference.setChecked(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean());
compactLayoutPreference.setChecked(SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean());
compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on"));
compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off"));
compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue(newValue);
SettingsEnum.RYD_COMPACT_LAYOUT.saveValue(newValue);
ReturnYouTubeDislike.clearCache();
updateUIState();
return true;
});
preferenceScreen.addPreference(compactLayoutPreference);
toastOnRYDNotAvailable = new SwitchPreference(context);
toastOnRYDNotAvailable.setChecked(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean());
toastOnRYDNotAvailable.setTitle(str("ryd_toast_on_connection_error_title"));
toastOnRYDNotAvailable.setSummaryOn(str("ryd_toast_on_connection_error_summary_on"));
toastOnRYDNotAvailable.setSummaryOff(str("ryd_toast_on_connection_error_summary_off"));
toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.saveValue(newValue);
updateUIState();
return true;
});
preferenceScreen.addPreference(toastOnRYDNotAvailable);
updateUIState();

View File

@ -20,6 +20,7 @@ import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.text.Html;
import android.text.InputType;
import android.util.TypedValue;
import android.widget.EditText;
import androidx.annotation.NonNull;
@ -51,6 +52,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
private SwitchPreference showSkipToast;
private SwitchPreference trackSkips;
private SwitchPreference showTimeWithoutSegments;
private SwitchPreference toastOnConnectionError;
private EditTextPreference newSegmentStep;
private EditTextPreference minSegmentDuration;
@ -67,41 +69,44 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
if (!enabled) {
SponsorBlockViewController.hideAll();
SegmentPlaybackController.setCurrentVideoId(null);
} else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) {
} else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean()) {
SponsorBlockViewController.hideNewSegmentLayout();
}
// voting and add new segment buttons automatically shows/hides themselves
sbEnabled.setChecked(enabled);
addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean());
addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean());
addNewSegment.setEnabled(enabled);
votingEnabled.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean());
votingEnabled.setChecked(SettingsEnum.SB_VOTING_BUTTON.getBoolean());
votingEnabled.setEnabled(enabled);
compactSkipButton.setChecked(SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.getBoolean());
compactSkipButton.setChecked(SettingsEnum.SB_COMPACT_SKIP_BUTTON.getBoolean());
compactSkipButton.setEnabled(enabled);
autoHideSkipSegmentButton.setChecked(SettingsEnum.SB_AUTO_HIDE_SKIP_BUTTON.getBoolean());
autoHideSkipSegmentButton.setEnabled(enabled);
showSkipToast.setChecked(SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean());
showSkipToast.setChecked(SettingsEnum.SB_TOAST_ON_SKIP.getBoolean());
showSkipToast.setEnabled(enabled);
toastOnConnectionError.setChecked(SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.getBoolean());
toastOnConnectionError.setEnabled(enabled);
trackSkips.setChecked(SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean());
trackSkips.setEnabled(enabled);
showTimeWithoutSegments.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
showTimeWithoutSegments.setChecked(SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean());
showTimeWithoutSegments.setEnabled(enabled);
newSegmentStep.setText(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getObjectValue().toString());
newSegmentStep.setText(SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getObjectValue().toString());
newSegmentStep.setEnabled(enabled);
minSegmentDuration.setText(SettingsEnum.SB_MIN_DURATION.getObjectValue().toString());
minSegmentDuration.setText(SettingsEnum.SB_SEGMENT_MIN_DURATION.getObjectValue().toString());
minSegmentDuration.setEnabled(enabled);
privateUserId.setText(SettingsEnum.SB_UUID.getString());
privateUserId.setText(SettingsEnum.SB_PRIVATE_USER_ID.getString());
privateUserId.setEnabled(enabled);
apiUrl.setEnabled(enabled);
@ -171,7 +176,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
votingEnabled.setSummaryOff(str("sb_enable_voting_sum_off"));
category.addPreference(votingEnabled);
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_VOTING_ENABLED.saveValue(newValue);
SettingsEnum.SB_VOTING_BUTTON.saveValue(newValue);
updateUI();
return true;
});
@ -182,7 +187,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
compactSkipButton.setSummaryOff(str("sb_enable_compact_skip_button_sum_off"));
category.addPreference(compactSkipButton);
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.saveValue(newValue);
SettingsEnum.SB_COMPACT_SKIP_BUTTON.saveValue(newValue);
updateUI();
return true;
});
@ -207,7 +212,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
return false;
});
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(newValue);
SettingsEnum.SB_TOAST_ON_SKIP.saveValue(newValue);
updateUI();
return true;
});
@ -218,7 +223,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
showTimeWithoutSegments.setSummaryOn(str("sb_general_time_without_sum_on"));
showTimeWithoutSegments.setSummaryOff(str("sb_general_time_without_sum_off"));
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue);
SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.saveValue(newValue);
updateUI();
return true;
});
@ -247,7 +252,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
.setCancelable(false)
.show();
}
SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue);
SettingsEnum.SB_CREATE_NEW_SEGMENT.saveValue(newValue);
updateUI();
return true;
});
@ -262,7 +267,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
ReVancedUtils.showToastLong(str("sb_general_adjusting_invalid"));
return false;
}
SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue);
SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue);
return true;
});
category.addPreference(newSegmentStep);
@ -282,6 +287,17 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
screen.addPreference(category);
category.setTitle(str("sb_general"));
toastOnConnectionError = new SwitchPreference(context);
toastOnConnectionError.setTitle(str("sb_toast_on_connection_error_title"));
toastOnConnectionError.setSummaryOn(str("sb_toast_on_connection_error_summary_on"));
toastOnConnectionError.setSummaryOff(str("sb_toast_on_connection_error_summary_off"));
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.saveValue(newValue);
updateUI();
return true;
});
category.addPreference(toastOnConnectionError);
trackSkips = new SwitchPreference(context);
trackSkips.setTitle(str("sb_general_skipcount"));
trackSkips.setSummaryOn(str("sb_general_skipcount_sum_on"));
@ -298,7 +314,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
minSegmentDuration.setSummary(str("sb_general_min_duration_sum"));
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString()));
SettingsEnum.SB_SEGMENT_MIN_DURATION.saveValue(Float.valueOf(newValue.toString()));
return true;
});
category.addPreference(minSegmentDuration);
@ -312,7 +328,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
ReVancedUtils.showToastLong(str("sb_general_uuid_invalid"));
return false;
}
SettingsEnum.SB_UUID.saveValue(newUUID);
SettingsEnum.SB_PRIVATE_USER_ID.saveValue(newUUID);
fetchAndDisplayStats();
return true;
});
@ -351,9 +367,22 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
});
category.addPreference(apiUrl);
importExport = new EditTextPreference(context);
importExport = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setNeutralButton(str("sb_settings_copy"), (dialog, which) -> {
ReVancedUtils.setClipboard(getEditText().getText().toString());
});
}
};
importExport.setTitle(str("sb_settings_ie"));
importExport.setSummary(str("sb_settings_ie_sum"));
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importExport.getEditText().setAutofillHints((String) null);
}
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
importExport.setOnPreferenceClickListener(preference1 -> {
importExport.getEditText().setText(SponsorBlockSettings.exportSettings());
return true;
@ -419,6 +448,12 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
private void fetchAndDisplayStats() {
try {
statsCategory.removeAll();
if (!SponsorBlockSettings.userHasSBPrivateId()) {
// User has never voted or created any segments. No stats to show.
addLocalUserStats();
return;
}
Preference loadingPlaceholderPreference = new Preference(this.getActivity());
loadingPlaceholderPreference.setEnabled(false);
statsCategory.addPreference(loadingPlaceholderPreference);
@ -428,6 +463,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
UserStats stats = SBRequester.retrieveUserStats();
ReVancedUtils.runOnMainThread(() -> { // get back on main thread to modify UI elements
addUserStats(loadingPlaceholderPreference, stats);
addLocalUserStats();
});
});
} else {
@ -450,7 +486,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
statsCategory.removeAll();
Context context = statsCategory.getContext();
{
if (stats.totalSegmentCountIncludingIgnored > 0) {
// If user has not created any segments, there's no reason to set a username.
EditTextPreference preference = new EditTextPreference(context);
statsCategory.addPreference(preference);
String userName = stats.userName;
@ -482,7 +519,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
statsCategory.addPreference(preference);
String formatted = statsNumberOfSegmentsSkippedFormatter.format(stats.segmentCount);
preference.setTitle(fromHtml(str("sb_stats_submissions", formatted)));
if (stats.segmentCount == 0) {
if (stats.totalSegmentCountIncludingIgnored == 0) {
preference.setSelectable(false);
} else {
preference.setOnPreferenceClickListener(preference1 -> {
@ -512,7 +549,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
String stats_saved;
String stats_saved_sum;
if (stats.segmentCount == 0) {
if (stats.totalSegmentCountIncludingIgnored == 0) {
stats_saved = str("sb_stats_saved_zero");
stats_saved_sum = str("sb_stats_saved_sum_zero");
} else {
@ -528,34 +565,34 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment {
return false;
});
}
{
// time the user saved by using SB
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
Runnable updateStatsSelfSaved = () -> {
String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt());
preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted)));
String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / 1000);
preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved)));
};
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.defaultValue);
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.defaultValue);
updateStatsSelfSaved.run();
})
.setNegativeButton(android.R.string.no, null).show();
return true;
});
}
} catch (Exception ex) {
LogHelper.printException(() -> "fetchAndDisplayStats failure", ex);
LogHelper.printException(() -> "addUserStats failure", ex);
}
}
private void addLocalUserStats() {
// time the user saved by using SB
Preference preference = new Preference(statsCategory.getContext());
statsCategory.addPreference(preference);
Runnable updateStatsSelfSaved = () -> {
String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt());
preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted)));
String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() / 1000);
preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved)));
};
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.defaultValue);
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.defaultValue);
updateStatsSelfSaved.run();
})
.setNegativeButton(android.R.string.no, null).show();
return true;
});
}
}

View File

@ -1,44 +1,63 @@
package app.revanced.integrations.shared
import app.revanced.integrations.utils.Event
import app.revanced.integrations.utils.LogHelper
/**
* WatchWhile player type
*/
@Suppress("unused")
enum class PlayerType {
NONE, // includes Shorts and Stories playback
HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened
/**
* Includes Shorts and Stories playback.
*/
NONE,
/**
* A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened.
*/
HIDDEN,
/**
* When spoofing to an old version of YouTube, and watching a short with a regular video in the background,
* the type will be this (and not [HIDDEN]).
*/
WATCH_WHILE_MINIMIZED,
WATCH_WHILE_MAXIMIZED,
WATCH_WHILE_FULLSCREEN,
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
/**
* When opening a short while a regular video is minimized, the type can momentarily be this.
*/
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
INLINE_MINIMAL, // home feed video playback
/**
* Home feed video playback.
*/
INLINE_MINIMAL,
VIRTUAL_REALITY_FULLSCREEN,
WATCH_WHILE_PICTURE_IN_PICTURE;
companion object {
/**
* safely parse from a string
*
* @param name the name to find
* @return the enum constant, or null if not found
*/
private val nameToPlayerType = values().associateBy { it.name }
@JvmStatic
fun safeParseFromString(name: String): PlayerType? {
return values().firstOrNull { it.name == name }
fun setFromString(enumName: String) {
val newType = nameToPlayerType[enumName]
if (newType == null) {
LogHelper.printException { "Unknown PlayerType encountered: $enumName" }
} else if (current != newType) {
LogHelper.printDebug { "PlayerType changed to: $newType" }
current = newType
}
}
/**
* the current player type, as reported by [app.revanced.integrations.patches.PlayerTypeHookPatch.YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX]
* The current player type.
*/
@JvmStatic
var current
get() = currentPlayerType
set(value) {
private set(value) {
currentPlayerType = value
onChange(currentPlayerType)
}
@ -53,11 +72,30 @@ enum class PlayerType {
}
/**
* Check if the current player type is [NONE] or [HIDDEN]
* Check if the current player type is [NONE] or [HIDDEN].
* Useful to check if a short is currently playing.
*
* @return True, if nothing, a Short, or a Story is playing.
* Does not include the first moment after a short is opened when a regular video is minimized on screen,
* or while watching a short with a regular video present on a spoofed old version of YouTube.
* To include those situations instead use [isNoneHiddenOrMinimized].
*/
fun isNoneOrHidden(): Boolean {
return this == NONE || this == HIDDEN
}
/**
* Check if the current player type is [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED].
*
* Useful to check if a Short is being played,
* although can return false positive if the player is minimized.
*
* @return If nothing, a Short, a Story,
* or a regular video is minimized video or sliding off screen to a dismissed or hidden state.
*/
fun isNoneHiddenOrMinimized(): Boolean {
return this == NONE || this == HIDDEN
|| this == WATCH_WHILE_MINIMIZED
|| this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED
}
}

View File

@ -0,0 +1,48 @@
package app.revanced.integrations.shared
import app.revanced.integrations.utils.LogHelper
import app.revanced.integrations.patches.VideoInformation
/**
* VideoState playback state.
*/
enum class VideoState {
NEW,
PLAYING,
PAUSED,
RECOVERABLE_ERROR,
UNRECOVERABLE_ERROR,
/**
* @see [VideoInformation.isAtEndOfVideo]
*/
ENDED;
companion object {
private val nameToVideoState = values().associateBy { it.name }
@JvmStatic
fun setFromString(enumName: String) {
val state = nameToVideoState[enumName]
if (state == null) {
LogHelper.printException { "Unknown VideoState encountered: $enumName" }
} else if (currentVideoState != state) {
LogHelper.printDebug { "VideoState changed to: $state" }
currentVideoState = state
}
}
/**
* Depending on which hook this is called from,
* this value may not be up to date with the actual playback state.
*/
@JvmStatic
var current: VideoState?
get() = currentVideoState
private set(value) {
currentVideoState = value
}
private var currentVideoState : VideoState? = null
}
}

View File

@ -1,25 +1,15 @@
package app.revanced.integrations.sponsorblock;
import static app.revanced.integrations.utils.StringRef.str;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.shared.VideoState;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
@ -28,6 +18,11 @@ import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import java.lang.reflect.Field;
import java.util.*;
import static app.revanced.integrations.utils.StringRef.str;
/**
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
*
@ -38,12 +33,12 @@ public class SegmentPlaybackController {
* Length of time to show a skip button for a highlight segment,
* or a regular segment if {@link SettingsEnum#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
*
* Because Effectively, this value is rounded up to the next second.
* Effectively this value is rounded up to the next second.
*/
private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800;
/*
* Highlight segments have zero length, as they are a point in time.
* Highlight segments have zero length as they are a point in time.
* Draw them on screen using a fixed width bar.
* Value is independent of device dpi.
*/
@ -102,9 +97,9 @@ public class SegmentPlaybackController {
@Nullable
private static String timeWithoutSegments;
private static float sponsorBarLeft = 1f;
private static float sponsorBarRight = 1f;
private static float sponsorBarThickness = 2f;
private static int sponsorBarAbsoluteLeft;
private static int sponsorAbsoluteBarRight;
private static int sponsorBarThickness;
@Nullable
static SponsorSegment[] getSegments() {
@ -177,7 +172,7 @@ public class SegmentPlaybackController {
* Injection point.
* Initializes SponsorBlock when the video player starts playing a new video.
*/
public static void initialize(Object _o) {
public static void initialize(Object ignoredPlayerController) {
try {
ReVancedUtils.verifyOnMainThread();
SponsorBlockSettings.initialize();
@ -235,7 +230,7 @@ public class SegmentPlaybackController {
SponsorSegment[] segments = SBRequester.getSegments(videoId);
ReVancedUtils.runOnMainThread(()-> {
if (!videoId.equals(SegmentPlaybackController.currentVideoId)) {
if (!videoId.equals(currentVideoId)) {
// user changed videos before get segments network call could complete
LogHelper.printDebug(() -> "Ignoring segments for prior video: " + videoId);
return;
@ -522,17 +517,21 @@ public class SegmentPlaybackController {
return;
}
final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED;
if (!userManuallySkipped) {
// check for any smaller embedded segments, and count those as autoskipped
final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean();
for (final SponsorSegment otherSegment : segments) {
final boolean showSkipToast = SettingsEnum.SB_TOAST_ON_SKIP.getBoolean();
for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
if (segmentToSkip.end < otherSegment.start) {
break; // no other segments can be contained
}
if (otherSegment == segmentToSkip ||
(otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) {
otherSegment.didAutoSkipped = true;
if (showSkipToast) {
// Do not show a toast if the user is scrubbing thru a paused video.
// Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date.
// So instead, only hide toasts because all other skip logic done while paused causes no harm.
if (showSkipToast && !videoIsPaused) {
showSkippedSegmentToast(otherSegment);
}
}
@ -542,7 +541,7 @@ public class SegmentPlaybackController {
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
removeUnsubmittedSegments();
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
} else {
} else if (!videoIsPaused) {
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
}
} catch (Exception ex) {
@ -599,20 +598,6 @@ public class SegmentPlaybackController {
}
}
/**
* Injection point.
*/
public static void setSponsorBarAbsoluteLeft(final Rect rect) {
setSponsorBarAbsoluteLeft(rect.left);
}
public static void setSponsorBarAbsoluteLeft(final float left) {
if (sponsorBarLeft != left) {
LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteLeft: left=%.2f", left));
sponsorBarLeft = left;
}
}
/**
* Injection point
*/
@ -620,42 +605,36 @@ public class SegmentPlaybackController {
try {
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
field.setAccessible(true);
Rect rect = (Rect) field.get(self);
if (rect == null) {
LogHelper.printException(() -> "Could not find sponsorblock rect");
} else {
setSponsorBarAbsoluteLeft(rect.left);
setSponsorBarAbsoluteRight(rect.right);
}
Rect rect = (Rect) Objects.requireNonNull(field.get(self));
setSponsorBarAbsoluteLeft(rect);
setSponsorBarAbsoluteRight(rect);
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarRect failure", ex);
}
}
/**
* Injection point.
*/
public static void setSponsorBarAbsoluteRight(final Rect rect) {
setSponsorBarAbsoluteRight(rect.right);
private static void setSponsorBarAbsoluteLeft(Rect rect) {
final int left = rect.left;
if (sponsorBarAbsoluteLeft != left) {
LogHelper.printDebug(() -> "setSponsorBarAbsoluteLeft: " + left);
sponsorBarAbsoluteLeft = left;
}
}
public static void setSponsorBarAbsoluteRight(final float right) {
if (sponsorBarRight != right) {
LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteRight: right=%.2f", right));
sponsorBarRight = right;
private static void setSponsorBarAbsoluteRight(Rect rect) {
final int right = rect.right;
if (sponsorAbsoluteBarRight != right) {
LogHelper.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
sponsorAbsoluteBarRight = right;
}
}
/**
* Injection point
*/
public static void setSponsorBarThickness(final int thickness) {
setSponsorBarThickness((float) thickness);
}
public static void setSponsorBarThickness(final float thickness) {
public static void setSponsorBarThickness(int thickness) {
if (sponsorBarThickness != thickness) {
LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness));
LogHelper.printDebug(() -> "setSponsorBarThickness: " + thickness);
sponsorBarThickness = thickness;
}
}
@ -665,7 +644,7 @@ public class SegmentPlaybackController {
*/
public static String appendTimeWithoutSegments(String totalTime) {
try {
if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()
if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean()
&& !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) {
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
return "\u202D" + totalTime + timeWithoutSegments; // u202D = left to right override
@ -679,7 +658,7 @@ public class SegmentPlaybackController {
private static void calculateTimeWithoutSegments() {
final long currentVideoLength = VideoInformation.getVideoLength();
if (!SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0
if (!SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0
|| segments == null || segments.length == 0) {
timeWithoutSegments = null;
return;
@ -736,25 +715,23 @@ public class SegmentPlaybackController {
*/
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
try {
if (sponsorBarThickness < 0.1) return;
if (segments == null) return;
final long videoLength = VideoInformation.getVideoLength();
if (videoLength <= 0) return;
final float thicknessDiv2 = sponsorBarThickness / 2;
final float top = posY - thicknessDiv2;
final int thicknessDiv2 = sponsorBarThickness / 2; // rounds down
final float top = posY - (sponsorBarThickness - thicknessDiv2);
final float bottom = posY + thicknessDiv2;
final float absoluteLeft = sponsorBarLeft;
final float absoluteRight = sponsorBarRight;
final float videoMillisecondsToPixels = (1f / videoLength) * (sponsorAbsoluteBarRight - sponsorBarAbsoluteLeft);
final float leftPadding = sponsorBarAbsoluteLeft;
final float tmp1 = (1f / videoLength) * (absoluteRight - absoluteLeft);
for (SponsorSegment segment : segments) {
final float left = segment.start * tmp1 + absoluteLeft;
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
final float right;
if (segment.category == SegmentCategory.HIGHLIGHT) {
right = left + getHighlightSegmentTimeBarScreenWidth();
} else {
right = segment.end * tmp1 + absoluteLeft;
right = leftPadding + segment.end * videoMillisecondsToPixels;
}
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}

View File

@ -2,10 +2,13 @@ package app.revanced.integrations.sponsorblock;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Patterns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
@ -21,6 +24,10 @@ import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class SponsorBlockSettings {
/**
* Minimum length a SB user id must be, as set by SB API.
*/
private static final int SB_PRIVATE_USER_ID_MINIMUM_LENGTH = 30;
public static void importSettings(@NonNull String json) {
ReVancedUtils.verifyOnMainThread();
@ -66,43 +73,43 @@ public class SponsorBlockSettings {
}
editor.apply();
String userID = settingsJson.getString("userID");
if (!isValidSBUserId(userID)) {
throw new IllegalArgumentException("userId is blank");
if (settingsJson.has("userID")) {
// User id does not exist if user never voted or created any segments.
String userID = settingsJson.getString("userID");
if (isValidSBUserId(userID)) {
SettingsEnum.SB_PRIVATE_USER_ID.saveValue(userID);
}
}
SettingsEnum.SB_UUID.saveValue(userID);
SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip"));
SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice"));
SettingsEnum.SB_USER_IS_VIP.saveValue(settingsJson.getBoolean("isVip"));
SettingsEnum.SB_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice"));
SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(settingsJson.getBoolean("trackViewCount"));
SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
String serverAddress = settingsJson.getString("serverAddress");
if (!isValidSBServerAddress(serverAddress)) {
throw new IllegalArgumentException(str("sb_api_url_invalid"));
if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format
SettingsEnum.SB_API_URL.saveValue(serverAddress);
}
SettingsEnum.SB_API_URL.saveValue(serverAddress);
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
final float minDuration = (float)settingsJson.getDouble("minDuration");
final float minDuration = (float) settingsJson.getDouble("minDuration");
if (minDuration < 0) {
throw new IllegalArgumentException("invalid minDuration: " + minDuration);
}
SettingsEnum.SB_MIN_DURATION.saveValue(minDuration);
SettingsEnum.SB_SEGMENT_MIN_DURATION.saveValue(minDuration);
try {
if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced
int skipCount = settingsJson.getInt("skipCount");
if (skipCount < 0) {
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
}
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(skipCount);
SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(skipCount);
}
if (settingsJson.has("minutesSaved")) {
final double minutesSaved = settingsJson.getDouble("minutesSaved");
if (minutesSaved < 0) {
throw new IllegalArgumentException("invalid minutesSaved: " + minutesSaved);
}
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue((long)(minutesSaved * 60 * 1000));
} catch (JSONException ex) {
// ignore. values were not exported in prior versions of ReVanced
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue((long) (minutesSaved * 60 * 1000));
}
ReVancedUtils.showToastLong(str("sb_settings_import_successful"));
@ -136,15 +143,17 @@ public class SponsorBlockSettings {
categorySelectionsArray.put(behaviorObject);
}
}
json.put("userID", SettingsEnum.SB_UUID.getString());
json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean());
if (SponsorBlockSettings.userHasSBPrivateId()) {
json.put("userID", SettingsEnum.SB_PRIVATE_USER_ID.getString());
}
json.put("isVip", SettingsEnum.SB_USER_IS_VIP.getBoolean());
json.put("serverAddress", SettingsEnum.SB_API_URL.getString());
json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean());
json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat());
json.put("dontShowNotice", !SettingsEnum.SB_TOAST_ON_SKIP.getBoolean());
json.put("showTimeWithSkips", SettingsEnum.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.getBoolean());
json.put("minDuration", SettingsEnum.SB_SEGMENT_MIN_DURATION.getFloat());
json.put("trackViewCount", SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean());
json.put("skipCount", SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt());
json.put("minutesSaved", SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / (60f * 1000));
json.put("skipCount", SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt());
json.put("minutesSaved", SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() / (60f * 1000));
json.put("categorySelections", categorySelectionsArray);
json.put("barTypes", barTypesObject);
@ -152,13 +161,59 @@ public class SponsorBlockSettings {
return json.toString(2);
} catch (Exception ex) {
LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
ReVancedUtils.showToastLong(str("sb_settings_export_failed"));
ReVancedUtils.showToastLong(str("sb_settings_export_failed", ex));
return "";
}
}
/**
* Export the categories using flatten json (no embedded dictionaries or arrays).
*/
public static void exportCategoriesToFlatJson(@Nullable Context dialogContext,
@NonNull JSONObject json) throws JSONException {
ReVancedUtils.verifyOnMainThread();
initialize();
// If user has a SponsorBlock user id then show a warning.
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
&& !SettingsEnum.SB_HIDE_EXPORT_WARNING.getBoolean()) {
new AlertDialog.Builder(dialogContext)
.setMessage(str("sb_settings_revanced_export_user_id_warning"))
.setNeutralButton(str("sb_settings_revanced_export_user_id_warning_dismiss"),
(dialog, which) -> SettingsEnum.SB_HIDE_EXPORT_WARNING.saveValue(true))
.setPositiveButton(android.R.string.ok, null)
.setCancelable(false)
.show();
}
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
category.exportToFlatJSON(json);
}
}
/**
* Import the categories using flatten json (no embedded dictionaries or arrays).
*
* @return the number of settings imported
*/
public static int importCategoriesFromFlatJson(JSONObject json) throws JSONException {
ReVancedUtils.verifyOnMainThread();
initialize();
int numberOfImportedSettings = 0;
SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit();
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
numberOfImportedSettings += category.importFromFlatJSON(json, editor);
}
editor.apply();
SegmentCategory.updateEnabledCategories();
return numberOfImportedSettings;
}
public static boolean isValidSBUserId(@NonNull String userId) {
return !userId.isEmpty();
return !userId.isEmpty() && userId.length() >= SB_PRIVATE_USER_ID_MINIMUM_LENGTH;
}
/**
@ -180,6 +235,29 @@ public class SponsorBlockSettings {
return true;
}
/**
* @return if the user has ever voted, created a segment, or imported existing SB settings.
*/
public static boolean userHasSBPrivateId() {
return !SettingsEnum.SB_PRIVATE_USER_ID.getString().isEmpty();
}
/**
* Use this only if a user id is required (creating segments, voting).
*/
@NonNull
public static String getSBPrivateUserID() {
String uuid = SettingsEnum.SB_PRIVATE_USER_ID.getString();
if (uuid.isEmpty()) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())
.replace("-", "");
SettingsEnum.SB_PRIVATE_USER_ID.saveValue(uuid);
}
return uuid;
}
private static boolean initialized;
public static void initialize() {
@ -188,15 +266,6 @@ public class SponsorBlockSettings {
}
initialized = true;
String uuid = SettingsEnum.SB_UUID.getString();
if (uuid.isEmpty()) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())
.replace("-", "");
SettingsEnum.SB_UUID.saveValue(uuid);
}
SegmentCategory.loadFromPreferences();
}
}

View File

@ -172,7 +172,7 @@ public class SponsorBlockUtils {
for (int i = 0; i < voteOptions.length; i++) {
SegmentVote voteOption = voteOptions[i];
String title = voteOption.title.toString();
if (SettingsEnum.SB_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) {
if (SettingsEnum.SB_USER_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) {
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
} else {
items[i] = title;
@ -214,20 +214,18 @@ public class SponsorBlockUtils {
private static void submitNewSegment() {
try {
ReVancedUtils.verifyOnMainThread();
final String uuid = SettingsEnum.SB_UUID.getString();
final long start = newSponsorSegmentStartMillis;
final long end = newSponsorSegmentEndMillis;
final String videoId = VideoInformation.getVideoId();
final long videoLength = VideoInformation.getVideoLength();
final SegmentCategory segmentCategory = newUserCreatedSegmentCategory;
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty()
|| segmentCategory == null || uuid.isEmpty()) {
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty() || segmentCategory == null) {
LogHelper.printException(() -> "invalid parameters");
return;
}
clearUnsubmittedSegmentTimes();
ReVancedUtils.runOnBackgroundThread(() -> {
SBRequester.submitSegments(uuid, videoId, segmentCategory.key, start, end, videoLength);
SBRequester.submitSegments(videoId, segmentCategory.key, start, end, videoLength);
SegmentPlaybackController.executeDownloadSegments(videoId);
});
} catch (Exception e) {
@ -380,9 +378,9 @@ public class SponsorBlockUtils {
return;
}
segment.recordedAsSkipped = true;
final long totalTimeSkipped = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() + segment.length();
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(totalTimeSkipped);
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt() + 1);
final long totalTimeSkipped = SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.getLong() + segment.length();
SettingsEnum.SB_LOCAL_TIME_SAVED_MILLISECONDS.saveValue(totalTimeSkipped);
SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.saveValue(SettingsEnum.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.getInt() + 1);
if (SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()) {
ReVancedUtils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment));

View File

@ -16,6 +16,9 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -59,6 +62,11 @@ public enum SegmentCategory {
private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight");
/**
* Prefix to use when serializing to flat JSON layout used with ReVanced import/export.
*/
private static final String FLAT_JSON_IMPORT_EXPORT_PREFIX = "sb_";
private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
@ -189,6 +197,8 @@ public enum SegmentCategory {
*/
@NonNull
public CategoryBehaviour behaviour;
@NonNull
public final CategoryBehaviour defaultBehaviour;
SegmentCategory(String key, StringRef title, StringRef description,
StringRef skipButtonText,
@ -213,7 +223,7 @@ public enum SegmentCategory {
this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning);
this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle);
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviour = Objects.requireNonNull(defaultBehavior);
this.behaviour = this.defaultBehaviour = Objects.requireNonNull(defaultBehavior);
this.color = this.defaultColor = defaultColor;
this.paint = new Paint();
setColor(defaultColor);
@ -231,10 +241,13 @@ public enum SegmentCategory {
}
String behaviorString = preferences.getString(key, null);
if (behaviorString != null) {
if (behaviorString == null) {
behaviour = defaultBehaviour;
} else {
CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString);
if (preferenceBehavior == null) {
LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen
behaviour = defaultBehaviour;
} else {
behaviour = preferenceBehavior;
}
@ -253,6 +266,50 @@ public enum SegmentCategory {
editor.putString(key, behaviour.key);
}
private String getFlatJsonBehaviorKey() {
return FLAT_JSON_IMPORT_EXPORT_PREFIX + key;
}
private String getFlatJsonColorKey() {
return FLAT_JSON_IMPORT_EXPORT_PREFIX + key + COLOR_PREFERENCE_KEY_SUFFIX;
}
public void exportToFlatJSON(JSONObject json) throws JSONException {
if (behaviour != defaultBehaviour) {
json.put(getFlatJsonBehaviorKey(), behaviour.key);
}
if (color != defaultColor) {
json.put(getFlatJsonColorKey(), colorString());
}
}
/**
* Calling code is responsible for calling {@link #updateEnabledCategories()} and {@link SharedPreferences.Editor#apply()}
*/
public int importFromFlatJSON(JSONObject json, SharedPreferences.Editor editor) throws JSONException {
int numberOfSettingsImported = 0;
String behaviorKey = getFlatJsonBehaviorKey();
if (json.has(behaviorKey)) {
String behaviorString = json.getString(behaviorKey);
CategoryBehaviour importedBehavior = CategoryBehaviour.byStringKey(behaviorString);
if (importedBehavior == null) {
throw new IllegalArgumentException("unknown behavior: " + behaviorString);
}
behaviour = importedBehavior;
numberOfSettingsImported++;
} else {
behaviour = defaultBehaviour;
}
String colorKey = getFlatJsonColorKey();
if (json.has(colorKey)) {
setColor(json.getString(colorKey));
numberOfSettingsImported++;
} else {
color = defaultColor;
}
save(editor);
return numberOfSettingsImported;
}
/**
* @return HTML color format string
*/
@ -300,7 +357,7 @@ public enum SegmentCategory {
*/
@NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (SettingsEnum.SB_USE_COMPACT_SKIP_BUTTON.getBoolean()) {
if (SettingsEnum.SB_COMPACT_SKIP_BUTTON.getBoolean()) {
return (this == SegmentCategory.HIGHLIGHT)
? skipSponsorTextCompactHighlight
: skipSponsorTextCompact;

View File

@ -17,7 +17,12 @@ public class UserStats {
* "User reputation". Unclear how SB determines this value.
*/
public final float reputation;
/**
* {@link #segmentCount} plus {@link #ignoredSegmentCount}
*/
public final int totalSegmentCountIncludingIgnored;
public final int segmentCount;
public final int ignoredSegmentCount;
public final int viewCount;
public final double minutesSaved;
@ -26,6 +31,8 @@ public class UserStats {
userName = json.getString("userName");
reputation = (float)json.getDouble("reputation");
segmentCount = json.getInt("segmentCount");
ignoredSegmentCount = json.getInt("ignoredSegmentCount");
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
viewCount = json.getInt("viewCount");
minutesSaved = json.getDouble("minutesSaved");
}
@ -38,6 +45,7 @@ public class UserStats {
+ ", userName='" + userName + '\''
+ ", reputation=" + reputation
+ ", segmentCount=" + segmentCount
+ ", ignoredSegmentCount=" + ignoredSegmentCount
+ ", viewCount=" + viewCount
+ ", minutesSaved=" + minutesSaved
+ '}';

View File

@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.requests.Route;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote;
@ -49,6 +49,15 @@ public class SBRequester {
private SBRequester() {
}
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
if (SettingsEnum.SB_TOAST_ON_CONNECTION_ERROR.getBoolean()) {
ReVancedUtils.showToastShort(toastMessage);
}
if (ex != null) {
LogHelper.printInfo(() -> toastMessage, ex);
}
}
@NonNull
public static SponsorSegment[] getSegments(@NonNull String videoId) {
ReVancedUtils.verifyOffMainThread();
@ -59,7 +68,7 @@ public class SBRequester {
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
JSONArray responseArray = Requester.parseJSONArray(connection);
final long minSegmentDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000);
final long minSegmentDuration = (long) (SettingsEnum.SB_SEGMENT_MIN_DURATION.getFloat() * 1000);
for (int i = 0, length = responseArray.length(); i < length; i++) {
JSONObject obj = (JSONObject) responseArray.get(i);
JSONArray segment = obj.getJSONArray("segment");
@ -88,14 +97,16 @@ public class SBRequester {
// no segments are found. a normal response
LogHelper.printDebug(() -> "No segments found for video: " + videoId);
} else {
LogHelper.printException(() -> "getSegments failed with response code: " + responseCode,
null, str("sb_sponsorblock_connection_failure_status", responseCode));
handleConnectionError(str("sb_sponsorblock_connection_failure_status", responseCode), null);
connection.disconnect(); // something went wrong, might as well disconnect
}
} catch (SocketTimeoutException ex) {
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_timeout"));
handleConnectionError(str("sb_sponsorblock_connection_failure_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("sb_sponsorblock_connection_failure_generic"), ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_generic"));
// Should never happen
LogHelper.printException(() -> "getSegments failure", ex);
}
// Crude debug tests to verify random features
@ -127,15 +138,16 @@ public class SBRequester {
return segments.toArray(new SponsorSegment[0]);
}
public static void submitSegments(@NonNull String userPrivateId, @NonNull String videoId, @NonNull String category,
public static void submitSegments(@NonNull String videoId, @NonNull String category,
long startTime, long endTime, long videoLength) {
ReVancedUtils.verifyOffMainThread();
try {
String privateUserId = SponsorBlockSettings.getSBPrivateUserID();
String start = String.format(Locale.US, TIME_TEMPLATE, startTime / 1000f);
String end = String.format(Locale.US, TIME_TEMPLATE, endTime / 1000f);
String duration = String.format(Locale.US, TIME_TEMPLATE, videoLength / 1000f);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, userPrivateId, videoId, category, start, end, duration);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration);
final int responseCode = connection.getResponseCode();
final String messageToToast;
@ -161,7 +173,10 @@ public class SBRequester {
}
ReVancedUtils.showToastLong(messageToToast);
} catch (SocketTimeoutException ex) {
// Always show, even if show connection toasts is turned off
ReVancedUtils.showToastLong(str("sb_submit_failed_timeout"));
} catch (IOException ex) {
ReVancedUtils.showToastLong(str("sb_submit_failed_unknown_error", 0, ex.getMessage()));
} catch (Exception ex) {
LogHelper.printException(() -> "failed to submit segments", ex);
}
@ -196,7 +211,7 @@ public class SBRequester {
ReVancedUtils.runOnBackgroundThread(() -> {
try {
String segmentUuid = segment.UUID;
String uuid = SettingsEnum.SB_UUID.getString();
String uuid = SponsorBlockSettings.getSBPrivateUserID();
HttpURLConnection connection = (voteOption == SegmentVote.CATEGORY_CHANGE)
? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, uuid, segmentUuid, categoryToVoteFor.key)
: getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, uuid, segmentUuid, String.valueOf(voteOption.apiVoteType));
@ -216,7 +231,9 @@ public class SBRequester {
break;
}
} catch (SocketTimeoutException ex) {
LogHelper.printException(() -> "failed to vote for segment", ex, str("sb_vote_failed_timeout"));
ReVancedUtils.showToastShort(str("sb_vote_failed_timeout"));
} catch (IOException ex) {
ReVancedUtils.showToastShort(str("sb_vote_failed_unknown_error", 0, ex.getMessage()));
} catch (Exception ex) {
LogHelper.printException(() -> "failed to vote for segment", ex); // should never happen
}
@ -230,7 +247,7 @@ public class SBRequester {
public static UserStats retrieveUserStats() {
ReVancedUtils.verifyOffMainThread();
try {
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString()));
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID()));
LogHelper.printDebug(() -> "user stats: " + stats);
return stats;
} catch (IOException ex) {
@ -248,7 +265,7 @@ public class SBRequester {
public static String setUsername(@NonNull String username) {
ReVancedUtils.verifyOffMainThread();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username);
final int responseCode = connection.getResponseCode();
String responseMessage = connection.getResponseMessage();
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
@ -262,15 +279,18 @@ public class SBRequester {
}
public static void runVipCheckInBackgroundIfNeeded() {
if (!SponsorBlockSettings.userHasSBPrivateId()) {
return; // User cannot be a VIP. User has never voted, created any segments, or has imported a SB user id.
}
long now = System.currentTimeMillis();
if (now < (SettingsEnum.SB_LAST_VIP_CHECK.getLong() + TimeUnit.DAYS.toMillis(3))) {
return;
}
ReVancedUtils.runOnBackgroundThread(() -> {
try {
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString());
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SponsorBlockSettings.getSBPrivateUserID());
boolean vip = json.getBoolean("vip");
SettingsEnum.SB_IS_VIP.saveValue(vip);
SettingsEnum.SB_USER_IS_VIP.saveValue(vip);
SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now);
} catch (IOException ex) {
LogHelper.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown

View File

@ -9,7 +9,7 @@ class SBRoutes {
static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}");
static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}");
static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}");
static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"viewCount\",\"minutesSaved\"]");
static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]");
static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}");
static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}");
static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}");

View File

@ -3,52 +3,35 @@ package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.videoplayer.BottomControlButton;
public class CreateSegmentButtonController {
private static WeakReference<ImageView> buttonReference = new WeakReference<>(null);
private static Animation fadeIn;
private static Animation fadeOut;
private static boolean isShowing;
/**
* injection point
*/
public static void initialize(Object viewStub) {
public static void initialize(View youtubeControlsLayout) {
try {
LogHelper.printDebug(() -> "initializing new segment button");
RelativeLayout youtubeControlsLayout = (RelativeLayout) viewStub;
String buttonIdentifier = "sb_sponsorblock_button";
ImageView imageView = youtubeControlsLayout.findViewById(getResourceIdentifier(buttonIdentifier, "id"));
if (imageView == null) {
LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonIdentifier + "\"");
return;
}
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
getResourceIdentifier("sb_sponsorblock_button", "id")));
imageView.setVisibility(View.GONE);
imageView.setOnClickListener(v -> {
LogHelper.printDebug(() -> "New segment button clicked");
SponsorBlockViewController.toggleNewSegmentLayoutVisibility();
});
buttonReference = new WeakReference<>(imageView);
// Animations
if (fadeIn == null) {
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
}
isShowing = true;
changeVisibilityImmediate(false);
buttonReference = new WeakReference<>(imageView);
} catch (Exception ex) {
LogHelper.printException(() -> "initialize failure", ex);
}
@ -86,7 +69,7 @@ public class CreateSegmentButtonController {
return;
}
if (!immediate) {
iView.startAnimation(fadeIn);
iView.startAnimation(BottomControlButton.getButtonFadeIn());
}
iView.setVisibility(View.VISIBLE);
return;
@ -95,7 +78,7 @@ public class CreateSegmentButtonController {
if (iView.getVisibility() == View.VISIBLE) {
iView.clearAnimation();
if (!immediate) {
iView.startAnimation(fadeOut);
iView.startAnimation(BottomControlButton.getButtonFadeOut());
}
iView.setVisibility(View.GONE);
}
@ -105,7 +88,7 @@ public class CreateSegmentButtonController {
}
private static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT.getBoolean()
&& !VideoInformation.isAtEndOfVideo();
}

View File

@ -53,14 +53,14 @@ public final class NewSegmentLayout extends FrameLayout {
initializeButton(
context,
"sb_new_segment_rewind",
() -> VideoInformation.seekToRelative(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()),
() -> VideoInformation.seekToRelative(-SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getInt()),
"Rewind button clicked"
);
initializeButton(
context,
"sb_new_segment_forward",
() -> VideoInformation.seekToRelative(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()),
() -> VideoInformation.seekToRelative(SettingsEnum.SB_CREATE_NEW_SEGMENT_STEP.getInt()),
"Forward button clicked"
);

View File

@ -51,7 +51,7 @@ public class SponsorBlockViewController {
/**
* Injection point.
*/
public static void initialize(Object obj) {
public static void initialize(ViewGroup viewGroup) {
try {
LogHelper.printDebug(() -> "initializing");
@ -64,7 +64,6 @@ public class SponsorBlockViewController {
LayoutInflater.from(context).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout);
inlineSponsorOverlayRef = new WeakReference<>(layout);
ViewGroup viewGroup = (ViewGroup) obj;
viewGroup.addView(layout);
viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@Override
@ -214,7 +213,7 @@ public class SponsorBlockViewController {
// the buttons automatically set themselves to visible when appropriate,
// but if buttons are showing when the end of the video is reached then they need
// to be forcefully hidden
if (!SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean()) {
if (!SettingsEnum.AUTO_REPEAT.getBoolean()) {
CreateSegmentButtonController.hide();
VotingButtonController.hide();
}

View File

@ -3,11 +3,10 @@ package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
@ -15,40 +14,26 @@ import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.videoplayer.BottomControlButton;
public class VotingButtonController {
private static WeakReference<ImageView> buttonReference = new WeakReference<>(null);
private static Animation fadeIn;
private static Animation fadeOut;
private static boolean isShowing;
/**
* injection point
*/
public static void initialize(Object viewStub) {
public static void initialize(View youtubeControlsLayout) {
try {
LogHelper.printDebug(() -> "initializing voting button");
RelativeLayout controlsLayout = (RelativeLayout) viewStub;
String buttonResourceName = "sb_voting_button";
ImageView imageView = controlsLayout.findViewById(getResourceIdentifier(buttonResourceName, "id"));
if (imageView == null) {
LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonResourceName + "\"");
return;
}
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
getResourceIdentifier("sb_voting_button", "id")));
imageView.setVisibility(View.GONE);
imageView.setOnClickListener(v -> {
SponsorBlockUtils.onVotingClicked(v.getContext());
});
buttonReference = new WeakReference<>(imageView);
// Animations
if (fadeIn == null) {
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
}
isShowing = true;
changeVisibilityImmediate(false);
buttonReference = new WeakReference<>(imageView);
} catch (Exception ex) {
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
}
@ -86,7 +71,7 @@ public class VotingButtonController {
return;
}
if (!immediate) {
iView.startAnimation(fadeIn);
iView.startAnimation(BottomControlButton.getButtonFadeIn());
}
iView.setVisibility(View.VISIBLE);
return;
@ -95,7 +80,7 @@ public class VotingButtonController {
if (iView.getVisibility() == View.VISIBLE) {
iView.clearAnimation();
if (!immediate) {
iView.startAnimation(fadeOut);
iView.startAnimation(BottomControlButton.getButtonFadeOut());
}
iView.setVisibility(View.GONE);
}
@ -105,7 +90,7 @@ public class VotingButtonController {
}
private static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean()
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_BUTTON.getBoolean()
&& SegmentPlaybackController.videoHasSegments() && !VideoInformation.isAtEndOfVideo();
}
@ -116,7 +101,6 @@ public class VotingButtonController {
ReVancedUtils.verifyOnMainThread();
View v = buttonReference.get();
if (v == null) {
LogHelper.printDebug(() -> "Cannot hide voting button (value is null)");
return;
}
v.setVisibility(View.GONE);

View File

@ -24,13 +24,13 @@ class SwipeControlsConfigurationProvider(
* should swipe controls for volume be enabled?
*/
val enableVolumeControls: Boolean
get() = SettingsEnum.ENABLE_SWIPE_VOLUME.boolean
get() = SettingsEnum.SWIPE_VOLUME.boolean
/**
* should swipe controls for volume be enabled?
*/
val enableBrightnessControl: Boolean
get() = SettingsEnum.ENABLE_SWIPE_BRIGHTNESS.boolean
get() = SettingsEnum.SWIPE_BRIGHTNESS.boolean
/**
* is the video player currently in fullscreen mode?
@ -52,14 +52,14 @@ class SwipeControlsConfigurationProvider(
* should press-to-swipe be enabled?
*/
val shouldEnablePressToSwipe: Boolean
get() = SettingsEnum.ENABLE_PRESS_TO_SWIPE.boolean
get() = SettingsEnum.SWIPE_PRESS_TO_ENGAGE.boolean
/**
* threshold for swipe detection
* this may be called rapidly in onScroll, so we have to load it once and then leave it constant
*/
val swipeMagnitudeThreshold: Float
get() = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD.float
val swipeMagnitudeThreshold: Int
get() = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD.int
//endregion
//region overlay adjustments
@ -68,7 +68,7 @@ class SwipeControlsConfigurationProvider(
* should the overlay enable haptic feedback?
*/
val shouldEnableHapticFeedback: Boolean
get() = SettingsEnum.ENABLE_SWIPE_HAPTIC_FEEDBACK.boolean
get() = SettingsEnum.SWIPE_HAPTIC_FEEDBACK.boolean
/**
* how long the overlay should be shown on changes
@ -79,8 +79,8 @@ class SwipeControlsConfigurationProvider(
/**
* text size for the overlay, in sp
*/
val overlayTextSize: Float
get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE.float
val overlayTextSize: Int
get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE.int
/**
* get the background color for text on the overlay, as a color int

View File

@ -74,7 +74,7 @@ class SwipeControlsOverlayLayout(
setColor(config.overlayTextBackgroundColor)
}
setTextColor(config.overlayForegroundColor)
setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize)
setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize.toFloat())
compoundDrawablePadding = compoundIconPadding
visibility = GONE
}

View File

@ -125,7 +125,7 @@ public class LogHelper {
} else {
Log.e(logMessage, messageString, ex);
}
if (SettingsEnum.DEBUG_SHOW_TOAST_ON_ERROR.getBoolean()) {
if (SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean()) {
String toastMessageToDisplay = (userToastMessage != null)
? userToastMessage
: outerClassSimpleName + ": " + messageString;

View File

@ -6,9 +6,15 @@ import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -22,6 +28,8 @@ import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import app.revanced.integrations.settings.SettingsEnum;
public class ReVancedUtils {
@SuppressLint("StaticFieldLeak")
@ -30,6 +38,35 @@ public class ReVancedUtils {
private ReVancedUtils() {
} // utility class
/**
* Hide a view by setting its layout height and width to 1dp.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy1dpUnderCondition(SettingsEnum condition, View view) {
if (!condition.getBoolean()) return;
LogHelper.printDebug(() -> "Hiding view with setting: " + condition);
hideViewByLayoutParams(view);
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(SettingsEnum condition, View view) {
if (!condition.getBoolean()) return;
LogHelper.printDebug(() -> "Hiding view with setting: " + condition);
view.setVisibility(View.GONE);
}
/**
* General purpose pool for network calls and other background tasks.
* All tasks run at max thread priority.
@ -97,6 +134,24 @@ public class ReVancedUtils {
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
/**
* @return The first child view that matches the filter.
*/
@Nullable
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, @NonNull MatchFilter filter) {
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
if (filter.matches(childAt)) {
return (T) childAt;
}
}
return null;
}
public interface MatchFilter<T> {
boolean matches(T object);
}
public static Context getContext() {
if (context != null) {
return context;
@ -117,6 +172,7 @@ public class ReVancedUtils {
@Nullable
private static Boolean isRightToLeftTextLayout;
/**
* If the device language uses right to left text layout (hebrew, arabic, etc)
*/
@ -239,6 +295,31 @@ public class ReVancedUtils {
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
}
/**
* Hide a view by setting its layout params to 1x1
* @param view The view to hide.
*/
public static void hideViewByLayoutParams(View view) {
if (view instanceof LinearLayout) {
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams);
} else if (view instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams2);
} else if (view instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(1, 1);
view.setLayoutParams(layoutParams3);
} else if (view instanceof Toolbar) {
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(1, 1);
view.setLayoutParams(layoutParams4);
} else if (view instanceof ViewGroup) {
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1);
view.setLayoutParams(layoutParams5);
} else {
LogHelper.printDebug(() -> "Hidden view with id " + view.getId());
}
}
public enum NetworkType {
NONE,
MOBILE,

View File

@ -1,20 +1,15 @@
package app.revanced.integrations.utils;
import android.app.Activity;
public class ThemeHelper {
private static int themeValue;
public static void setTheme(int value) {
if (themeValue != value) {
themeValue = value;
LogHelper.printDebug(() -> "Theme value: " + themeValue);
}
}
public static void setTheme(Object value) {
final int newOrdinalValue = ((Enum) value).ordinal();
if (themeValue != newOrdinalValue) {
themeValue = newOrdinalValue;
LogHelper.printDebug(() -> "Theme value: " + themeValue);
LogHelper.printDebug(() -> "Theme value: " + newOrdinalValue);
}
}
@ -22,4 +17,11 @@ public class ThemeHelper {
return themeValue == 1;
}
public static void setActivityTheme(Activity activity) {
final var theme = isDarkTheme()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style"));
}
}

View File

@ -4,35 +4,52 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public abstract class BottomControlButton {
private static final Animation fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
private static final Animation fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
private static final Animation fadeIn;
private static final Animation fadeOut;
private final WeakReference<ImageView> buttonRef;
private final SettingsEnum setting;
protected boolean isVisible;
static {
// TODO: check if these durations are correct.
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
}
@NonNull
public static Animation getButtonFadeIn() {
return fadeIn;
}
@NonNull
public static Animation getButtonFadeOut() {
return fadeOut;
}
public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId,
@NonNull SettingsEnum booleanSetting, @NonNull View.OnClickListener onClickListener) {
@NonNull SettingsEnum booleanSetting, @NonNull View.OnClickListener onClickListener,
@Nullable View.OnLongClickListener longClickListener) {
LogHelper.printDebug(() -> "Initializing button: " + imageViewButtonId);
if (booleanSetting.returnType != SettingsEnum.ReturnType.BOOLEAN) {
throw new IllegalArgumentException();
}
setting = booleanSetting;
// Create the button.
@ -40,6 +57,9 @@ public abstract class BottomControlButton {
ReVancedUtils.getResourceIdentifier(imageViewButtonId, "id")
));
imageView.setOnClickListener(onClickListener);
if (longClickListener != null) {
imageView.setOnLongClickListener(longClickListener);
}
imageView.setVisibility(View.GONE);
buttonRef = new WeakReference<>(imageView);

View File

@ -1,5 +1,6 @@
package app.revanced.integrations.videoplayer;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
@ -16,17 +17,21 @@ public class CopyVideoUrlButton extends BottomControlButton {
super(
viewGroup,
"copy_video_url_button",
SettingsEnum.COPY_VIDEO_URL_BUTTON_SHOWN,
view -> CopyVideoUrlPatch.copyUrl(false)
SettingsEnum.COPY_VIDEO_URL,
view -> CopyVideoUrlPatch.copyUrl(false),
view -> {
CopyVideoUrlPatch.copyUrl(true);
return true;
}
);
}
/**
* Injection point.
*/
public static void initializeButton(Object obj) {
public static void initializeButton(View view) {
try {
instance = new CopyVideoUrlButton((ViewGroup) obj);
instance = new CopyVideoUrlButton((ViewGroup) view);
} catch (Exception ex) {
LogHelper.printException(() -> "initializeButton failure", ex);
}

View File

@ -1,5 +1,6 @@
package app.revanced.integrations.videoplayer;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
@ -16,15 +17,19 @@ public class CopyVideoUrlTimestampButton extends BottomControlButton {
super(
bottomControlsViewGroup,
"copy_video_url_timestamp_button",
SettingsEnum.COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN,
view -> CopyVideoUrlPatch.copyUrl(true)
SettingsEnum.COPY_VIDEO_URL_TIMESTAMP,
view -> CopyVideoUrlPatch.copyUrl(true),
view -> {
CopyVideoUrlPatch.copyUrl(false);
return true;
}
);
}
/**
* Injection point.
*/
public static void initializeButton(Object bottomControlsViewGroup) {
public static void initializeButton(View bottomControlsViewGroup) {
try {
instance = new CopyVideoUrlTimestampButton((ViewGroup) bottomControlsViewGroup);
} catch (Exception ex) {

View File

@ -21,17 +21,18 @@ public class DownloadButton extends BottomControlButton {
super(
viewGroup,
"download_button",
SettingsEnum.DOWNLOADS_BUTTON_SHOWN,
DownloadButton::onDownloadClick
SettingsEnum.EXTERNAL_DOWNLOADER,
DownloadButton::onDownloadClick,
null
);
}
/**
* Injection point.
*/
public static void initializeButton(Object obj) {
public static void initializeButton(View view) {
try {
instance = new DownloadButton((ViewGroup) obj);
instance = new DownloadButton((ViewGroup) view);
} catch (Exception ex) {
LogHelper.printException(() -> "initializeButton failure", ex);
}
@ -48,7 +49,7 @@ public class DownloadButton extends BottomControlButton {
LogHelper.printDebug(() -> "Download button clicked");
final var context = view.getContext();
var downloaderPackageName = SettingsEnum.DOWNLOADS_PACKAGE_NAME.getString();
var downloaderPackageName = SettingsEnum.EXTERNAL_DOWNLOADER_PACKAGE_NAME.getString();
boolean packageEnabled = false;
try {

View File

@ -0,0 +1,25 @@
package app.revanced.reddit.patches;
import app.revanced.integrations.utils.LogHelper;
import java.net.MalformedURLException;
import java.net.URL;
public final class SanitizeUrlQueryPatch {
/**
* Strip query parameters from a given URL string.
*
* @param urlString URL string to strip query parameters from.
* @return URL string without query parameters if possible, otherwise the original string.
*/
public static String stripQueryParameters(final String urlString) {
try {
final var url = new URL(urlString);
return url.getProtocol() + "://" + url.getHost() + url.getPath();
} catch (MalformedURLException e) {
LogHelper.printException(() -> "Can not parse URL", e);
return urlString;
}
}
}

View File

@ -0,0 +1,9 @@
package app.revanced.twitch.patches;
import app.revanced.twitch.settings.SettingsEnum;
public class AutoClaimChannelPointsPatch {
public static boolean shouldAutoClaim() {
return SettingsEnum.AUTO_CLAIM_CHANNEL_POINTS.getBoolean();
}
}

View File

@ -21,6 +21,7 @@ public enum SettingsEnum {
/* Chat */
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", STRING, "cross-out"),
AUTO_CLAIM_CHANNEL_POINTS("revanced_auto_claim_channel_points", BOOLEAN, TRUE),
/* Misc */
DEBUG_MODE("revanced_debug_mode", BOOLEAN, FALSE, true);
@ -153,6 +154,14 @@ public enum SettingsEnum {
return (String) value;
}
/**
* @return the value of this setting as as generic object type.
*/
@NonNull
public Object getObjectValue() {
return value;
}
public enum ReturnType {
BOOLEAN,
INTEGER,

View File

@ -88,7 +88,23 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
)
);
// Sync all preferences with UI
// TODO: for a developer that uses Twitch: remove duplicated settings data
// 1. remove all default values from the Patches Setting preferences (SwitchPreference, TextPreference, ListPreference)
// 2. enable this code and verify the default is applied
if (false) {
for (SettingsEnum setting : SettingsEnum.values()) {
Preference pref = this.findPreference(setting.path);
if (pref instanceof SwitchPreference) {
((SwitchPreference) pref).setChecked(setting.getBoolean());
} else if (pref instanceof EditTextPreference) {
((EditTextPreference) pref).setText(setting.getObjectValue().toString());
} else if (pref instanceof ListPreference) {
((ListPreference) pref).setValue(setting.getObjectValue().toString());
}
}
}
// TODO: remove this line. On load the UI should apply the values from Settings using the code above.
// It should not apply the UI values to the Settings here
syncPreference(null);
this.registered = true;

View File

@ -1,5 +1,6 @@
package app.revanced.twitter.patches.hook.json
import app.revanced.twitter.patches.hook.patch.dummy.DummyHook
import app.revanced.twitter.utils.json.JsonUtils.parseJson
import app.revanced.twitter.utils.stream.StreamUtils
import org.json.JSONException
@ -7,8 +8,9 @@ import java.io.IOException
import java.io.InputStream
object JsonHookPatch {
// Additional hooks added by corresponding patch.
private val hooks = buildList<JsonHook> {
// Modified by corresponding patch.
add(DummyHook)
}
@JvmStatic

View File

@ -0,0 +1,14 @@
package app.revanced.twitter.patches.hook.patch.dummy
import app.revanced.twitter.patches.hook.json.BaseJsonHook
import app.revanced.twitter.patches.hook.json.JsonHookPatch
import org.json.JSONObject
/**
* Dummy hook to reserve a register in [JsonHookPatch.hooks] list.
*/
object DummyHook : BaseJsonHook() {
override fun apply(json: JSONObject) {
// Do nothing.
}
}

View File

@ -51,7 +51,7 @@ internal object TwiFucker {
private fun JSONObject.dataCheckAndRemove() {
dataGetInstructions()?.forEach { instruction ->
instruction.instructionCheckAndRemove()
instruction.instructionCheckAndRemove { it.entriesRemoveAnnoyance() }
}
}
@ -107,9 +107,9 @@ internal object TwiFucker {
private fun JSONObject.instructionGetAddEntries(): JSONArray? =
optJSONObject("addEntries")?.optJSONArray("entries")
private fun JSONObject.instructionCheckAndRemove() {
instructionTimelineAddEntries()?.entriesRemoveAnnoyance()
instructionGetAddEntries()?.entriesRemoveAnnoyance()
private fun JSONObject.instructionCheckAndRemove(action: (JSONArray) -> Unit) {
instructionTimelineAddEntries()?.let(action)
instructionGetAddEntries()?.let(action)
}
// entries
@ -164,14 +164,57 @@ internal object TwiFucker {
entriesRemoveTweetDetailRelatedTweets()
}
private fun JSONObject.entryIsWhoToFollow(): Boolean = optString("entryId").let {
it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-")
}
private fun JSONObject.itemContainsPromotedUser(): Boolean =
optJSONObject("item")?.optJSONObject("content")
?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content")
?.optJSONObject("user")
?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content")
?.optJSONObject("user")?.has("promotedMetadata") == true
fun JSONArray.entriesRemoveWhoToFollow() {
val entryRemoveIndex = mutableListOf<Int>()
forEachIndexed { entryIndex, entry ->
if (!entry.entryIsWhoToFollow()) return@forEachIndexed
Log.d("revanced", "Handle whoToFollow $entryIndex $entry")
entryRemoveIndex.add(entryIndex)
val items = entry.entryGetContentItems()
val userRemoveIndex = mutableListOf<Int>()
items?.forEachIndexed { index, item ->
item.itemContainsPromotedUser().let {
if (it) {
Log.d("revanced", "Handle whoToFollow promoted user $index $item")
userRemoveIndex.add(index)
}
}
}
for (i in userRemoveIndex.reversed()) {
items?.remove(i)
}
}
for (i in entryRemoveIndex.reversed()) {
remove(i)
}
}
fun hideRecommendedUsers(json: JSONObject) {
json.filterInstructions { it.entriesRemoveWhoToFollow() }
json.jsonCheckAndRemoveRecommendedUsers()
}
fun hidePromotedAds(json: JSONObject) {
json.jsonGetInstructions()?.forEach { instruction ->
instruction.instructionCheckAndRemove()
}
json.filterInstructions { it.entriesRemoveAnnoyance() }
json.jsonGetData()?.dataCheckAndRemove()
}
private fun JSONObject.filterInstructions(action: (JSONArray) -> Unit) {
jsonGetInstructions()?.forEach { instruction ->
instruction.instructionCheckAndRemove(action)
}
}
}

View File

@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.0.0")
classpath("com.android.tools.build:gradle:8.0.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// NOTE: Do not place your application dependencies here; they belong

View File

@ -1,7 +0,0 @@
package com.google.android.libraries.social.licenses;
import android.app.Activity;
// Dummy class
public final class LicenseActivity extends Activity { }

View File

@ -0,0 +1,10 @@
package com.google.android.libraries.youtube.rendering.ui.pivotbar;
import android.content.Context;
import android.widget.HorizontalScrollView;
public class PivotBar extends HorizontalScrollView {
public PivotBar(Context context) {
super(context);
}
}

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs = -Xmx2048m
android.useAndroidX = true
version = 0.107.0
version = 0.108.0-dev.24