mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-02 16:15:58 +01:00
chore: merge branch dev
to main
(#361)
This commit is contained in:
commit
c2ff0c45ab
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
@ -26,5 +26,5 @@ jobs:
|
||||
|
||||
## Dependencies before merge
|
||||
|
||||
- [] https://github.com/revanced/revanced-patches
|
||||
pr_draft: true
|
||||
- [ ] https://github.com/revanced/revanced-patches
|
||||
pr_draft: true
|
||||
|
64
CHANGELOG.md
64
CHANGELOG.md
@ -1,3 +1,67 @@
|
||||
# [0.103.0-dev.7](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.6...v0.103.0-dev.7) (2023-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **youtube/hide-video-action-buttons:** fix 'hide share button' ([#360](https://github.com/revanced/revanced-integrations/issues/360)) ([a2af2c0](https://github.com/revanced/revanced-integrations/commit/a2af2c0c9ffc2f961773bfb8d546aff68c2d1c27))
|
||||
|
||||
# [0.103.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.5...v0.103.0-dev.6) (2023-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **youtube/sponsorblock:** always show the video time without segments using left to right layout ([#359](https://github.com/revanced/revanced-integrations/issues/359)) ([86c2789](https://github.com/revanced/revanced-integrations/commit/86c27890ada8739ea272f8783eb4ef526b808a27))
|
||||
|
||||
# [0.103.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.4...v0.103.0-dev.5) (2023-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **youtube/return-youtube-dislike:** render dislikes when scrolling into the screen ([#350](https://github.com/revanced/revanced-integrations/issues/350)) ([41c07f7](https://github.com/revanced/revanced-integrations/commit/41c07f77f47d726fdc16120bb5695407a7dec1fc))
|
||||
|
||||
# [0.103.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.3...v0.103.0-dev.4) (2023-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor syntax error ([8797765](https://github.com/revanced/revanced-integrations/commit/8797765efa0fb98b6e11a7198ecce3943df3daf5))
|
||||
|
||||
# [0.103.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.2...v0.103.0-dev.3) (2023-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **youtube/general-ads:** hide new type of movie offer ad ([da7b669](https://github.com/revanced/revanced-integrations/commit/da7b669c97d18bef51de98ddfde8a514ebb61ecf))
|
||||
|
||||
# [0.103.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.1...v0.103.0-dev.2) (2023-04-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **youtube/sponsorblock:** skip to video highlight ([#352](https://github.com/revanced/revanced-integrations/issues/352)) ([03f09cf](https://github.com/revanced/revanced-integrations/commit/03f09cf7bce1747b1d402f3a3e16dd69c364dfce))
|
||||
|
||||
# [0.103.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.102.0...v0.103.0-dev.1) (2023-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **youtube/general-ads:** hide new types of ads ([#339](https://github.com/revanced/revanced-integrations/issues/339)) ([6528d44](https://github.com/revanced/revanced-integrations/commit/6528d444b49759ee1137c9b0eb8e1079fb4cc97c))
|
||||
* **youtube/return-youtube-dislike:** fix error toast when voting ([#349](https://github.com/revanced/revanced-integrations/issues/349)) ([e3d923d](https://github.com/revanced/revanced-integrations/commit/e3d923d564ae572c5e0e10a86ce17b8009ec8c42))
|
||||
* **youtube/return-youtube-dislike:** stale dislike data shown after opening / closing the app during shorts playback ([#356](https://github.com/revanced/revanced-integrations/issues/356)) ([212e4f2](https://github.com/revanced/revanced-integrations/commit/212e4f2ce43360776fe20467c4142c9936b22d42))
|
||||
* **youtube/settings:** fix dialog not shown if dismissed with back button ([584de16](https://github.com/revanced/revanced-integrations/commit/584de16236ff758c2067ee84ba4cc04d765d49ba))
|
||||
* **youtube/sponsorblock:** change default behavior to better match the browser ([#353](https://github.com/revanced/revanced-integrations/issues/353)) ([b959c8e](https://github.com/revanced/revanced-integrations/commit/b959c8ef98e869201c8bc2609943108283fea453))
|
||||
* **youtube/sponsorblock:** settings do not show default behavior ([#351](https://github.com/revanced/revanced-integrations/issues/351)) ([6dbccfd](https://github.com/revanced/revanced-integrations/commit/6dbccfd472d843b5c3f0efed39b575d3ea7ac04f))
|
||||
* **youtube/sponsorblock:** update HTTP user agent ([#344](https://github.com/revanced/revanced-integrations/issues/344)) ([3025103](https://github.com/revanced/revanced-integrations/commit/3025103014a4521a437cfde0a417535e7751b517))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **youtube/general-ads:** block new type of ad ([6265a91](https://github.com/revanced/revanced-integrations/commit/6265a91841f8b037be22dd5fd5399dbeeb666745))
|
||||
* **youtube/general-ads:** hide new type of ad ([5ba4cbd](https://github.com/revanced/revanced-integrations/commit/5ba4cbd4e097d863924731d363dd5bb8849e1394))
|
||||
* **youtube/general-ads:** hide new type of ad ([f818490](https://github.com/revanced/revanced-integrations/commit/f8184905bd9601bf63e30963ff337d24f2599794))
|
||||
* **youtube/settings:** disable preference control if the feature is turned off. show a dialog explaining side effects of some patches ([#328](https://github.com/revanced/revanced-integrations/issues/328)) ([a0ad968](https://github.com/revanced/revanced-integrations/commit/a0ad968aaa66422e67de2a61d76bc7aa88f08bf6))
|
||||
* **youtube:** user selectable default video speed and quality ([#354](https://github.com/revanced/revanced-integrations/issues/354)) ([14223f4](https://github.com/revanced/revanced-integrations/commit/14223f40b5ca48f35bbecfd849dff20dfd309d92))
|
||||
|
||||
# [0.102.0](https://github.com/revanced/revanced-integrations/compare/v0.101.1...v0.102.0) (2023-04-13)
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(mapOf("path" to ":dummy")))
|
||||
compileOnly("androidx.annotation:annotation:1.5.0")
|
||||
compileOnly("androidx.annotation:annotation:1.6.0")
|
||||
compileOnly("androidx.appcompat:appcompat:1.6.1")
|
||||
compileOnly("com.squareup.okhttp3:okhttp:5.0.0-alpha.11")
|
||||
compileOnly("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
|
@ -1,60 +1,30 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
final class ButtonsPatch extends Filter {
|
||||
private final BlockRule actionButtonsRule;
|
||||
private final BlockRule dislikeRule;
|
||||
private final BlockRule actionBarRule;
|
||||
|
||||
private final BlockRule[] rules;
|
||||
|
||||
public ButtonsPatch() {
|
||||
BlockRule like = new BlockRule(SettingsEnum.HIDE_LIKE_BUTTON, "|like_button");
|
||||
dislikeRule = new BlockRule(SettingsEnum.HIDE_DISLIKE_BUTTON, "dislike_button");
|
||||
BlockRule download = new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button");
|
||||
actionButtonsRule = new BlockRule(SettingsEnum.HIDE_ACTION_BUTTON, "ContainerType|video_action_button");
|
||||
BlockRule playlist = new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button");
|
||||
rules = new BlockRule[]{like, dislikeRule, download, actionButtonsRule, playlist};
|
||||
|
||||
actionBarRule = new BlockRule(null, "video_action_bar");
|
||||
|
||||
this.pathRegister.registerAll(
|
||||
like,
|
||||
dislikeRule,
|
||||
download,
|
||||
playlist
|
||||
pathRegister.registerAll(
|
||||
new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"),
|
||||
new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button")
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hideActionBar() {
|
||||
for (BlockRule rule : rules) if (!rule.isEnabled()) return false;
|
||||
private boolean canHideActionBar() {
|
||||
for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filter(final String path, final String identifier) {
|
||||
if (hideActionBar() && actionBarRule.check(identifier).isBlocked()) return true;
|
||||
// If everything is hidden, then also hide the video bar itself.
|
||||
if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true;
|
||||
|
||||
var currentIsActionButton = actionButtonsRule.check(path).isBlocked();
|
||||
|
||||
if (dislikeRule.check(path).isBlocked()) ActionButton.doNotBlockCounter = 4;
|
||||
|
||||
if (currentIsActionButton && ActionButton.doNotBlockCounter-- > 0) {
|
||||
if (SettingsEnum.HIDE_SHARE_BUTTON.getBoolean()) {
|
||||
LogHelper.printDebug(() -> "Hiding share button");
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
if ((currentIsActionButton && ActionButton.doNotBlockCounter <= 0 && actionButtonsRule.isEnabled()) || pathRegister.contains(path)) {
|
||||
LogHelper.printDebug(() -> "Blocked: " + path);
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
static class ActionButton {
|
||||
public static int doNotBlockCounter = 4;
|
||||
return pathRegister.contains(path);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.StringRef;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
@ -16,10 +14,8 @@ public class CopyVideoUrlPatch {
|
||||
url += String.format("?t=%s", seconds);
|
||||
}
|
||||
|
||||
Context context = ReVancedUtils.getContext();
|
||||
|
||||
ReVancedUtils.setClipboard(url);
|
||||
if (context != null) Toast.makeText(context, StringRef.str("share_copy_url_success"), Toast.LENGTH_SHORT).show();
|
||||
ReVancedUtils.showToastShort(str("share_copy_url_success"));
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Failed to generate video url", e);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"_buttoned_layout",
|
||||
"full_width_square_image_layout",
|
||||
"_ad_with",
|
||||
"video_display_button_group_layout",
|
||||
"landscape_image_wide_button_layout"
|
||||
);
|
||||
var generalAds = new BlockRule(
|
||||
@ -61,7 +62,11 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"hero_promo_image",
|
||||
"statement_banner",
|
||||
"carousel_footered_layout",
|
||||
"text_image_button_layout"
|
||||
"text_image_button_layout",
|
||||
"primetime_promo",
|
||||
"feature_grid_interstitial",
|
||||
"product_details",
|
||||
"brand_video_shelf"
|
||||
);
|
||||
var movieAds = new BlockRule(
|
||||
SettingsEnum.ADREMOVER_MOVIE_REMOVAL,
|
||||
@ -69,7 +74,8 @@ public final class GeneralAdsPatch extends Filter {
|
||||
"compact_movie",
|
||||
"horizontal_movie_shelf",
|
||||
"movie_and_show_upsell_card",
|
||||
"compact_tvfilm_item"
|
||||
"compact_tvfilm_item",
|
||||
"offer_module_root"
|
||||
);
|
||||
|
||||
this.pathRegister.registerAll(
|
||||
|
@ -1,20 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.integrations.adremover.AdRemoverAPI;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class HideReelsPatch {
|
||||
|
||||
/**
|
||||
* Used by app.revanced.patches.youtube.layout.reels.patch.HideReelsPatch
|
||||
*
|
||||
* @param view
|
||||
*/
|
||||
public static void HideReel(View view) {
|
||||
if (SettingsEnum.HIDE_REEL_BUTTON.getBoolean()) {
|
||||
AdRemoverAPI.HideViewWithLayout1dp(view);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getContext;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
public class LithoThemePatch {
|
||||
@ -44,29 +41,15 @@ public class LithoThemePatch {
|
||||
}
|
||||
|
||||
private static int getBlackColor() {
|
||||
if (blackColor == 0) blackColor = getColor("yt_black1");
|
||||
if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1");
|
||||
return blackColor;
|
||||
}
|
||||
|
||||
private static int getWhiteColor() {
|
||||
if (whiteColor == 0) whiteColor = getColor("yt_white1");
|
||||
if (whiteColor == 0) whiteColor = ReVancedUtils.getResourceColor("yt_white1");
|
||||
return whiteColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color for a color resource.
|
||||
*
|
||||
* @param name The color resource name.
|
||||
* @return The value of the color.
|
||||
*/
|
||||
private static int getColor(String name) {
|
||||
Context context = getContext();
|
||||
|
||||
return context != null ? context.getColor(context.getResources()
|
||||
.getIdentifier(name, "color", context.getPackageName())
|
||||
) : 0;
|
||||
}
|
||||
|
||||
private static boolean anyEquals(int value, int... of) {
|
||||
for (int v : of) if (value == v) return true;
|
||||
return false;
|
||||
|
@ -1,16 +1,16 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class MicroGSupport {
|
||||
private static final String MICROG_VENDOR = "com.mgoogle";
|
||||
@ -20,7 +20,7 @@ public class MicroGSupport {
|
||||
private static final Uri VANCED_MICROG_PROVIDER = Uri.parse("content://" + MICROG_VENDOR + ".android.gsf.gservices/prefix");
|
||||
|
||||
private static void startIntent(Context context, String uriString, String message) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
|
||||
ReVancedUtils.showToastLong(message);
|
||||
|
||||
var intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
@ -28,7 +28,7 @@ public class PlayerTypeHookPatch {
|
||||
LogHelper.printException(() -> "Unknown PlayerType encountered: " + type);
|
||||
} else {
|
||||
PlayerType.setCurrent(newType);
|
||||
LogHelper.printDebug(() -> "YouTubePlayerOverlaysLayout player type was updated to " + newType);
|
||||
LogHelper.printDebug(() -> "PlayerType was updated to: " + newType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,108 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Used by app.revanced.patches.youtube.layout.returnyoutubedislike.patch.ReturnYouTubeDislikePatch
|
||||
*/
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoLoaded(String videoId) {
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a litho text component is created
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param textRef Cache reference to the like/dislike char sequence,
|
||||
* which may or may not be the same as the original span parameter.
|
||||
* If dislikes are added, the atomic reference must be set to the replacement span.
|
||||
* @param original Original span that was created or reused by Litho.
|
||||
* @return The original span (if nothing should change), or a replacement span that contains dislikes.
|
||||
*/
|
||||
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
||||
ReturnYouTubeDislike.onComponentCreated(conversionContext, textRef);
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForContext(conversionContext, original);
|
||||
if (replacement != null) {
|
||||
textRef.set(replacement);
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onComponentCreated AtomicReference failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a Shorts dislike Spannable is created
|
||||
* Called when a Shorts dislike Spanned is created.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned dislike) {
|
||||
return ReturnYouTubeDislike.onShortsComponentCreated(dislike);
|
||||
public static Spanned onShortsComponentCreated(Spanned original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForShort(original);
|
||||
if (replacement != null) {
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when the like/dislike button is clicked
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote -1 (dislike), 0 (none) or 1 (like)
|
||||
* @param vote int that matches {@link ReturnYouTubeDislike.Vote#value}
|
||||
*/
|
||||
public static void sendVote(int vote) {
|
||||
ReturnYouTubeDislike.sendVote(vote);
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
ReturnYouTubeDislike.sendVote(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogHelper.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
@ -83,13 +81,8 @@ public class SpoofSignatureVerificationPatch {
|
||||
}
|
||||
|
||||
SettingsEnum.SIGNATURE_SPOOFING.saveValue(true);
|
||||
ReVancedUtils.runOnMainThread(() -> {
|
||||
Toast.makeText(
|
||||
ReVancedUtils.getContext(),
|
||||
"Spoofing app signature to prevent playback issues", Toast.LENGTH_LONG
|
||||
).show();
|
||||
// it would be great if the video could be forcefully reloaded, but currently there is no code to do this
|
||||
});
|
||||
ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues");
|
||||
// it would be great if the video could be forcefully reloaded, but currently there is no code to do this
|
||||
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onResponse failure", ex);
|
||||
|
@ -1,35 +1,42 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/**
|
||||
* Hooking class for the current playing video.
|
||||
*/
|
||||
public final class VideoInformation {
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
private static final String SEEK_METHOD_NAME = "seekTo";
|
||||
|
||||
private static WeakReference<Object> playerController;
|
||||
private static Method seekMethod;
|
||||
|
||||
@NonNull
|
||||
private static String videoId = "";
|
||||
private static long videoLength = 1;
|
||||
private static long videoTime = -1;
|
||||
|
||||
private static long videoLength = 0;
|
||||
private static volatile long videoTime = -1; // must be volatile. Value is set off main thread from high precision patch hook
|
||||
/**
|
||||
* The current playback speed
|
||||
*/
|
||||
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
|
||||
/**
|
||||
* Hook into PlayerController.onCreate() method.
|
||||
* Injection point.
|
||||
* Sets a reference to the YouTube playback controller.
|
||||
*
|
||||
* @param thisRef Reference to the player controller object.
|
||||
*/
|
||||
public static void playerController_onCreateHook(final Object thisRef) {
|
||||
playerController = new WeakReference<>(thisRef);
|
||||
videoLength = 1;
|
||||
videoLength = 0;
|
||||
videoTime = -1;
|
||||
|
||||
try {
|
||||
@ -41,81 +48,146 @@ public final class VideoInformation {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video id.
|
||||
* Injection point.
|
||||
*
|
||||
* @param videoId The id of the video.
|
||||
* @param newlyLoadedVideoId id of the current video
|
||||
*/
|
||||
public static void setVideoId(String videoId) {
|
||||
LogHelper.printDebug(() -> "Setting current video id to: " + videoId);
|
||||
|
||||
VideoInformation.videoId = videoId;
|
||||
public static void setVideoId(@NonNull String newlyLoadedVideoId) {
|
||||
if (!videoId.equals(newlyLoadedVideoId)) {
|
||||
LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId);
|
||||
videoId = newlyLoadedVideoId;
|
||||
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video length.
|
||||
* Injection point.
|
||||
* Called when user selects a playback speed.
|
||||
*
|
||||
* @param userSelectedPlaybackSpeed The playback speed the user selected
|
||||
*/
|
||||
public static void userSelectedPlaybackSpeed(float userSelectedPlaybackSpeed) {
|
||||
LogHelper.printDebug(() -> "User selected playback speed: " + userSelectedPlaybackSpeed);
|
||||
playbackSpeed = userSelectedPlaybackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the current playback speed.
|
||||
*
|
||||
* <b> Used exclusively by {@link RememberPlaybackSpeedPatch} </b>
|
||||
*/
|
||||
public static void overridePlaybackSpeed(float speedOverride) {
|
||||
if (playbackSpeed != speedOverride) {
|
||||
LogHelper.printDebug(() -> "Overriding playback speed to: " + speedOverride);
|
||||
playbackSpeed = speedOverride;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param length The length of the video in milliseconds.
|
||||
*/
|
||||
public static void setVideoLength(final long length) {
|
||||
LogHelper.printDebug(() -> "Setting current video length to " + length);
|
||||
videoLength = length;
|
||||
if (videoLength != length) {
|
||||
LogHelper.printDebug(() -> "Current video length: " + length);
|
||||
videoLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the video time.
|
||||
* Injection point.
|
||||
* Called off the main thread approximately every 50ms to 100ms
|
||||
*
|
||||
* @param time The time of the video in milliseconds.
|
||||
* @param currentPlaybackTime The current playback time of the video in milliseconds.
|
||||
*/
|
||||
public static void setVideoTime(final long time) {
|
||||
LogHelper.printDebug(() -> "Current video time " + time);
|
||||
videoTime = time;
|
||||
public static void setVideoTimeHighPrecision(final long currentPlaybackTime) {
|
||||
videoTime = currentPlaybackTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek on the current video.
|
||||
* Does not function for playback of Shorts or Stories.
|
||||
*
|
||||
* Caution: If called from a videoTimeHook() callback,
|
||||
* this will cause a recursive call into the same videoTimeHook() callback.
|
||||
*
|
||||
* @param millisecond The millisecond to seek the video to.
|
||||
* @return if the seek was successful
|
||||
*/
|
||||
public static void seekTo(final long millisecond) {
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
if (seekMethod == null) {
|
||||
LogHelper.printDebug(() -> "seekMethod was null");
|
||||
return;
|
||||
}
|
||||
public static boolean seekTo(final long millisecond) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
if (seekMethod == null) {
|
||||
LogHelper.printException(() -> "seekMethod was null");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Seeking to " + millisecond);
|
||||
seekMethod.invoke(playerController.get(), millisecond);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to seek", ex);
|
||||
}
|
||||
});
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Seeking to " + millisecond);
|
||||
return (Boolean) seekMethod.invoke(playerController.get(), millisecond);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to seek", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean seekToRelative(long millisecondsRelative) {
|
||||
return seekTo(videoTime + millisecondsRelative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the current video playing.
|
||||
* Id of the current video playing. Includes Shorts and YouTube Stories.
|
||||
*
|
||||
* @return The id of the video. Empty string if not set yet.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getCurrentVideoId() {
|
||||
return videoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the current video playing.
|
||||
* @return The current playback speed.
|
||||
*/
|
||||
public static float getCurrentPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the current video playing.
|
||||
* Includes Shorts playback.
|
||||
*
|
||||
* @return The length of the video in milliseconds. 1 if not set yet.
|
||||
* @return The length of the video in milliseconds.
|
||||
* If the video is not yet loaded, or if the video is playing in the background with no video visible,
|
||||
* then this returns zero.
|
||||
*/
|
||||
public static long getCurrentVideoLength() {
|
||||
return videoLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the current video playing.
|
||||
* Playback time of the current video playing.
|
||||
* Value can lag up to approximately 100ms behind the actual current video playback time.
|
||||
*
|
||||
* Note: Code inside a videoTimeHook patch callback
|
||||
* should use the callback video time and avoid using this method
|
||||
* (in situations of recursive hook callbacks, the value returned here may be outdated).
|
||||
*
|
||||
* Includes Shorts playback.
|
||||
*
|
||||
* @return The time of the video in milliseconds. -1 if not set yet.
|
||||
*/
|
||||
public static long getVideoTime() {
|
||||
return videoTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the playback is at the end of the video.
|
||||
*
|
||||
* If video is playing in the background with no video visible,
|
||||
* this always returns false (even if the video is actually at the end)
|
||||
*/
|
||||
public static boolean isAtEndOfVideo() {
|
||||
return videoTime > 0 && videoLength > 0 && videoTime >= videoLength;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
package app.revanced.integrations.patches.downloads.views
|
||||
|
||||
class DownloadOptions {
|
||||
}
|
@ -1,163 +1,168 @@
|
||||
package app.revanced.integrations.patches.playback.quality;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.widget.Toast;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.NetworkType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class RememberVideoQualityPatch {
|
||||
private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
|
||||
private static final SettingsEnum wifiQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_WIFI;
|
||||
private static final SettingsEnum mobileQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_MOBILE;
|
||||
|
||||
public static int selectedQuality1 = -2;
|
||||
private static Boolean newVideo = false;
|
||||
private static Boolean userChangedQuality = false;
|
||||
private static boolean qualityNeedsUpdating;
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
|
||||
public static void changeDefaultQuality(int defaultQuality) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
/**
|
||||
* If the user selected a new quality from the flyout menu,
|
||||
* and {@link SettingsEnum#VIDEO_QUALITY_REMEMBER_LAST_SELECTED} is enabled.
|
||||
*/
|
||||
private static boolean userChangedDefaultQuality;
|
||||
|
||||
var networkType = getNetworType(context);
|
||||
/**
|
||||
* Index of the video quality chosen by the user from the flyout menu.
|
||||
*/
|
||||
private static int userSelectedQualityIndex;
|
||||
|
||||
/**
|
||||
* The available qualities of the current video in human readable form: [1080, 720, 480]
|
||||
*/
|
||||
@Nullable
|
||||
private static List<Integer> videoQualities;
|
||||
|
||||
private static void changeDefaultQuality(int defaultQuality) {
|
||||
NetworkType networkType = ReVancedUtils.getNetworkType();
|
||||
if (networkType == NetworkType.NONE) {
|
||||
String message = "No internet connection.";
|
||||
LogHelper.printDebug(() -> message);
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
var preferenceKey = "wifi_quality";
|
||||
var networkTypeMessage = "WIFI";
|
||||
|
||||
if (networkType == NetworkType.MOBILE) {
|
||||
networkTypeMessage = "mobile";
|
||||
preferenceKey = "mobile_quality";
|
||||
}
|
||||
|
||||
SharedPrefHelper.saveString(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, defaultQuality + "");
|
||||
String message = "Changing default " + networkTypeMessage + " quality to: " + defaultQuality;
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
ReVancedUtils.showToastShort("No internet connection");
|
||||
return;
|
||||
}
|
||||
|
||||
userChangedQuality = false;
|
||||
String networkTypeMessage;
|
||||
if (networkType == NetworkType.MOBILE) {
|
||||
mobileQualitySetting.saveValue(defaultQuality);
|
||||
networkTypeMessage = "mobile";
|
||||
} else {
|
||||
wifiQualitySetting.saveValue(defaultQuality);
|
||||
networkTypeMessage = "Wi-Fi";
|
||||
}
|
||||
ReVancedUtils.showToastShort("Changed default " + networkTypeMessage
|
||||
+ " quality to: " + defaultQuality +"p");
|
||||
}
|
||||
|
||||
public static int setVideoQuality(Object[] qualities, int quality, Object qInterface, String qIndexMethod) {
|
||||
Field[] fields;
|
||||
|
||||
if (!(newVideo || userChangedQuality) || qInterface == null) {
|
||||
return quality;
|
||||
}
|
||||
|
||||
Class<?> intType = Integer.TYPE;
|
||||
ArrayList<Integer> iStreamQualities = new ArrayList<>();
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
|
||||
* @param originalQualityIndex quality index to use, as chosen by YouTube
|
||||
*/
|
||||
public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
|
||||
try {
|
||||
for (Object streamQuality : qualities) {
|
||||
for (Field field : streamQuality.getClass().getFields()) {
|
||||
if (field.getType().isAssignableFrom(intType)) { // converts quality index to actual readable resolution
|
||||
int value = field.getInt(streamQuality);
|
||||
if (field.getName().length() <= 2) {
|
||||
iStreamQualities.add(value);
|
||||
if (!(qualityNeedsUpdating || userChangedDefaultQuality) || qInterface == null) {
|
||||
return originalQualityIndex;
|
||||
}
|
||||
qualityNeedsUpdating = false;
|
||||
|
||||
final int preferredQuality;
|
||||
if (ReVancedUtils.getNetworkType() == NetworkType.MOBILE) {
|
||||
preferredQuality = mobileQualitySetting.getInt();
|
||||
} else {
|
||||
preferredQuality = wifiQualitySetting.getInt();
|
||||
}
|
||||
if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||
return originalQualityIndex; // nothing to do
|
||||
}
|
||||
|
||||
if (videoQualities == null || videoQualities.size() != qualities.length) {
|
||||
videoQualities = new ArrayList<>(qualities.length);
|
||||
for (Object streamQuality : qualities) {
|
||||
for (Field field : streamQuality.getClass().getFields()) {
|
||||
if (field.getType().isAssignableFrom(Integer.TYPE)
|
||||
&& field.getName().length() <= 2) {
|
||||
videoQualities.add(field.getInt(streamQuality));
|
||||
}
|
||||
}
|
||||
}
|
||||
LogHelper.printDebug(() -> "VideoId: " + currentVideoId + " videoQualities: " + videoQualities);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
Collections.sort(iStreamQualities);
|
||||
int index = 0;
|
||||
if (userChangedQuality) {
|
||||
for (int convertedQuality : iStreamQualities) {
|
||||
int selectedQuality2 = qualities.length - selectedQuality1 + 1;
|
||||
index++;
|
||||
if (selectedQuality2 == index) {
|
||||
final int indexToLog = index; // must be final for lambda
|
||||
LogHelper.printDebug(() -> "Quality index is: " + indexToLog + " and corresponding value is: " + convertedQuality);
|
||||
changeDefaultQuality(convertedQuality);
|
||||
return selectedQuality2;
|
||||
|
||||
if (userChangedDefaultQuality) {
|
||||
userChangedDefaultQuality = false;
|
||||
final int quality = videoQualities.get(userSelectedQualityIndex);
|
||||
LogHelper.printDebug(() -> "User changed default quality to: " + quality);
|
||||
changeDefaultQuality(quality);
|
||||
return userSelectedQualityIndex;
|
||||
}
|
||||
|
||||
// find the highest quality that is equal to or less than the preferred
|
||||
int qualityToUse = videoQualities.get(0); // first element is automatic mode
|
||||
int qualityIndexToUse = 0;
|
||||
int i = 0;
|
||||
for (Integer quality : videoQualities) {
|
||||
if (quality <= preferredQuality && qualityToUse < quality) {
|
||||
qualityToUse = quality;
|
||||
qualityIndexToUse = i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (qualityIndexToUse == originalQualityIndex) {
|
||||
LogHelper.printDebug(() -> "Video is already preferred quality: " + preferredQuality);
|
||||
return originalQualityIndex;
|
||||
}
|
||||
}
|
||||
newVideo = false;
|
||||
final int qualityToLog = quality;
|
||||
LogHelper.printDebug(() -> "Quality: " + qualityToLog);
|
||||
Context context = ReVancedUtils.getContext();
|
||||
if (context == null) {
|
||||
LogHelper.printException(() -> "Context is null or settings not initialized, returning quality: " + qualityToLog);
|
||||
return quality;
|
||||
}
|
||||
var networkType = getNetworType(context);
|
||||
if (networkType == NetworkType.NONE) {
|
||||
LogHelper.printDebug(() -> "No Internet connection!");
|
||||
return quality;
|
||||
} else {
|
||||
var preferenceKey = "wifi_quality";
|
||||
if (networkType == NetworkType.MOBILE) preferenceKey = "mobile_quality";
|
||||
|
||||
int preferredQuality = SharedPrefHelper.getInt(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, -2);
|
||||
if (preferredQuality == -2) return quality;
|
||||
final int qualityToUseLog = qualityToUse;
|
||||
LogHelper.printDebug(() -> "Quality changed from: "
|
||||
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog);
|
||||
|
||||
for (int streamQuality2 : iStreamQualities) {
|
||||
final int indexToLog = index;
|
||||
LogHelper.printDebug(() -> "Quality at index " + indexToLog + ": " + streamQuality2);
|
||||
index++;
|
||||
}
|
||||
for (Integer iStreamQuality : iStreamQualities) {
|
||||
int streamQuality3 = iStreamQuality;
|
||||
if (streamQuality3 <= preferredQuality) {
|
||||
quality = streamQuality3;
|
||||
}
|
||||
}
|
||||
if (quality == -2) return quality;
|
||||
|
||||
int qualityIndex = iStreamQualities.indexOf(quality);
|
||||
final int qualityToLog2 = quality;
|
||||
LogHelper.printDebug(() -> "Index of quality " + qualityToLog2 + " is " + qualityIndex);
|
||||
try {
|
||||
Class<?> cl = qInterface.getClass();
|
||||
Method m = cl.getMethod(qIndexMethod, Integer.TYPE);
|
||||
LogHelper.printDebug(() -> "Method is: " + qIndexMethod);
|
||||
m.invoke(qInterface, iStreamQualities.get(qualityIndex));
|
||||
LogHelper.printDebug(() -> "Quality changed to: " + qualityIndex);
|
||||
return qualityIndex;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to set quality", ex);
|
||||
return qualityIndex;
|
||||
}
|
||||
Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
|
||||
m.invoke(qInterface, qualityToUse);
|
||||
return qualityIndexToUse;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to set quality", ex);
|
||||
return originalQualityIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void userChangedQuality(int selectedQuality) {
|
||||
if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return;
|
||||
if (!SettingsEnum.VIDEO_QUALITY_REMEMBER_LAST_SELECTED.getBoolean()) return;
|
||||
|
||||
selectedQuality1 = selectedQuality;
|
||||
userChangedQuality = true;
|
||||
userSelectedQualityIndex = selectedQuality;
|
||||
userChangedDefaultQuality = true;
|
||||
}
|
||||
|
||||
public static void newVideoStarted(String videoId) {
|
||||
newVideo = true;
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
qualityNeedsUpdating = true;
|
||||
|
||||
private static NetworkType getNetworType(Context context) {
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
var networkInfo = cm.getActiveNetworkInfo();
|
||||
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
return NetworkType.NONE;
|
||||
} else {
|
||||
var type = networkInfo.getType();
|
||||
|
||||
return type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_BLUETOOTH ? NetworkType.MOBILE : NetworkType.OTHER;
|
||||
if (!videoId.equals(currentVideoId)) {
|
||||
currentVideoId = videoId;
|
||||
videoQualities = null;
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkType {
|
||||
MOBILE,
|
||||
OTHER,
|
||||
NONE
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
public class CustomVideoSpeedPatch {
|
||||
// Values are useless as they are being overridden by the respective patch.
|
||||
// This generates a .array segment in Dalvik bytecode
|
||||
// which the patch utilizes to store the video speeds in, only
|
||||
// if it has two or more default values.
|
||||
public static final float[] videoSpeeds = { 0, 0 };
|
||||
/**
|
||||
* Default playback speeds offered by YouTube.
|
||||
* Values are also used by {@link RememberPlaybackSpeedPatch}.
|
||||
*
|
||||
* If custom video speed is applied,
|
||||
* then this array is overwritten by the patch with custom speeds
|
||||
*/
|
||||
public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
||||
}
|
||||
|
@ -1,40 +1,24 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
import android.widget.Toast;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* The current playback speed
|
||||
* PreferenceList entries and values, of all available playback speeds.
|
||||
*/
|
||||
private static float currentPlaybackSpeed = getLastRememberedPlaybackSpeed();
|
||||
|
||||
private final static float DEFAULT_PLAYBACK_SPEED = (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getDefaultValue();
|
||||
private static String[] preferenceListEntries, preferenceListEntryValues;
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
|
||||
private static void showToast(final String message) {
|
||||
Toast.makeText(ReVancedUtils.getContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private static float getLastRememberedPlaybackSpeed() {
|
||||
return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat();
|
||||
}
|
||||
|
||||
private static void rememberPlaybackSpeed() {
|
||||
SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.saveValue(currentPlaybackSpeed);
|
||||
}
|
||||
|
||||
private static boolean rememberLastSelectedPlaybackSpeed() {
|
||||
return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when a new video loads.
|
||||
@ -43,41 +27,51 @@ public final class RememberPlaybackSpeedPatch {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentVideoId = videoId;
|
||||
currentPlaybackSpeed = getLastRememberedPlaybackSpeed();
|
||||
VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when a playback speed is selected.
|
||||
* Called when user selects a playback speed.
|
||||
*
|
||||
* @param playbackSpeed The playback speed to set.
|
||||
* @param playbackSpeed The playback speed the user selected
|
||||
*/
|
||||
public static void setPlaybackSpeed(final float playbackSpeed) {
|
||||
LogHelper.printDebug(() -> "Playback speed changed to: " + playbackSpeed);
|
||||
|
||||
currentPlaybackSpeed = playbackSpeed;
|
||||
|
||||
if (rememberLastSelectedPlaybackSpeed()) {
|
||||
rememberPlaybackSpeed();
|
||||
|
||||
showToast("Remembering playback speed: " + playbackSpeed + "x");
|
||||
} else {
|
||||
if (getLastRememberedPlaybackSpeed() == DEFAULT_PLAYBACK_SPEED) return;
|
||||
|
||||
showToast("Applying playback speed: " + playbackSpeed + "x");
|
||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||
if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) {
|
||||
SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed);
|
||||
ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Called when playback first starts,
|
||||
* and also called immediately after the user selects a new video speed.
|
||||
*
|
||||
* @return The currently set playback speed.
|
||||
* Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed
|
||||
*/
|
||||
public static float getCurrentPlaybackSpeed() {
|
||||
return currentPlaybackSpeed;
|
||||
public static float getPlaybackSpeedOverride() {
|
||||
return VideoInformation.getCurrentPlaybackSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,7 @@ public class Requester {
|
||||
String url = apiUrl + route.compile(params).getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
// TODO: change the user agent string
|
||||
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
|
||||
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced");
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package app.revanced.integrations.returnyoutubedislike;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
@ -31,7 +31,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
@ -41,10 +40,13 @@ import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
/**
|
||||
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
|
||||
*/
|
||||
public class ReturnYouTubeDislike {
|
||||
/**
|
||||
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
|
||||
* <p>
|
||||
*
|
||||
* Must be less than 5 seconds, as per:
|
||||
* https://developer.android.com/topic/performance/vitals/anr
|
||||
*/
|
||||
@ -52,18 +54,17 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
|
||||
* Can be any almost any non-visible character
|
||||
* Can be any almost any non-visible character.
|
||||
*/
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character
|
||||
|
||||
/**
|
||||
* Used to send votes, one by one, in the same order the user created them
|
||||
* Used to send votes, one by one, in the same order the user created them.
|
||||
*/
|
||||
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture},
|
||||
* as multiple threads access this class.
|
||||
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}.
|
||||
*/
|
||||
private static final Object videoIdLockObject = new Object();
|
||||
|
||||
@ -71,14 +72,13 @@ public class ReturnYouTubeDislike {
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static String currentVideoId;
|
||||
|
||||
|
||||
/**
|
||||
* If {@link #currentVideoId} and the RYD data is for the last shorts loaded
|
||||
* If {@link #currentVideoId} and the RYD data is for the last shorts loaded.
|
||||
*/
|
||||
private static volatile boolean lastVideoLoadedWasShort;
|
||||
|
||||
/**
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
@ -86,18 +86,30 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Original dislike span, before modifications.
|
||||
* Required for segmented layout
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Spanned originalDislikeSpan;
|
||||
|
||||
/**
|
||||
* Replacement like/dislike span that includes formatted dislikes and is ready to display
|
||||
* Replacement like/dislike span that includes formatted dislikes.
|
||||
* Used to prevent recreating the same span multiple times.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Spanned replacementLikeDislikeSpan;
|
||||
private static SpannableString replacementLikeDislikeSpan;
|
||||
|
||||
/**
|
||||
* For formatting dislikes as number.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* For formatting dislikes as percentage.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class")
|
||||
private static NumberFormat dislikePercentageFormatter;
|
||||
|
||||
public enum Vote {
|
||||
LIKE(1),
|
||||
@ -114,18 +126,6 @@ public class ReturnYouTubeDislike {
|
||||
private ReturnYouTubeDislike() {
|
||||
} // only static methods
|
||||
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class")
|
||||
private static NumberFormat dislikePercentageFormatter;
|
||||
|
||||
public static void onEnabledChange(boolean enabled) {
|
||||
if (!enabled) {
|
||||
// Must clear old values, to protect against using stale data
|
||||
@ -147,6 +147,18 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called after a user dislikes, or if the user changes settings for dislikes appearance.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
synchronized (videoIdLockObject) {
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
LogHelper.printDebug(() -> "Clearing cache");
|
||||
}
|
||||
replacementLikeDislikeSpan = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getCurrentVideoId() {
|
||||
synchronized (videoIdLockObject) {
|
||||
@ -162,136 +174,125 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
PlayerType currentPlayerType = PlayerType.getCurrent();
|
||||
if (currentPlayerType == PlayerType.INLINE_MINIMAL) {
|
||||
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + 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
|
||||
}
|
||||
if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id
|
||||
LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId);
|
||||
setCurrentVideoId(null);
|
||||
return;
|
||||
}
|
||||
synchronized (videoIdLockObject) {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return; // already loaded
|
||||
}
|
||||
LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
|
||||
setCurrentVideoId(videoId);
|
||||
// no need to wrap the call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
|
||||
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.
|
||||
lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden();
|
||||
|
||||
// No need to wrap the call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call.
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added)
|
||||
* @return NULL if the span does not need changing or if RYD is not available.
|
||||
*/
|
||||
public static void onComponentCreated(@NonNull Object conversionContext, @NonNull AtomicReference<Object> textRef) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
|
||||
// do not set lastVideoLoadedWasShort to false. It will be cleared when the next regular video is loaded.
|
||||
if (lastVideoLoadedWasShort || PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
final boolean isSegmentedButton;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
isSegmentedButton = true;
|
||||
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||
isSegmentedButton = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
Spanned replacement = waitForFetchAndUpdateReplacementSpan((Spanned) textRef.get(), isSegmentedButton);
|
||||
if (replacement != null) {
|
||||
textRef.set(replacement);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onComponentCreated failure", ex);
|
||||
@Nullable
|
||||
public static SpannableString getDislikeSpanForContext(@NonNull Object conversionContext, @NonNull CharSequence original) {
|
||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return null;
|
||||
}
|
||||
String conversionContextString = conversionContext.toString();
|
||||
final boolean isSegmentedButton;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
isSegmentedButton = true;
|
||||
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||
isSegmentedButton = false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastVideoLoadedWasShort) {
|
||||
// user:
|
||||
// 1, opened a video
|
||||
// 2. opened a short (without closing the regular video)
|
||||
// 3. closed the short
|
||||
// 4. regular video is now present, but the videoId and RYD data is still for the short
|
||||
LogHelper.printDebug(() -> "Ignoring getDislikeSpanForContext(), as data loaded is for prior short");
|
||||
return null;
|
||||
}
|
||||
|
||||
return waitForFetchAndUpdateReplacementSpan((Spannable) original, isSegmentedButton);
|
||||
}
|
||||
|
||||
public static Spanned onShortsComponentCreated(Spanned span) {
|
||||
try {
|
||||
if (SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
lastVideoLoadedWasShort = true;
|
||||
Spanned replacement = waitForFetchAndUpdateReplacementSpan(span, false);
|
||||
if (replacement != null) {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
}
|
||||
return span;
|
||||
/**
|
||||
* Called when a Shorts dislike Spannable is created.
|
||||
*/
|
||||
public static SpannableString getDislikeSpanForShort(@NonNull Spanned original) {
|
||||
lastVideoLoadedWasShort = true; // it's now certain the video and data are a short
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false);
|
||||
}
|
||||
|
||||
// alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick
|
||||
// Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick.
|
||||
private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) {
|
||||
return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NULL if the span does not need changing or if RYD is not available
|
||||
* @return NULL if the span does not need changing or if RYD is not available.
|
||||
*/
|
||||
@Nullable
|
||||
private static Spanned waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) {
|
||||
if (oldSpannable == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes (injection code was called with null Span)");
|
||||
return null;
|
||||
}
|
||||
// Must block the current thread until fetching is done
|
||||
// There's no known way to edit the text after creation yet
|
||||
long fetchStartTime = 0;
|
||||
private static SpannableString waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) {
|
||||
try {
|
||||
synchronized (videoIdLockObject) {
|
||||
if (oldSpannable.equals(replacementLikeDislikeSpan)) {
|
||||
LogHelper.printDebug(() -> "Ignoring previously created dislike span");
|
||||
return null;
|
||||
}
|
||||
if (isSegmentedButton) {
|
||||
if (isPreviouslyCreatedSegmentedSpan(oldSpannable)) {
|
||||
// need to recreate using original, as oldSpannable has prior outdated dislike values
|
||||
oldSpannable = originalDislikeSpan;
|
||||
if (oldSpannable == null) {
|
||||
// Regular video is opened, then a short is opened then closed,
|
||||
// then the app is closed then reopened (causes a call of NewVideoId() of the original videoId)
|
||||
// The original video (that was opened the entire time), is still showing the dislikes count
|
||||
// but the oldSpannable is now null because it was reset when the videoId was set again
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes - original span is null" +
|
||||
" (short was opened/closed, then app was closed/opened?) "); // ignore, with no toast
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
originalDislikeSpan = oldSpannable; // most up to date original
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
String oldSpannableString = oldSpannable.toString();
|
||||
if (replacementLikeDislikeSpan.toString().equals(oldSpannableString)) {
|
||||
LogHelper.printDebug(() -> "Ignoring previously created dislikes span");
|
||||
return null;
|
||||
}
|
||||
if (originalDislikeSpan.toString().equals(oldSpannableString)) {
|
||||
LogHelper.printDebug(() -> "Replacing span with previously created dislike span");
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
}
|
||||
if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(oldSpannable)) {
|
||||
// need to recreate using original, as oldSpannable has prior outdated dislike values
|
||||
oldSpannable = originalDislikeSpan;
|
||||
if (oldSpannable == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen
|
||||
return null;
|
||||
}
|
||||
} 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 null;
|
||||
}
|
||||
if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) {
|
||||
fetchStartTime = System.currentTimeMillis();
|
||||
}
|
||||
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 null;
|
||||
}
|
||||
|
||||
Spanned replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
|
||||
SpannableString replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData);
|
||||
synchronized (videoIdLockObject) {
|
||||
replacementLikeDislikeSpan = replacement;
|
||||
}
|
||||
@ -301,40 +302,23 @@ public class ReturnYouTubeDislike {
|
||||
} catch (TimeoutException e) {
|
||||
LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "createReplacementSpan failure", e); // should never happen
|
||||
} finally {
|
||||
recordTimeUISpentWaitingForNetworkCall(fetchStartTime);
|
||||
LogHelper.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void sendVote(int vote) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
|
||||
try {
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
sendVote(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogHelper.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendVote(@NonNull Vote vote) {
|
||||
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
|
||||
// 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 || (lastVideoLoadedWasShort && !PlayerType.getCurrent().isNoneOrHidden())) {
|
||||
if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
// 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
|
||||
LogHelper.printException(() -> "Cannot send vote",
|
||||
null, str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
|
||||
// Or shorts was loaded with regular video present, then shorts was closed,
|
||||
// and then user voted on the now visible original video.
|
||||
// Cannot send a vote, because the loaded videoId is for the wrong video.
|
||||
ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -349,17 +333,15 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (videoIdLockObject) {
|
||||
replacementLikeDislikeSpan = null; // ui values need updating
|
||||
}
|
||||
clearCache(); // UI needs updating
|
||||
|
||||
// update the downloaded vote data
|
||||
// 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
|
||||
// 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
|
||||
@ -373,21 +355,21 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered.
|
||||
*
|
||||
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
||||
* and the network call fails, this returns NULL
|
||||
* and the network call fails, this returns NULL.
|
||||
*/
|
||||
@Nullable
|
||||
private static String getUserId() {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
|
||||
String userId = SettingsEnum.RYD_USER_ID.getString();
|
||||
if (userId != null) {
|
||||
if (!userId.isEmpty()) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed
|
||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser();
|
||||
if (userId != null) {
|
||||
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
||||
}
|
||||
@ -395,25 +377,25 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike
|
||||
* @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike.
|
||||
*/
|
||||
private static Spanned createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
|
||||
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
|
||||
if (!isSegmentedButton) {
|
||||
// simple replacement of 'dislike' with a number/percentage
|
||||
// Simple replacement of 'dislike' with a number/percentage.
|
||||
return newSpannableWithDislikes(oldSpannable, voteData);
|
||||
}
|
||||
|
||||
// note: some locales use right to left layout (arabic, hebrew, etc),
|
||||
// and care must be taken to retain the existing RTL encoding character on the likes string
|
||||
// otherwise text will incorrectly show as left to right
|
||||
// if making changes to this code, change device settings to a RTL language and verify layout is correct
|
||||
// Note: Some locales use right to left layout (arabic, hebrew, etc),
|
||||
// and care must be taken to retain the existing RTL encoding character on the likes string,
|
||||
// otherwise text will incorrectly show as left to right.
|
||||
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
|
||||
String oldLikesString = oldSpannable.toString();
|
||||
|
||||
// YouTube creators can hide the like count on a video,
|
||||
// and the like count appears as a device language specific string that says 'Like'
|
||||
// check if the string contains any numbers
|
||||
// and the like count appears as a device language specific string that says 'Like'.
|
||||
// Check if the string contains any numbers.
|
||||
if (!stringContainsNumber(oldLikesString)) {
|
||||
// likes are hidden.
|
||||
// Likes are hidden.
|
||||
// RYD does not provide usable data for these types of videos,
|
||||
// and the API returns bogus data (zero likes and zero dislikes)
|
||||
// discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530
|
||||
@ -421,7 +403,7 @@ public class ReturnYouTubeDislike {
|
||||
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
|
||||
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
|
||||
//
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden.
|
||||
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
|
||||
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
|
||||
}
|
||||
@ -431,7 +413,7 @@ public class ReturnYouTubeDislike {
|
||||
final int separatorColor = ThemeHelper.isDarkTheme()
|
||||
? 0x29AAAAAA // transparent dark gray
|
||||
: 0xFFD9D9D9; // light gray
|
||||
DisplayMetrics dp = ReVancedUtils.getContext().getResources().getDisplayMetrics();
|
||||
DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics();
|
||||
|
||||
if (!compactLayout) {
|
||||
// left separator
|
||||
@ -475,9 +457,9 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly handles any unicode numbers (such as Arabic numbers)
|
||||
* Correctly handles any unicode numbers (such as Arabic numbers).
|
||||
*
|
||||
* @return if the string contains at least 1 number
|
||||
* @return if the string contains at least 1 number.
|
||||
*/
|
||||
private static boolean stringContainsNumber(@NonNull String text) {
|
||||
for (int index = 0, length = text.length(); index < length; index++) {
|
||||
@ -488,14 +470,14 @@ public class ReturnYouTubeDislike {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Spannable newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
|
||||
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
|
||||
? formatDislikePercentage(voteData.getDislikePercentage())
|
||||
: formatDislikeCount(voteData.getDislikeCount()));
|
||||
}
|
||||
|
||||
private static Spannable newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) {
|
||||
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) {
|
||||
SpannableString destination = new SpannableString(newSpanText);
|
||||
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
|
||||
for (Object span : spans) {
|
||||
@ -509,10 +491,10 @@ public class ReturnYouTubeDislike {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikeCountFormatter == null) {
|
||||
// Note: Java number formatters will use the locale specific number characters.
|
||||
// such as Arabic which formats "1.2" into "١٫٢"
|
||||
// such as Arabic which formats "1.234" into "۱,۲۳٤"
|
||||
// But YouTube disregards locale specific number characters
|
||||
// and instead shows english number characters everywhere.
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
}
|
||||
@ -527,7 +509,7 @@ public class ReturnYouTubeDislike {
|
||||
private static String formatDislikePercentage(float dislikePercentage) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||
}
|
||||
@ -539,33 +521,6 @@ public class ReturnYouTubeDislike {
|
||||
return dislikePercentageFormatter.format(dislikePercentage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Number of times the UI was forced to wait on a network fetch to complete
|
||||
*/
|
||||
private static volatile int numberOfTimesUIWaitedOnNetworkCalls;
|
||||
|
||||
/**
|
||||
* Total time the UI waited, of all times it was forced to wait.
|
||||
*/
|
||||
private static volatile long totalTimeUIWaitedOnNetworkCalls;
|
||||
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField")
|
||||
private static void recordTimeUISpentWaitingForNetworkCall(long timeUIWaitStarted) {
|
||||
if (timeUIWaitStarted == 0 || !SettingsEnum.DEBUG.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
final long timeUIWaitingTotal = System.currentTimeMillis() - timeUIWaitStarted;
|
||||
LogHelper.printDebug(() -> "UI thread waited for: " + timeUIWaitingTotal + "ms for vote fetch to complete");
|
||||
|
||||
totalTimeUIWaitedOnNetworkCalls += timeUIWaitingTotal;
|
||||
numberOfTimesUIWaitedOnNetworkCalls++;
|
||||
final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls;
|
||||
LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, "
|
||||
+ "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, "
|
||||
+ "average wait time: " + averageTimeForcedToWait + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
class VerticallyCenteredImageSpan extends ImageSpan {
|
||||
|
@ -1,12 +1,12 @@
|
||||
package app.revanced.integrations.returnyoutubedislike.requests;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@ -20,8 +20,10 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute;
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import app.revanced.integrations.requests.Requester;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class ReturnYouTubeDislikeApi {
|
||||
/**
|
||||
@ -192,9 +194,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
numberOfRateLimitRequestsEncountered++;
|
||||
LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next "
|
||||
+ RATE_LIMIT_BACKOFF_SECONDS + " seconds");
|
||||
ReVancedUtils.runOnMainThread(() -> { // must show toasts on main thread
|
||||
Toast.makeText(ReVancedUtils.getContext(), str("revanced_ryd_failure_client_rate_limit_requested"), Toast.LENGTH_LONG).show();
|
||||
});
|
||||
ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested"));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -203,7 +203,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField") // do not want to pay performance cost of full synchronization for debug fields that are only estimates anyways
|
||||
private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) {
|
||||
if (connectionError && rateLimitHit) {
|
||||
throw new IllegalArgumentException("both connection error and rate limit parameter were true");
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted;
|
||||
fetchCallResponseTimeTotal += responseTimeOfFetchCall;
|
||||
@ -320,7 +320,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
return confirmRegistration(userId, solution);
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to register new user: " + userId
|
||||
+ " response code was: " + responseCode);
|
||||
+ " response code was: " + responseCode); // failed attempt, and ok to log userId
|
||||
connection.disconnect();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to register user", ex);
|
||||
@ -337,7 +337,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("confirmRegistration")) {
|
||||
return null;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to confirm registration for user: " + userId + " with solution: " + solution);
|
||||
LogHelper.printDebug(() -> "Trying to confirm registration with solution: " + solution);
|
||||
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
@ -355,7 +355,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
|
||||
String result = Requester.parseJson(connection);
|
||||
if (result.equalsIgnoreCase("true")) {
|
||||
LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId);
|
||||
LogHelper.printDebug(() -> "Registration confirmation successful");
|
||||
return userId;
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
|
||||
@ -382,8 +382,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("sendVote")) {
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to vote for video: "
|
||||
+ videoId + " with vote: " + vote + " user: " + userId);
|
||||
LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote);
|
||||
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
@ -408,11 +407,10 @@ public class ReturnYouTubeDislikeApi {
|
||||
return confirmVote(videoId, userId, solution);
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
|
||||
+ " userId: " + userId + " vote: " + vote + " response code was: " + responseCode);
|
||||
+ " vote: " + vote + " response code was: " + responseCode);
|
||||
connection.disconnect(); // something went wrong, might as well disconnect
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
|
||||
+ " user: " + userId + " vote: " + vote, ex);
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -427,8 +425,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("confirmVote")) {
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to confirm vote for video: "
|
||||
+ videoId + " user: " + userId + " solution: " + solution);
|
||||
LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution);
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
|
||||
@ -450,15 +447,15 @@ public class ReturnYouTubeDislikeApi {
|
||||
return true;
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution + " response string was: " + result);
|
||||
+ " solution: " + solution + " response string was: " + result);
|
||||
} else {
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution + " response code was: " + responseCode);
|
||||
+ " solution: " + solution + " response code was: " + responseCode);
|
||||
}
|
||||
connection.disconnect(); // something went wrong, might as well disconnect
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution, ex);
|
||||
+ " solution: " + solution, ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.integrations.settings;
|
||||
|
||||
public enum ReturnType {
|
||||
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
STRING,
|
||||
LONG,
|
||||
FLOAT;
|
||||
}
|
@ -1,263 +1,396 @@
|
||||
package app.revanced.integrations.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.utils.StringRef;
|
||||
|
||||
public enum SettingsEnum {
|
||||
//Download Settings
|
||||
// TODO: DOWNLOAD_PATH("revanced_download_path", Environment.getExternalStorageDirectory().getPath() + "/Download", ReturnType.STRING),
|
||||
DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", true, ReturnType.BOOLEAN, true),
|
||||
DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", "org.schabi.newpipe" /* NewPipe */, ReturnType.STRING),
|
||||
// TODO: DOWNLOAD_PATH("revanced_download_path", STRING, Environment.getExternalStorageDirectory().getPath() + "/Download"),
|
||||
DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", BOOLEAN, TRUE, true),
|
||||
DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe" /* NewPipe */, parents(DOWNLOADS_BUTTON_SHOWN)),
|
||||
|
||||
// Copy video URL settings
|
||||
COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", true, ReturnType.BOOLEAN, true),
|
||||
COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", true, ReturnType.BOOLEAN, true),
|
||||
COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", BOOLEAN, TRUE, true),
|
||||
COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE, true),
|
||||
|
||||
// Video settings
|
||||
OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", true, ReturnType.BOOLEAN),
|
||||
REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", true, ReturnType.BOOLEAN),
|
||||
REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", true, ReturnType.BOOLEAN),
|
||||
REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE("revanced_remember_playback_speed_last_selected_value", 1.0f, ReturnType.FLOAT),
|
||||
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),
|
||||
|
||||
// TODO: Unused currently
|
||||
// Whitelist settings
|
||||
ENABLE_WHITELIST("revanced_whitelist_ads_enabled", false, ReturnType.BOOLEAN),
|
||||
//ENABLE_WHITELIST("revanced_whitelist_ads_enabled", BOOLEAN, FALSE),
|
||||
|
||||
// Ad settings
|
||||
ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", false, ReturnType.BOOLEAN),
|
||||
ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", "", ReturnType.STRING, true),
|
||||
VIDEO_ADS_REMOVAL("revanced_video_ads_removal", true, ReturnType.BOOLEAN, true),
|
||||
ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_COMMUNITY_POSTS_REMOVAL("revanced_adremover_community_posts_removal", false, ReturnType.BOOLEAN),
|
||||
ADREMOVER_COMPACT_BANNER_REMOVAL("revanced_adremover_compact_banner_removal", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_MOVIE_REMOVAL("revanced_adremover_movie", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_SHORTS_REMOVAL("revanced_adremover_shorts", true, ReturnType.BOOLEAN, true),
|
||||
ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_community_guidelines", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL("revanced_adremover_channel_member_shelf_removal", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_EMERGENCY_BOX_REMOVAL("revanced_adremover_emergency_box_removal", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_PAID_CONTENT_REMOVAL("revanced_adremover_paid_content", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_SELF_SPONSOR_REMOVAL("revanced_adremover_self_sponsor", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_CHAPTER_TEASER_REMOVAL("revanced_adremover_chapter_teaser", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_GRAY_SEPARATOR("revanced_adremover_separator", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_VIEW_PRODUCTS("revanced_adremover_view_products", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", true, ReturnType.BOOLEAN),
|
||||
ADREMOVER_CHANNEL_BAR("revanced_hide_channel_bar", false, ReturnType.BOOLEAN),
|
||||
ADREMOVER_QUICK_ACTIONS("revanced_hide_quick_actions", false, ReturnType.BOOLEAN),
|
||||
ADREMOVER_RELATED_VIDEOS("revanced_hide_related_videos", false, ReturnType.BOOLEAN),
|
||||
ADREMOVER_IMAGE_SHELF("revanced_hide_image_shelf", true, ReturnType.BOOLEAN),
|
||||
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),
|
||||
|
||||
// Action buttons
|
||||
HIDE_LIKE_BUTTON("revanced_hide_like_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_DISLIKE_BUTTON("revanced_hide_dislike_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_ACTION_BUTTON("revanced_hide_action_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_SHARE_BUTTON("revanced_hide_share_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_LIKE_DISLIKE_BUTTON("revanced_hide_like_dislike_button", BOOLEAN, FALSE),
|
||||
HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", BOOLEAN, FALSE),
|
||||
HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", BOOLEAN, FALSE),
|
||||
HIDE_ACTION_BUTTONS("revanced_hide_action_buttons", BOOLEAN, FALSE),
|
||||
|
||||
// Layout settings
|
||||
DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", false, ReturnType.BOOLEAN),
|
||||
PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", false, ReturnType.BOOLEAN),
|
||||
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", false, ReturnType.BOOLEAN, true),
|
||||
SPOOF_APP_VERSION("revanced_spoof_app_version", false, ReturnType.BOOLEAN, true),
|
||||
WIDE_SEARCHBAR("revanced_wide_searchbar", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_ALBUM_CARDS("revanced_hide_album_cards", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_ARTIST_CARDS("revanced_hide_artist_cards", false, ReturnType.BOOLEAN),
|
||||
HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", true, ReturnType.BOOLEAN),
|
||||
HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_CAST_BUTTON("revanced_hide_cast_button", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_COMMENTS_SECTION("revanced_hide_comments_section", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_CREATE_BUTTON("revanced_hide_create_button", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_EMAIL_ADDRESS("revanced_hide_email_address", false, ReturnType.BOOLEAN),
|
||||
HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", true, ReturnType.BOOLEAN),
|
||||
HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", true, ReturnType.BOOLEAN), //ToDo: Add to prefs
|
||||
HIDE_INFO_CARDS("revanced_hide_infocards", true, ReturnType.BOOLEAN),
|
||||
HIDE_MIX_PLAYLISTS("revanced_hide_mix_playlists", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_REEL_BUTTON("revanced_hide_reel_button", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", false, ReturnType.BOOLEAN),
|
||||
HIDE_TIMESTAMP("revanced_hide_timestamp", false, ReturnType.BOOLEAN),
|
||||
HIDE_SEEKBAR("revanced_hide_seekbar", false, ReturnType.BOOLEAN),
|
||||
HIDE_WATCH_IN_VR("revanced_hide_watch_in_vr", false, ReturnType.BOOLEAN, true),
|
||||
HIDE_BREAKING_NEWS("revanced_hide_breaking_news", true, ReturnType.BOOLEAN, true),
|
||||
HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", false, ReturnType.BOOLEAN),
|
||||
HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", true, ReturnType.BOOLEAN, true),
|
||||
DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", BOOLEAN, FALSE),
|
||||
HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true),
|
||||
HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE),
|
||||
HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", BOOLEAN, TRUE, true),
|
||||
HIDE_BREAKING_NEWS("revanced_hide_breaking_news", BOOLEAN, TRUE, true),
|
||||
HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", BOOLEAN, FALSE),
|
||||
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),
|
||||
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_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE),
|
||||
HIDE_MIX_PLAYLISTS("revanced_hide_mix_playlists", BOOLEAN, FALSE, true),
|
||||
HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", BOOLEAN, FALSE),
|
||||
HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", BOOLEAN, FALSE, true),
|
||||
HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE),
|
||||
HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", BOOLEAN, TRUE, 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),
|
||||
SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"),
|
||||
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
|
||||
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
|
||||
|
||||
// Misc. Settings
|
||||
SIGNATURE_SPOOFING("revanced_spoof_signature_verification", true, ReturnType.BOOLEAN),
|
||||
CAPTIONS_ENABLED("revanced_autocaptions_enabled", false, ReturnType.BOOLEAN),
|
||||
PREFERRED_AUTO_REPEAT("revanced_pref_auto_repeat", false, ReturnType.BOOLEAN),
|
||||
USE_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", true, ReturnType.BOOLEAN),
|
||||
TAP_SEEKING_ENABLED("revanced_enable_tap_seeking", true, ReturnType.BOOLEAN),
|
||||
ENABLE_MINIMIZED_PLAYBACK("revanced_enable_minimized_playback", true, ReturnType.BOOLEAN),
|
||||
OPEN_LINKS_DIRECTLY("revanced_uri_redirect", true, ReturnType.BOOLEAN, true),
|
||||
DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", true, ReturnType.BOOLEAN),
|
||||
ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", true, ReturnType.BOOLEAN, true),
|
||||
SIGNATURE_SPOOFING("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"),
|
||||
CAPTIONS_ENABLED("revanced_autocaptions_enabled", BOOLEAN, FALSE),
|
||||
DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", BOOLEAN, TRUE),
|
||||
ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE, true),
|
||||
ENABLE_MINIMIZED_PLAYBACK("revanced_enable_minimized_playback", BOOLEAN, 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),
|
||||
|
||||
// Swipe controls
|
||||
ENABLE_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", true, ReturnType.BOOLEAN),
|
||||
ENABLE_SWIPE_VOLUME("revanced_enable_swipe_volume", true, ReturnType.BOOLEAN),
|
||||
ENABLE_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", false, ReturnType.BOOLEAN),
|
||||
ENABLE_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", true, ReturnType.BOOLEAN),
|
||||
SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", 500L, ReturnType.LONG),
|
||||
SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_overlay_text_size", 22f, ReturnType.FLOAT),
|
||||
SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", 127, ReturnType.INTEGER),
|
||||
SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_magnitude_threshold", 30f, ReturnType.FLOAT),
|
||||
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_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)),
|
||||
SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", LONG, 500L,
|
||||
parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)),
|
||||
|
||||
// Debug settings
|
||||
DEBUG("revanced_debug_enabled", false, ReturnType.BOOLEAN),
|
||||
DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", false, ReturnType.BOOLEAN),
|
||||
DEBUG_SHOW_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", true, ReturnType.BOOLEAN),
|
||||
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"),
|
||||
|
||||
USE_DARK_THEME("app_theme_dark", false, ReturnType.BOOLEAN),
|
||||
|
||||
// RYD settings
|
||||
RYD_USER_ID("ryd_userId", null, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.STRING),
|
||||
RYD_ENABLED("ryd_enabled", true, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
|
||||
RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
|
||||
RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
|
||||
// ReturnYoutubeDislike settings
|
||||
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)),
|
||||
|
||||
// SponsorBlock settings
|
||||
SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_SHOW_TOAST_WHEN_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_COUNT_SKIPS("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_UUID("uuid", "", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING),
|
||||
SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", 150, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER),
|
||||
SB_MIN_DURATION("sb-min-duration", 0F, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.FLOAT),
|
||||
SB_SEEN_GUIDELINES("sb-seen-gl", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_SKIPPED_SEGMENTS("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER),
|
||||
SB_SKIPPED_SEGMENTS_TIME("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG),
|
||||
SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_IS_VIP("sb-is-vip", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_LAST_VIP_CHECK("sb-last-vip-check", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG),
|
||||
SB_SHOW_BROWSER_BUTTON("sb-browser-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
SB_API_URL("sb-api-host-url", "https://sponsor.ajay.app", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING);
|
||||
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_SKIPBUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, 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);
|
||||
|
||||
private final String path;
|
||||
private final Object defaultValue;
|
||||
private final SharedPrefHelper.SharedPrefNames sharedPref;
|
||||
private final ReturnType returnType;
|
||||
private final boolean rebootApp;
|
||||
private static SettingsEnum[] parents(SettingsEnum ... parents) {
|
||||
return parents;
|
||||
}
|
||||
|
||||
// must be volatile, as some settings are read/write from different threads
|
||||
// of note, the object value is persistently stored using SharedPreferences (which is thread safe)
|
||||
@NonNull
|
||||
public final String path;
|
||||
@NonNull
|
||||
public final Object defaultValue;
|
||||
@NonNull
|
||||
public final SharedPrefCategory sharedPref;
|
||||
@NonNull
|
||||
public final ReturnType returnType;
|
||||
/**
|
||||
* If the app should be rebooted, if this setting is changed
|
||||
*/
|
||||
public final boolean rebootApp;
|
||||
/**
|
||||
* Set of boolean parent settings.
|
||||
* If any of the parents are enabled, then this setting is available to configure.
|
||||
*
|
||||
* For example: {@link #DEBUG_STACKTRACE} is non-functional and cannot be configured,
|
||||
* unless it's parent {@link #DEBUG} is enabled.
|
||||
*
|
||||
* Declaration is not needed for items that do not appear in the ReVanced Settings UI.
|
||||
*/
|
||||
@Nullable
|
||||
private final SettingsEnum[] parents;
|
||||
|
||||
/**
|
||||
* Confirmation message to display, if the user tries to change the setting from the default value.
|
||||
* Can only be used for {@link ReturnType#BOOLEAN} setting types.
|
||||
*/
|
||||
@Nullable
|
||||
public final StringRef userDialogMessage;
|
||||
|
||||
// Must be volatile, as some settings are read/write from different threads.
|
||||
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
|
||||
@NonNull
|
||||
private volatile Object value;
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
||||
this(path, defaultValue, SharedPrefHelper.SharedPrefNames.YOUTUBE, returnType, false);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType, boolean rebootApp) {
|
||||
this(path, defaultValue, SharedPrefHelper.SharedPrefNames.YOUTUBE, returnType, rebootApp);
|
||||
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName) {
|
||||
this(path, returnType, defaultValue, prefName, false, null, null);
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType) {
|
||||
this(path, defaultValue, prefName, returnType, false);
|
||||
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
|
||||
boolean rebootApp) {
|
||||
this(path, returnType, defaultValue, prefName, rebootApp, null, null);
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType, boolean rebootApp) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.sharedPref = prefName;
|
||||
this.returnType = returnType;
|
||||
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) {
|
||||
this.path = Objects.requireNonNull(path);
|
||||
this.returnType = Objects.requireNonNull(returnType);
|
||||
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
|
||||
this.sharedPref = Objects.requireNonNull(prefName);
|
||||
this.rebootApp = rebootApp;
|
||||
|
||||
if (userDialogMessage == null) {
|
||||
this.userDialogMessage = null;
|
||||
} else {
|
||||
if (returnType != ReturnType.BOOLEAN) {
|
||||
throw new IllegalArgumentException("must be Boolean type: " + path);
|
||||
}
|
||||
this.userDialogMessage = new StringRef(userDialogMessage);
|
||||
}
|
||||
|
||||
this.parents = parents;
|
||||
if (parents != null) {
|
||||
for (SettingsEnum parent : parents) {
|
||||
if (parent.returnType != ReturnType.BOOLEAN) {
|
||||
throw new IllegalArgumentException("parent must be Boolean type: " + parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
load();
|
||||
loadAllSettings();
|
||||
}
|
||||
|
||||
private static void load() {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
if (context == null) {
|
||||
LogHelper.printException(() -> "SettingsEnum.load() called before ReVancedUtils.init()");
|
||||
return;
|
||||
}
|
||||
@Nullable
|
||||
public static SettingsEnum settingFromPath(@NonNull String str) {
|
||||
for (SettingsEnum setting : values()) {
|
||||
var path = setting.getPath();
|
||||
var defaultValue = setting.getDefaultValue();
|
||||
switch (setting.getReturnType()) {
|
||||
case FLOAT:
|
||||
defaultValue = SharedPrefHelper.getFloat(setting.sharedPref, path, (float) defaultValue);
|
||||
break;
|
||||
case LONG:
|
||||
defaultValue = SharedPrefHelper.getLong(setting.sharedPref, path, (long) defaultValue);
|
||||
break;
|
||||
case BOOLEAN:
|
||||
defaultValue = SharedPrefHelper.getBoolean(setting.sharedPref, path, (boolean) defaultValue);
|
||||
break;
|
||||
case INTEGER:
|
||||
defaultValue = SharedPrefHelper.getInt(setting.sharedPref, path, (int) defaultValue);
|
||||
break;
|
||||
case STRING:
|
||||
defaultValue = SharedPrefHelper.getString(setting.sharedPref, path, (String) defaultValue);
|
||||
break;
|
||||
default:
|
||||
LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + setting.name());
|
||||
break;
|
||||
}
|
||||
setting.setValue(defaultValue);
|
||||
if (setting.path.equals(str)) return setting;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void loadAllSettings() {
|
||||
for (SettingsEnum setting : values()) {
|
||||
setting.load();
|
||||
}
|
||||
}
|
||||
|
||||
private void load() {
|
||||
switch (returnType) {
|
||||
case BOOLEAN:
|
||||
value = sharedPref.getBoolean(path, (boolean) defaultValue);
|
||||
break;
|
||||
case INTEGER:
|
||||
value = sharedPref.getIntegerString(path, (Integer) defaultValue);
|
||||
break;
|
||||
case LONG:
|
||||
value = sharedPref.getLongString(path, (Long) defaultValue);
|
||||
break;
|
||||
case FLOAT:
|
||||
value = sharedPref.getFloatString(path, (Float) defaultValue);
|
||||
break;
|
||||
case STRING:
|
||||
value = sharedPref.getString(path, (String) defaultValue);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, but does _not_ persistently save the value.
|
||||
*
|
||||
* @see #saveValue(Object)
|
||||
* This intentionally is a static method, to deter accidental usage
|
||||
* when {@link #saveValue(Object)} was intended.
|
||||
*
|
||||
* This method is only to be used by the Settings preference code.
|
||||
*/
|
||||
public void setValue(Object newValue) {
|
||||
this.value = newValue;
|
||||
public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) {
|
||||
Objects.requireNonNull(newValue);
|
||||
switch (setting.returnType) {
|
||||
case BOOLEAN:
|
||||
setting.value = Boolean.valueOf(newValue);
|
||||
break;
|
||||
case INTEGER:
|
||||
setting.value = Integer.valueOf(newValue);
|
||||
break;
|
||||
case LONG:
|
||||
setting.value = Long.valueOf(newValue);
|
||||
break;
|
||||
case FLOAT:
|
||||
setting.value = Float.valueOf(newValue);
|
||||
break;
|
||||
case STRING:
|
||||
setting.value = newValue;
|
||||
break;
|
||||
default:
|
||||
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.value = newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value, and persistently saves it
|
||||
* Sets the value, and persistently saves it.
|
||||
*/
|
||||
public void saveValue(Object newValue) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
|
||||
if (context == null) {
|
||||
LogHelper.printException(() -> "Context on SaveValue is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (getReturnType()) {
|
||||
case FLOAT:
|
||||
SharedPrefHelper.saveFloat(sharedPref, path, (float) newValue);
|
||||
break;
|
||||
case LONG:
|
||||
SharedPrefHelper.saveLong(sharedPref, path, (long) newValue);
|
||||
break;
|
||||
public void saveValue(@NonNull Object newValue) {
|
||||
Objects.requireNonNull(newValue);
|
||||
switch (returnType) {
|
||||
case BOOLEAN:
|
||||
SharedPrefHelper.saveBoolean(sharedPref, path, (boolean) newValue);
|
||||
sharedPref.saveBoolean(path, (boolean) newValue);
|
||||
break;
|
||||
case INTEGER:
|
||||
SharedPrefHelper.saveInt(sharedPref, path, (int) newValue);
|
||||
sharedPref.saveIntegerString(path, (Integer) newValue);
|
||||
break;
|
||||
case LONG:
|
||||
sharedPref.saveLongString(path, (Long) newValue);
|
||||
break;
|
||||
case FLOAT:
|
||||
sharedPref.saveFloatString(path, (Float) newValue);
|
||||
break;
|
||||
case STRING:
|
||||
SharedPrefHelper.saveString(sharedPref, path, (String) newValue);
|
||||
sharedPref.saveString(path, (String) newValue);
|
||||
break;
|
||||
default:
|
||||
LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + name());
|
||||
break;
|
||||
throw new IllegalStateException(name());
|
||||
}
|
||||
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if this setting can be configured and used.
|
||||
*
|
||||
* Not to be confused with {@link #getBoolean()}
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
if (parents == null) {
|
||||
return true;
|
||||
}
|
||||
for (SettingsEnum parent : parents) {
|
||||
if (parent.getBoolean()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean getBoolean() {
|
||||
return (Boolean) value;
|
||||
}
|
||||
@ -274,23 +407,24 @@ public enum SettingsEnum {
|
||||
return (Float) value;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public Object getDefaultValue() {
|
||||
return defaultValue;
|
||||
/**
|
||||
* @return the value of this setting as as generic object type.
|
||||
*/
|
||||
@NonNull
|
||||
public Object getObjectValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
public enum ReturnType {
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
STRING,
|
||||
LONG,
|
||||
FLOAT,
|
||||
}
|
||||
|
||||
public ReturnType getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
public boolean shouldRebootOnChange() {
|
||||
return rebootApp;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package app.revanced.integrations.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/**
|
||||
* Shared categories, and helper methods.
|
||||
*
|
||||
* The various save methods store numbers as Strings,
|
||||
* which is required if using {@link android.preference.PreferenceFragment}.
|
||||
*
|
||||
* If saved numbers will not be used with a preference fragment,
|
||||
* then store the primitive numbers using {@link #preferences}.
|
||||
*/
|
||||
public enum SharedPrefCategory {
|
||||
YOUTUBE("youtube"),
|
||||
RETURN_YOUTUBE_DISLIKE("ryd"),
|
||||
SPONSOR_BLOCK("sponsor-block"),
|
||||
REVANCED_PREFS("revanced_prefs");
|
||||
|
||||
@NonNull
|
||||
public final String prefName;
|
||||
@NonNull
|
||||
public final SharedPreferences preferences;
|
||||
|
||||
SharedPrefCategory(@NonNull String prefName) {
|
||||
this.prefName = Objects.requireNonNull(prefName);
|
||||
preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
||||
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
|
||||
}
|
||||
|
||||
public void saveBoolean(@NonNull String key, boolean value) {
|
||||
preferences.edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveLongString(@NonNull String key, @Nullable Long value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveFloatString(@NonNull String key, @Nullable Float value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value a NULL parameter removes the value from the preferences
|
||||
*/
|
||||
public void saveString(@NonNull String key, @Nullable String value) {
|
||||
saveObjectAsString(key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getString(@NonNull String key, @NonNull String _default) {
|
||||
Objects.requireNonNull(_default);
|
||||
return preferences.getString(key, _default);
|
||||
}
|
||||
|
||||
|
||||
public boolean getBoolean(@NonNull String key, boolean _default) {
|
||||
return preferences.getBoolean(key, _default);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
return _default;
|
||||
} catch (ClassCastException ex) {
|
||||
return preferences.getInt(key, _default); // old data, previously stored as primitive
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Long getLongString(@NonNull String key, @NonNull Long _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Long.valueOf(value);
|
||||
}
|
||||
return _default;
|
||||
} catch (ClassCastException ex) {
|
||||
return preferences.getLong(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Float getFloatString(@NonNull String key, @NonNull Float _default) {
|
||||
try {
|
||||
String value = preferences.getString(key, null);
|
||||
if (value != null) {
|
||||
return Float.valueOf(value);
|
||||
}
|
||||
return _default;
|
||||
} catch (ClassCastException ex) {
|
||||
return preferences.getFloat(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return prefName;
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package app.revanced.integrations.settingsmenu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -17,6 +16,9 @@ import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
public class ReVancedSettingActivity {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setTheme(LicenseActivity base) {
|
||||
final var whiteTheme = "Theme.YouTube.Settings";
|
||||
final var darkTheme = "Theme.YouTube.Settings.Dark";
|
||||
@ -24,11 +26,14 @@ public class ReVancedSettingActivity {
|
||||
final var theme = ThemeHelper.isDarkTheme() ? darkTheme : whiteTheme;
|
||||
|
||||
LogHelper.printDebug(() -> "Using theme: " + theme);
|
||||
base.setTheme(getIdentifier(theme, "style"));
|
||||
base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initializeSettings(LicenseActivity base) {
|
||||
base.setContentView(getIdentifier("revanced_settings_with_toolbar", "layout"));
|
||||
base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout"));
|
||||
|
||||
PreferenceFragment preferenceFragment;
|
||||
String preferenceIdentifier;
|
||||
@ -46,7 +51,7 @@ public class ReVancedSettingActivity {
|
||||
}
|
||||
|
||||
try {
|
||||
TextView toolbar = getTextView((ViewGroup) base.findViewById(getIdentifier("toolbar", "id")));
|
||||
TextView toolbar = getTextView((ViewGroup) base.findViewById(ReVancedUtils.getResourceIdentifier("toolbar", "id")));
|
||||
if (toolbar == null) {
|
||||
// FIXME
|
||||
// https://github.com/revanced/revanced-patches/issues/1384
|
||||
@ -58,7 +63,7 @@ public class ReVancedSettingActivity {
|
||||
LogHelper.printException(() -> "Could not set Toolbar title", e);
|
||||
}
|
||||
|
||||
base.getFragmentManager().beginTransaction().replace(getIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit();
|
||||
base.getFragmentManager().beginTransaction().replace(ReVancedUtils.getResourceIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit();
|
||||
}
|
||||
|
||||
|
||||
@ -86,10 +91,4 @@ public class ReVancedSettingActivity {
|
||||
public static TextView getTextView(ViewGroup viewGroup) {
|
||||
return getView(TextView.class, viewGroup);
|
||||
}
|
||||
|
||||
private static int getIdentifier(String name, String defType) {
|
||||
Context appContext = ReVancedUtils.getContext();
|
||||
assert appContext != null;
|
||||
return appContext.getResources().getIdentifier(name, defType, appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.settingsmenu;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlarmManager;
|
||||
@ -8,170 +10,163 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.apps.youtube.app.application.Shell_HomeActivity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
|
||||
public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private List<PreferenceScreen> screens;
|
||||
|
||||
private boolean Registered = false;
|
||||
private boolean settingsInitialized = false;
|
||||
|
||||
private final CharSequence[] videoSpeedEntries = {"Auto", "0.25x", "0.5x", "0.75x", "Normal", "1.25x", "1.5x", "1.75x", "2x", "3x", "4x", "5x"};
|
||||
private final CharSequence[] videoSpeedentryValues = {"-2", "0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "3.0", "4.0", "5.0"};
|
||||
//private final CharSequence[] buttonLocationEntries = {"None", "In player", "Under player", "Both"};
|
||||
//private final CharSequence[] buttonLocationentryValues = {"NONE", "PLAYER", "BUTTON_BAR", "BOTH"};
|
||||
/**
|
||||
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
|
||||
*/
|
||||
private boolean showingUserDialogMessage;
|
||||
|
||||
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
if (!setting.getPath().equals(str)) continue;
|
||||
Preference pref = this.findPreferenceOnScreen(str);
|
||||
|
||||
LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref.toString());
|
||||
try {
|
||||
SettingsEnum setting = SettingsEnum.settingFromPath(str);
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
Preference pref = this.findPreference(str);
|
||||
LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref);
|
||||
|
||||
if (pref instanceof SwitchPreference) {
|
||||
SwitchPreference switchPref = (SwitchPreference) pref;
|
||||
setting.setValue(switchPref.isChecked());
|
||||
SettingsEnum.setValue(setting, switchPref.isChecked());
|
||||
} else if (pref instanceof EditTextPreference) {
|
||||
EditTextPreference editPref = (EditTextPreference) pref;
|
||||
Object value = null;
|
||||
switch (setting.getReturnType()) {
|
||||
case FLOAT:
|
||||
value = Float.parseFloat(editPref.getText());
|
||||
break;
|
||||
case LONG:
|
||||
value = Long.parseLong(editPref.getText());
|
||||
break;
|
||||
case STRING:
|
||||
value = editPref.getText();
|
||||
break;
|
||||
case INTEGER:
|
||||
value = Integer.parseInt(editPref.getText());
|
||||
break;
|
||||
default:
|
||||
LogHelper.printException(() -> "Setting has no valid return type! " + setting.getReturnType());
|
||||
break;
|
||||
}
|
||||
setting.setValue(value);
|
||||
String editText = ((EditTextPreference) pref).getText();
|
||||
SettingsEnum.setValue(setting, editText);
|
||||
} else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
SettingsEnum.setValue(setting, listPref.getValue());
|
||||
updateListPreferenceSummary((ListPreference) pref, setting);
|
||||
} else {
|
||||
LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref.toString());
|
||||
LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReVancedUtils.getContext() != null && settingsInitialized && setting.shouldRebootOnChange()) {
|
||||
rebootDialog(getActivity());
|
||||
if (!showingUserDialogMessage) {
|
||||
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
|
||||
showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting);
|
||||
} else if (setting.rebootApp) {
|
||||
rebootDialog(getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
enableDisablePreferences();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override // android.preference.PreferenceFragment, android.app.Fragment
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.YOUTUBE.getName());
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
try {
|
||||
int identifier = getResources().getIdentifier("revanced_prefs", "xml", getPackageName());
|
||||
addPreferencesFromResource(identifier);
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(SharedPrefCategory.YOUTUBE.prefName);
|
||||
addPreferencesFromResource(ReVancedUtils.getResourceIdentifier("revanced_prefs", "xml"));
|
||||
|
||||
SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();
|
||||
this.settingsInitialized = sharedPreferences.getBoolean("revanced_initialized", false);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this.listener);
|
||||
this.Registered = true;
|
||||
enableDisablePreferences();
|
||||
|
||||
this.settingsInitialized = true;
|
||||
} catch (Throwable th) {
|
||||
LogHelper.printException(() -> "Error during onCreate()", th);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// set the summary text for any ListPreferences
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
Preference preference = findPreference(setting.path);
|
||||
if (preference instanceof ListPreference) {
|
||||
updateListPreferenceSummary((ListPreference) preference, setting);
|
||||
}
|
||||
}
|
||||
|
||||
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onActivityCreated() error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override // android.preference.PreferenceFragment, android.app.Fragment
|
||||
public void onDestroy() {
|
||||
if (this.Registered) {
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this.listener);
|
||||
this.Registered = false;
|
||||
}
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private Preference findPreferenceOnScreen(CharSequence key) {
|
||||
if (key == null) {
|
||||
LogHelper.printException(() -> "Key cannot be null!");
|
||||
return null;
|
||||
}
|
||||
Preference pref = null;
|
||||
if (this.findPreference(key) != null) {
|
||||
pref = this.findPreference(key);
|
||||
} else {
|
||||
for (PreferenceScreen screen : this.screens) {
|
||||
Preference toCheck = screen.findPreference(key);
|
||||
if (toCheck == null) continue;
|
||||
pref = toCheck;
|
||||
LogHelper.printDebug(() -> "Found preference " + key + " on screen: " + screen.getTitle());
|
||||
private void enableDisablePreferences() {
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
Preference preference = this.findPreference(setting.path);
|
||||
if (preference != null) {
|
||||
preference.setEnabled(setting.isAvailable());
|
||||
}
|
||||
}
|
||||
|
||||
return pref;
|
||||
}
|
||||
|
||||
/*
|
||||
private void setCopyLinkListPreferenceData(ListPreference listPreference, String str) {
|
||||
listPreference.setEntries(this.buttonLocationEntries);
|
||||
listPreference.setEntryValues(this.buttonLocationentryValues);
|
||||
String string = this.sharedPreferences.getString(str, "NONE");
|
||||
if (listPreference.getValue() == null) {
|
||||
listPreference.setValue(string);
|
||||
private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) {
|
||||
final int entryIndex = listPreference.findIndexOfValue(setting.getObjectValue().toString());
|
||||
if (entryIndex >= 0) {
|
||||
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
|
||||
}
|
||||
listPreference.setSummary(this.buttonLocationEntries[listPreference.findIndexOfValue(string)]);
|
||||
}
|
||||
*/
|
||||
|
||||
private String getPackageName() {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
if (context == null) {
|
||||
LogHelper.printException(() -> "Context is null, returning com.google.android.youtube!");
|
||||
return "com.google.android.youtube";
|
||||
}
|
||||
String PACKAGE_NAME = context.getPackageName();
|
||||
LogHelper.printDebug(() -> "getPackageName: " + PACKAGE_NAME);
|
||||
|
||||
return PACKAGE_NAME;
|
||||
}
|
||||
|
||||
private void reboot(Activity activity, Class homeActivityClass) {
|
||||
int intent;
|
||||
intent = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
((AlarmManager) activity.getSystemService(Context.ALARM_SERVICE)).setExact(AlarmManager.ELAPSED_REALTIME, 1500L, PendingIntent.getActivity(activity, 0, new Intent(activity, Shell_HomeActivity.class), intent));
|
||||
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(final Activity activity) {
|
||||
new AlertDialog.Builder(activity).setMessage(getStringByName(activity, "pref_refresh_config")).setPositiveButton(getStringByName(activity, "in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(getStringByName(activity, "sign_in_cancel"), null).show();
|
||||
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 String getStringByName(Context context, String name) {
|
||||
try {
|
||||
Resources res = context.getResources();
|
||||
return res.getString(res.getIdentifier(name, "string", context.getPackageName()));
|
||||
} catch (Throwable exception) {
|
||||
LogHelper.printException(() -> "Resource not found.", exception);
|
||||
return "";
|
||||
}
|
||||
private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) {
|
||||
showingUserDialogMessage = true;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(str("revanced_settings_confirm_user_dialog_title"))
|
||||
.setMessage(setting.userDialogMessage.toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
if (setting.rebootApp) {
|
||||
rebootDialog(activity);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
Boolean defaultBooleanValue = (Boolean) setting.defaultValue;
|
||||
SettingsEnum.setValue(setting, defaultBooleanValue);
|
||||
switchPref.setChecked(defaultBooleanValue);
|
||||
})
|
||||
.setOnDismissListener(dialog -> {
|
||||
showingUserDialogMessage = false;
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package app.revanced.integrations.settingsmenu;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@ -15,7 +15,7 @@ import android.preference.SwitchPreference;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
|
||||
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
|
||||
@ -35,27 +35,25 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
private SwitchPreference compactLayoutPreference;
|
||||
|
||||
private void updateUIState() {
|
||||
final boolean rydIsEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
||||
|
||||
enabledPreference.setSummary(rydIsEnabled
|
||||
enabledPreference.setSummary(SettingsEnum.RYD_ENABLED.getBoolean()
|
||||
? str("revanced_ryd_enable_summary_on")
|
||||
: str("revanced_ryd_enable_summary_off"));
|
||||
|
||||
percentagePreference.setSummary(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
|
||||
? str("revanced_ryd_dislike_percentage_summary_on")
|
||||
: str("revanced_ryd_dislike_percentage_summary_off"));
|
||||
percentagePreference.setEnabled(rydIsEnabled);
|
||||
percentagePreference.setEnabled(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.isAvailable());
|
||||
|
||||
compactLayoutPreference.setSummary(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean()
|
||||
? str("revanced_ryd_compact_layout_summary_on")
|
||||
: str("revanced_ryd_compact_layout_summary_off"));
|
||||
compactLayoutPreference.setEnabled(rydIsEnabled);
|
||||
compactLayoutPreference.setEnabled(SettingsEnum.RYD_USE_COMPACT_LAYOUT.isAvailable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.RYD.getName());
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefCategory.RETURN_YOUTUBE_DISLIKE.prefName);
|
||||
|
||||
Activity context = this.getActivity();
|
||||
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
|
||||
@ -78,8 +76,8 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean());
|
||||
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
|
||||
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue((Boolean)newValue);
|
||||
|
||||
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue(newValue);
|
||||
ReturnYouTubeDislike.clearCache();
|
||||
updateUIState();
|
||||
return true;
|
||||
});
|
||||
@ -89,8 +87,8 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
compactLayoutPreference.setChecked(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean());
|
||||
compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
|
||||
compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue((Boolean)newValue);
|
||||
|
||||
SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue(newValue);
|
||||
ReturnYouTubeDislike.clearCache();
|
||||
updateUIState();
|
||||
return true;
|
||||
});
|
||||
@ -102,7 +100,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
// About category
|
||||
|
||||
PreferenceCategory aboutCategory = new PreferenceCategory(context);
|
||||
aboutCategory.setTitle(str("about"));
|
||||
aboutCategory.setTitle(str("revanced_ryd_about"));
|
||||
preferenceScreen.addPreference(aboutCategory);
|
||||
|
||||
// ReturnYouTubeDislike Website
|
||||
|
@ -1,206 +1,373 @@
|
||||
package app.revanced.integrations.settingsmenu;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import static android.text.Html.fromHtml;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.util.Patterns;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
import app.revanced.integrations.sponsorblock.objects.EditTextListPreference;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategoryListPreference;
|
||||
import app.revanced.integrations.sponsorblock.objects.UserStats;
|
||||
import app.revanced.integrations.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SponsorBlockSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public static final DecimalFormat FORMATTER = new DecimalFormat("#,###,###");
|
||||
public static final String SAVED_TEMPLATE = "%dh %.1f %s";
|
||||
private static final APIURLChangeListener API_URL_CHANGE_LISTENER = new APIURLChangeListener();
|
||||
private final ArrayList<Preference> preferencesToDisableWhenSBDisabled = new ArrayList<>();
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SponsorBlockSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private SwitchPreference sbEnabled;
|
||||
private SwitchPreference addNewSegment;
|
||||
private SwitchPreference votingEnabled;
|
||||
private SwitchPreference compactSkipButton;
|
||||
private SwitchPreference showSkipToast;
|
||||
private SwitchPreference trackSkips;
|
||||
private SwitchPreference showTimeWithoutSegments;
|
||||
|
||||
private EditTextPreference newSegmentStep;
|
||||
private EditTextPreference minSegmentDuration;
|
||||
private EditTextPreference privateUserId;
|
||||
private EditTextPreference importExport;
|
||||
private Preference apiUrl;
|
||||
|
||||
private PreferenceCategory statsCategory;
|
||||
private PreferenceCategory segmentCategory;
|
||||
|
||||
private void updateUI() {
|
||||
try {
|
||||
final boolean enabled = SettingsEnum.SB_ENABLED.getBoolean();
|
||||
if (!enabled) {
|
||||
SponsorBlockViewController.hideAll();
|
||||
SegmentPlaybackController.setCurrentVideoId(null);
|
||||
} else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.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.setEnabled(enabled);
|
||||
|
||||
votingEnabled.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean());
|
||||
votingEnabled.setEnabled(enabled);
|
||||
|
||||
compactSkipButton.setChecked(SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean());
|
||||
compactSkipButton.setEnabled(enabled);
|
||||
|
||||
showSkipToast.setChecked(SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean());
|
||||
showSkipToast.setEnabled(enabled);
|
||||
|
||||
trackSkips.setChecked(SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean());
|
||||
trackSkips.setEnabled(enabled);
|
||||
|
||||
showTimeWithoutSegments.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
|
||||
showTimeWithoutSegments.setEnabled(enabled);
|
||||
|
||||
newSegmentStep.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()));
|
||||
newSegmentStep.setEnabled(enabled);
|
||||
|
||||
minSegmentDuration.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat()));
|
||||
minSegmentDuration.setEnabled(enabled);
|
||||
|
||||
privateUserId.setText(SettingsEnum.SB_UUID.getString());
|
||||
privateUserId.setEnabled(enabled);
|
||||
|
||||
apiUrl.setEnabled(enabled);
|
||||
importExport.setEnabled(enabled);
|
||||
segmentCategory.setEnabled(enabled);
|
||||
statsCategory.setEnabled(enabled);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "update settings UI failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName());
|
||||
try {
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(SharedPrefCategory.SPONSOR_BLOCK.prefName);
|
||||
|
||||
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
|
||||
Activity context = this.getActivity();
|
||||
PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(context);
|
||||
setPreferenceScreen(preferenceScreen);
|
||||
|
||||
final Activity context = this.getActivity();
|
||||
SponsorBlockSettings.initialize();
|
||||
|
||||
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
|
||||
setPreferenceScreen(preferenceScreen);
|
||||
|
||||
SponsorBlockSettings.update(getActivity());
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preferenceScreen.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.SB_ENABLED.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.SB_ENABLED.getDefaultValue());
|
||||
preference.setChecked(SettingsEnum.SB_ENABLED.getBoolean());
|
||||
preference.setTitle(str("enable_sb"));
|
||||
preference.setSummary(str("enable_sb_sum"));
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
final boolean value = (Boolean) newValue;
|
||||
enableCategoriesIfNeeded(value);
|
||||
sbEnabled = new SwitchPreference(context);
|
||||
sbEnabled.setTitle(str("sb_enable_sb"));
|
||||
sbEnabled.setSummary(str("sb_enable_sb_sum"));
|
||||
preferenceScreen.addPreference(sbEnabled);
|
||||
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_ENABLED.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preferenceScreen.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getDefaultValue());
|
||||
preference.setChecked(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean());
|
||||
preference.setTitle(str("enable_segmadding"));
|
||||
preference.setSummary(str("enable_segmadding_sum"));
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
preference.setOnPreferenceChangeListener((preference12, o) -> {
|
||||
final boolean value = (Boolean) o;
|
||||
if (value && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) {
|
||||
new AlertDialog.Builder(preference12.getContext())
|
||||
addNewSegment = new SwitchPreference(context);
|
||||
addNewSegment.setTitle(str("sb_enable_create_segment"));
|
||||
addNewSegment.setSummaryOn(str("sb_enable_create_segment_sum_on"));
|
||||
addNewSegment.setSummaryOff(str("sb_enable_create_segment_sum_off"));
|
||||
preferenceScreen.addPreference(addNewSegment);
|
||||
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
||||
Boolean newValue = (Boolean) o;
|
||||
if (newValue && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) {
|
||||
new AlertDialog.Builder(preference1.getContext())
|
||||
.setTitle(str("sb_guidelines_popup_title"))
|
||||
.setMessage(str("sb_guidelines_popup_content"))
|
||||
.setNegativeButton(str("sb_guidelines_popup_already_read"), null)
|
||||
.setPositiveButton(str("sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
||||
.setOnDismissListener(dialog -> SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true))
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
SettingsEnum.SB_NEW_SEGMENT_ENABLED.saveValue(value);
|
||||
SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preferenceScreen.addPreference(preference);
|
||||
preference.setTitle(str("enable_voting"));
|
||||
preference.setSummary(str("enable_voting_sum"));
|
||||
preference.setKey(SettingsEnum.SB_VOTING_ENABLED.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.SB_VOTING_ENABLED.getDefaultValue());
|
||||
preference.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean());
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
preference.setOnPreferenceChangeListener((preference12, o) -> {
|
||||
final boolean value = (Boolean) o;
|
||||
SettingsEnum.SB_VOTING_ENABLED.saveValue(value);
|
||||
votingEnabled = new SwitchPreference(context);
|
||||
votingEnabled.setTitle(str("sb_enable_voting"));
|
||||
votingEnabled.setSummaryOn(str("sb_enable_voting_sum_on"));
|
||||
votingEnabled.setSummaryOff(str("sb_enable_voting_sum_off"));
|
||||
preferenceScreen.addPreference(votingEnabled);
|
||||
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_VOTING_ENABLED.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
|
||||
compactSkipButton = new SwitchPreference(context);
|
||||
compactSkipButton.setTitle(str("sb_enable_compact_skip_button"));
|
||||
compactSkipButton.setSummaryOn(str("sb_enable_compact_skip_button_sum_on"));
|
||||
compactSkipButton.setSummaryOff(str("sb_enable_compact_skip_button_sum_off"));
|
||||
preferenceScreen.addPreference(compactSkipButton);
|
||||
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
|
||||
addGeneralCategory(context, preferenceScreen);
|
||||
|
||||
segmentCategory = new PreferenceCategory(context);
|
||||
segmentCategory.setTitle(str("sb_diff_segments"));
|
||||
preferenceScreen.addPreference(segmentCategory);
|
||||
updateSegmentCategories();
|
||||
|
||||
statsCategory = new PreferenceCategory(context);
|
||||
statsCategory.setTitle(str("sb_stats"));
|
||||
preferenceScreen.addPreference(statsCategory);
|
||||
fetchAndDisplayStats();
|
||||
|
||||
addAboutCategory(context, preferenceScreen);
|
||||
|
||||
updateUI();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onCreate failure", ex);
|
||||
}
|
||||
|
||||
addGeneralCategory(context, preferenceScreen);
|
||||
addSegmentsCategory(context, preferenceScreen);
|
||||
addStatsCategory(context, preferenceScreen);
|
||||
addAboutCategory(context, preferenceScreen);
|
||||
|
||||
enableCategoriesIfNeeded(SettingsEnum.SB_ENABLED.getBoolean());
|
||||
}
|
||||
|
||||
private void openGuidelines() {
|
||||
final Context context = getActivity();
|
||||
SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void enableCategoriesIfNeeded(boolean value) {
|
||||
for (Preference preference : preferencesToDisableWhenSBDisabled)
|
||||
preference.setEnabled(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
private void addSegmentsCategory(Context context, PreferenceScreen screen) {
|
||||
PreferenceCategory category = new PreferenceCategory(context);
|
||||
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
|
||||
final PreferenceCategory category = new PreferenceCategory(context);
|
||||
screen.addPreference(category);
|
||||
preferencesToDisableWhenSBDisabled.add(category);
|
||||
category.setTitle(str("diff_segments"));
|
||||
category.setTitle(str("sb_general"));
|
||||
|
||||
SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values();
|
||||
String[] entries = new String[segmentBehaviours.length];
|
||||
String[] entryValues = new String[segmentBehaviours.length];
|
||||
for (int i = 0, segmentBehavioursLength = segmentBehaviours.length; i < segmentBehavioursLength; i++) {
|
||||
SponsorBlockSettings.SegmentBehaviour behaviour = segmentBehaviours[i];
|
||||
entries[i] = behaviour.name.toString();
|
||||
entryValues[i] = behaviour.key;
|
||||
}
|
||||
Preference guidelinePreferences = new Preference(context);
|
||||
guidelinePreferences.setTitle(str("sb_guidelines_preference_title"));
|
||||
guidelinePreferences.setSummary(str("sb_guidelines_preference_sum"));
|
||||
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
|
||||
openGuidelines();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(guidelinePreferences);
|
||||
|
||||
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
|
||||
|
||||
for (SponsorBlockSettings.SegmentInfo segmentInfo : categories) {
|
||||
EditTextListPreference preference = new EditTextListPreference(context);
|
||||
preference.setTitle(segmentInfo.getTitleWithDot());
|
||||
preference.setSummary(segmentInfo.description.toString());
|
||||
preference.setKey(segmentInfo.key);
|
||||
preference.setDefaultValue(segmentInfo.behaviour.key);
|
||||
preference.setEntries(entries);
|
||||
preference.setEntryValues(entryValues);
|
||||
showSkipToast = new SwitchPreference(context);
|
||||
showSkipToast.setTitle(str("sb_general_skiptoast"));
|
||||
showSkipToast.setSummaryOn(str("sb_general_skiptoast_sum_on"));
|
||||
showSkipToast.setSummaryOff(str("sb_general_skiptoast_sum_off"));
|
||||
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
||||
ReVancedUtils.showToastShort(str("sb_skipped_sponsor"));
|
||||
return false;
|
||||
});
|
||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(showSkipToast);
|
||||
|
||||
category.addPreference(preference);
|
||||
}
|
||||
|
||||
Preference colorPreference = new Preference(context); // TODO remove this after the next major update
|
||||
screen.addPreference(colorPreference);
|
||||
colorPreference.setTitle(str("color_change"));
|
||||
colorPreference.setSummary(str("color_change_sum"));
|
||||
colorPreference.setSelectable(false);
|
||||
preferencesToDisableWhenSBDisabled.add(colorPreference);
|
||||
trackSkips = new SwitchPreference(context);
|
||||
trackSkips.setTitle(str("sb_general_skipcount"));
|
||||
trackSkips.setSummaryOn(str("sb_general_skipcount_sum_on"));
|
||||
trackSkips.setSummaryOff(str("sb_general_skipcount_sum_off"));
|
||||
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(trackSkips);
|
||||
|
||||
|
||||
showTimeWithoutSegments = new SwitchPreference(context);
|
||||
showTimeWithoutSegments.setTitle(str("sb_general_time_without"));
|
||||
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);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(showTimeWithoutSegments);
|
||||
|
||||
|
||||
newSegmentStep = new EditTextPreference(context);
|
||||
newSegmentStep.setTitle(str("sb_general_adjusting"));
|
||||
newSegmentStep.setSummary(str("sb_general_adjusting_sum"));
|
||||
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
||||
if (newAdjustmentValue == 0) {
|
||||
ReVancedUtils.showToastLong(str("sb_general_adjusting_invalid"));
|
||||
return false;
|
||||
}
|
||||
SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue);
|
||||
return true;
|
||||
});
|
||||
category.addPreference(newSegmentStep);
|
||||
|
||||
|
||||
minSegmentDuration = new EditTextPreference(context);
|
||||
minSegmentDuration.setTitle(str("sb_general_min_duration"));
|
||||
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()));
|
||||
return true;
|
||||
});
|
||||
category.addPreference(minSegmentDuration);
|
||||
|
||||
|
||||
privateUserId = new EditTextPreference(context);
|
||||
privateUserId.setTitle(str("sb_general_uuid"));
|
||||
privateUserId.setSummary(str("sb_general_uuid_sum"));
|
||||
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
String newUUID = newValue.toString();
|
||||
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
||||
ReVancedUtils.showToastLong(str("sb_general_uuid_invalid"));
|
||||
return false;
|
||||
}
|
||||
SettingsEnum.SB_UUID.saveValue(newUUID);
|
||||
fetchAndDisplayStats();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(privateUserId);
|
||||
|
||||
|
||||
apiUrl = new Preference(context);
|
||||
apiUrl.setTitle(str("sb_general_api_url"));
|
||||
apiUrl.setSummary(Html.fromHtml(str("sb_general_api_url_sum")));
|
||||
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
||||
EditText editText = new EditText(context);
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
editText.setText(SettingsEnum.SB_API_URL.getString());
|
||||
|
||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||
SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.defaultValue);
|
||||
ReVancedUtils.showToastLong(str("sb_api_url_reset"));
|
||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||
String serverAddress = editText.getText().toString();
|
||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||
ReVancedUtils.showToastLong(str("sb_api_url_invalid"));
|
||||
} else if (!serverAddress.equals(SettingsEnum.SB_API_URL.getString())) {
|
||||
SettingsEnum.SB_API_URL.saveValue(serverAddress);
|
||||
ReVancedUtils.showToastLong(str("sb_api_url_changed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(apiUrl.getTitle())
|
||||
.setView(editText)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("sb_reset"), urlChangeListener)
|
||||
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(apiUrl);
|
||||
|
||||
|
||||
importExport = new EditTextPreference(context);
|
||||
importExport.setTitle(str("sb_settings_ie"));
|
||||
importExport.setSummary(str("sb_settings_ie_sum"));
|
||||
importExport.setOnPreferenceClickListener(preference1 -> {
|
||||
importExport.getEditText().setText(SponsorBlockSettings.exportSettings());
|
||||
return true;
|
||||
});
|
||||
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SponsorBlockSettings.importSettings((String) newValue);
|
||||
updateSegmentCategories();
|
||||
fetchAndDisplayStats();
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
category.addPreference(importExport);
|
||||
}
|
||||
|
||||
private void addStatsCategory(Context context, PreferenceScreen screen) {
|
||||
PreferenceCategory category = new PreferenceCategory(context);
|
||||
screen.addPreference(category);
|
||||
category.setTitle(str("stats"));
|
||||
preferencesToDisableWhenSBDisabled.add(category);
|
||||
private void updateSegmentCategories() {
|
||||
try {
|
||||
segmentCategory.removeAll();
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
category.addPreference(preference);
|
||||
preference.setTitle(str("stats_loading"));
|
||||
preference.setSelectable(false);
|
||||
|
||||
SBRequester.retrieveUserStats(category, preference);
|
||||
Activity activity = getActivity();
|
||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||
segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "updateSegmentCategories failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAboutCategory(Context context, PreferenceScreen screen) {
|
||||
PreferenceCategory category = new PreferenceCategory(context);
|
||||
screen.addPreference(category);
|
||||
category.setTitle(str("about"));
|
||||
category.setTitle(str("sb_about"));
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
screen.addPreference(preference);
|
||||
preference.setTitle(str("about_api"));
|
||||
preference.setSummary(str("about_api_sum"));
|
||||
preference.setTitle(str("sb_about_api"));
|
||||
preference.setSummary(str("sb_about_api_sum"));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sponsor.ajay.app"));
|
||||
@ -212,197 +379,160 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment implements
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
screen.addPreference(preference);
|
||||
preference.setTitle(str("about_madeby"));
|
||||
preference.setSummary(str("sb_about_made_by"));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
preference.setSingleLineTitle(false);
|
||||
}
|
||||
preference.setSelectable(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
|
||||
final PreferenceCategory category = new PreferenceCategory(context);
|
||||
preferencesToDisableWhenSBDisabled.add(category);
|
||||
screen.addPreference(category);
|
||||
category.setTitle(str("general"));
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
preference.setTitle(str("sb_guidelines_preference_title"));
|
||||
preference.setSummary(str("sb_guidelines_preference_sum"));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
openGuidelines();
|
||||
return false;
|
||||
});
|
||||
screen.addPreference(preference);
|
||||
}
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preference.setTitle(str("general_skiptoast"));
|
||||
preference.setSummary(str("general_skiptoast_sum"));
|
||||
preference.setChecked(SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean());
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(newValue);
|
||||
return true;
|
||||
});
|
||||
preference.setOnPreferenceClickListener(preference12 -> {
|
||||
Toast.makeText(preference12.getContext(), str("skipped_sponsor"), Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
});
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
screen.addPreference(preference);
|
||||
}
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preference.setTitle(str("general_skipcount"));
|
||||
preference.setSummary(str("general_skipcount_sum"));
|
||||
preference.setChecked(SettingsEnum.SB_COUNT_SKIPS.getBoolean());
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_COUNT_SKIPS.saveValue(newValue);
|
||||
return true;
|
||||
});
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
screen.addPreference(preference);
|
||||
}
|
||||
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preference.setTitle(str("general_time_without_sb"));
|
||||
preference.setSummary(str("general_time_without_sb_sum"));
|
||||
preference.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
screen.addPreference(preference);
|
||||
}
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
preference.setTitle(str("general_adjusting"));
|
||||
preference.setSummary(str("general_adjusting_sum"));
|
||||
preference.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()));
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(Integer.valueOf(newValue.toString()));
|
||||
return true;
|
||||
});
|
||||
screen.addPreference(preference);
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
}
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||
preference.setTitle(str("general_min_duration"));
|
||||
preference.setSummary(str("general_min_duration_sum"));
|
||||
preference.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat()));
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString()));
|
||||
return true;
|
||||
});
|
||||
screen.addPreference(preference);
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
}
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
preference.setTitle(str("general_uuid"));
|
||||
preference.setSummary(str("general_uuid_sum"));
|
||||
preference.setText(SettingsEnum.SB_UUID.getString());
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SettingsEnum.SB_UUID.saveValue(newValue.toString());
|
||||
return true;
|
||||
});
|
||||
|
||||
screen.addPreference(preference);
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
}
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
String title = str("general_api_url");
|
||||
preference.setTitle(title);
|
||||
preference.setSummary(Html.fromHtml(str("general_api_url_sum")));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
EditText editText = new EditText(context);
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
editText.setText(SettingsEnum.SB_API_URL.getString());
|
||||
|
||||
API_URL_CHANGE_LISTENER.setEditTextRef(editText);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setView(editText)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("reset"), API_URL_CHANGE_LISTENER)
|
||||
.setPositiveButton(android.R.string.ok, API_URL_CHANGE_LISTENER)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
|
||||
screen.addPreference(preference);
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
}
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
|
||||
preference.setTitle(str("settings_ie"));
|
||||
preference.setSummary(str("settings_ie_sum"));
|
||||
preference.setText(SponsorBlockUtils.exportSettings(applicationContext));
|
||||
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
SponsorBlockUtils.importSettings((String) newValue, applicationContext);
|
||||
return false;
|
||||
});
|
||||
screen.addPreference(preference);
|
||||
preferencesToDisableWhenSBDisabled.add(preference);
|
||||
}
|
||||
private void openGuidelines() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
||||
getActivity().startActivity(intent);
|
||||
}
|
||||
|
||||
private static class APIURLChangeListener implements DialogInterface.OnClickListener {
|
||||
private WeakReference<EditText> editTextRef;
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
EditText editText = editTextRef.get();
|
||||
if (editText == null)
|
||||
return;
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_NEUTRAL:
|
||||
SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue());
|
||||
Toast.makeText(applicationContext, str("api_url_reset"), Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
Editable text = editText.getText();
|
||||
Toast invalidToast = Toast.makeText(applicationContext, str("api_url_invalid"), Toast.LENGTH_SHORT);
|
||||
if (text == null) {
|
||||
invalidToast.show();
|
||||
} else {
|
||||
String textAsString = text.toString();
|
||||
if (textAsString.isEmpty() || !Patterns.WEB_URL.matcher(textAsString).matches()) {
|
||||
invalidToast.show();
|
||||
} else {
|
||||
SettingsEnum.SB_API_URL.saveValue(textAsString);
|
||||
Toast.makeText(applicationContext, str("api_url_changed"), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
break;
|
||||
private void fetchAndDisplayStats() {
|
||||
try {
|
||||
statsCategory.removeAll();
|
||||
Preference loadingPlaceholderPreference = new Preference(this.getActivity());
|
||||
loadingPlaceholderPreference.setEnabled(false);
|
||||
statsCategory.addPreference(loadingPlaceholderPreference);
|
||||
if (SettingsEnum.SB_ENABLED.getBoolean()) {
|
||||
loadingPlaceholderPreference.setTitle(str("sb_stats_loading"));
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
UserStats stats = SBRequester.retrieveUserStats();
|
||||
ReVancedUtils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
||||
addUserStats(loadingPlaceholderPreference, stats);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
loadingPlaceholderPreference.setTitle(str("sb_stats_sb_disabled"));
|
||||
}
|
||||
}
|
||||
|
||||
public void setEditTextRef(EditText editText) {
|
||||
editTextRef = new WeakReference<>(editText);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "fetchAndDisplayStats failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
SponsorBlockSettings.update(getActivity());
|
||||
private static final DecimalFormat statsNumberOfSegmentsSkippedFormatter = new DecimalFormat("#,###,###");
|
||||
|
||||
private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
try {
|
||||
if (stats == null) {
|
||||
loadingPlaceholder.setTitle(str("sb_stats_connection_failure"));
|
||||
return;
|
||||
}
|
||||
statsCategory.removeAll();
|
||||
Context context = statsCategory.getContext();
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
String userName = stats.userName;
|
||||
preference.setTitle(fromHtml(str("sb_stats_username", userName)));
|
||||
preference.setSummary(str("sb_stats_username_change"));
|
||||
preference.setText(userName);
|
||||
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
String newUserName = (String) value;
|
||||
String errorMessage = SBRequester.setUsername(newUserName);
|
||||
ReVancedUtils.runOnMainThread(() -> {
|
||||
if (errorMessage == null) {
|
||||
preference.setTitle(fromHtml(str("sb_stats_username", newUserName)));
|
||||
preference.setText(newUserName);
|
||||
ReVancedUtils.showToastLong(str("sb_stats_username_changed"));
|
||||
} else {
|
||||
preference.setText(userName); // revert to previous
|
||||
ReVancedUtils.showToastLong(errorMessage);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// number of segment submissions (does not include ignored segments)
|
||||
Preference preference = new Preference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
String formatted = statsNumberOfSegmentsSkippedFormatter.format(stats.segmentCount);
|
||||
preference.setTitle(fromHtml(str("sb_stats_submissions", formatted)));
|
||||
if (stats.segmentCount == 0) {
|
||||
preference.setSelectable(false);
|
||||
} else {
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
||||
preference1.getContext().startActivity(i);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// "user reputation". Usually not useful, since it appears most users have zero reputation.
|
||||
// But if there is a reputation, then show it here
|
||||
Preference preference = new Preference(context);
|
||||
preference.setTitle(fromHtml(str("sb_stats_reputation", stats.reputation)));
|
||||
preference.setSelectable(false);
|
||||
if (stats.reputation != 0) {
|
||||
statsCategory.addPreference(preference);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// time saved for other users
|
||||
Preference preference = new Preference(context);
|
||||
statsCategory.addPreference(preference);
|
||||
|
||||
String stats_saved;
|
||||
String stats_saved_sum;
|
||||
if (stats.segmentCount == 0) {
|
||||
stats_saved = str("sb_stats_saved_zero");
|
||||
stats_saved_sum = str("sb_stats_saved_sum_zero");
|
||||
} else {
|
||||
stats_saved = str("sb_stats_saved", statsNumberOfSegmentsSkippedFormatter.format(stats.viewCount));
|
||||
stats_saved_sum = str("sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
||||
}
|
||||
preference.setTitle(fromHtml(stats_saved));
|
||||
preference.setSummary(fromHtml(stats_saved_sum));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||
preference1.getContext().startActivity(i);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,13 +19,13 @@ class PlayerControlsVisibilityObserverImpl(
|
||||
* id of the direct parent of controls_layout, R.id.youtube_controls_overlay
|
||||
*/
|
||||
private val controlsLayoutParentId =
|
||||
ReVancedUtils.getResourceIdByName(activity, "id", "youtube_controls_overlay")
|
||||
ReVancedUtils.getResourceIdentifier(activity, "youtube_controls_overlay", "id")
|
||||
|
||||
/**
|
||||
* id of R.id.controls_layout
|
||||
*/
|
||||
private val controlsLayoutId =
|
||||
ReVancedUtils.getResourceIdByName(activity, "id", "controls_layout")
|
||||
ReVancedUtils.getResourceIdentifier(activity, "controls_layout", "id")
|
||||
|
||||
/**
|
||||
* reference to the controls layout view
|
||||
|
@ -7,8 +7,8 @@ import app.revanced.integrations.utils.Event
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class PlayerType {
|
||||
NONE, // includes Shorts playback
|
||||
HIDDEN, // also includes YouTube Shorts and Stories, if a regular video is minimized and a Short/Story is then opened
|
||||
NONE, // includes Shorts and Stories playback
|
||||
HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened
|
||||
WATCH_WHILE_MINIMIZED,
|
||||
WATCH_WHILE_MAXIMIZED,
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
@ -16,7 +16,7 @@ enum class PlayerType {
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
INLINE_MINIMAL,
|
||||
INLINE_MINIMAL, // home feed video playback
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
|
||||
@ -48,6 +48,7 @@ enum class PlayerType {
|
||||
/**
|
||||
* player type change listener
|
||||
*/
|
||||
@JvmStatic
|
||||
val onChange = Event<PlayerType>()
|
||||
}
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
// invoke-static {p0}, Lpl/jakubweg/InjectedPlugin;->inject(Landroid/content/Context;)V
|
||||
// invoke-static {}, Lpl/jakubweg/InjectedPlugin;->printSomething()V
|
||||
// InlineTimeBar
|
||||
public class InjectedPlugin {
|
||||
|
||||
public static void printSomething() {
|
||||
LogHelper.printDebug(() -> "printSomething called");
|
||||
}
|
||||
|
||||
public static void printObject(Object o, int recursive) {
|
||||
if (o == null)
|
||||
LogHelper.printDebug(() -> "Printed object is null");
|
||||
else {
|
||||
LogHelper.printDebug(() -> "Printed object ("
|
||||
+ o.getClass().getName()
|
||||
+ ") = " + o.toString());
|
||||
for (Field field : o.getClass().getDeclaredFields()) {
|
||||
if (field.getType().isPrimitive())
|
||||
continue;
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object value = field.get(o);
|
||||
try {
|
||||
// if ("java.lang.String".equals(field.getType().getName()))
|
||||
LogHelper.printDebug(() -> "Field: " + field.toString() + " has value " + value);
|
||||
} catch (Exception e) {
|
||||
LogHelper.printDebug(() -> "Field: " + field.toString() + " has value that thrown an exception in toString method");
|
||||
}
|
||||
if (recursive > 0 && value != null && !value.getClass().isPrimitive())
|
||||
printObject(value, recursive - 1);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void printObject(Object o) {
|
||||
printObject(o, 0);
|
||||
}
|
||||
|
||||
public static void printObject(int o) {
|
||||
printObject(Integer.valueOf(o));
|
||||
}
|
||||
|
||||
public static void printObject(float o) {
|
||||
printObject(Float.valueOf(o));
|
||||
}
|
||||
|
||||
public static void printObject(long o) {
|
||||
printObject(Long.valueOf(o));
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
|
||||
LogHelper.printDebug(() -> "Printing stack trace:");
|
||||
for (StackTraceElement element : stackTrace) {
|
||||
LogHelper.printDebug(() -> element.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void printViewStack(final View view, int spaces) {
|
||||
StringBuilder builder = new StringBuilder(spaces);
|
||||
for (int i = 0; i < spaces; i++) {
|
||||
builder.append('-');
|
||||
}
|
||||
String spacesStr = builder.toString();
|
||||
|
||||
if (view == null) {
|
||||
LogHelper.printDebug(() -> spacesStr + "Null view");
|
||||
return;
|
||||
}
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
LogHelper.printDebug(() -> spacesStr + "View group: " + view);
|
||||
int childCount = group.getChildCount();
|
||||
LogHelper.printDebug(() -> spacesStr + "Children count: " + childCount);
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
printViewStack(group.getChildAt(i), spaces + 1);
|
||||
}
|
||||
} else {
|
||||
LogHelper.printDebug(() -> spacesStr + "Normal view: " + view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideNewSegmentLayout;
|
||||
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showNewSegmentLayout;
|
||||
|
||||
public class NewSegmentHelperLayout {
|
||||
public static Context context;
|
||||
private static boolean isShown = false;
|
||||
|
||||
public static void show() {
|
||||
if (isShown) return;
|
||||
isShown = true;
|
||||
showNewSegmentLayout();
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
if (!isShown) return;
|
||||
isShown = false;
|
||||
hideNewSegmentLayout();
|
||||
}
|
||||
|
||||
public static void toggle() {
|
||||
if (isShown) hide();
|
||||
else show();
|
||||
}
|
||||
}
|
@ -1,431 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments;
|
||||
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments;
|
||||
|
||||
public class PlayerController {
|
||||
|
||||
private static final Timer sponsorTimer = new Timer("sponsor-skip-timer");
|
||||
public static WeakReference<Activity> playerActivity = new WeakReference<>(null);
|
||||
public static SponsorSegment[] sponsorSegmentsOfCurrentVideo;
|
||||
private static long allowNextSkipRequestTime = 0L;
|
||||
private static String currentVideoId;
|
||||
private static long lastKnownVideoTime = -1L;
|
||||
private static final Runnable findAndSkipSegmentRunnable = () -> {
|
||||
findAndSkipSegment(false);
|
||||
};
|
||||
private static float sponsorBarLeft = 1f;
|
||||
private static float sponsorBarRight = 1f;
|
||||
private static float sponsorBarThickness = 2f;
|
||||
private static TimerTask skipSponsorTask = null;
|
||||
|
||||
public static String getCurrentVideoId() {
|
||||
return currentVideoId;
|
||||
}
|
||||
|
||||
public static void setCurrentVideoId(final String videoId) {
|
||||
try {
|
||||
if (videoId == null) {
|
||||
currentVideoId = null;
|
||||
sponsorSegmentsOfCurrentVideo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// currently this runs every time a video is loaded (regardless if sponsorblock is turned on or off)
|
||||
// FIXME: change this so if sponsorblock is disabled, then run this method exactly once and once only
|
||||
SponsorBlockSettings.update(null);
|
||||
|
||||
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
|
||||
currentVideoId = null;
|
||||
return;
|
||||
}
|
||||
if (PlayerType.getCurrent() == PlayerType.NONE) {
|
||||
LogHelper.printDebug(() -> "ignoring shorts video");
|
||||
currentVideoId = null;
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(currentVideoId))
|
||||
return;
|
||||
|
||||
currentVideoId = videoId;
|
||||
sponsorSegmentsOfCurrentVideo = null;
|
||||
LogHelper.printDebug(() -> "setCurrentVideoId: videoId=" + videoId);
|
||||
|
||||
sponsorTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
executeDownloadSegments(currentVideoId);
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Failed to download segments", e);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setCurrentVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when creating some kind of youtube internal player controlled, every time when new video starts to play
|
||||
*/
|
||||
public static void initialize(Object _o) {
|
||||
try {
|
||||
lastKnownVideoTime = 0;
|
||||
SkipSegmentView.hide();
|
||||
NewSegmentHelperLayout.hide();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void executeDownloadSegments(String videoId) {
|
||||
try {
|
||||
videoHasSegments = false;
|
||||
timeWithoutSegments = "";
|
||||
|
||||
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
||||
Arrays.sort(segments);
|
||||
|
||||
for (SponsorSegment segment : segments) {
|
||||
LogHelper.printDebug(() -> "Detected segment: " + segment.toString());
|
||||
}
|
||||
|
||||
sponsorSegmentsOfCurrentVideo = segments;
|
||||
// new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "executeDownloadSegments failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void setVideoTime(long millis) {
|
||||
try {
|
||||
if (!SettingsEnum.SB_ENABLED.getBoolean()) return;
|
||||
LogHelper.printDebug(() -> "setCurrentVideoTime: current video time: " + millis);
|
||||
// fixme? if (millis == lastKnownVideoTime), should it return here and not continue?
|
||||
lastKnownVideoTime = millis;
|
||||
if (millis <= 0) return;
|
||||
//findAndSkipSegment(false);
|
||||
|
||||
if (millis == VideoInformation.getCurrentVideoLength()) {
|
||||
SponsorBlockUtils.hideShieldButton();
|
||||
SponsorBlockUtils.hideVoteButton();
|
||||
return;
|
||||
}
|
||||
|
||||
SponsorSegment[] segments = sponsorSegmentsOfCurrentVideo;
|
||||
if (segments == null || segments.length == 0) return;
|
||||
|
||||
final long START_TIMER_BEFORE_SEGMENT_MILLIS = 1200;
|
||||
final long startTimerAtMillis = millis + START_TIMER_BEFORE_SEGMENT_MILLIS;
|
||||
|
||||
for (final SponsorSegment segment : segments) {
|
||||
if (segment.start > millis) {
|
||||
if (segment.start > startTimerAtMillis)
|
||||
break; // it's more then START_TIMER_BEFORE_SEGMENT_MILLIS far away
|
||||
if (!segment.category.behaviour.skip)
|
||||
break;
|
||||
|
||||
if (skipSponsorTask == null) {
|
||||
LogHelper.printDebug(() -> "Scheduling skipSponsorTask");
|
||||
skipSponsorTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
skipSponsorTask = null;
|
||||
lastKnownVideoTime = segment.start + 1;
|
||||
ReVancedUtils.runOnMainThread(findAndSkipSegmentRunnable);
|
||||
}
|
||||
};
|
||||
sponsorTimer.schedule(skipSponsorTask, segment.start - millis);
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "skipSponsorTask is already scheduled...");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (segment.end < millis)
|
||||
continue;
|
||||
|
||||
// we are in the segment!
|
||||
if (segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) {
|
||||
sendViewRequestAsync(millis, segment);
|
||||
skipSegment(segment, false);
|
||||
break;
|
||||
} else {
|
||||
SkipSegmentView.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
SkipSegmentView.hide();
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "setVideoTime failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendViewRequestAsync(final long millis, final SponsorSegment segment) {
|
||||
if (segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
if (context != null) {
|
||||
long newSkippedTime = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() + (segment.end - segment.start);
|
||||
SettingsEnum.SB_SKIPPED_SEGMENTS.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt() + 1);
|
||||
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.saveValue(newSkippedTime);
|
||||
}
|
||||
}
|
||||
if (SettingsEnum.SB_COUNT_SKIPS.getBoolean()
|
||||
&& segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED
|
||||
&& millis - segment.start < 2000) { // Only skips from the start should count as a view
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
SBRequester.sendViewCountRequest(segment);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void setHighPrecisionVideoTime(final long millis) {
|
||||
try {
|
||||
if ((millis < lastKnownVideoTime && lastKnownVideoTime >= VideoInformation.getCurrentVideoLength()) || millis == 0) {
|
||||
SponsorBlockUtils.showShieldButton(); // skipping from end to the video will show the buttons again
|
||||
SponsorBlockUtils.showVoteButton();
|
||||
}
|
||||
if (lastKnownVideoTime > 0) {
|
||||
lastKnownVideoTime = millis;
|
||||
} else
|
||||
setVideoTime(millis);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setHighPrecisionVideoTime failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getCurrentVideoLength() {
|
||||
return VideoInformation.getCurrentVideoLength();
|
||||
}
|
||||
|
||||
public static long getLastKnownVideoTime() {
|
||||
return lastKnownVideoTime;
|
||||
}
|
||||
|
||||
public static void setSponsorBarAbsoluteLeft(final Rect rect) {
|
||||
setSponsorBarAbsoluteLeft(rect.left);
|
||||
}
|
||||
|
||||
public static void setSponsorBarAbsoluteLeft(final float left) {
|
||||
LogHelper.printDebug(() -> String.format("setSponsorBarLeft: left=%.2f", left));
|
||||
|
||||
sponsorBarLeft = left;
|
||||
}
|
||||
|
||||
public static void setSponsorBarRect(final Object self) {
|
||||
try {
|
||||
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
|
||||
field.setAccessible(true);
|
||||
Rect rect = (Rect) field.get(self);
|
||||
if (rect != null) {
|
||||
setSponsorBarAbsoluteLeft(rect.left);
|
||||
setSponsorBarAbsoluteRight(rect.right);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setSponsorBarRect failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setSponsorBarAbsoluteRight(final Rect rect) {
|
||||
setSponsorBarAbsoluteRight(rect.right);
|
||||
}
|
||||
|
||||
public static void setSponsorBarAbsoluteRight(final float right) {
|
||||
LogHelper.printDebug(() -> String.format("setSponsorBarRight: right=%.2f", right));
|
||||
|
||||
sponsorBarRight = right;
|
||||
}
|
||||
|
||||
public static void setSponsorBarThickness(final int thickness) {
|
||||
try {
|
||||
setSponsorBarThickness((float) thickness);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setSponsorBarThickness(final float thickness) {
|
||||
// if (VERBOSE_DRAW_OPTIONS)
|
||||
// LogH(PlayerController.class, String.format("setSponsorBarThickness: thickness=%.2f", thickness));
|
||||
|
||||
sponsorBarThickness = thickness;
|
||||
}
|
||||
|
||||
public static void onSkipSponsorClicked() {
|
||||
LogHelper.printDebug(() -> "Skip segment clicked");
|
||||
findAndSkipSegment(true);
|
||||
}
|
||||
|
||||
|
||||
public static void addSkipSponsorView15(final View view) {
|
||||
try {
|
||||
playerActivity = new WeakReference<>((Activity) view.getContext());
|
||||
LogHelper.printDebug(() -> "addSkipSponsorView15: view=" + view.toString());
|
||||
|
||||
ReVancedUtils.runOnMainThreadDelayed(() -> {
|
||||
final ViewGroup viewGroup = (ViewGroup) ((ViewGroup) view).getChildAt(2);
|
||||
Activity context = ((Activity) viewGroup.getContext());
|
||||
NewSegmentHelperLayout.context = context;
|
||||
}, 500);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "addSkipSponsorView15 failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Edit: Is this method ever called? Where is the patch code that calls this?
|
||||
public static void addSkipSponsorView14(final View view) {
|
||||
try {
|
||||
playerActivity = new WeakReference<>((Activity) view.getContext());
|
||||
LogHelper.printDebug(() -> "addSkipSponsorView14: view=" + view.toString());
|
||||
ReVancedUtils.runOnMainThreadDelayed(() -> {
|
||||
final ViewGroup viewGroup = (ViewGroup) view.getParent();
|
||||
Activity activity = (Activity) viewGroup.getContext();
|
||||
NewSegmentHelperLayout.context = activity;
|
||||
}, 500);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "addSkipSponsorView14 failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when it's time to draw time bar
|
||||
*/
|
||||
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
|
||||
try {
|
||||
if (sponsorBarThickness < 0.1) return;
|
||||
if (sponsorSegmentsOfCurrentVideo == null) return;
|
||||
|
||||
|
||||
final float thicknessDiv2 = sponsorBarThickness / 2;
|
||||
final float top = posY - thicknessDiv2;
|
||||
final float bottom = posY + thicknessDiv2;
|
||||
final float absoluteLeft = sponsorBarLeft;
|
||||
final float absoluteRight = sponsorBarRight;
|
||||
|
||||
final float tmp1 = 1f / (float) VideoInformation.getCurrentVideoLength() * (absoluteRight - absoluteLeft);
|
||||
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
|
||||
float left = segment.start * tmp1 + absoluteLeft;
|
||||
float right = segment.end * tmp1 + absoluteLeft;
|
||||
canvas.drawRect(left, top, right, bottom, segment.category.paint);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "drawSponsorTimeBars failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// private final static Pattern videoIdRegex = Pattern.compile(".*\\.be\\/([A-Za-z0-9_\\-]{0,50}).*");
|
||||
public static String substringVideoIdFromLink(String link) {
|
||||
return link.substring(link.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
public static void skipRelativeMilliseconds(int millisRelative) {
|
||||
skipToMillisecond(lastKnownVideoTime + millisRelative);
|
||||
}
|
||||
|
||||
public static boolean skipToMillisecond(long millisecond) {
|
||||
// in 15.x if sponsor clip hits the end, then it crashes the app, because of too many function invocations
|
||||
// I put this block so that skip can be made only once per some time
|
||||
long now = System.currentTimeMillis();
|
||||
if (now < allowNextSkipRequestTime) {
|
||||
LogHelper.printDebug(() -> "skipToMillisecond: to fast, slow down, because you'll fail");
|
||||
return false;
|
||||
}
|
||||
allowNextSkipRequestTime = now + 100;
|
||||
|
||||
LogHelper.printDebug(() -> String.format("Requesting skip to millis=%d on thread %s", millisecond, Thread.currentThread().toString()));
|
||||
|
||||
final long finalMillisecond = millisecond;
|
||||
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Skipping to millis=" + finalMillisecond);
|
||||
lastKnownVideoTime = finalMillisecond;
|
||||
VideoInformation.seekTo(finalMillisecond);
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Cannot skip to millisecond", e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void findAndSkipSegment(boolean wasClicked) {
|
||||
try {
|
||||
if (sponsorSegmentsOfCurrentVideo == null)
|
||||
return;
|
||||
|
||||
final long millis = lastKnownVideoTime;
|
||||
|
||||
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
|
||||
if (segment.start > millis)
|
||||
break;
|
||||
|
||||
if (segment.end < millis)
|
||||
continue;
|
||||
|
||||
SkipSegmentView.show();
|
||||
if (!((segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) || wasClicked))
|
||||
return;
|
||||
|
||||
sendViewRequestAsync(millis, segment);
|
||||
skipSegment(segment, wasClicked);
|
||||
break;
|
||||
}
|
||||
|
||||
SkipSegmentView.hide();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "findAndSkipSegment failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void skipSegment(SponsorSegment segment, boolean wasClicked) {
|
||||
try {
|
||||
// if (lastSkippedSegment == segment) return;
|
||||
// lastSkippedSegment = segment;
|
||||
LogHelper.printDebug(() -> "Skipping segment: " + segment.toString());
|
||||
|
||||
if (SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean() && !wasClicked)
|
||||
SkipSegmentView.notifySkipped(segment);
|
||||
|
||||
boolean didSucceed = skipToMillisecond(segment.end + 2);
|
||||
if (didSucceed && !wasClicked) {
|
||||
segment.didAutoSkipped = true;
|
||||
}
|
||||
SkipSegmentView.hide();
|
||||
if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
|
||||
SponsorSegment[] newSegments = new SponsorSegment[sponsorSegmentsOfCurrentVideo.length - 1];
|
||||
int i = 0;
|
||||
for (SponsorSegment sponsorSegment : sponsorSegmentsOfCurrentVideo) {
|
||||
if (sponsorSegment != segment)
|
||||
newSegments[i++] = sponsorSegment;
|
||||
}
|
||||
sponsorSegmentsOfCurrentVideo = newSegments;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "skipSegment failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,672 @@
|
||||
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.Arrays;
|
||||
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.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
/**
|
||||
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
|
||||
*
|
||||
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
||||
*/
|
||||
public class SegmentPlaybackController {
|
||||
/**
|
||||
* Length of time to show a highlight segment manual skip.
|
||||
* Because there is no scheduled hide of a skip to highlight,
|
||||
* effectively this time value is rounded up to the next second.
|
||||
*/
|
||||
private static final long HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT = 3800;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7;
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
@Nullable
|
||||
private static SponsorSegment[] segmentsOfCurrentVideo;
|
||||
|
||||
/**
|
||||
* Highlight segment, if one exists.
|
||||
*/
|
||||
@Nullable
|
||||
private static SponsorSegment highlightSegment;
|
||||
|
||||
/**
|
||||
* Because loading can take time, show the skip to highlight for a few seconds after the segments load.
|
||||
* This is the system time (in milliseconds) to no longer show the initial display skip to highlight.
|
||||
*/
|
||||
private static long highlightSegmentInitialShowEndTime;
|
||||
|
||||
/**
|
||||
* Current (non-highlight) segment that user can manually skip.
|
||||
*/
|
||||
@Nullable
|
||||
private static SponsorSegment segmentCurrentlyPlaying;
|
||||
/**
|
||||
* Currently playing manual skip segment, that is scheduled to hide.
|
||||
* This will always be NULL or equal to {@link #segmentCurrentlyPlaying}.
|
||||
*/
|
||||
@Nullable
|
||||
private static SponsorSegment scheduledHideSegment;
|
||||
/**
|
||||
* Upcoming segment that is scheduled to either autoskip or show the manual skip button.
|
||||
*/
|
||||
@Nullable
|
||||
private static SponsorSegment scheduledUpcomingSegment;
|
||||
|
||||
@Nullable
|
||||
private static String timeWithoutSegments;
|
||||
|
||||
private static float sponsorBarLeft = 1f;
|
||||
private static float sponsorBarRight = 1f;
|
||||
private static float sponsorBarThickness = 2f;
|
||||
|
||||
@Nullable
|
||||
public static SponsorSegment[] getSegmentsOfCurrentVideo() {
|
||||
return segmentsOfCurrentVideo;
|
||||
}
|
||||
|
||||
static void setSegmentsOfCurrentVideo(@NonNull SponsorSegment[] segments) {
|
||||
Arrays.sort(segments);
|
||||
segmentsOfCurrentVideo = segments;
|
||||
calculateTimeWithoutSegments();
|
||||
|
||||
for (SponsorSegment segment : segments) {
|
||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
highlightSegment = segment;
|
||||
return;
|
||||
}
|
||||
}
|
||||
highlightSegment = null;
|
||||
}
|
||||
|
||||
public static boolean currentVideoHasSegments() {
|
||||
return segmentsOfCurrentVideo != null && segmentsOfCurrentVideo.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all downloaded data.
|
||||
*/
|
||||
private static void clearData() {
|
||||
currentVideoId = null;
|
||||
segmentsOfCurrentVideo = null;
|
||||
highlightSegment = null;
|
||||
highlightSegmentInitialShowEndTime = 0;
|
||||
timeWithoutSegments = null;
|
||||
segmentCurrentlyPlaying = null;
|
||||
scheduledUpcomingSegment = null; // prevent any existing scheduled skip from running
|
||||
scheduledHideSegment = null;
|
||||
toastSegmentSkipped = null; // prevent any scheduled skip toasts from showing
|
||||
toastNumberOfSegmentsSkipped = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Initializes SponsorBlock when the video player starts playing a new video.
|
||||
*/
|
||||
public static void initialize(Object _o) {
|
||||
try {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
SponsorBlockSettings.initialize();
|
||||
clearData();
|
||||
SponsorBlockViewController.hideAll();
|
||||
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
|
||||
LogHelper.printDebug(() -> "Initialized SponsorBlock");
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to initialize SponsorBlock", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setCurrentVideoId(@Nullable String videoId) {
|
||||
try {
|
||||
if (Objects.equals(currentVideoId, videoId)) {
|
||||
return;
|
||||
}
|
||||
clearData();
|
||||
if (videoId == null || !SettingsEnum.SB_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
LogHelper.printDebug(() -> "ignoring short or story");
|
||||
return;
|
||||
}
|
||||
if (!ReVancedUtils.isNetworkConnected()) {
|
||||
LogHelper.printDebug(() -> "Network not connected, ignoring video");
|
||||
return;
|
||||
}
|
||||
|
||||
currentVideoId = videoId;
|
||||
LogHelper.printDebug(() -> "setCurrentVideoId: " + videoId);
|
||||
|
||||
//noinspection UnnecessaryLocalVariable
|
||||
String videoIdToDownload = videoId; // make a copy, to use off main thread
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
executeDownloadSegments(videoIdToDownload);
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Failed to download segments", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setCurrentVideoId failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called off main thread
|
||||
*/
|
||||
static void executeDownloadSegments(@NonNull String videoId) {
|
||||
Objects.requireNonNull(videoId);
|
||||
try {
|
||||
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
||||
|
||||
ReVancedUtils.runOnMainThread(()-> {
|
||||
if (!videoId.equals(currentVideoId)) {
|
||||
// user changed videos before get segments network call could complete
|
||||
LogHelper.printDebug(() -> "Ignoring segments for prior video: " + videoId);
|
||||
return;
|
||||
}
|
||||
setSegmentsOfCurrentVideo(segments);
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
// if the current video time is before the highlight
|
||||
if (highlightSegment != null && videoTime < highlightSegment.end) {
|
||||
if (highlightSegment.shouldAutoSkip()) {
|
||||
skipSegment(highlightSegment, false);
|
||||
return;
|
||||
}
|
||||
highlightSegmentInitialShowEndTime = System.currentTimeMillis()
|
||||
+ HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT;
|
||||
}
|
||||
// check for any skips now, instead of waiting for the next update to setVideoTime()
|
||||
setVideoTime(videoTime);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "executeDownloadSegments failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Updates SponsorBlock every 1000ms.
|
||||
* When changing videos, this is first called with value 0 and then the video is changed.
|
||||
*/
|
||||
public static void setVideoTime(long millis) {
|
||||
try {
|
||||
if (!SettingsEnum.SB_ENABLED.getBoolean()
|
||||
|| PlayerType.getCurrent().isNoneOrHidden() // shorts playback
|
||||
|| segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) {
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "setVideoTime: " + millis);
|
||||
|
||||
// to debug the timing logic, set this to a very large value (5000 or more)
|
||||
// then try manually seeking just playback reaches a skip/hide of different segments
|
||||
final long lookAheadMilliseconds = 1500; // must be larger than the average time between calls to this method
|
||||
final float playbackSpeed = VideoInformation.getCurrentPlaybackSpeed();
|
||||
final long startTimerLookAheadThreshold = millis + (long)(playbackSpeed * lookAheadMilliseconds);
|
||||
|
||||
SponsorSegment foundCurrentSegment = null;
|
||||
SponsorSegment foundUpcomingSegment = null;
|
||||
|
||||
for (final SponsorSegment segment : segmentsOfCurrentVideo) {
|
||||
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|
||||
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|
||||
|| segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
continue;
|
||||
}
|
||||
if (segment.end <= millis) {
|
||||
continue; // past this segment
|
||||
}
|
||||
|
||||
if (segment.start <= millis) {
|
||||
// we are in the segment!
|
||||
if (segment.shouldAutoSkip()) {
|
||||
skipSegment(segment, false);
|
||||
return; // must return, as skipping causes a recursive call back into this method
|
||||
}
|
||||
|
||||
// first found segment, or it's an embedded segment and fully inside the outer segment
|
||||
if (foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment)) {
|
||||
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
|
||||
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
|
||||
// Also prevents showing the skip button if user seeks into the last half second of the segment.
|
||||
final long minMillisOfSegmentRemainingThreshold = 500;
|
||||
if (segmentCurrentlyPlaying == segment
|
||||
|| !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) {
|
||||
foundCurrentSegment = segment;
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "Ignoring segment that ends very soon: " + segment);
|
||||
}
|
||||
}
|
||||
// Keep iterating and looking. There may be an upcoming autoskip,
|
||||
// or there may be another smaller segment nested inside this segment
|
||||
continue;
|
||||
}
|
||||
|
||||
// segment is upcoming
|
||||
if (startTimerLookAheadThreshold < segment.start) {
|
||||
break; // segment is not close enough to schedule, and no segments after this are of interest
|
||||
}
|
||||
if (segment.shouldAutoSkip()) { // upcoming autoskip
|
||||
foundUpcomingSegment = segment;
|
||||
break; // must stop here
|
||||
}
|
||||
|
||||
// upcoming manual skip
|
||||
|
||||
// do not schedule upcoming segment, if it is not fully contained inside the current segment
|
||||
if ((foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment))
|
||||
// use the most inner upcoming segment
|
||||
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
|
||||
|
||||
// Only schedule, if the segment start time is not near the end time of the current segment.
|
||||
// This check is needed to prevent scheduled hide and show from clashing with each other.
|
||||
final long minTimeBetweenStartEndOfSegments = 1000;
|
||||
if (foundCurrentSegment == null
|
||||
|| !foundCurrentSegment.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) {
|
||||
foundUpcomingSegment = segment;
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightSegment != null && (millis < HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT
|
||||
|| System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
|
||||
SponsorBlockViewController.showSkipHighlightButton(highlightSegment);
|
||||
} else {
|
||||
SponsorBlockViewController.hideSkipHighlightButton();
|
||||
}
|
||||
|
||||
if (segmentCurrentlyPlaying != foundCurrentSegment) {
|
||||
if (foundCurrentSegment == null) {
|
||||
LogHelper.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying);
|
||||
segmentCurrentlyPlaying = null;
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
} else {
|
||||
segmentCurrentlyPlaying = foundCurrentSegment;
|
||||
LogHelper.printDebug(() -> "Showing segment: " + segmentCurrentlyPlaying);
|
||||
SponsorBlockViewController.showSkipSegmentButton(foundCurrentSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// must be greater than the average time between updates to VideoInformation time
|
||||
final long videoInformationTimeUpdateThresholdMilliseconds = 250;
|
||||
|
||||
// schedule a hide, only if the segment end is near
|
||||
final SponsorSegment segmentToHide =
|
||||
(foundCurrentSegment != null && foundCurrentSegment.endIsNear(millis, lookAheadMilliseconds))
|
||||
? foundCurrentSegment
|
||||
: null;
|
||||
|
||||
if (scheduledHideSegment != segmentToHide) {
|
||||
if (segmentToHide == null) {
|
||||
LogHelper.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment);
|
||||
scheduledHideSegment = null;
|
||||
} else {
|
||||
scheduledHideSegment = segmentToHide;
|
||||
LogHelper.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed);
|
||||
final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed);
|
||||
ReVancedUtils.runOnMainThreadDelayed(() -> {
|
||||
if (scheduledHideSegment != segmentToHide) {
|
||||
LogHelper.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide);
|
||||
return;
|
||||
}
|
||||
scheduledHideSegment = null;
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (!segmentToHide.endIsNear(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) {
|
||||
// current video time is not what's expected. User paused playback
|
||||
LogHelper.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
|
||||
+ " videoInformation time: " + videoTime);
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Running scheduled hide segment: " + segmentToHide);
|
||||
// Need more than just hide the skip button, as this may have been an embedded segment
|
||||
// Instead call back into setVideoTime to check everything again.
|
||||
// Should not use VideoInformation time as it is less accurate,
|
||||
// but this scheduled handler was scheduled precisely so we can just use the segment end time
|
||||
segmentCurrentlyPlaying = null;
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
setVideoTime(segmentToHide.end);
|
||||
}, delayUntilHide);
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduledUpcomingSegment != foundUpcomingSegment) {
|
||||
if (foundUpcomingSegment == null) {
|
||||
LogHelper.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment);
|
||||
scheduledUpcomingSegment = null;
|
||||
} else {
|
||||
scheduledUpcomingSegment = foundUpcomingSegment;
|
||||
final SponsorSegment segmentToSkip = foundUpcomingSegment;
|
||||
|
||||
LogHelper.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed);
|
||||
final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed);
|
||||
ReVancedUtils.runOnMainThreadDelayed(() -> {
|
||||
if (scheduledUpcomingSegment != segmentToSkip) {
|
||||
LogHelper.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip);
|
||||
return;
|
||||
}
|
||||
scheduledUpcomingSegment = null;
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (!segmentToSkip.startIsNear(videoTime,
|
||||
videoInformationTimeUpdateThresholdMilliseconds)) {
|
||||
// current video time is not what's expected. User paused playback
|
||||
LogHelper.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
|
||||
+ " videoInformation time: " + videoTime);
|
||||
return;
|
||||
}
|
||||
if (segmentToSkip.shouldAutoSkip()) {
|
||||
LogHelper.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
|
||||
skipSegment(segmentToSkip, false);
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "Running scheduled show segment: " + segmentToSkip);
|
||||
segmentCurrentlyPlaying = segmentToSkip;
|
||||
SponsorBlockViewController.showSkipSegmentButton(segmentToSkip);
|
||||
}
|
||||
}, delayUntilSkip);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "setVideoTime failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static SponsorSegment lastSegmentSkipped;
|
||||
private static long lastSegmentSkippedTime;
|
||||
|
||||
private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) {
|
||||
try {
|
||||
// If trying to seek to end of the video, YouTube can seek just short of the actual end.
|
||||
// (especially if the video does not end on a whole second boundary).
|
||||
// This causes additional segment skip attempts, even though it cannot seek any closer to the desired time.
|
||||
// Check for and ignore repeated skip attempts of the same segment over a short time period.
|
||||
final long now = System.currentTimeMillis();
|
||||
final long minimumMillisecondsBetweenSkippingSameSegment = 500;
|
||||
if ((lastSegmentSkipped == segmentToSkip) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) {
|
||||
LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip);
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Skipping segment: " + segmentToSkip);
|
||||
lastSegmentSkipped = segmentToSkip;
|
||||
lastSegmentSkippedTime = now;
|
||||
segmentCurrentlyPlaying = null;
|
||||
scheduledHideSegment = null;
|
||||
scheduledUpcomingSegment = null;
|
||||
if (segmentToSkip == highlightSegment) {
|
||||
highlightSegmentInitialShowEndTime = 0;
|
||||
}
|
||||
SponsorBlockViewController.hideSkipHighlightButton();
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
|
||||
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
|
||||
if (!seekSuccessful) {
|
||||
// can happen when switching videos and is normal
|
||||
LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
|
||||
return;
|
||||
}
|
||||
|
||||
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 : segmentsOfCurrentVideo) {
|
||||
if (segmentToSkip.end < otherSegment.start) {
|
||||
break; // no other segments can be contained
|
||||
}
|
||||
if (segmentToSkip.containsSegment(otherSegment)) { // includes checking the segment against itself
|
||||
otherSegment.didAutoSkipped = true; // skipped this segment as well
|
||||
if (showSkipToast) {
|
||||
showSkippedSegmentToast(otherSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
|
||||
// skipped segment was a preview of unsubmitted segment
|
||||
// remove the segment from the UI view
|
||||
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
|
||||
SponsorSegment[] newSegments = new SponsorSegment[segmentsOfCurrentVideo.length - 1];
|
||||
int i = 0;
|
||||
for (SponsorSegment segment : segmentsOfCurrentVideo) {
|
||||
if (segment != segmentToSkip)
|
||||
newSegments[i++] = segment;
|
||||
}
|
||||
setSegmentsOfCurrentVideo(newSegments);
|
||||
} else {
|
||||
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "skipSegment failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static int toastNumberOfSegmentsSkipped;
|
||||
@Nullable
|
||||
private static SponsorSegment toastSegmentSkipped;
|
||||
|
||||
private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
toastNumberOfSegmentsSkipped++;
|
||||
if (toastNumberOfSegmentsSkipped > 1) {
|
||||
return; // toast already scheduled
|
||||
}
|
||||
toastSegmentSkipped = segment;
|
||||
|
||||
final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments
|
||||
ReVancedUtils.runOnMainThreadDelayed(() -> {
|
||||
try {
|
||||
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
|
||||
LogHelper.printDebug(() -> "Ignoring old scheduled show toast");
|
||||
return;
|
||||
}
|
||||
ReVancedUtils.showToastShort(toastNumberOfSegmentsSkipped == 1
|
||||
? toastSegmentSkipped.getSkippedToastText()
|
||||
: str("sb_skipped_multiple_segments"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "showSkippedSegmentToast failure", ex);
|
||||
} finally {
|
||||
toastNumberOfSegmentsSkipped = 0;
|
||||
toastSegmentSkipped = null;
|
||||
}
|
||||
}, delayToToastMilliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segment can be either a highlight or a regular manual skip segment.
|
||||
*/
|
||||
public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) {
|
||||
try {
|
||||
if (segment != highlightSegment && segment != segmentCurrentlyPlaying) {
|
||||
LogHelper.printException(() -> "error: segment not available to skip"); // should never happen
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
SponsorBlockViewController.hideSkipHighlightButton();
|
||||
return;
|
||||
}
|
||||
skipSegment(segment, true);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onSkipSegmentClicked failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void setSponsorBarRect(final Object self) {
|
||||
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);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setSponsorBarRect failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setSponsorBarAbsoluteRight(final Rect rect) {
|
||||
setSponsorBarAbsoluteRight(rect.right);
|
||||
}
|
||||
|
||||
public static void setSponsorBarAbsoluteRight(final float right) {
|
||||
if (sponsorBarRight != right) {
|
||||
LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteRight: right=%.2f", right));
|
||||
sponsorBarRight = right;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
*/
|
||||
public static void setSponsorBarThickness(final int thickness) {
|
||||
setSponsorBarThickness((float) thickness);
|
||||
}
|
||||
|
||||
public static void setSponsorBarThickness(final float thickness) {
|
||||
try {
|
||||
if (sponsorBarThickness != thickness) {
|
||||
LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness));
|
||||
sponsorBarThickness = thickness;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String appendTimeWithoutSegments(String totalTime) {
|
||||
try {
|
||||
if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_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
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex);
|
||||
}
|
||||
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
private static void calculateTimeWithoutSegments() {
|
||||
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
|
||||
if (!SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0
|
||||
|| segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) {
|
||||
timeWithoutSegments = null;
|
||||
return;
|
||||
}
|
||||
|
||||
long timeWithoutSegmentsValue = currentVideoLength + 500; // YouTube:tm:
|
||||
for (SponsorSegment segment : segmentsOfCurrentVideo) {
|
||||
timeWithoutSegmentsValue -= segment.length();
|
||||
}
|
||||
final long hours = timeWithoutSegmentsValue / 3600000;
|
||||
final long minutes = (timeWithoutSegmentsValue / 60000) % 60;
|
||||
final long seconds = (timeWithoutSegmentsValue / 1000) % 60;
|
||||
if (hours > 0) {
|
||||
timeWithoutSegments = String.format("\u2009(%d:%02d:%02d)", hours, minutes, seconds);
|
||||
} else {
|
||||
timeWithoutSegments = String.format("\u2009(%d:%02d)", minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use
|
||||
private static int getHighlightSegmentTimeBarScreenWidth() {
|
||||
if (highlightSegmentTimeBarScreenWidth == -1) {
|
||||
highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH,
|
||||
Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics());
|
||||
}
|
||||
return highlightSegmentTimeBarScreenWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
|
||||
try {
|
||||
if (sponsorBarThickness < 0.1) return;
|
||||
if (segmentsOfCurrentVideo == null) return;
|
||||
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
|
||||
if (currentVideoLength <= 0) return;
|
||||
|
||||
final float thicknessDiv2 = sponsorBarThickness / 2;
|
||||
final float top = posY - thicknessDiv2;
|
||||
final float bottom = posY + thicknessDiv2;
|
||||
final float absoluteLeft = sponsorBarLeft;
|
||||
final float absoluteRight = sponsorBarRight;
|
||||
|
||||
final float tmp1 = (1f / currentVideoLength) * (absoluteRight - absoluteLeft);
|
||||
for (SponsorSegment segment : segmentsOfCurrentVideo) {
|
||||
final float left = segment.start * tmp1 + absoluteLeft;
|
||||
final float right;
|
||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
right = left + getHighlightSegmentTimeBarScreenWidth();
|
||||
} else {
|
||||
right = segment.end * tmp1 + absoluteLeft;
|
||||
}
|
||||
canvas.drawRect(left, top, right, bottom, segment.category.paint);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "drawSponsorTimeBars failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class ShieldButton {
|
||||
static RelativeLayout _youtubeControlsLayout;
|
||||
static WeakReference<ImageView> _shieldBtn = new WeakReference<>(null);
|
||||
static int fadeDurationFast;
|
||||
static int fadeDurationScheduled;
|
||||
static Animation fadeIn;
|
||||
static Animation fadeOut;
|
||||
static boolean isShowing;
|
||||
|
||||
public static void initialize(Object viewStub) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "initializing shield button");
|
||||
|
||||
_youtubeControlsLayout = (RelativeLayout) viewStub;
|
||||
|
||||
ImageView imageView = (ImageView) _youtubeControlsLayout
|
||||
.findViewById(getIdentifier("sponsorblock_button", "id"));
|
||||
|
||||
if (imageView == null) {
|
||||
LogHelper.printDebug(() -> "Couldn't find imageView with \"sponsorblock_button\"");
|
||||
}
|
||||
if (imageView == null) return;
|
||||
imageView.setOnClickListener(SponsorBlockUtils.sponsorBlockBtnListener);
|
||||
_shieldBtn = new WeakReference<>(imageView);
|
||||
|
||||
// Animations
|
||||
fadeDurationFast = getInteger("fade_duration_fast");
|
||||
fadeDurationScheduled = getInteger("fade_duration_scheduled");
|
||||
fadeIn = getAnimation("fade_in");
|
||||
fadeIn.setDuration(fadeDurationFast);
|
||||
fadeOut = getAnimation("fade_out");
|
||||
fadeOut.setDuration(fadeDurationScheduled);
|
||||
isShowing = true;
|
||||
changeVisibilityImmediate(false);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void changeVisibilityImmediate(boolean visible) {
|
||||
changeVisibility(visible, true);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate(boolean visible) {
|
||||
changeVisibility(!visible, true);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible) {
|
||||
changeVisibility(visible, false);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible, boolean immediate) {
|
||||
try {
|
||||
if (isShowing == visible) return;
|
||||
isShowing = visible;
|
||||
|
||||
ImageView iView = _shieldBtn.get();
|
||||
if (_youtubeControlsLayout == null || iView == null) return;
|
||||
|
||||
if (visible && shouldBeShown()) {
|
||||
if (getLastKnownVideoTime() >= getCurrentVideoLength()) {
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Fading in");
|
||||
|
||||
iView.setVisibility(View.VISIBLE);
|
||||
if (!immediate)
|
||||
iView.startAnimation(fadeIn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (iView.getVisibility() == View.VISIBLE) {
|
||||
LogHelper.printDebug(() -> "Fading out");
|
||||
if (!immediate)
|
||||
iView.startAnimation(fadeOut);
|
||||
iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "changeVisibility failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldBeShown() {
|
||||
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean();
|
||||
}
|
||||
|
||||
//region Helpers
|
||||
private static int getIdentifier(String name, String defType) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
}
|
||||
|
||||
private static int getInteger(String name) {
|
||||
return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer"));
|
||||
}
|
||||
|
||||
private static Animation getAnimation(String name) {
|
||||
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim"));
|
||||
}
|
||||
//endregion
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideSkipButton;
|
||||
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showSkipButton;
|
||||
|
||||
@SuppressLint({"RtlHardcoded", "SetTextI18n", "AppCompatCustomView"})
|
||||
public class SkipSegmentView {
|
||||
|
||||
private static SponsorSegment lastNotifiedSegment;
|
||||
|
||||
public static void show() {
|
||||
showSkipButton();
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
hideSkipButton();
|
||||
}
|
||||
|
||||
public static void notifySkipped(SponsorSegment segment) {
|
||||
if (segment == lastNotifiedSegment) {
|
||||
LogHelper.printDebug(() -> "notifySkipped; segment == lastNotifiedSegment");
|
||||
return;
|
||||
}
|
||||
lastNotifiedSegment = segment;
|
||||
String skipMessage = segment.category.skipMessage.toString();
|
||||
Context context = ReVancedUtils.getContext();
|
||||
LogHelper.printDebug(() -> String.format("notifySkipped; message=%s", skipMessage));
|
||||
|
||||
if (context != null)
|
||||
Toast.makeText(context, skipMessage, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public static float convertDpToPixel(float dp, Context context) {
|
||||
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
|
||||
}
|
||||
}
|
@ -1,202 +1,202 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.sf;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Patterns;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SponsorBlockSettings {
|
||||
|
||||
public static final String CATEGORY_COLOR_SUFFIX = "_color";
|
||||
public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.IGNORE;
|
||||
public static String sponsorBlockUrlCategories = "[]";
|
||||
public static void importSettings(@NonNull String json) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
try {
|
||||
JSONObject settingsJson = new JSONObject(json);
|
||||
JSONObject barTypesObject = settingsJson.getJSONObject("barTypes");
|
||||
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
|
||||
|
||||
public static void update(Activity _activity) {
|
||||
SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK);
|
||||
|
||||
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
|
||||
SkipSegmentView.hide();
|
||||
NewSegmentHelperLayout.hide();
|
||||
SponsorBlockUtils.hideShieldButton();
|
||||
SponsorBlockUtils.hideVoteButton();
|
||||
PlayerController.sponsorSegmentsOfCurrentVideo = null;
|
||||
} else { /*isAddNewSegmentEnabled*/
|
||||
SponsorBlockUtils.showShieldButton();
|
||||
}
|
||||
|
||||
if (!SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean()) {
|
||||
NewSegmentHelperLayout.hide();
|
||||
SponsorBlockUtils.hideShieldButton();
|
||||
} else {
|
||||
SponsorBlockUtils.showShieldButton();
|
||||
}
|
||||
|
||||
|
||||
if (!SettingsEnum.SB_VOTING_ENABLED.getBoolean())
|
||||
SponsorBlockUtils.hideVoteButton();
|
||||
else
|
||||
SponsorBlockUtils.showVoteButton();
|
||||
|
||||
SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values();
|
||||
final ArrayList<String> enabledCategories = new ArrayList<>(possibleBehaviours.length);
|
||||
for (SegmentInfo segment : SegmentInfo.values()) {
|
||||
String categoryColor = preferences.getString(segment.key + CATEGORY_COLOR_SUFFIX, SponsorBlockUtils.formatColorString(segment.defaultColor));
|
||||
segment.setColor(Color.parseColor(categoryColor));
|
||||
|
||||
SegmentBehaviour behaviour = null;
|
||||
String value = preferences.getString(segment.key, null);
|
||||
if (value != null) {
|
||||
for (SegmentBehaviour possibleBehaviour : possibleBehaviours) {
|
||||
if (possibleBehaviour.key.equals(value)) {
|
||||
behaviour = possibleBehaviour;
|
||||
break;
|
||||
}
|
||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||
// clear existing behavior, as browser plugin exports no behavior for ignored categories
|
||||
category.behaviour = CategoryBehaviour.IGNORE;
|
||||
if (barTypesObject.has(category.key)) {
|
||||
JSONObject categoryObject = barTypesObject.getJSONObject(category.key);
|
||||
category.setColor(categoryObject.getString("color"));
|
||||
}
|
||||
}
|
||||
if (behaviour != null) {
|
||||
segment.behaviour = behaviour;
|
||||
} else {
|
||||
behaviour = segment.behaviour;
|
||||
|
||||
for (int i = 0; i < categorySelectionsArray.length(); i++) {
|
||||
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
|
||||
|
||||
String categoryKey = categorySelectionObject.getString("name");
|
||||
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
|
||||
if (category == null) {
|
||||
continue; // unsupported category, ignore
|
||||
}
|
||||
|
||||
final int desktopKey = categorySelectionObject.getInt("option");
|
||||
CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKey(desktopKey);
|
||||
if (behaviour == null) {
|
||||
ReVancedUtils.showToastLong(categoryKey + " unknown behavior key: " + desktopKey);
|
||||
} else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) {
|
||||
ReVancedUtils.showToastLong("Skip-once behavior not allowed for " + category.key);
|
||||
category.behaviour = CategoryBehaviour.SKIP_AUTOMATICALLY; // use closest match
|
||||
} else {
|
||||
category.behaviour = behaviour;
|
||||
}
|
||||
}
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
|
||||
SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit();
|
||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||
category.save(editor);
|
||||
}
|
||||
editor.apply();
|
||||
|
||||
String userID = settingsJson.getString("userID");
|
||||
if (!isValidSBUserId(userID)) {
|
||||
throw new IllegalArgumentException("userId is blank");
|
||||
}
|
||||
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_TRACK_SKIP_COUNT.saveValue(settingsJson.getBoolean("trackViewCount"));
|
||||
|
||||
String serverAddress = settingsJson.getString("serverAddress");
|
||||
if (!isValidSBServerAddress(serverAddress)) {
|
||||
throw new IllegalArgumentException(str("sb_api_url_invalid"));
|
||||
}
|
||||
SettingsEnum.SB_API_URL.saveValue(serverAddress);
|
||||
|
||||
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
|
||||
final float minDuration = (float)settingsJson.getDouble("minDuration");
|
||||
if (minDuration < 0) {
|
||||
throw new IllegalArgumentException("invalid minDuration: " + minDuration);
|
||||
}
|
||||
SettingsEnum.SB_MIN_DURATION.saveValue(minDuration);
|
||||
|
||||
try {
|
||||
int skipCount = settingsJson.getInt("skipCount");
|
||||
if (skipCount < 0) {
|
||||
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
|
||||
}
|
||||
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(skipCount);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (behaviour.showOnTimeBar && segment != SegmentInfo.UNSUBMITTED)
|
||||
enabledCategories.add(segment.key);
|
||||
ReVancedUtils.showToastLong(str("sb_settings_import_successful"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
|
||||
ReVancedUtils.showToastLong(str("sb_settings_import_failed", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
|
||||
if (enabledCategories.isEmpty())
|
||||
sponsorBlockUrlCategories = "[]";
|
||||
else
|
||||
sponsorBlockUrlCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
|
||||
@NonNull
|
||||
public static String exportSettings() {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
try {
|
||||
LogHelper.printDebug(() -> "Creating SponsorBlock export settings string");
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
JSONObject barTypesObject = new JSONObject(); // categories' colors
|
||||
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
|
||||
|
||||
SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted();
|
||||
for (SegmentCategory category : categories) {
|
||||
JSONObject categoryObject = new JSONObject();
|
||||
String categoryKey = category.key;
|
||||
categoryObject.put("color", category.colorString());
|
||||
barTypesObject.put(categoryKey, categoryObject);
|
||||
|
||||
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
||||
JSONObject behaviorObject = new JSONObject();
|
||||
behaviorObject.put("name", categoryKey);
|
||||
behaviorObject.put("option", category.behaviour.desktopKey);
|
||||
categorySelectionsArray.put(behaviorObject);
|
||||
}
|
||||
}
|
||||
json.put("userID", SettingsEnum.SB_UUID.getString());
|
||||
json.put("isVip", SettingsEnum.SB_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("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("categorySelections", categorySelectionsArray);
|
||||
json.put("barTypes", barTypesObject);
|
||||
|
||||
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"));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isValidSBUserId(@NonNull String userId) {
|
||||
return !userId.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* A non comprehensive check if a SB api server address is valid.
|
||||
*/
|
||||
public static boolean isValidSBServerAddress(@NonNull String serverAddress) {
|
||||
if (!Patterns.WEB_URL.matcher(serverAddress).matches()) {
|
||||
return false;
|
||||
}
|
||||
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
|
||||
// Could use Patterns.compile, but this is simpler
|
||||
final int lastDotIndex = serverAddress.lastIndexOf('.');
|
||||
if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) {
|
||||
return false;
|
||||
}
|
||||
// Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)"
|
||||
// but that should not be done on the main thread.
|
||||
// Instead, assume the domain exists and the user knows what they're doing.
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean initialized;
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
String uuid = SettingsEnum.SB_UUID.getString();
|
||||
if (uuid == null || uuid.length() == 0) {
|
||||
if (uuid.isEmpty()) {
|
||||
uuid = (UUID.randomUUID().toString() +
|
||||
UUID.randomUUID().toString() +
|
||||
UUID.randomUUID().toString())
|
||||
.replace("-", "");
|
||||
SettingsEnum.SB_UUID.saveValue(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SegmentBehaviour {
|
||||
SKIP_AUTOMATICALLY_ONCE("skip-once", 3, sf("skip_automatically_once"), true, true),
|
||||
SKIP_AUTOMATICALLY("skip", 2, sf("skip_automatically"), true, true),
|
||||
MANUAL_SKIP("manual-skip", 1, sf("skip_showbutton"), false, true),
|
||||
IGNORE("ignore", -1, sf("skip_ignore"), false, false);
|
||||
|
||||
public final String key;
|
||||
public final int desktopKey;
|
||||
public final StringRef name;
|
||||
public final boolean skip;
|
||||
public final boolean showOnTimeBar;
|
||||
|
||||
SegmentBehaviour(String key,
|
||||
int desktopKey,
|
||||
StringRef name,
|
||||
boolean skip,
|
||||
boolean showOnTimeBar) {
|
||||
this.key = key;
|
||||
this.desktopKey = desktopKey;
|
||||
this.name = name;
|
||||
this.skip = skip;
|
||||
this.showOnTimeBar = showOnTimeBar;
|
||||
}
|
||||
|
||||
public static SegmentBehaviour byDesktopKey(int desktopKey) {
|
||||
for (SegmentBehaviour behaviour : values()) {
|
||||
if (behaviour.desktopKey == desktopKey) {
|
||||
return behaviour;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SegmentInfo {
|
||||
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFF00d400),
|
||||
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF00ffff),
|
||||
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF0202ed),
|
||||
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFcc00ff),
|
||||
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFffff00),
|
||||
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFFff9900),
|
||||
PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), DefaultBehaviour, 0xFF008fd6),
|
||||
FILLER("filler", sf("segments_filler"), sf("skipped_filler"), sf("segments_filler_sum"), DefaultBehaviour, 0xFF7300FF),
|
||||
UNSUBMITTED("unsubmitted", StringRef.empty, sf("skipped_unsubmitted"), StringRef.empty, SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFFFFFFF);
|
||||
|
||||
private static final SegmentInfo[] mValuesWithoutUnsubmitted = new SegmentInfo[]{
|
||||
SPONSOR,
|
||||
INTRO,
|
||||
OUTRO,
|
||||
INTERACTION,
|
||||
SELF_PROMO,
|
||||
MUSIC_OFFTOPIC,
|
||||
PREVIEW,
|
||||
FILLER
|
||||
};
|
||||
private static final Map<String, SegmentInfo> mValuesMap = new HashMap<>(values().length);
|
||||
|
||||
static {
|
||||
for (SegmentInfo value : valuesWithoutUnsubmitted())
|
||||
mValuesMap.put(value.key, value);
|
||||
}
|
||||
|
||||
public final String key;
|
||||
public final StringRef title;
|
||||
public final StringRef skipMessage;
|
||||
public final StringRef description;
|
||||
public final Paint paint;
|
||||
public final int defaultColor;
|
||||
public int color;
|
||||
public SegmentBehaviour behaviour;
|
||||
|
||||
SegmentInfo(String key,
|
||||
StringRef title,
|
||||
StringRef skipMessage,
|
||||
StringRef description,
|
||||
SegmentBehaviour behaviour,
|
||||
int defaultColor) {
|
||||
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.skipMessage = skipMessage;
|
||||
this.description = description;
|
||||
this.behaviour = behaviour;
|
||||
this.defaultColor = defaultColor;
|
||||
this.color = defaultColor;
|
||||
this.paint = new Paint();
|
||||
}
|
||||
|
||||
public static SegmentInfo[] valuesWithoutUnsubmitted() {
|
||||
return mValuesWithoutUnsubmitted;
|
||||
}
|
||||
|
||||
public static SegmentInfo byCategoryKey(String key) {
|
||||
return mValuesMap.get(key);
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
color = color & 0xFFFFFF;
|
||||
this.color = color;
|
||||
paint.setColor(color);
|
||||
paint.setAlpha(255);
|
||||
}
|
||||
|
||||
public CharSequence getTitleWithDot() {
|
||||
return Html.fromHtml(String.format("<font color=\"#%06X\">⬤</font> %s", color, title));
|
||||
}
|
||||
SegmentCategory.loadFromPreferences();
|
||||
}
|
||||
}
|
||||
|
@ -1,641 +1,443 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import static android.text.Html.fromHtml;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoId;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.sponsorSegmentsOfCurrentVideo;
|
||||
import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.FORMATTER;
|
||||
import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.SAVED_TEMPLATE;
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import static app.revanced.integrations.sponsorblock.requests.SBRequester.voteForSegment;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
||||
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote;
|
||||
import app.revanced.integrations.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.sponsorblock.objects.UserStats;
|
||||
import app.revanced.integrations.sponsorblock.requests.SBRequester;
|
||||
|
||||
public abstract class SponsorBlockUtils {
|
||||
public static final String DATE_FORMAT = "HH:mm:ss.SSS";
|
||||
/**
|
||||
* Not thread safe. All fields/methods must be accessed from the main thread.
|
||||
*/
|
||||
public class SponsorBlockUtils {
|
||||
private static final String MANUAL_EDIT_TIME_FORMAT = "HH:mm:ss.SSS";
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
public static final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
|
||||
public static boolean videoHasSegments = false;
|
||||
public static String timeWithoutSegments = "";
|
||||
private static final int sponsorBtnId = 1234;
|
||||
private static final SimpleDateFormat manualEditTimeFormatter = new SimpleDateFormat(MANUAL_EDIT_TIME_FORMAT);
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private static final SimpleDateFormat voteSegmentTimeFormatter = new SimpleDateFormat();
|
||||
static {
|
||||
TimeZone utc = TimeZone.getTimeZone("UTC");
|
||||
manualEditTimeFormatter.setTimeZone(utc);
|
||||
voteSegmentTimeFormatter.setTimeZone(utc);
|
||||
}
|
||||
private static final String LOCKED_COLOR = "#FFC83D";
|
||||
public static final View.OnClickListener sponsorBlockBtnListener = v -> {
|
||||
LogHelper.printDebug(() -> "Shield button clicked");
|
||||
NewSegmentHelperLayout.toggle();
|
||||
};
|
||||
public static final View.OnClickListener voteButtonListener = v -> {
|
||||
LogHelper.printDebug(() -> "Vote button clicked");
|
||||
SponsorBlockUtils.onVotingClicked(v.getContext());
|
||||
};
|
||||
private static int shareBtnId = -1;
|
||||
|
||||
private static long newSponsorSegmentDialogShownMillis;
|
||||
private static long newSponsorSegmentStartMillis = -1;
|
||||
private static long newSponsorSegmentEndMillis = -1;
|
||||
private static boolean newSponsorSegmentPreviewed;
|
||||
private static final DialogInterface.OnClickListener newSponsorSegmentDialogListener = new DialogInterface.OnClickListener() {
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
// start
|
||||
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
|
||||
Toast.makeText(context.getApplicationContext(), str("new_segment_time_start_set"), Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
// end
|
||||
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
|
||||
Toast.makeText(context.getApplicationContext(), str("new_segment_time_end_set"), Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
};
|
||||
private static SponsorBlockSettings.SegmentInfo newSponsorBlockSegmentType;
|
||||
private static SegmentCategory newUserCreatedSegmentCategory;
|
||||
private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted()[which];
|
||||
boolean enableButton;
|
||||
if (!segmentType.behaviour.showOnTimeBar) {
|
||||
Toast.makeText(
|
||||
((AlertDialog) dialog).getContext().getApplicationContext(),
|
||||
str("new_segment_disabled_category"),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
enableButton = false;
|
||||
} else {
|
||||
Toast.makeText(
|
||||
((AlertDialog) dialog).getContext().getApplicationContext(),
|
||||
segmentType.description.toString(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
newSponsorBlockSegmentType = segmentType;
|
||||
enableButton = true;
|
||||
}
|
||||
try {
|
||||
SegmentCategory category = SegmentCategory.categoriesWithoutHighlights()[which];
|
||||
final boolean enableButton;
|
||||
if (category.behaviour == CategoryBehaviour.IGNORE) {
|
||||
ReVancedUtils.showToastLong(str("sb_new_segment_disabled_category"));
|
||||
enableButton = false;
|
||||
} else {
|
||||
newUserCreatedSegmentCategory = category;
|
||||
enableButton = true;
|
||||
}
|
||||
|
||||
((AlertDialog) dialog)
|
||||
.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setEnabled(enableButton);
|
||||
((AlertDialog) dialog)
|
||||
.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setEnabled(enableButton);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "segmentTypeListener failure", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
private static final DialogInterface.OnClickListener segmentReadyDialogButtonListener = new DialogInterface.OnClickListener() {
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
NewSegmentHelperLayout.hide();
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
dialog.dismiss();
|
||||
try {
|
||||
SponsorBlockViewController.hideNewSegmentLayout();
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
dialog.dismiss();
|
||||
|
||||
SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
|
||||
CharSequence[] titles = new CharSequence[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
// titles[i] = values[i].title;
|
||||
titles[i] = values[i].getTitleWithDot();
|
||||
SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights();
|
||||
CharSequence[] titles = new CharSequence[categories.length];
|
||||
for (int i = 0, length = categories.length; i < length; i++) {
|
||||
titles[i] = categories[i].getTitleWithColorDot();
|
||||
}
|
||||
|
||||
newUserCreatedSegmentCategory = null;
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("sb_new_segment_choose_category"))
|
||||
.setSingleChoiceItems(titles, -1, segmentTypeListener)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener)
|
||||
.show()
|
||||
.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setEnabled(false);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "segmentReadyDialogButtonListener failure", ex);
|
||||
}
|
||||
|
||||
newSponsorBlockSegmentType = null;
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("new_segment_choose_category"))
|
||||
.setSingleChoiceItems(titles, -1, segmentTypeListener)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener)
|
||||
.show()
|
||||
.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setEnabled(false);
|
||||
}
|
||||
};
|
||||
private static WeakReference<Context> appContext = new WeakReference<>(null);
|
||||
private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = new DialogInterface.OnClickListener() {
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
Context context = ((AlertDialog) dialog).getContext().getApplicationContext();
|
||||
Toast.makeText(context, str("submit_started"), Toast.LENGTH_SHORT).show();
|
||||
|
||||
appContext = new WeakReference<>(context);
|
||||
ReVancedUtils.runOnBackgroundThread(submitRunnable);
|
||||
}
|
||||
private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
submitNewSegment();
|
||||
};
|
||||
public static String messageToToast = "";
|
||||
private static final EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener();
|
||||
private static final DialogInterface.OnClickListener editByHandDialogListener = (dialog, which) -> {
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
try {
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
|
||||
final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which;
|
||||
final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which;
|
||||
|
||||
final EditText textView = new EditText(context);
|
||||
textView.setHint(DATE_FORMAT);
|
||||
if (isStart) {
|
||||
if (newSponsorSegmentStartMillis >= 0)
|
||||
textView.setText(dateFormatter.format(new Date(newSponsorSegmentStartMillis)));
|
||||
} else {
|
||||
if (newSponsorSegmentEndMillis >= 0)
|
||||
textView.setText(dateFormatter.format(new Date(newSponsorSegmentEndMillis)));
|
||||
final EditText textView = new EditText(context);
|
||||
textView.setHint(MANUAL_EDIT_TIME_FORMAT);
|
||||
if (isStart) {
|
||||
if (newSponsorSegmentStartMillis >= 0)
|
||||
textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentStartMillis)));
|
||||
} else {
|
||||
if (newSponsorSegmentEndMillis >= 0)
|
||||
textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentEndMillis)));
|
||||
}
|
||||
|
||||
editByHandSaveDialogListener.settingStart = isStart;
|
||||
editByHandSaveDialogListener.editText = new WeakReference<>(textView);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str(isStart ? "sb_new_segment_time_start" : "sb_new_segment_time_end"))
|
||||
.setView(textView)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("sb_new_segment_now"), editByHandSaveDialogListener)
|
||||
.setPositiveButton(android.R.string.ok, editByHandSaveDialogListener)
|
||||
.show();
|
||||
|
||||
dialog.dismiss();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "editByHandDialogListener failure", ex);
|
||||
}
|
||||
|
||||
editByHandSaveDialogListener.settingStart = isStart;
|
||||
editByHandSaveDialogListener.editText = new WeakReference<>(textView);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str(isStart ? "new_segment_time_start" : "new_segment_time_end"))
|
||||
.setView(textView)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("new_segment_now"), editByHandSaveDialogListener)
|
||||
.setPositiveButton(android.R.string.ok, editByHandSaveDialogListener)
|
||||
.show();
|
||||
|
||||
dialog.dismiss();
|
||||
};
|
||||
private static final Runnable toastRunnable = () -> {
|
||||
Context context = appContext.get();
|
||||
if (context != null && messageToToast != null)
|
||||
Toast.makeText(context, messageToToast, Toast.LENGTH_LONG).show();
|
||||
};
|
||||
private static final DialogInterface.OnClickListener segmentVoteClickListener = (dialog, which) -> {
|
||||
final Context context = ((AlertDialog) dialog).getContext();
|
||||
final SponsorSegment segment = sponsorSegmentsOfCurrentVideo[which];
|
||||
|
||||
final VoteOption[] voteOptions = VoteOption.values();
|
||||
CharSequence[] items = new CharSequence[voteOptions.length];
|
||||
|
||||
for (int i = 0; i < voteOptions.length; i++) {
|
||||
VoteOption voteOption = voteOptions[i];
|
||||
String title = voteOption.title;
|
||||
if (SettingsEnum.SB_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;
|
||||
}
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(items, (dialog1, which1) -> {
|
||||
appContext = new WeakReference<>(context.getApplicationContext());
|
||||
VoteOption voteOption = voteOptions[which1];
|
||||
switch (voteOption) {
|
||||
case UPVOTE:
|
||||
case DOWNVOTE:
|
||||
voteForSegment(segment, voteOption, appContext.get());
|
||||
break;
|
||||
case CATEGORY_CHANGE:
|
||||
onNewCategorySelect(segment, context);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.show();
|
||||
};
|
||||
private static final Runnable submitRunnable = () -> {
|
||||
messageToToast = null;
|
||||
final String uuid = SettingsEnum.SB_UUID.getString();
|
||||
final long start = newSponsorSegmentStartMillis;
|
||||
final long end = newSponsorSegmentEndMillis;
|
||||
final String videoId = getCurrentVideoId();
|
||||
final SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockUtils.newSponsorBlockSegmentType;
|
||||
try {
|
||||
if (start < 0 || end < 0 || start >= end || segmentType == null || videoId == null || uuid == null) {
|
||||
LogHelper.printException(() -> "Unable to submit times, invalid parameters");
|
||||
final Context context = ((AlertDialog) dialog).getContext();
|
||||
SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo();
|
||||
if (currentSegments == null || currentSegments.length == 0) {
|
||||
// should never be reached
|
||||
LogHelper.printException(() -> "Segment is no longer available on the client");
|
||||
return;
|
||||
}
|
||||
SBRequester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
|
||||
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to submit segment", e);
|
||||
SponsorSegment segment = currentSegments[which];
|
||||
|
||||
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
||||
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
|
||||
: SegmentVote.values();
|
||||
CharSequence[] items = new CharSequence[voteOptions.length];
|
||||
|
||||
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) {
|
||||
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
|
||||
} else {
|
||||
items[i] = title;
|
||||
}
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(items, (dialog1, which1) -> {
|
||||
SegmentVote voteOption = voteOptions[which1];
|
||||
switch (voteOption) {
|
||||
case UPVOTE:
|
||||
case DOWNVOTE:
|
||||
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
|
||||
break;
|
||||
case CATEGORY_CHANGE:
|
||||
onNewCategorySelect(segment, context);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "segmentVoteClickListener failure", ex);
|
||||
}
|
||||
|
||||
if (videoId != null)
|
||||
PlayerController.executeDownloadSegments(videoId);
|
||||
};
|
||||
|
||||
static {
|
||||
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private SponsorBlockUtils() {
|
||||
}
|
||||
|
||||
public static void showShieldButton() {
|
||||
View i = ShieldButton._shieldBtn.get();
|
||||
if (i == null || !ShieldButton.shouldBeShown()) return;
|
||||
i.setVisibility(VISIBLE);
|
||||
i.bringToFront();
|
||||
i.requestLayout();
|
||||
i.invalidate();
|
||||
static void setNewSponsorSegmentPreviewed() {
|
||||
newSponsorSegmentPreviewed = true;
|
||||
}
|
||||
|
||||
public static void hideShieldButton() {
|
||||
View i = ShieldButton._shieldBtn.get();
|
||||
if (i != null)
|
||||
i.setVisibility(GONE);
|
||||
static void clearUnsubmittedSegmentTimes() {
|
||||
newSponsorSegmentDialogShownMillis = 0;
|
||||
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
|
||||
newSponsorSegmentPreviewed = false;
|
||||
}
|
||||
|
||||
public static void showVoteButton() {
|
||||
View i = VotingButton._votingButton.get();
|
||||
if (i == null || !VotingButton.shouldBeShown()) return;
|
||||
i.setVisibility(VISIBLE);
|
||||
i.bringToFront();
|
||||
i.requestLayout();
|
||||
i.invalidate();
|
||||
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.getCurrentVideoId();
|
||||
final long videoLength = VideoInformation.getCurrentVideoLength();
|
||||
final SegmentCategory segmentCategory = newUserCreatedSegmentCategory;
|
||||
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty()
|
||||
|| segmentCategory == null || uuid.isEmpty()) {
|
||||
LogHelper.printException(() -> "invalid parameters");
|
||||
return;
|
||||
}
|
||||
clearUnsubmittedSegmentTimes();
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
SBRequester.submitSegments(uuid, videoId, segmentCategory.key, start, end, videoLength);
|
||||
SegmentPlaybackController.executeDownloadSegments(videoId);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to submit segment", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideVoteButton() {
|
||||
View i = VotingButton._votingButton.get();
|
||||
if (i != null)
|
||||
i.setVisibility(GONE);
|
||||
}
|
||||
public static void onMarkLocationClicked() {
|
||||
try {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
newSponsorSegmentDialogShownMillis = VideoInformation.getVideoTime();
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static void onMarkLocationClicked(Context context) {
|
||||
newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime();
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("new_segment_title"))
|
||||
.setMessage(str("new_segment_mark_time_as_question",
|
||||
newSponsorSegmentDialogShownMillis / 60000,
|
||||
newSponsorSegmentDialogShownMillis / 1000 % 60,
|
||||
newSponsorSegmentDialogShownMillis % 1000))
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(str("new_segment_mark_start"), newSponsorSegmentDialogListener)
|
||||
.setPositiveButton(str("new_segment_mark_end"), newSponsorSegmentDialogListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static void onPublishClicked(Context context) {
|
||||
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
|
||||
long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000;
|
||||
long start = (newSponsorSegmentStartMillis) / 1000;
|
||||
long end = (newSponsorSegmentEndMillis) / 1000;
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("new_segment_confirm_title"))
|
||||
.setMessage(str("new_segment_confirm_content",
|
||||
start / 60, start % 60,
|
||||
end / 60, end % 60,
|
||||
length / 60, length % 60))
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener)
|
||||
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
|
||||
.setTitle(str("sb_new_segment_title"))
|
||||
.setMessage(str("sb_new_segment_mark_time_as_question",
|
||||
newSponsorSegmentDialogShownMillis / 60000,
|
||||
newSponsorSegmentDialogShownMillis / 1000 % 60,
|
||||
newSponsorSegmentDialogShownMillis % 1000))
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(str("sb_new_segment_mark_start"), newSponsorSegmentDialogListener)
|
||||
.setPositiveButton(str("sb_new_segment_mark_end"), newSponsorSegmentDialogListener)
|
||||
.show();
|
||||
} else {
|
||||
Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onMarkLocationClicked failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onVotingClicked(final Context context) {
|
||||
if (sponsorSegmentsOfCurrentVideo == null || sponsorSegmentsOfCurrentVideo.length == 0) {
|
||||
Toast.makeText(context.getApplicationContext(), str("vote_no_segments"), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
int segmentAmount = sponsorSegmentsOfCurrentVideo.length;
|
||||
List<CharSequence> titles = new ArrayList<>(segmentAmount); // I've replaced an array with a list to prevent null elements in the array as unsubmitted segments get filtered out
|
||||
for (int i = 0; i < segmentAmount; i++) {
|
||||
SponsorSegment segment = sponsorSegmentsOfCurrentVideo[i];
|
||||
if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String start = dateFormatter.format(new Date(segment.start));
|
||||
String end = dateFormatter.format(new Date(segment.end));
|
||||
StringBuilder htmlBuilder = new StringBuilder();
|
||||
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br> %s to %s",
|
||||
segment.category.color, segment.category.title, start, end));
|
||||
if (i + 1 != segmentAmount) // prevents trailing new line after last segment
|
||||
htmlBuilder.append("<br>");
|
||||
titles.add(Html.fromHtml(htmlBuilder.toString()));
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void onNewCategorySelect(final SponsorSegment segment, Context context) {
|
||||
final SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
|
||||
CharSequence[] titles = new CharSequence[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
titles[i] = values[i].getTitleWithDot();
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("new_segment_choose_category"))
|
||||
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), values[which].key))
|
||||
.show();
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static void onPreviewClicked(Context context) {
|
||||
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
|
||||
// Toast t = Toast.makeText(context, "Preview", Toast.LENGTH_SHORT);
|
||||
// t.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP, t.getXOffset(), t.getYOffset());
|
||||
// t.show();
|
||||
PlayerController.skipToMillisecond(newSponsorSegmentStartMillis - 3000);
|
||||
final SponsorSegment[] original = PlayerController.sponsorSegmentsOfCurrentVideo;
|
||||
final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1);
|
||||
|
||||
segments[segments.length - 1] = new SponsorSegment(newSponsorSegmentStartMillis, newSponsorSegmentEndMillis,
|
||||
SponsorBlockSettings.SegmentInfo.UNSUBMITTED, null, false);
|
||||
|
||||
Arrays.sort(segments);
|
||||
sponsorSegmentsOfCurrentVideo = segments;
|
||||
} else {
|
||||
Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static void onEditByHandClicked(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("new_segment_edit_by_hand_title"))
|
||||
.setMessage(str("new_segment_edit_by_hand_content"))
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(str("new_segment_mark_start"), editByHandDialogListener)
|
||||
.setPositiveButton(str("new_segment_mark_end"), editByHandDialogListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void notifyShareBtnVisibilityChanged(View v) {
|
||||
if (v.getId() != shareBtnId || !/*SponsorBlockSettings.isAddNewSegmentEnabled*/false)
|
||||
return;
|
||||
// if (VERBOSE)
|
||||
// LogH(TAG, "VISIBILITY CHANGED of view " + v);
|
||||
ImageView sponsorBtn = ShieldButton._shieldBtn.get();
|
||||
if (sponsorBtn != null) {
|
||||
sponsorBtn.setVisibility(v.getVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
public static String appendTimeWithoutSegments(String totalTime) {
|
||||
public static void onPublishClicked() {
|
||||
try {
|
||||
if (videoHasSegments && (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) && !TextUtils.isEmpty(totalTime) && getCurrentVideoLength() > 1) {
|
||||
if (timeWithoutSegments.isEmpty()) {
|
||||
timeWithoutSegments = getTimeWithoutSegments(sponsorSegmentsOfCurrentVideo);
|
||||
}
|
||||
return totalTime + timeWithoutSegments;
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) {
|
||||
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
|
||||
} else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) {
|
||||
ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end"));
|
||||
} else if (!newSponsorSegmentPreviewed && newSponsorSegmentStartMillis != 0) {
|
||||
ReVancedUtils.showToastLong(str("sb_new_segment_preview_segment_first"));
|
||||
} else {
|
||||
long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000;
|
||||
long start = (newSponsorSegmentStartMillis) / 1000;
|
||||
long end = (newSponsorSegmentEndMillis) / 1000;
|
||||
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
|
||||
.setTitle(str("sb_new_segment_confirm_title"))
|
||||
.setMessage(str("sb_new_segment_confirm_content",
|
||||
start / 60, start % 60,
|
||||
end / 60, end % 60,
|
||||
length / 60, length % 60))
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener)
|
||||
.show();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex);
|
||||
LogHelper.printException(() -> "onPublishClicked failure", ex);
|
||||
}
|
||||
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
public static String getTimeWithoutSegments(SponsorSegment[] sponsorSegmentsOfCurrentVideo) {
|
||||
long currentVideoLength = getCurrentVideoLength();
|
||||
if (!(SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) || sponsorSegmentsOfCurrentVideo == null || currentVideoLength <= 1) {
|
||||
return "";
|
||||
}
|
||||
long timeWithoutSegments = currentVideoLength + 500; // YouTube:tm:
|
||||
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
|
||||
timeWithoutSegments -= segment.end - segment.start;
|
||||
}
|
||||
long hours = timeWithoutSegments / 3600000;
|
||||
long minutes = (timeWithoutSegments / 60000) % 60;
|
||||
long seconds = (timeWithoutSegments / 1000) % 60;
|
||||
String format = (hours > 0 ? "%d:%02" : "%") + "d:%02d"; // mmLul
|
||||
String formatted = hours > 0 ? String.format(format, hours, minutes, seconds) : String.format(format, minutes, seconds);
|
||||
return String.format(" (%s)", formatted);
|
||||
}
|
||||
|
||||
public static void playerTypeChanged(PlayerType playerType) {
|
||||
public static void onVotingClicked(@NonNull Context context) {
|
||||
try {
|
||||
if (videoHasSegments && (playerType == PlayerType.NONE)) {
|
||||
PlayerController.setCurrentVideoId(null);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Player type changed caused a crash.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatColorString(int color) {
|
||||
return String.format("#%06X", color);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void addUserStats(PreferenceCategory category, Preference loadingPreference, UserStats stats) {
|
||||
category.removePreference(loadingPreference);
|
||||
|
||||
Context context = category.getContext();
|
||||
String minutesStr = str("minutes");
|
||||
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
category.addPreference(preference);
|
||||
String userName = stats.getUserName();
|
||||
preference.setTitle(fromHtml(str("stats_username", userName)));
|
||||
preference.setSummary(str("stats_username_change"));
|
||||
preference.setText(userName);
|
||||
preference.setOnPreferenceChangeListener((preference1, newUsername) -> {
|
||||
appContext = new WeakReference<>(context.getApplicationContext());
|
||||
SBRequester.setUsername((String) newUsername, preference, toastRunnable);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
category.addPreference(preference);
|
||||
String formatted = FORMATTER.format(stats.getSegmentCount());
|
||||
preference.setTitle(fromHtml(str("stats_submissions", formatted)));
|
||||
preference.setSelectable(false);
|
||||
}
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
category.addPreference(preference);
|
||||
String formatted = FORMATTER.format(stats.getViewCount());
|
||||
|
||||
double saved = stats.getMinutesSaved();
|
||||
int hoursSaved = (int) (saved / 60);
|
||||
double minutesSaved = saved % 60;
|
||||
String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr);
|
||||
|
||||
preference.setTitle(fromHtml(str("stats_saved", formatted)));
|
||||
preference.setSummary(fromHtml(str("stats_saved_sum", formattedSaved)));
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||
preference1.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
category.addPreference(preference);
|
||||
String formatted = FORMATTER.format(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt());
|
||||
|
||||
long hoursSaved = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 3600000;
|
||||
double minutesSaved = (SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 60000d) % 60;
|
||||
String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr);
|
||||
|
||||
preference.setTitle(fromHtml(str("stats_self_saved", formatted)));
|
||||
preference.setSummary(fromHtml(str("stats_self_saved_sum", formattedSaved)));
|
||||
preference.setSelectable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void importSettings(String json, Context context) {
|
||||
try {
|
||||
JSONObject settingsJson = new JSONObject(json);
|
||||
|
||||
JSONObject barTypesObject = settingsJson.getJSONObject("barTypes");
|
||||
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
|
||||
|
||||
|
||||
SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit();
|
||||
|
||||
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
|
||||
for (SponsorBlockSettings.SegmentInfo category : categories) {
|
||||
String categoryKey = category.key;
|
||||
JSONObject categoryObject = barTypesObject.getJSONObject(categoryKey);
|
||||
String color = categoryObject.getString("color");
|
||||
|
||||
editor.putString(categoryKey + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX, color);
|
||||
editor.putString(categoryKey, SponsorBlockSettings.SegmentBehaviour.IGNORE.key);
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo();
|
||||
if (currentSegments == null || currentSegments.length == 0) {
|
||||
// button is hidden if no segments exist.
|
||||
// But if prior video had segments, and current video does not,
|
||||
// then the button persists until the overlay fades out (this is intentional, as abruptly hiding the button is jarring)
|
||||
ReVancedUtils.showToastShort(str("sb_vote_no_segments"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < categorySelectionsArray.length(); i++) {
|
||||
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
|
||||
// use same time formatting as shown in the video player
|
||||
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
|
||||
final String formatPattern;
|
||||
if (currentVideoLength < (10 * 60 * 1000)) {
|
||||
formatPattern = "m:ss"; // less than 10 minutes
|
||||
} else if (currentVideoLength < (60 * 60 * 1000)) {
|
||||
formatPattern = "mm:ss"; // less than 1 hour
|
||||
} else if (currentVideoLength < (10 * 60 * 60 * 1000)) {
|
||||
formatPattern = "H:mm:ss"; // less than 10 hours
|
||||
} else {
|
||||
formatPattern = "HH:mm:ss"; // why is this on YouTube
|
||||
}
|
||||
voteSegmentTimeFormatter.applyPattern(formatPattern);
|
||||
|
||||
String categoryKey = categorySelectionObject.getString("name");
|
||||
SponsorBlockSettings.SegmentInfo category = SponsorBlockSettings.SegmentInfo.byCategoryKey(categoryKey);
|
||||
|
||||
if (category == null) {
|
||||
final int numberOfSegments = currentSegments.length;
|
||||
CharSequence[] titles = new CharSequence[numberOfSegments];
|
||||
for (int i = 0; i < numberOfSegments; i++) {
|
||||
SponsorSegment segment = currentSegments[i];
|
||||
if (segment.category == SegmentCategory.UNSUBMITTED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int desktopKey = categorySelectionObject.getInt("option");
|
||||
SponsorBlockSettings.SegmentBehaviour behaviour = SponsorBlockSettings.SegmentBehaviour.byDesktopKey(desktopKey);
|
||||
editor.putString(category.key, behaviour.key);
|
||||
}
|
||||
|
||||
SettingsEnum.SB_UUID.saveValue(settingsJson.getString("userID"));
|
||||
SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip"));
|
||||
SettingsEnum.SB_API_URL.saveValue(settingsJson.getString("serverAddress"));
|
||||
SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice"));
|
||||
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
|
||||
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(settingsJson.getString("minDuration")));
|
||||
SettingsEnum.SB_COUNT_SKIPS.saveValue(settingsJson.getBoolean("trackViewCount"));
|
||||
|
||||
Toast.makeText(context, str("settings_import_successful"), Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
|
||||
Toast.makeText(context, str("settings_import_failed"), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static String exportSettings(Context context) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
JSONObject barTypesObject = new JSONObject(); // categories' colors
|
||||
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
|
||||
|
||||
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
|
||||
for (SponsorBlockSettings.SegmentInfo category : categories) {
|
||||
JSONObject categoryObject = new JSONObject();
|
||||
String categoryKey = category.key;
|
||||
categoryObject.put("color", formatColorString(category.color));
|
||||
barTypesObject.put(categoryKey, categoryObject);
|
||||
|
||||
int desktopKey = category.behaviour.desktopKey;
|
||||
if (desktopKey != -1) {
|
||||
JSONObject behaviorObject = new JSONObject();
|
||||
behaviorObject.put("name", categoryKey);
|
||||
behaviorObject.put("option", desktopKey);
|
||||
categorySelectionsArray.put(behaviorObject);
|
||||
StringBuilder htmlBuilder = new StringBuilder();
|
||||
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>",
|
||||
segment.category.color, segment.category.title));
|
||||
htmlBuilder.append(voteSegmentTimeFormatter.format(new Date(segment.start)));
|
||||
if (segment.category != SegmentCategory.HIGHLIGHT) {
|
||||
htmlBuilder.append(" to ").append(voteSegmentTimeFormatter.format(new Date(segment.end)));
|
||||
}
|
||||
htmlBuilder.append("</b>");
|
||||
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
|
||||
htmlBuilder.append("<br>");
|
||||
titles[i] = Html.fromHtml(htmlBuilder.toString());
|
||||
}
|
||||
json.put("userID", SettingsEnum.SB_UUID.getString());
|
||||
json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean());
|
||||
json.put("serverAddress", SettingsEnum.SB_API_URL.getString());
|
||||
json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean());
|
||||
json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
|
||||
json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat());
|
||||
json.put("trackViewCount", SettingsEnum.SB_COUNT_SKIPS.getBoolean());
|
||||
json.put("categorySelections", categorySelectionsArray);
|
||||
json.put("barTypes", barTypesObject);
|
||||
|
||||
return json.toString();
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(titles, segmentVoteClickListener)
|
||||
.show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
|
||||
Toast.makeText(context, str("settings_export_failed"), Toast.LENGTH_SHORT).show();
|
||||
return "";
|
||||
LogHelper.printException(() -> "onVotingClicked failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isSBButtonEnabled(Context context, String key) {
|
||||
return SettingsEnum.SB_ENABLED.getBoolean() && SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, key, false);
|
||||
private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) {
|
||||
try {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights();
|
||||
CharSequence[] titles = new CharSequence[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
titles[i] = values[i].getTitleWithColorDot();
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("sb_new_segment_choose_category"))
|
||||
.setItems(titles, (dialog, which) -> SBRequester.voteToChangeCategoryOnBackgroundThread(segment, values[which]))
|
||||
.show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onNewCategorySelect failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public enum VoteOption {
|
||||
UPVOTE(str("vote_upvote"), false),
|
||||
DOWNVOTE(str("vote_downvote"), true),
|
||||
CATEGORY_CHANGE(str("vote_category"), true);
|
||||
public static void onPreviewClicked() {
|
||||
try {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) {
|
||||
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
|
||||
} else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) {
|
||||
ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end"));
|
||||
} else {
|
||||
VideoInformation.seekTo(newSponsorSegmentStartMillis - 2500);
|
||||
final SponsorSegment[] original = SegmentPlaybackController.getSegmentsOfCurrentVideo();
|
||||
final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1);
|
||||
|
||||
public final String title;
|
||||
public final boolean shouldHighlight;
|
||||
segments[segments.length - 1] = new SponsorSegment(SegmentCategory.UNSUBMITTED, null,
|
||||
newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false);
|
||||
|
||||
|
||||
VoteOption(String title, boolean shouldHighlight) {
|
||||
this.title = title;
|
||||
this.shouldHighlight = shouldHighlight;
|
||||
SegmentPlaybackController.setSegmentsOfCurrentVideo(segments);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onPreviewClicked failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void sendViewRequestAsync(@NonNull SponsorSegment segment) {
|
||||
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
||||
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);
|
||||
|
||||
if (SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()) {
|
||||
ReVancedUtils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment));
|
||||
}
|
||||
}
|
||||
|
||||
public static void onEditByHandClicked() {
|
||||
try {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
|
||||
.setTitle(str("sb_new_segment_edit_by_hand_title"))
|
||||
.setMessage(str("sb_new_segment_edit_by_hand_content"))
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(str("sb_new_segment_mark_start"), editByHandDialogListener)
|
||||
.setPositiveButton(str("sb_new_segment_mark_end"), editByHandDialogListener)
|
||||
.show();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onEditByHandClicked failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTimeSavedString(long totalSecondsSaved) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
Duration duration = Duration.ofSeconds(totalSecondsSaved);
|
||||
final long hoursSaved = duration.toHours();
|
||||
final long minutesSaved = duration.toMinutes() % 60;
|
||||
if (hoursSaved > 0) {
|
||||
return str("sb_stats_saved_hour_format", hoursSaved, minutesSaved);
|
||||
}
|
||||
final long secondsSaved = duration.getSeconds() % 60;
|
||||
if (minutesSaved > 0) {
|
||||
return str("sb_stats_saved_minute_format", minutesSaved, secondsSaved);
|
||||
}
|
||||
return str("sb_stats_saved_second_format", secondsSaved);
|
||||
}
|
||||
return "error"; // will never be reached. YouTube requires Android O or greater
|
||||
}
|
||||
|
||||
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
|
||||
public boolean settingStart;
|
||||
public WeakReference<EditText> editText;
|
||||
boolean settingStart;
|
||||
WeakReference<EditText> editText;
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final EditText editText = this.editText.get();
|
||||
if (editText == null) return;
|
||||
Context context = ((AlertDialog) dialog).getContext();
|
||||
|
||||
try {
|
||||
final EditText editText = this.editText.get();
|
||||
if (editText == null) return;
|
||||
|
||||
long time = (which == DialogInterface.BUTTON_NEUTRAL) ?
|
||||
getLastKnownVideoTime() :
|
||||
(Objects.requireNonNull(dateFormatter.parse(editText.getText().toString())).getTime());
|
||||
VideoInformation.getVideoTime() :
|
||||
(Objects.requireNonNull(manualEditTimeFormatter.parse(editText.getText().toString())).getTime());
|
||||
|
||||
if (settingStart)
|
||||
newSponsorSegmentStartMillis = Math.max(time, 0);
|
||||
@ -646,10 +448,10 @@ public abstract class SponsorBlockUtils {
|
||||
editByHandDialogListener.onClick(dialog, settingStart ?
|
||||
DialogInterface.BUTTON_NEGATIVE :
|
||||
DialogInterface.BUTTON_POSITIVE);
|
||||
else
|
||||
Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_saved"), Toast.LENGTH_SHORT).show();
|
||||
} catch (ParseException e) {
|
||||
Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_parse_error"), Toast.LENGTH_LONG).show();
|
||||
ReVancedUtils.showToastLong(str("sb_new_segment_edit_by_hand_parse_error"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "EditByHandSaveDialogListener failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
|
||||
public class SwipeHelper {
|
||||
static FrameLayout _frameLayout;
|
||||
public static boolean isTabletMode;
|
||||
public static ViewGroup nextGenWatchLayout;
|
||||
|
||||
public static void SetFrameLayout(Object obj) {
|
||||
try {
|
||||
_frameLayout = (FrameLayout) obj;
|
||||
Context appContext = ReVancedUtils.getContext();
|
||||
if (ReVancedUtils.isTablet(appContext) || SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.YOUTUBE, "pref_swipe_tablet", false)) {
|
||||
isTabletMode = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to set FrameLayout", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setNextGenWatchLayout(Object obj) {
|
||||
try {
|
||||
nextGenWatchLayout = (ViewGroup) obj;
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to set _nextGenWatchLayout", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean IsControlsShown() {
|
||||
FrameLayout frameLayout;
|
||||
if (isTabletMode || (frameLayout = _frameLayout) == null || frameLayout.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to get related_endscreen_results visibility", e);
|
||||
}
|
||||
if (_frameLayout.getChildCount() > 0) {
|
||||
return _frameLayout.getChildAt(0).getVisibility() == View.VISIBLE;
|
||||
}
|
||||
refreshLayout();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void refreshLayout() {
|
||||
View findViewById;
|
||||
try {
|
||||
if (isWatchWhileFullScreen() && (findViewById = nextGenWatchLayout.findViewById(getIdentifier())) != null) {
|
||||
_frameLayout = (FrameLayout) findViewById.getParent();
|
||||
LogHelper.printDebug(() -> "related_endscreen_results refreshed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogHelper.printException(() -> "Unable to refresh related_endscreen_results layout", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static boolean isWatchWhileFullScreen() {
|
||||
if (ReVancedUtils.getPlayerType() == null) {
|
||||
return false;
|
||||
}
|
||||
return ReVancedUtils.getPlayerType() == PlayerType.WATCH_WHILE_FULLSCREEN;
|
||||
}
|
||||
|
||||
private static String getViewMessage(View view) {
|
||||
try {
|
||||
String resourceName = view.getResources() != null ? view.getId() != 0 ? view.getResources().getResourceName(view.getId()) : "no_id" : "no_resources";
|
||||
return "[" + view.getClass().getSimpleName() + "] " + resourceName + "\n";
|
||||
} catch (Resources.NotFoundException unused) {
|
||||
return "[" + view.getClass().getSimpleName() + "] name_not_found\n";
|
||||
}
|
||||
}
|
||||
|
||||
private static int getIdentifier() {
|
||||
Context appContext = ReVancedUtils.getContext();
|
||||
assert appContext != null;
|
||||
return appContext.getResources().getIdentifier("related_endscreen_results", "id", appContext.getPackageName());
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
|
||||
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class VotingButton {
|
||||
static RelativeLayout _youtubeControlsLayout;
|
||||
static WeakReference<ImageView> _votingButton = new WeakReference<>(null);
|
||||
static int fadeDurationFast;
|
||||
static int fadeDurationScheduled;
|
||||
static Animation fadeIn;
|
||||
static Animation fadeOut;
|
||||
static boolean isShowing;
|
||||
|
||||
public static void initialize(Object viewStub) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "initializing voting button");
|
||||
_youtubeControlsLayout = (RelativeLayout) viewStub;
|
||||
|
||||
ImageView imageView = (ImageView) _youtubeControlsLayout
|
||||
.findViewById(getIdentifier("voting_button", "id"));
|
||||
|
||||
if (imageView == null) {
|
||||
LogHelper.printDebug(() -> "Couldn't find imageView with \"voting_button\"");
|
||||
}
|
||||
if (imageView == null) return;
|
||||
imageView.setOnClickListener(SponsorBlockUtils.voteButtonListener);
|
||||
_votingButton = new WeakReference<>(imageView);
|
||||
|
||||
// Animations
|
||||
fadeDurationFast = getInteger("fade_duration_fast");
|
||||
fadeDurationScheduled = getInteger("fade_duration_scheduled");
|
||||
fadeIn = getAnimation("fade_in");
|
||||
fadeIn.setDuration(fadeDurationFast);
|
||||
fadeOut = getAnimation("fade_out");
|
||||
fadeOut.setDuration(fadeDurationScheduled);
|
||||
isShowing = true;
|
||||
changeVisibilityImmediate(false);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void changeVisibilityImmediate(boolean visible) {
|
||||
changeVisibility(visible, true);
|
||||
}
|
||||
|
||||
public static void changeVisibilityNegatedImmediate(boolean visible) {
|
||||
changeVisibility(!visible, true);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible) {
|
||||
changeVisibility(visible, false);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible, boolean immediate) {
|
||||
try {
|
||||
if (isShowing == visible) return;
|
||||
isShowing = visible;
|
||||
|
||||
ImageView iView = _votingButton.get();
|
||||
if (_youtubeControlsLayout == null || iView == null) return;
|
||||
|
||||
if (visible && shouldBeShown()) {
|
||||
if (getLastKnownVideoTime() >= getCurrentVideoLength()) {
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Fading in");
|
||||
iView.setVisibility(View.VISIBLE);
|
||||
if (!immediate)
|
||||
iView.startAnimation(fadeIn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (iView.getVisibility() == View.VISIBLE) {
|
||||
LogHelper.printDebug(() -> "Fading out");
|
||||
if (!immediate)
|
||||
iView.startAnimation(fadeOut);
|
||||
iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "changeVisibility failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldBeShown() {
|
||||
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean();
|
||||
}
|
||||
|
||||
//region Helpers
|
||||
private static int getIdentifier(String name, String defType) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
}
|
||||
|
||||
private static int getInteger(String name) {
|
||||
return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer"));
|
||||
}
|
||||
|
||||
private static Animation getAnimation(String name) {
|
||||
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim"));
|
||||
}
|
||||
//endregion
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.sf;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.StringRef;
|
||||
|
||||
public enum CategoryBehaviour {
|
||||
SKIP_AUTOMATICALLY("skip", 2, true, sf("sb_skip_automatically")),
|
||||
// desktop does not have skip-once behavior. Key is unique to ReVanced
|
||||
SKIP_AUTOMATICALLY_ONCE("skip-once", 3, true, sf("sb_skip_automatically_once")),
|
||||
MANUAL_SKIP("manual-skip", 1, false, sf("sb_skip_showbutton")),
|
||||
SHOW_IN_SEEKBAR("seekbar-only", 0, false, sf("sb_skip_seekbaronly")),
|
||||
// ignored categories are not exported to json, and ignore is the default behavior when importing
|
||||
IGNORE("ignore", -1, false, sf("sb_skip_ignore"));
|
||||
|
||||
@NonNull
|
||||
public final String key;
|
||||
public final int desktopKey;
|
||||
/**
|
||||
* If the segment should skip automatically
|
||||
*/
|
||||
public final boolean skipAutomatically;
|
||||
@NonNull
|
||||
public final StringRef description;
|
||||
|
||||
CategoryBehaviour(String key, int desktopKey, boolean skipAutomatically, StringRef description) {
|
||||
this.key = Objects.requireNonNull(key);
|
||||
this.desktopKey = desktopKey;
|
||||
this.skipAutomatically = skipAutomatically;
|
||||
this.description = Objects.requireNonNull(description);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static CategoryBehaviour byStringKey(@NonNull String key) {
|
||||
for (CategoryBehaviour behaviour : values()){
|
||||
if (behaviour.key.equals(key)) {
|
||||
return behaviour;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static CategoryBehaviour byDesktopKey(int desktopKey) {
|
||||
for (CategoryBehaviour behaviour : values()) {
|
||||
if (behaviour.desktopKey == desktopKey) {
|
||||
return behaviour;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String[] behaviorKeys;
|
||||
private static String[] behaviorDescriptions;
|
||||
|
||||
private static String[] behaviorKeysWithoutSkipOnce;
|
||||
private static String[] behaviorDescriptionsWithoutSkipOnce;
|
||||
|
||||
private static void createNameAndKeyArrays() {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
|
||||
CategoryBehaviour[] behaviours = values();
|
||||
final int behaviorLength = behaviours.length;
|
||||
behaviorKeys = new String[behaviorLength];
|
||||
behaviorDescriptions = new String[behaviorLength];
|
||||
behaviorKeysWithoutSkipOnce = new String[behaviorLength - 1];
|
||||
behaviorDescriptionsWithoutSkipOnce = new String[behaviorLength - 1];
|
||||
|
||||
int behaviorIndex = 0, behaviorHighlightIndex = 0;
|
||||
while (behaviorIndex < behaviorLength) {
|
||||
CategoryBehaviour behaviour = behaviours[behaviorIndex];
|
||||
String key = behaviour.key;
|
||||
String description = behaviour.description.toString();
|
||||
behaviorKeys[behaviorIndex] = key;
|
||||
behaviorDescriptions[behaviorIndex] = description;
|
||||
behaviorIndex++;
|
||||
if (behaviour != SKIP_AUTOMATICALLY_ONCE) {
|
||||
behaviorKeysWithoutSkipOnce[behaviorHighlightIndex] = key;
|
||||
behaviorDescriptionsWithoutSkipOnce[behaviorHighlightIndex] = description;
|
||||
behaviorHighlightIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String[] getBehaviorKeys() {
|
||||
if (behaviorKeys == null) {
|
||||
createNameAndKeyArrays();
|
||||
}
|
||||
return behaviorKeys;
|
||||
}
|
||||
static String[] getBehaviorKeysWithoutSkipOnce() {
|
||||
if (behaviorKeysWithoutSkipOnce == null) {
|
||||
createNameAndKeyArrays();
|
||||
}
|
||||
return behaviorKeysWithoutSkipOnce;
|
||||
}
|
||||
|
||||
static String[] getBehaviorDescriptions() {
|
||||
if (behaviorDescriptions == null) {
|
||||
createNameAndKeyArrays();
|
||||
}
|
||||
return behaviorDescriptions;
|
||||
}
|
||||
static String[] getBehaviorDescriptionsWithoutSkipOnce() {
|
||||
if (behaviorDescriptionsWithoutSkipOnce == null) {
|
||||
createNameAndKeyArrays();
|
||||
}
|
||||
return behaviorDescriptionsWithoutSkipOnce;
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.formatColorString;
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.preference.ListPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class EditTextListPreference extends ListPreference {
|
||||
|
||||
private EditText mEditText;
|
||||
private int mClickedDialogEntryIndex;
|
||||
|
||||
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public EditTextListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EditTextListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
|
||||
|
||||
mEditText = new EditText(builder.getContext());
|
||||
mEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
mEditText.setText(formatColorString(category.color));
|
||||
mEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
try {
|
||||
Color.parseColor(s.toString()); // validation
|
||||
getDialog().setTitle(Html.fromHtml(String.format("<font color=\"%s\">⬤</font> %s", s, category.title)));
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setView(mEditText);
|
||||
builder.setTitle(category.getTitleWithDot());
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
});
|
||||
builder.setNeutralButton(str("reset"), (dialog, which) -> {
|
||||
//EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
|
||||
int defaultColor = category.defaultColor;
|
||||
category.setColor(defaultColor);
|
||||
Toast.makeText(getContext().getApplicationContext(), str("color_reset"), Toast.LENGTH_SHORT).show();
|
||||
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(defaultColor)).apply();
|
||||
reformatTitle();
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
mClickedDialogEntryIndex = findIndexOfValue(getValue());
|
||||
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
}
|
||||
String colorString = mEditText.getText().toString();
|
||||
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
|
||||
if (colorString.equals(formatColorString(category.color))) {
|
||||
return;
|
||||
}
|
||||
Context applicationContext = getContext().getApplicationContext();
|
||||
try {
|
||||
int color = Color.parseColor(colorString);
|
||||
category.setColor(color);
|
||||
Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show();
|
||||
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(color)).apply();
|
||||
reformatTitle();
|
||||
} catch (Exception ex) {
|
||||
Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SponsorBlockSettings.SegmentInfo getCategoryBySelf() {
|
||||
return SponsorBlockSettings.SegmentInfo.byCategoryKey(getKey());
|
||||
}
|
||||
|
||||
private String getColorPreferenceKey() {
|
||||
return getKey() + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX;
|
||||
}
|
||||
|
||||
private void reformatTitle() {
|
||||
this.setTitle(getCategoryBySelf().getTitleWithDot());
|
||||
}
|
||||
}
|
@ -0,0 +1,339 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
||||
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||
import static app.revanced.integrations.utils.StringRef.sf;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.settings.SharedPrefCategory;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.StringRef;
|
||||
|
||||
public enum SegmentCategory {
|
||||
SPONSOR("sponsor", sf("sb_segments_sponsor"), sf("sb_segments_sponsor_sum"), sf("sb_skip_button_sponsor"), sf("sb_skipped_sponsor"),
|
||||
SKIP_AUTOMATICALLY_ONCE, 0x00D400),
|
||||
SELF_PROMO("selfpromo", sf("sb_segments_selfpromo"), sf("sb_segments_selfpromo_sum"), sf("sb_skip_button_selfpromo"), sf("sb_skipped_selfpromo"),
|
||||
MANUAL_SKIP, 0xFFFF00),
|
||||
INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"),
|
||||
MANUAL_SKIP, 0xCC00FF),
|
||||
/**
|
||||
* Unique category that is treated differently than the rest.
|
||||
*/
|
||||
HIGHLIGHT("poi_highlight", sf("sb_segments_highlight"), sf("sb_segments_highlight_sum"), sf("sb_skip_button_highlight"), sf("sb_skipped_highlight"),
|
||||
MANUAL_SKIP, 0xFF1684),
|
||||
INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"),
|
||||
sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"),
|
||||
sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"),
|
||||
MANUAL_SKIP, 0x00FFFF),
|
||||
OUTRO("outro", sf("sb_segments_outro"), sf("sb_segments_outro_sum"), sf("sb_skip_button_outro"), sf("sb_skipped_outro"),
|
||||
MANUAL_SKIP, 0x0202ED),
|
||||
PREVIEW("preview", sf("sb_segments_preview"), sf("sb_segments_preview_sum"),
|
||||
sf("sb_skip_button_preview_beginning"), sf("sb_skip_button_preview_middle"), sf("sb_skip_button_preview_end"),
|
||||
sf("sb_skipped_preview_beginning"), sf("sb_skipped_preview_middle"), sf("sb_skipped_preview_end"),
|
||||
IGNORE, 0x008FD6),
|
||||
FILLER("filler", sf("sb_segments_filler"), sf("sb_segments_filler_sum"), sf("sb_skip_button_filler"), sf("sb_skipped_filler"),
|
||||
IGNORE, 0x7300FF),
|
||||
MUSIC_OFFTOPIC("music_offtopic", sf("sb_segments_nomusic"), sf("sb_segments_nomusic_sum"), sf("sb_skip_button_nomusic"), sf("sb_skipped_nomusic"),
|
||||
MANUAL_SKIP, 0xFF9900),
|
||||
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"),
|
||||
SKIP_AUTOMATICALLY, 0xFFFFFF);
|
||||
|
||||
private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact");
|
||||
private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight");
|
||||
|
||||
private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{
|
||||
SPONSOR,
|
||||
SELF_PROMO,
|
||||
INTERACTION,
|
||||
INTRO,
|
||||
OUTRO,
|
||||
PREVIEW,
|
||||
FILLER,
|
||||
MUSIC_OFFTOPIC,
|
||||
};
|
||||
|
||||
private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{
|
||||
SPONSOR,
|
||||
SELF_PROMO,
|
||||
INTERACTION,
|
||||
HIGHLIGHT,
|
||||
INTRO,
|
||||
OUTRO,
|
||||
PREVIEW,
|
||||
FILLER,
|
||||
MUSIC_OFFTOPIC,
|
||||
};
|
||||
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
|
||||
|
||||
private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color";
|
||||
|
||||
/**
|
||||
* Categories currently enabled, formatted for an API call
|
||||
*/
|
||||
public static String sponsorBlockAPIFetchCategories = "[]";
|
||||
|
||||
static {
|
||||
for (SegmentCategory value : categoriesWithoutUnsubmitted)
|
||||
mValuesMap.put(value.key, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
||||
return categoriesWithoutUnsubmitted;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SegmentCategory[] categoriesWithoutHighlights() {
|
||||
return categoriesWithoutHighlights;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static SegmentCategory byCategoryKey(@NonNull String key) {
|
||||
return mValuesMap.get(key);
|
||||
}
|
||||
|
||||
public static void loadFromPreferences() {
|
||||
SharedPreferences preferences = SharedPrefCategory.SPONSOR_BLOCK.preferences;
|
||||
LogHelper.printDebug(() -> "loadFromPreferences");
|
||||
for (SegmentCategory category : categoriesWithoutUnsubmitted()) {
|
||||
category.load(preferences);
|
||||
}
|
||||
updateEnabledCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called if behavior of any category is changed
|
||||
*/
|
||||
public static void updateEnabledCategories() {
|
||||
SegmentCategory[] categories = categoriesWithoutUnsubmitted();
|
||||
List<String> enabledCategories = new ArrayList<>(categories.length);
|
||||
for (SegmentCategory category : categories) {
|
||||
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
||||
enabledCategories.add(category.key);
|
||||
}
|
||||
}
|
||||
|
||||
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
|
||||
if (enabledCategories.isEmpty())
|
||||
sponsorBlockAPIFetchCategories = "[]";
|
||||
else
|
||||
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public final String key;
|
||||
@NonNull
|
||||
public final StringRef title;
|
||||
@NonNull
|
||||
public final StringRef description;
|
||||
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the first quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextBeginning;
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the middle half of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextMiddle;
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the last quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextEnd;
|
||||
/**
|
||||
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skippedToastBeginning;
|
||||
/**
|
||||
* Skipped segment toast, if the skip occurred in the middle half of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skippedToastMiddle;
|
||||
/**
|
||||
* Skipped segment toast, if the skip occurred in the last quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skippedToastEnd;
|
||||
|
||||
@NonNull
|
||||
public final Paint paint;
|
||||
public final int defaultColor;
|
||||
/**
|
||||
* If value is changed, then also call {@link #save(SharedPreferences.Editor)}
|
||||
*/
|
||||
public int color;
|
||||
|
||||
/**
|
||||
* If value is changed, then also call {@link #updateEnabledCategories()}
|
||||
*/
|
||||
@NonNull
|
||||
public CategoryBehaviour behaviour;
|
||||
|
||||
SegmentCategory(String key, StringRef title, StringRef description,
|
||||
StringRef skipButtonText,
|
||||
StringRef skippedToastText,
|
||||
CategoryBehaviour defaultBehavior, int defaultColor) {
|
||||
this(key, title, description,
|
||||
skipButtonText, skipButtonText, skipButtonText,
|
||||
skippedToastText, skippedToastText, skippedToastText,
|
||||
defaultBehavior, defaultColor);
|
||||
}
|
||||
|
||||
SegmentCategory(String key, StringRef title, StringRef description,
|
||||
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
||||
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
||||
CategoryBehaviour defaultBehavior, int defaultColor) {
|
||||
this.key = Objects.requireNonNull(key);
|
||||
this.title = Objects.requireNonNull(title);
|
||||
this.description = Objects.requireNonNull(description);
|
||||
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
|
||||
this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle);
|
||||
this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd);
|
||||
this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning);
|
||||
this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle);
|
||||
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
||||
this.behaviour = Objects.requireNonNull(defaultBehavior);
|
||||
this.color = this.defaultColor = defaultColor;
|
||||
this.paint = new Paint();
|
||||
setColor(defaultColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caller must also call {@link #updateEnabledCategories()}
|
||||
*/
|
||||
private void load(SharedPreferences preferences) {
|
||||
String categoryColor = preferences.getString(key + COLOR_PREFERENCE_KEY_SUFFIX, null);
|
||||
if (categoryColor == null) {
|
||||
setColor(defaultColor);
|
||||
} else {
|
||||
setColor(categoryColor);
|
||||
}
|
||||
|
||||
String behaviorString = preferences.getString(key, null);
|
||||
if (behaviorString != null) {
|
||||
CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString);
|
||||
if (preferenceBehavior == null) {
|
||||
LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen
|
||||
} else {
|
||||
behaviour = preferenceBehavior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current color and behavior.
|
||||
* Calling code is responsible for calling {@link SharedPreferences.Editor#apply()}
|
||||
*/
|
||||
public void save(SharedPreferences.Editor editor) {
|
||||
String colorString = (color == defaultColor)
|
||||
? null // remove any saved preference, so default is used on the next load
|
||||
: colorString();
|
||||
editor.putString(key + COLOR_PREFERENCE_KEY_SUFFIX, colorString);
|
||||
editor.putString(key, behaviour.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTML color format string
|
||||
*/
|
||||
@NonNull
|
||||
public String colorString() {
|
||||
return String.format("#%06X", color);
|
||||
}
|
||||
|
||||
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
|
||||
setColor(Color.parseColor(colorString));
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
color &= 0xFFFFFF;
|
||||
this.color = color;
|
||||
paint.setColor(color);
|
||||
paint.setAlpha(255);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getCategoryColorDotHTML(int color) {
|
||||
color &= 0xFFFFFF;
|
||||
return String.format("<font color=\"#%06X\">⬤</font>", color);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Spanned getCategoryColorDot(int color) {
|
||||
return Html.fromHtml(getCategoryColorDotHTML(color));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Spanned getCategoryColorDot() {
|
||||
return getCategoryColorDot(color);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Spanned getTitleWithColorDot() {
|
||||
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segmentStartTime video time the segment category started
|
||||
* @param videoLength length of the video
|
||||
* @return the skip button text
|
||||
*/
|
||||
@NonNull
|
||||
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
||||
if (SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()) {
|
||||
return (this == SegmentCategory.HIGHLIGHT)
|
||||
? skipSponsorTextCompactHighlight
|
||||
: skipSponsorTextCompact;
|
||||
}
|
||||
|
||||
if (videoLength == 0) {
|
||||
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
|
||||
}
|
||||
final float position = segmentStartTime / (float) videoLength;
|
||||
if (position < 0.25f) {
|
||||
return skipButtonTextBeginning;
|
||||
} else if (position < 0.75f) {
|
||||
return skipButtonTextMiddle;
|
||||
}
|
||||
return skipButtonTextEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segmentStartTime video time the segment category started
|
||||
* @param videoLength length of the video
|
||||
* @return 'skipped segment' toast message
|
||||
*/
|
||||
@NonNull
|
||||
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
||||
if (videoLength == 0) {
|
||||
return skippedToastBeginning; // video is still loading. Assume it's the beginning
|
||||
}
|
||||
final float position = segmentStartTime / (float) videoLength;
|
||||
if (position < 0.25f) {
|
||||
return skippedToastBeginning;
|
||||
} else if (position < 0.75f) {
|
||||
return skippedToastMiddle;
|
||||
}
|
||||
return skippedToastEnd;
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.preference.ListPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SegmentCategoryListPreference extends ListPreference {
|
||||
private final SegmentCategory category;
|
||||
private EditText mEditText;
|
||||
private int mClickedDialogEntryIndex;
|
||||
|
||||
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
|
||||
super(context);
|
||||
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
|
||||
this.category = Objects.requireNonNull(category);
|
||||
setKey(category.key);
|
||||
setDefaultValue(category.behaviour.key);
|
||||
setEntries(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorDescriptions());
|
||||
setEntryValues(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorKeysWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorKeys());
|
||||
setSummary(category.description.toString());
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
try {
|
||||
Context context = builder.getContext();
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(70, 0, 150, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
|
||||
TextView colorTextLabel = new TextView(context);
|
||||
colorTextLabel.setText(str("sb_color_dot_label"));
|
||||
row.addView(colorTextLabel);
|
||||
|
||||
TextView colorDotView = new TextView(context);
|
||||
colorDotView.setText(category.getCategoryColorDot());
|
||||
colorDotView.setPadding(30, 0, 30, 0);
|
||||
row.addView(colorDotView);
|
||||
|
||||
mEditText = new EditText(context);
|
||||
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||
mEditText.setText(category.colorString());
|
||||
mEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
try {
|
||||
String colorString = s.toString();
|
||||
if (!colorString.startsWith("#")) {
|
||||
s.insert(0, "#"); // recursively calls back into this method
|
||||
return;
|
||||
}
|
||||
if (colorString.length() > 7) {
|
||||
s.delete(7, colorString.length());
|
||||
return;
|
||||
}
|
||||
final int color = Color.parseColor(colorString);
|
||||
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(mEditText);
|
||||
|
||||
table.addView(row);
|
||||
builder.setView(table);
|
||||
builder.setTitle(category.title.toString());
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
});
|
||||
builder.setNeutralButton(str("sb_reset_color"), (dialog, which) -> {
|
||||
try {
|
||||
SharedPreferences.Editor editor = getSharedPreferences().edit();
|
||||
category.setColor(category.defaultColor);
|
||||
category.save(editor);
|
||||
editor.apply();
|
||||
updateTitle();
|
||||
ReVancedUtils.showToastShort(str("sb_color_reset"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "setNeutralButton failure", ex);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
mClickedDialogEntryIndex = findIndexOfValue(getValue());
|
||||
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
try {
|
||||
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
category.behaviour = Objects.requireNonNull(CategoryBehaviour.byStringKey(value));
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
}
|
||||
String colorString = mEditText.getText().toString();
|
||||
try {
|
||||
final int color = Color.parseColor(colorString) & 0xFFFFFF;
|
||||
if (color != category.color) {
|
||||
category.setColor(color);
|
||||
ReVancedUtils.showToastShort(str("sb_color_changed"));
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
ReVancedUtils.showToastShort(str("sb_color_invalid"));
|
||||
}
|
||||
// behavior is already saved, but color needs to be saved
|
||||
SharedPreferences.Editor editor = getSharedPreferences().edit();
|
||||
category.save(editor);
|
||||
editor.apply();
|
||||
updateTitle();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onDialogClosed failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
setTitle(category.getTitleWithColorDot());
|
||||
}
|
||||
}
|
@ -1,35 +1,127 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.integrations.utils.StringRef.sf;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.utils.StringRef;
|
||||
|
||||
public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
public final long start;
|
||||
public final long end;
|
||||
public final SponsorBlockSettings.SegmentInfo category;
|
||||
public final String UUID;
|
||||
public final boolean isLocked;
|
||||
public boolean didAutoSkipped = false;
|
||||
public enum SegmentVote {
|
||||
UPVOTE(sf("sb_vote_upvote"), 1,false),
|
||||
DOWNVOTE(sf("sb_vote_downvote"), 0, true),
|
||||
CATEGORY_CHANGE(sf("sb_vote_category"), -1, true); // apiVoteType is not used for category change
|
||||
|
||||
public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID, boolean isLocked) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.category = category;
|
||||
this.UUID = UUID;
|
||||
this.isLocked = isLocked;
|
||||
public static final SegmentVote[] voteTypesWithoutCategoryChange = {
|
||||
UPVOTE,
|
||||
DOWNVOTE,
|
||||
};
|
||||
|
||||
@NonNull
|
||||
public final StringRef title;
|
||||
public final int apiVoteType;
|
||||
public final boolean shouldHighlight;
|
||||
|
||||
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) {
|
||||
this.title = title;
|
||||
this.apiVoteType = apiVoteType;
|
||||
this.shouldHighlight = shouldHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return MessageFormat.format("SegmentInfo'{'start={0}, end={1}, category=''{2}'', locked={3}'}'", start, end, category, isLocked);
|
||||
public final SegmentCategory category;
|
||||
/**
|
||||
* NULL if segment is unsubmitted
|
||||
*/
|
||||
@Nullable
|
||||
public final String UUID;
|
||||
public final long start;
|
||||
public final long end;
|
||||
public final boolean isLocked;
|
||||
public boolean didAutoSkipped = false;
|
||||
/**
|
||||
* If this segment has been counted as 'skipped'
|
||||
*/
|
||||
public boolean recordedAsSkipped = false;
|
||||
|
||||
public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) {
|
||||
this.category = category;
|
||||
this.UUID = UUID;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.isLocked = isLocked;
|
||||
}
|
||||
|
||||
public boolean shouldAutoSkip() {
|
||||
return category.behaviour.skipAutomatically && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
||||
*/
|
||||
public boolean startIsNear(long videoTime, long nearThreshold) {
|
||||
return Math.abs(start - videoTime) <= nearThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
||||
*/
|
||||
public boolean endIsNear(long videoTime, long nearThreshold) {
|
||||
return Math.abs(end - videoTime) <= nearThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the time parameter is within this segment
|
||||
*/
|
||||
public boolean containsTime(long videoTime) {
|
||||
return start <= videoTime && videoTime < end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the segment is completely contained inside this segment
|
||||
*/
|
||||
public boolean containsSegment(SponsorSegment other) {
|
||||
return start <= other.start && other.end <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the length of this segment, in milliseconds. Always a positive number.
|
||||
*/
|
||||
public long length() {
|
||||
return end - start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'skip segment' UI overlay button text
|
||||
*/
|
||||
@NonNull
|
||||
public String getSkipButtonText() {
|
||||
return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength()).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'skipped segment' toast message
|
||||
*/
|
||||
@NonNull
|
||||
public String getSkippedToastText() {
|
||||
return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SponsorSegment o) {
|
||||
return (int) (this.start - o.start);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SponsorSegment{"
|
||||
+ "category=" + category
|
||||
+ ", start=" + start
|
||||
+ ", end=" + end
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,45 @@
|
||||
package app.revanced.integrations.sponsorblock.objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* SponsorBlock user stats
|
||||
*/
|
||||
public class UserStats {
|
||||
private final String userName;
|
||||
private final double minutesSaved;
|
||||
private final int segmentCount;
|
||||
private final int viewCount;
|
||||
@NonNull
|
||||
public final String publicUserId;
|
||||
@NonNull
|
||||
public final String userName;
|
||||
/**
|
||||
* "User reputation". Unclear how SB determines this value.
|
||||
*/
|
||||
public final float reputation;
|
||||
public final int segmentCount;
|
||||
public final int viewCount;
|
||||
public final double minutesSaved;
|
||||
|
||||
public UserStats(String userName, double minutesSaved, int segmentCount, int viewCount) {
|
||||
this.userName = userName;
|
||||
this.minutesSaved = minutesSaved;
|
||||
this.segmentCount = segmentCount;
|
||||
this.viewCount = viewCount;
|
||||
public UserStats(@NonNull JSONObject json) throws JSONException {
|
||||
publicUserId = json.getString("userID");
|
||||
userName = json.getString("userName");
|
||||
reputation = (float)json.getDouble("reputation");
|
||||
segmentCount = json.getInt("segmentCount");
|
||||
viewCount = json.getInt("viewCount");
|
||||
minutesSaved = json.getDouble("minutesSaved");
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public double getMinutesSaved() {
|
||||
return minutesSaved;
|
||||
}
|
||||
|
||||
public int getSegmentCount() {
|
||||
return segmentCount;
|
||||
}
|
||||
|
||||
public int getViewCount() {
|
||||
return viewCount;
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserStats{"
|
||||
+ "publicUserId='" + publicUserId + '\''
|
||||
+ ", userName='" + userName + '\''
|
||||
+ ", reputation=" + reputation
|
||||
+ ", segmentCount=" + segmentCount
|
||||
+ ", viewCount=" + viewCount
|
||||
+ ", minutesSaved=" + minutesSaved
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class ChannelModel implements Serializable {
|
||||
private String author;
|
||||
private String channelId;
|
||||
|
||||
public ChannelModel(String author, String channelId) {
|
||||
this.author = author;
|
||||
this.channelId = channelId;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getChannelId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
public void setChannelId(String channelId) {
|
||||
this.channelId = channelId;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player;
|
||||
|
||||
public enum PlayerType {
|
||||
|
||||
NONE,
|
||||
HIDDEN,
|
||||
WATCH_WHILE_MINIMIZED,
|
||||
WATCH_WHILE_MAXIMIZED,
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
INLINE_MINIMAL,
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
|
||||
public class ButtonVisibility {
|
||||
public static Visibility getButtonVisibility(String key) {
|
||||
return getButtonVisibility(key, SharedPrefHelper.SharedPrefNames.YOUTUBE);
|
||||
}
|
||||
|
||||
public static Visibility getButtonVisibility(String key, SharedPrefHelper.SharedPrefNames name) {
|
||||
String value = SharedPrefHelper.getString(name, key, null);
|
||||
|
||||
if (value == null || value.isEmpty()) return Visibility.NONE;
|
||||
|
||||
switch (value.toUpperCase()) {
|
||||
case "PLAYER":
|
||||
return Visibility.PLAYER;
|
||||
case "BUTTON_CONTAINER":
|
||||
return Visibility.BUTTON_CONTAINER;
|
||||
case "BOTH":
|
||||
return Visibility.BOTH;
|
||||
default:
|
||||
return Visibility.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isVisibleInContainer(String key) {
|
||||
return isVisibleInContainer(getButtonVisibility(key));
|
||||
}
|
||||
|
||||
public static boolean isVisibleInContainer(String key, SharedPrefHelper.SharedPrefNames name) {
|
||||
return isVisibleInContainer(getButtonVisibility(key, name));
|
||||
}
|
||||
|
||||
public static boolean isVisibleInContainer(Visibility visibility) {
|
||||
return visibility == Visibility.BOTH || visibility == Visibility.BUTTON_CONTAINER;
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.sponsorblock.NewSegmentHelperLayout;
|
||||
import app.revanced.integrations.sponsorblock.PlayerController;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
|
||||
|
||||
public class NewSegmentLayout extends FrameLayout {
|
||||
|
||||
private LinearLayout newSegmentContainer;
|
||||
public int defaultBottomMargin;
|
||||
public int ctaBottomMargin;
|
||||
public ImageButton rewindButton;
|
||||
public ImageButton forwardButton;
|
||||
public ImageButton adjustButton;
|
||||
public ImageButton compareButton;
|
||||
public ImageButton editButton;
|
||||
public ImageButton publishButton;
|
||||
private int rippleEffectId;
|
||||
|
||||
public NewSegmentLayout(Context context) {
|
||||
super(context);
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) {
|
||||
super(context, attributeSet, defStyleAttr);
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
private final void initialize(Context context) {
|
||||
LayoutInflater.from(context).inflate(getIdentifier(context, "new_segment", "layout"), this, true);
|
||||
Resources resources = context.getResources();
|
||||
|
||||
TypedValue rippleEffect = new TypedValue();
|
||||
getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true);
|
||||
rippleEffectId = rippleEffect.resourceId;
|
||||
|
||||
this.newSegmentContainer = (LinearLayout) this.findViewById(getIdentifier(context, "new_segment_container", "id"));
|
||||
|
||||
this.rewindButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_rewind", "id"));
|
||||
if (this.rewindButton != null) {
|
||||
setClickEffect(this.rewindButton);
|
||||
this.rewindButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Rewind button clicked");
|
||||
PlayerController.skipRelativeMilliseconds(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
|
||||
}
|
||||
});
|
||||
}
|
||||
this.forwardButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_forward", "id"));
|
||||
if (this.forwardButton != null) {
|
||||
setClickEffect(this.forwardButton);
|
||||
this.forwardButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Forward button clicked");
|
||||
PlayerController.skipRelativeMilliseconds(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
|
||||
}
|
||||
});
|
||||
}
|
||||
this.adjustButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_adjust", "id"));
|
||||
if (this.adjustButton != null) {
|
||||
setClickEffect(this.adjustButton);
|
||||
this.adjustButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Adjust button clicked");
|
||||
SponsorBlockUtils.onMarkLocationClicked(NewSegmentHelperLayout.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.compareButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_compare", "id"));
|
||||
if (this.compareButton != null) {
|
||||
setClickEffect(this.compareButton);
|
||||
this.compareButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Compare button clicked");
|
||||
SponsorBlockUtils.onPreviewClicked(NewSegmentHelperLayout.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_edit", "id"));
|
||||
if (this.editButton != null) {
|
||||
setClickEffect(this.editButton);
|
||||
this.editButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Edit button clicked");
|
||||
SponsorBlockUtils.onEditByHandClicked(NewSegmentHelperLayout.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.publishButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_publish", "id"));
|
||||
if (this.publishButton != null) {
|
||||
setClickEffect(this.publishButton);
|
||||
this.publishButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Publish button clicked");
|
||||
SponsorBlockUtils.onPublishClicked(NewSegmentHelperLayout.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_default_bottom_margin", "dimen"));
|
||||
this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_cta_bottom_margin", "dimen"));
|
||||
}
|
||||
|
||||
private void setClickEffect(ImageButton btn) {
|
||||
btn.setBackgroundResource(rippleEffectId);
|
||||
|
||||
RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground();
|
||||
|
||||
int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}};
|
||||
int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white
|
||||
|
||||
ColorStateList colorStateList = new ColorStateList(states, colors);
|
||||
rippleDrawable.setColor(colorStateList);
|
||||
}
|
||||
|
||||
private int getIdentifier(Context context, String name, String defType) {
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.sponsorblock.PlayerController;
|
||||
|
||||
public class SkipSponsorButton extends FrameLayout {
|
||||
public CharSequence skipSponsorTextViewText;
|
||||
public CharSequence skipSponsorText;
|
||||
public ImageView skipSponsorButtonIcon;
|
||||
public TextView skipSponsorTextView;
|
||||
public int currentTextColor;
|
||||
public int invertedButtonForegroundColor;
|
||||
public int backgroundColor;
|
||||
public int invertedBackgroundColor;
|
||||
public ColorDrawable backgroundColorDrawable;
|
||||
public int defaultBottomMargin;
|
||||
public int ctaBottomMargin;
|
||||
private LinearLayout skipSponsorBtnContainer;
|
||||
private final Paint background;
|
||||
private final Paint border;
|
||||
private boolean highContrast = true;
|
||||
|
||||
public SkipSponsorButton(Context context) {
|
||||
super(context);
|
||||
this.background = new Paint();
|
||||
this.border = new Paint();
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
this.background = new Paint();
|
||||
this.border = new Paint();
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) {
|
||||
super(context, attributeSet, defStyleAttr);
|
||||
this.background = new Paint();
|
||||
this.border = new Paint();
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||
this.background = new Paint();
|
||||
this.border = new Paint();
|
||||
this.initialize(context);
|
||||
}
|
||||
|
||||
private final void initialize(Context context) {
|
||||
LayoutInflater.from(context).inflate(getIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
|
||||
this.setMinimumHeight(this.getResources().getDimensionPixelSize(getIdentifier(context, "ad_skip_ad_button_min_height", "dimen"))); // dimen:ad_skip_ad_button_min_height
|
||||
this.skipSponsorBtnContainer = (LinearLayout) this.findViewById(getIdentifier(context, "skip_sponsor_button_container", "id")); // id:skip_ad_button_container
|
||||
this.skipSponsorButtonIcon = (ImageView) this.findViewById(getIdentifier(context, "skip_sponsor_button_icon", "id")); // id:skip_ad_button_icon
|
||||
this.backgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_background_color", "color")); // color:skip_ad_button_background_color
|
||||
this.invertedBackgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_background_color", "color")); // color:skip_ad_button_inverted_background_color
|
||||
this.background.setColor(this.backgroundColor);
|
||||
this.background.setStyle(Paint.Style.FILL);
|
||||
int borderColor = getColor(context, getIdentifier(context, "skip_ad_button_border_color", "color")); // color:skip_ad_button_border_color
|
||||
this.border.setColor(borderColor);
|
||||
float borderWidth = this.getResources().getDimension(getIdentifier(context, "ad_skip_ad_button_border_width", "dimen")); // dimen:ad_skip_ad_button_border_width
|
||||
this.border.setStrokeWidth(borderWidth);
|
||||
this.border.setStyle(Paint.Style.STROKE);
|
||||
TextView skipSponsorText = (TextView) this.findViewById(getIdentifier(context, "skip_sponsor_button_text", "id")); // id:skip_ad_button_text
|
||||
this.skipSponsorTextView = skipSponsorText;
|
||||
this.skipSponsorTextViewText = skipSponsorText.getText();
|
||||
this.currentTextColor = this.skipSponsorTextView.getCurrentTextColor();
|
||||
this.invertedButtonForegroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_foreground_color", "color")); // color:skip_ad_button_inverted_foreground_color
|
||||
this.backgroundColorDrawable = new ColorDrawable(this.backgroundColor);
|
||||
Resources resources = context.getResources();
|
||||
this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_default_bottom_margin", "dimen")); // dimen:skip_button_default_bottom_margin
|
||||
this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_cta_bottom_margin", "dimen")); // dimen:skip_button_cta_bottom_margin
|
||||
this.skipSponsorText = resources.getText(getIdentifier(context, "skip_sponsor", "string")); // string:skip_ads "Skip ads"
|
||||
|
||||
this.skipSponsorBtnContainer.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogHelper.printDebug(() -> "Skip button clicked");
|
||||
PlayerController.onSkipSponsorClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override // android.view.ViewGroup
|
||||
protected final void dispatchDraw(Canvas canvas) {
|
||||
int width = this.skipSponsorBtnContainer.getWidth();
|
||||
int height = this.skipSponsorBtnContainer.getHeight();
|
||||
int top = this.skipSponsorBtnContainer.getTop();
|
||||
int left = this.skipSponsorBtnContainer.getLeft();
|
||||
float floatLeft = (float) left;
|
||||
float floatTop = (float) top;
|
||||
float floatWidth = (float) (left + width);
|
||||
float floatHeight = (float) (top + height);
|
||||
canvas.drawRect(floatLeft, floatTop, floatWidth, floatHeight, this.background);
|
||||
if (!this.highContrast) {
|
||||
canvas.drawLines(new float[]{floatWidth, floatTop, floatLeft, floatTop, floatLeft, floatTop, floatLeft, floatHeight, floatLeft, floatHeight, floatWidth, floatHeight}, this.border);
|
||||
}
|
||||
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
|
||||
|
||||
public static int getColor(Context context, int arg3) {
|
||||
return context.getColor(arg3);
|
||||
}
|
||||
|
||||
private int getIdentifier(Context context, String name, String defType) {
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public abstract class SlimButton implements View.OnClickListener {
|
||||
public static int SLIM_METADATA_BUTTON_ID;
|
||||
public final View view;
|
||||
public final Context context;
|
||||
private final ViewGroup container;
|
||||
protected final ImageView button_icon;
|
||||
protected final TextView button_text;
|
||||
private boolean viewAdded = false;
|
||||
|
||||
static {
|
||||
SLIM_METADATA_BUTTON_ID = ReVancedUtils.getIdentifier("slim_metadata_button", "layout");
|
||||
}
|
||||
|
||||
public SlimButton(Context context, ViewGroup container, int id, boolean visible) {
|
||||
LogHelper.printDebug(() -> "Adding button with id " + id + " and visibility of " + visible);
|
||||
this.context = context;
|
||||
this.container = container;
|
||||
view = LayoutInflater.from(context).inflate(id, container, false);
|
||||
button_icon = (ImageView) view.findViewById(ReVancedUtils.getIdentifier("button_icon", "id"));
|
||||
button_text = (TextView) view.findViewById(ReVancedUtils.getIdentifier("button_text", "id"));
|
||||
|
||||
view.setOnClickListener(this);
|
||||
|
||||
setVisible(visible);
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
try {
|
||||
if (!viewAdded && visible) {
|
||||
container.addView(view);
|
||||
viewAdded = true;
|
||||
} else if (viewAdded && !visible) {
|
||||
container.removeView(view);
|
||||
viewAdded = false;
|
||||
}
|
||||
setContainerVisibility();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Error while changing button visibility", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void setContainerVisibility() {
|
||||
if (container == null) return;
|
||||
|
||||
for (int i = 0; i < container.getChildCount(); i++) {
|
||||
if (container.getChildAt(i).getVisibility() == View.VISIBLE) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
container.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.sponsorblock.SwipeHelper;
|
||||
|
||||
public class SponsorBlockView {
|
||||
|
||||
static RelativeLayout inlineSponsorOverlay;
|
||||
static ViewGroup _youtubeOverlaysLayout;
|
||||
static WeakReference<SkipSponsorButton> _skipSponsorButton = new WeakReference<>(null);
|
||||
static WeakReference<NewSegmentLayout> _newSegmentLayout = new WeakReference<>(null);
|
||||
static boolean shouldShowOnPlayerType = true;
|
||||
|
||||
public static void initialize(Object viewGroup) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "initializing");
|
||||
|
||||
_youtubeOverlaysLayout = (ViewGroup) viewGroup;
|
||||
|
||||
addView();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Unable to set ViewGroup", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showSkipButton() {
|
||||
skipSponsorButtonVisibility(true);
|
||||
}
|
||||
|
||||
public static void hideSkipButton() {
|
||||
skipSponsorButtonVisibility(false);
|
||||
}
|
||||
|
||||
public static void showNewSegmentLayout() {
|
||||
newSegmentLayoutVisibility(true);
|
||||
}
|
||||
|
||||
public static void hideNewSegmentLayout() {
|
||||
newSegmentLayoutVisibility(false);
|
||||
}
|
||||
|
||||
public static void playerTypeChanged(PlayerType playerType) {
|
||||
try {
|
||||
shouldShowOnPlayerType = (playerType == PlayerType.WATCH_WHILE_FULLSCREEN || playerType == PlayerType.WATCH_WHILE_MAXIMIZED);
|
||||
|
||||
if (playerType == PlayerType.WATCH_WHILE_FULLSCREEN) {
|
||||
setSkipBtnMargins(true);
|
||||
setNewSegmentLayoutMargins(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setSkipBtnMargins(false);
|
||||
setNewSegmentLayoutMargins(false);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Player type changed caused a crash.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addView() {
|
||||
inlineSponsorOverlay = new RelativeLayout(ReVancedUtils.getContext());
|
||||
setLayoutParams(inlineSponsorOverlay);
|
||||
LayoutInflater.from(ReVancedUtils.getContext()).inflate(getIdentifier("inline_sponsor_overlay", "layout"), inlineSponsorOverlay);
|
||||
|
||||
_youtubeOverlaysLayout.addView(inlineSponsorOverlay, _youtubeOverlaysLayout.getChildCount() - 2);
|
||||
|
||||
SkipSponsorButton skipSponsorButton = (SkipSponsorButton) inlineSponsorOverlay.findViewById(getIdentifier("skip_sponsor_button", "id"));
|
||||
_skipSponsorButton = new WeakReference<>(skipSponsorButton);
|
||||
|
||||
NewSegmentLayout newSegmentView = (NewSegmentLayout) inlineSponsorOverlay.findViewById(getIdentifier("new_segment_view", "id"));
|
||||
_newSegmentLayout = new WeakReference<>(newSegmentView);
|
||||
}
|
||||
|
||||
private static void setLayoutParams(RelativeLayout relativeLayout) {
|
||||
relativeLayout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
private static void setSkipBtnMargins(boolean fullScreen) {
|
||||
SkipSponsorButton skipSponsorButton = _skipSponsorButton.get();
|
||||
if (skipSponsorButton == null) {
|
||||
LogHelper.printException(() -> "Unable to setSkipBtnMargins");
|
||||
return;
|
||||
}
|
||||
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams();
|
||||
if (params == null) {
|
||||
LogHelper.printException(() -> "Unable to setSkipBtnMargins");
|
||||
return;
|
||||
}
|
||||
params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin;
|
||||
skipSponsorButton.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private static void skipSponsorButtonVisibility(boolean visible) {
|
||||
SkipSponsorButton skipSponsorButton = _skipSponsorButton.get();
|
||||
if (skipSponsorButton == null) {
|
||||
LogHelper.printException(() -> "Unable to skipSponsorButtonVisibility");
|
||||
return;
|
||||
}
|
||||
|
||||
visible &= shouldShowOnPlayerType;
|
||||
|
||||
skipSponsorButton.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
bringLayoutToFront();
|
||||
}
|
||||
|
||||
private static void setNewSegmentLayoutMargins(boolean fullScreen) {
|
||||
NewSegmentLayout newSegmentLayout = _newSegmentLayout.get();
|
||||
if (newSegmentLayout == null) {
|
||||
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins");
|
||||
return;
|
||||
}
|
||||
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams();
|
||||
if (params == null) {
|
||||
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins");
|
||||
return;
|
||||
}
|
||||
params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin;
|
||||
newSegmentLayout.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private static void newSegmentLayoutVisibility(boolean visible) {
|
||||
NewSegmentLayout newSegmentLayout = _newSegmentLayout.get();
|
||||
if (newSegmentLayout == null) {
|
||||
LogHelper.printException(() -> "Unable to newSegmentLayoutVisibility");
|
||||
return;
|
||||
}
|
||||
|
||||
visible &= shouldShowOnPlayerType;
|
||||
|
||||
newSegmentLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
bringLayoutToFront();
|
||||
}
|
||||
|
||||
private static void bringLayoutToFront() {
|
||||
checkLayout();
|
||||
inlineSponsorOverlay.bringToFront();
|
||||
inlineSponsorOverlay.requestLayout();
|
||||
inlineSponsorOverlay.invalidate();
|
||||
}
|
||||
|
||||
private static void checkLayout() {
|
||||
if (inlineSponsorOverlay.getHeight() == 0) {
|
||||
ViewGroup watchLayout = SwipeHelper.nextGenWatchLayout;
|
||||
if (watchLayout == null) {
|
||||
LogHelper.printDebug(() -> "nextGenWatchLayout is null!");
|
||||
return;
|
||||
}
|
||||
View layout = watchLayout.findViewById(getIdentifier("player_overlays", "id"));
|
||||
|
||||
if (layout == null) {
|
||||
LogHelper.printDebug(() -> "player_overlays was not found for SB");
|
||||
return;
|
||||
}
|
||||
|
||||
initialize(layout);
|
||||
LogHelper.printDebug(() -> "player_overlays refreshed for SB");
|
||||
}
|
||||
}
|
||||
|
||||
private static int getIdentifier(String name, String defType) {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SponsorBlockVoting extends SlimButton {
|
||||
public SponsorBlockVoting(Context context, ViewGroup container) {
|
||||
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false);
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.button_icon.setImageResource(ReVancedUtils.getIdentifier("revanced_sb_voting", "drawable"));
|
||||
this.button_text.setText("SB Voting");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Toast.makeText(ReVancedUtils.getContext(), "Nothing atm", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package app.revanced.integrations.sponsorblock.player.ui;
|
||||
|
||||
public enum Visibility {
|
||||
NONE,
|
||||
PLAYER,
|
||||
BUTTON_CONTAINER,
|
||||
BOTH,
|
||||
}
|
@ -1,22 +1,17 @@
|
||||
package app.revanced.integrations.sponsorblock.requests;
|
||||
|
||||
import static android.text.Html.fromHtml;
|
||||
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments;
|
||||
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments;
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.runOnMainThread;
|
||||
import static app.revanced.integrations.utils.StringRef.str;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -25,207 +20,249 @@ 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.PlayerController;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils.VoteOption;
|
||||
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote;
|
||||
import app.revanced.integrations.sponsorblock.objects.UserStats;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SBRequester {
|
||||
private static final String TIME_TEMPLATE = "%.3f";
|
||||
|
||||
/**
|
||||
* TCP timeout
|
||||
*/
|
||||
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000;
|
||||
|
||||
/**
|
||||
* HTTP response timeout
|
||||
*/
|
||||
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000;
|
||||
|
||||
/**
|
||||
* Response code of a successful API call
|
||||
*/
|
||||
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
|
||||
|
||||
private SBRequester() {
|
||||
}
|
||||
|
||||
public static synchronized SponsorSegment[] getSegments(String videoId) {
|
||||
@NonNull
|
||||
public static SponsorSegment[] getSegments(@NonNull String videoId) {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
List<SponsorSegment> segments = new ArrayList<>();
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
|
||||
int responseCode = connection.getResponseCode();
|
||||
runVipCheck();
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories);
|
||||
final int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == 200) {
|
||||
// FIXME? should this use Requester#getJSONArray and not disconnect?
|
||||
// HTTPURLConnection#disconnect says:
|
||||
// disconnect if other requests to the server
|
||||
// are unlikely in the near future.
|
||||
JSONArray responseArray = Requester.parseJSONArrayAndDisconnect(connection);
|
||||
int length = responseArray.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
|
||||
JSONArray responseArray = Requester.parseJSONArray(connection);
|
||||
final long minSegmentDuration = (long) (SettingsEnum.SB_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");
|
||||
long start = (long) (segment.getDouble(0) * 1000);
|
||||
long end = (long) (segment.getDouble(1) * 1000);
|
||||
final long start = (long) (segment.getDouble(0) * 1000);
|
||||
final long end = (long) (segment.getDouble(1) * 1000);
|
||||
|
||||
long minDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000);
|
||||
if ((end - start) < minDuration)
|
||||
continue;
|
||||
|
||||
String category = obj.getString("category");
|
||||
String uuid = obj.getString("UUID");
|
||||
boolean locked = obj.getInt("locked") == 1;
|
||||
|
||||
SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category);
|
||||
if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) {
|
||||
SponsorSegment sponsorSegment = new SponsorSegment(start, end, segmentCategory, uuid, locked);
|
||||
segments.add(sponsorSegment);
|
||||
final boolean locked = obj.getInt("locked") == 1;
|
||||
String categoryKey = obj.getString("category");
|
||||
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
|
||||
if (category == null) {
|
||||
LogHelper.printException(() -> "Received unknown category: " + categoryKey); // should never happen
|
||||
} else if ((end - start) >= minSegmentDuration || category == SegmentCategory.HIGHLIGHT) {
|
||||
segments.add(new SponsorSegment(category, uuid, start, end, locked));
|
||||
}
|
||||
}
|
||||
if (!segments.isEmpty()) {
|
||||
videoHasSegments = true;
|
||||
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
|
||||
}
|
||||
LogHelper.printDebug(() -> {
|
||||
StringBuilder builder = new StringBuilder("Downloaded segments:");
|
||||
for (SponsorSegment segment : segments) {
|
||||
builder.append('\n').append(segment);
|
||||
}
|
||||
return builder.toString();
|
||||
});
|
||||
runVipCheckInBackgroundIfNeeded();
|
||||
} else if (responseCode == 404) {
|
||||
// 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));
|
||||
connection.disconnect(); // something went wrong, might as well disconnect
|
||||
}
|
||||
connection.disconnect();
|
||||
} catch (SocketTimeoutException ex) {
|
||||
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_timeout"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to get segments", ex);
|
||||
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_generic"));
|
||||
}
|
||||
return segments.toArray(new SponsorSegment[0]);
|
||||
}
|
||||
|
||||
public static void submitSegments(String videoId, String uuid, float startTime, float endTime, String category, Runnable toastRunnable) {
|
||||
public static void submitSegments(@NonNull String userPrivateId, @NonNull String videoId, @NonNull String category,
|
||||
long startTime, long endTime, long videoLength) {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
try {
|
||||
String start = String.format(Locale.US, TIME_TEMPLATE, startTime);
|
||||
String end = String.format(Locale.US, TIME_TEMPLATE, endTime);
|
||||
String duration = String.valueOf(PlayerController.getCurrentVideoLength() / 1000);
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, videoId, uuid, start, end, category, duration);
|
||||
int responseCode = connection.getResponseCode();
|
||||
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);
|
||||
final int responseCode = connection.getResponseCode();
|
||||
|
||||
final String messageToToast;
|
||||
switch (responseCode) {
|
||||
case 200:
|
||||
SponsorBlockUtils.messageToToast = str("submit_succeeded");
|
||||
case HTTP_STATUS_CODE_SUCCESS:
|
||||
messageToToast = str("sb_submit_succeeded");
|
||||
break;
|
||||
case 409:
|
||||
SponsorBlockUtils.messageToToast = str("submit_failed_duplicate");
|
||||
messageToToast = str("sb_submit_failed_duplicate");
|
||||
break;
|
||||
case 403:
|
||||
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
|
||||
messageToToast = str("sb_submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
|
||||
break;
|
||||
case 429:
|
||||
SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit");
|
||||
messageToToast = str("sb_submit_failed_rate_limit");
|
||||
break;
|
||||
case 400:
|
||||
SponsorBlockUtils.messageToToast = str("submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection));
|
||||
messageToToast = str("sb_submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection));
|
||||
break;
|
||||
default:
|
||||
SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage());
|
||||
messageToToast = str("sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage());
|
||||
break;
|
||||
}
|
||||
runOnMainThread(toastRunnable);
|
||||
connection.disconnect();
|
||||
ReVancedUtils.showToastLong(messageToToast);
|
||||
} catch (SocketTimeoutException ex) {
|
||||
ReVancedUtils.showToastLong(str("sb_submit_failed_timeout"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to submit segments", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendViewCountRequest(SponsorSegment segment) {
|
||||
public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
|
||||
connection.disconnect();
|
||||
final int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
|
||||
LogHelper.printDebug(() -> "Successfully sent view count for segment: " + segment);
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "Failed to sent view count for segment: " + segment.UUID
|
||||
+ " responseCode: " + responseCode); // debug level, no toast is shown
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LogHelper.printInfo(() -> "Failed to send view count", ex); // do not show a toast
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to send view count request", ex);
|
||||
LogHelper.printException(() -> "Failed to send view count request", ex); // should never happen
|
||||
}
|
||||
}
|
||||
|
||||
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, String... args) {
|
||||
public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) {
|
||||
voteOrRequestCategoryChange(segment, voteOption, null);
|
||||
}
|
||||
public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) {
|
||||
voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor);
|
||||
}
|
||||
private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
String segmentUuid = segment.UUID;
|
||||
String uuid = SettingsEnum.SB_UUID.getString();
|
||||
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
|
||||
|
||||
HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE
|
||||
? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0])
|
||||
: getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote);
|
||||
int responseCode = connection.getResponseCode();
|
||||
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));
|
||||
final int responseCode = connection.getResponseCode();
|
||||
|
||||
switch (responseCode) {
|
||||
case 200:
|
||||
SponsorBlockUtils.messageToToast = str("vote_succeeded");
|
||||
case HTTP_STATUS_CODE_SUCCESS:
|
||||
LogHelper.printDebug(() -> "Vote success for segment: " + segment);
|
||||
break;
|
||||
case 403:
|
||||
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
|
||||
ReVancedUtils.showToastLong(
|
||||
str("sb_vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)));
|
||||
break;
|
||||
default:
|
||||
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
|
||||
ReVancedUtils.showToastLong(
|
||||
str("sb_vote_failed_unknown_error", responseCode, connection.getResponseMessage()));
|
||||
break;
|
||||
}
|
||||
runOnMainThread(() -> Toast.makeText(context, SponsorBlockUtils.messageToToast, Toast.LENGTH_LONG).show());
|
||||
connection.disconnect();
|
||||
} catch (SocketTimeoutException ex) {
|
||||
LogHelper.printException(() -> "failed to vote for segment", ex, str("sb_vote_failed_timeout"));
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to vote for segment", ex);
|
||||
LogHelper.printException(() -> "failed to vote for segment", ex); // should never happen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) {
|
||||
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
|
||||
loadingPreference.setTitle(str("stats_sb_disabled"));
|
||||
return;
|
||||
/**
|
||||
* @return NULL, if stats fetch failed
|
||||
*/
|
||||
@Nullable
|
||||
public static UserStats retrieveUserStats() {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
try {
|
||||
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString()));
|
||||
LogHelper.printDebug(() -> "user stats: " + stats);
|
||||
return stats;
|
||||
} catch (IOException ex) {
|
||||
LogHelper.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failure retrieving user stats", ex); // should never happen
|
||||
}
|
||||
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
JSONObject json = getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString());
|
||||
UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"),
|
||||
json.getInt("viewCount"));
|
||||
runOnMainThread(() -> { // get back on main thread to modify UI elements
|
||||
SponsorBlockUtils.addUserStats(category, loadingPreference, stats);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to retrieve user stats", ex);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void setUsername(String username, EditTextPreference preference, Runnable toastRunnable) {
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username);
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == 200) {
|
||||
SponsorBlockUtils.messageToToast = str("stats_username_changed");
|
||||
runOnMainThread(() -> {
|
||||
preference.setTitle(fromHtml(str("stats_username", username)));
|
||||
preference.setText(username);
|
||||
});
|
||||
} else {
|
||||
SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage());
|
||||
}
|
||||
runOnMainThread(toastRunnable);
|
||||
connection.disconnect();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to set username", ex);
|
||||
/**
|
||||
* @return NULL if the call was successful. If unsuccessful, an error message is returned.
|
||||
*/
|
||||
@Nullable
|
||||
public static String setUsername(@NonNull String username) {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username);
|
||||
final int responseCode = connection.getResponseCode();
|
||||
String responseMessage = connection.getResponseMessage();
|
||||
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return str("sb_stats_username_change_unknown_error", responseCode, responseMessage);
|
||||
} catch (Exception ex) { // should never happen
|
||||
LogHelper.printInfo(() -> "failed to set username", ex); // do not toast
|
||||
return str("sb_stats_username_change_unknown_error", 0, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void runVipCheck() {
|
||||
public static void runVipCheckInBackgroundIfNeeded() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now < (SettingsEnum.SB_LAST_VIP_CHECK.getLong() + TimeUnit.DAYS.toMillis(3))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString());
|
||||
boolean vip = json.getBoolean("vip");
|
||||
SettingsEnum.SB_IS_VIP.saveValue(vip);
|
||||
SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "failed to check VIP", ex);
|
||||
}
|
||||
ReVancedUtils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString());
|
||||
boolean vip = json.getBoolean("vip");
|
||||
SettingsEnum.SB_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
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to check VIP", ex); // should never happen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
|
||||
return Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params);
|
||||
private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException {
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params);
|
||||
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
|
||||
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static JSONObject getJSONObject(Route route, String... params) throws Exception {
|
||||
return Requester.parseJSONObjectAndDisconnect(getConnectionFromRoute(route, params));
|
||||
private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException {
|
||||
return Requester.parseJSONObject(getConnectionFromRoute(route, params));
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ 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=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
|
||||
static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"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?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}&videoDuration={duration}");
|
||||
static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
|
||||
static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
|
||||
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}");
|
||||
static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}");
|
||||
|
||||
private SBRoutes() {
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
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 app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
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) {
|
||||
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.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);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void changeVisibilityImmediate(boolean visible) {
|
||||
changeVisibility(visible, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
*/
|
||||
public static void changeVisibilityNegatedImmediate(boolean visible) {
|
||||
changeVisibility(!visible, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
*/
|
||||
public static void changeVisibility(boolean visible) {
|
||||
changeVisibility(visible, false);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible, boolean immediate) {
|
||||
try {
|
||||
if (isShowing == visible) return;
|
||||
isShowing = visible;
|
||||
|
||||
ImageView iView = buttonReference.get();
|
||||
if (iView == null) return;
|
||||
|
||||
if (visible) {
|
||||
iView.clearAnimation();
|
||||
if (!shouldBeShown()) {
|
||||
return;
|
||||
}
|
||||
if (!immediate) {
|
||||
iView.startAnimation(fadeIn);
|
||||
}
|
||||
iView.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (iView.getVisibility() == View.VISIBLE) {
|
||||
iView.clearAnimation();
|
||||
if (!immediate) {
|
||||
iView.startAnimation(fadeOut);
|
||||
}
|
||||
iView.setVisibility(View.GONE);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "changeVisibility failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldBeShown() {
|
||||
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()
|
||||
&& !VideoInformation.isAtEndOfVideo();
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
if (!isShowing) {
|
||||
return;
|
||||
}
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
View v = buttonReference.get();
|
||||
if (v == null) {
|
||||
return;
|
||||
}
|
||||
v.setVisibility(View.GONE);
|
||||
isShowing = false;
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package app.revanced.integrations.sponsorblock.ui;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class NewSegmentLayout extends FrameLayout {
|
||||
private final int rippleEffectId;
|
||||
final int defaultBottomMargin;
|
||||
final int ctaBottomMargin;
|
||||
|
||||
public NewSegmentLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet) {
|
||||
this(context, attributeSet, 0);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) {
|
||||
this(context, attributeSet, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||
|
||||
LayoutInflater.from(context).inflate(getResourceIdentifier(context, "new_segment", "layout"), this, true);
|
||||
|
||||
TypedValue rippleEffect = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true);
|
||||
rippleEffectId = rippleEffect.resourceId;
|
||||
|
||||
// LinearLayout newSegmentContainer = findViewById(getResourceIdentifier(context, "sb_new_segment_container", "id"));
|
||||
|
||||
ImageButton rewindButton = findViewById(getResourceIdentifier(context, "sb_new_segment_rewind", "id"));
|
||||
if (rewindButton == null) {
|
||||
LogHelper.printException(() -> "Could not find rewindButton");
|
||||
} else {
|
||||
setClickEffect(rewindButton);
|
||||
rewindButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Rewind button clicked");
|
||||
VideoInformation.seekToRelative(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
|
||||
});
|
||||
}
|
||||
ImageButton forwardButton = findViewById(getResourceIdentifier(context, "sb_new_segment_forward", "id"));
|
||||
if (forwardButton == null) {
|
||||
LogHelper.printException(() -> "Could not find forwardButton");
|
||||
} else {
|
||||
setClickEffect(forwardButton);
|
||||
forwardButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Forward button clicked");
|
||||
VideoInformation.seekToRelative(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
|
||||
});
|
||||
}
|
||||
ImageButton adjustButton = findViewById(getResourceIdentifier(context, "sb_new_segment_adjust", "id"));
|
||||
if (adjustButton == null) {
|
||||
LogHelper.printException(() -> "Could not find adjustButton");
|
||||
} else {
|
||||
setClickEffect(adjustButton);
|
||||
adjustButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Adjust button clicked");
|
||||
SponsorBlockUtils.onMarkLocationClicked();
|
||||
});
|
||||
}
|
||||
ImageButton compareButton = findViewById(getResourceIdentifier(context, "sb_new_segment_compare", "id"));
|
||||
if (compareButton == null) {
|
||||
LogHelper.printException(() -> "Could not find compareButton");
|
||||
} else {
|
||||
setClickEffect(compareButton);
|
||||
compareButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Compare button clicked");
|
||||
SponsorBlockUtils.onPreviewClicked();
|
||||
});
|
||||
}
|
||||
ImageButton editButton = findViewById(getResourceIdentifier(context, "sb_new_segment_edit", "id"));
|
||||
if (editButton == null) {
|
||||
LogHelper.printException(() -> "Could not find editButton");
|
||||
} else {
|
||||
setClickEffect(editButton);
|
||||
editButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Edit button clicked");
|
||||
SponsorBlockUtils.onEditByHandClicked();
|
||||
});
|
||||
}
|
||||
ImageButton publishButton = findViewById(getResourceIdentifier(context, "sb_new_segment_publish", "id"));
|
||||
if (publishButton == null) {
|
||||
LogHelper.printException(() -> "Could not find publishButton");
|
||||
} else {
|
||||
setClickEffect(publishButton);
|
||||
publishButton.setOnClickListener(v -> {
|
||||
LogHelper.printDebug(() -> "Publish button clicked");
|
||||
SponsorBlockUtils.onPublishClicked();
|
||||
});
|
||||
}
|
||||
|
||||
defaultBottomMargin = getResourceDimensionPixelSize("brand_interaction_default_bottom_margin");
|
||||
ctaBottomMargin = getResourceDimensionPixelSize("brand_interaction_cta_bottom_margin");
|
||||
}
|
||||
|
||||
private void setClickEffect(ImageButton btn) {
|
||||
btn.setBackgroundResource(rippleEffectId);
|
||||
|
||||
RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground();
|
||||
|
||||
int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}};
|
||||
int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white
|
||||
|
||||
ColorStateList colorStateList = new ColorStateList(states, colors);
|
||||
rippleDrawable.setColor(colorStateList);
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package app.revanced.integrations.sponsorblock.ui;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceColor;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimension;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class SkipSponsorButton extends FrameLayout {
|
||||
private static final boolean highContrast = true;
|
||||
private final LinearLayout skipSponsorBtnContainer;
|
||||
private final TextView skipSponsorTextView;
|
||||
private final Paint background;
|
||||
private final Paint border;
|
||||
private SponsorSegment segment;
|
||||
final int defaultBottomMargin;
|
||||
final int ctaBottomMargin;
|
||||
|
||||
public SkipSponsorButton(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet) {
|
||||
this(context, attributeSet, 0);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) {
|
||||
this(context, attributeSet, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||
|
||||
LayoutInflater.from(context).inflate(getResourceIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
|
||||
setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height
|
||||
skipSponsorBtnContainer = Objects.requireNonNull((LinearLayout) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
|
||||
background = new Paint();
|
||||
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);
|
||||
background.setStyle(Paint.Style.FILL);
|
||||
border = new Paint();
|
||||
border.setColor(getResourceColor("skip_ad_button_border_color")); // color:skip_ad_button_border_color);
|
||||
border.setStrokeWidth(getResourceDimension("ad_skip_ad_button_border_width")); // dimen:ad_skip_ad_button_border_width);
|
||||
border.setStyle(Paint.Style.STROKE);
|
||||
skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text;
|
||||
defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin
|
||||
ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin
|
||||
|
||||
skipSponsorBtnContainer.setOnClickListener(v -> {
|
||||
SegmentPlaybackController.onSkipSegmentClicked(segment);
|
||||
});
|
||||
}
|
||||
|
||||
@Override // android.view.ViewGroup
|
||||
protected final void dispatchDraw(Canvas canvas) {
|
||||
final int left = skipSponsorBtnContainer.getLeft();
|
||||
final int top = skipSponsorBtnContainer.getTop();
|
||||
final int leftPlusWidth = (left + skipSponsorBtnContainer.getWidth());
|
||||
final int topPlusHeight = (top + skipSponsorBtnContainer.getHeight());
|
||||
canvas.drawRect(left, top, leftPlusWidth, topPlusHeight, background);
|
||||
if (!highContrast) {
|
||||
canvas.drawLines(new float[]{
|
||||
leftPlusWidth, top, left, top,
|
||||
left, top, left, topPlusHeight,
|
||||
left, topPlusHeight, leftPlusWidth, topPlusHeight},
|
||||
border);
|
||||
}
|
||||
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true, if this button state was changed
|
||||
*/
|
||||
public boolean updateSkipButtonText(@NonNull SponsorSegment segment) {
|
||||
this.segment = segment;
|
||||
CharSequence newText = segment.getSkipButtonText();
|
||||
if (newText.equals(skipSponsorTextView.getText())) {
|
||||
return false;
|
||||
}
|
||||
skipSponsorTextView.setText(newText);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package app.revanced.integrations.sponsorblock.ui;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.shared.PlayerType;
|
||||
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
public class SponsorBlockViewController {
|
||||
private static WeakReference<RelativeLayout> inlineSponsorOverlayRef = new WeakReference<>(null);
|
||||
private static WeakReference<ViewGroup> youtubeOverlaysLayoutRef = new WeakReference<>(null);
|
||||
private static WeakReference<SkipSponsorButton> skipHighlightButtonRef = new WeakReference<>(null);
|
||||
private static WeakReference<SkipSponsorButton> skipSponsorButtonRef = new WeakReference<>(null);
|
||||
private static WeakReference<NewSegmentLayout> newSegmentLayoutRef = new WeakReference<>(null);
|
||||
private static boolean canShowViewElements;
|
||||
private static boolean newSegmentLayoutVisible;
|
||||
@Nullable
|
||||
private static SponsorSegment skipHighlight;
|
||||
@Nullable
|
||||
private static SponsorSegment skipSegment;
|
||||
|
||||
static {
|
||||
PlayerType.getOnChange().addObserver((PlayerType type) -> {
|
||||
playerTypeChanged(type);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static Context getOverLaysViewGroupContext() {
|
||||
ViewGroup group = youtubeOverlaysLayoutRef.get();
|
||||
if (group == null) {
|
||||
return null;
|
||||
}
|
||||
return group.getContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initialize(Object obj) {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "initializing");
|
||||
|
||||
Context context = ReVancedUtils.getContext();
|
||||
RelativeLayout layout = new RelativeLayout(context);
|
||||
layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
|
||||
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
|
||||
public void onChildViewAdded(View parent, View child) {
|
||||
// ensure SB buttons and controls are always on top, otherwise the endscreen cards can cover the skip button
|
||||
RelativeLayout layout = inlineSponsorOverlayRef.get();
|
||||
if (layout != null) {
|
||||
layout.bringToFront();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onChildViewRemoved(View parent, View child) {
|
||||
}
|
||||
});
|
||||
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
|
||||
|
||||
skipHighlightButtonRef = new WeakReference<>(
|
||||
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_highlight_button", "id"))));
|
||||
skipSponsorButtonRef = new WeakReference<>(
|
||||
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_sponsor_button", "id"))));
|
||||
newSegmentLayoutRef = new WeakReference<>(
|
||||
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_new_segment_view", "id"))));
|
||||
|
||||
newSegmentLayoutVisible = false;
|
||||
skipHighlight = null;
|
||||
skipSegment = null;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void hideAll() {
|
||||
hideSkipHighlightButton();
|
||||
hideSkipSegmentButton();
|
||||
hideNewSegmentLayout();
|
||||
}
|
||||
|
||||
public static void showSkipHighlightButton(@NonNull SponsorSegment segment) {
|
||||
skipHighlight = Objects.requireNonNull(segment);
|
||||
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
|
||||
// don't show highlight button if create new segment is visible
|
||||
final boolean buttonVisibility = newSegmentLayout != null && newSegmentLayout.getVisibility() != View.VISIBLE;
|
||||
updateSkipButton(skipHighlightButtonRef.get(), segment, buttonVisibility);
|
||||
}
|
||||
public static void showSkipSegmentButton(@NonNull SponsorSegment segment) {
|
||||
skipSegment = Objects.requireNonNull(segment);
|
||||
updateSkipButton(skipSponsorButtonRef.get(), segment, true);
|
||||
}
|
||||
|
||||
public static void hideSkipHighlightButton() {
|
||||
skipHighlight = null;
|
||||
updateSkipButton(skipHighlightButtonRef.get(), null, false);
|
||||
}
|
||||
public static void hideSkipSegmentButton() {
|
||||
skipSegment = null;
|
||||
updateSkipButton(skipSponsorButtonRef.get(), null, false);
|
||||
}
|
||||
|
||||
private static void updateSkipButton(@Nullable SkipSponsorButton button,
|
||||
@Nullable SponsorSegment segment, boolean visible) {
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
if (segment != null) {
|
||||
button.updateSkipButtonText(segment);
|
||||
}
|
||||
setViewVisibility(button, visible);
|
||||
}
|
||||
|
||||
public static void toggleNewSegmentLayoutVisibility() {
|
||||
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
|
||||
if (newSegmentLayout == null) { // should never happen
|
||||
LogHelper.printException(() -> "toggleNewSegmentLayoutVisibility failure");
|
||||
return;
|
||||
}
|
||||
newSegmentLayoutVisible = (newSegmentLayout.getVisibility() != View.VISIBLE);
|
||||
if (skipHighlight != null) {
|
||||
setViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible);
|
||||
}
|
||||
setViewVisibility(newSegmentLayout, newSegmentLayoutVisible);
|
||||
}
|
||||
|
||||
public static void hideNewSegmentLayout() {
|
||||
newSegmentLayoutVisible = false;
|
||||
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
|
||||
if (newSegmentLayout == null) {
|
||||
return;
|
||||
}
|
||||
setViewVisibility(newSegmentLayout, false);
|
||||
}
|
||||
|
||||
private static void setViewVisibility(@Nullable View view, boolean visible) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
visible &= canShowViewElements;
|
||||
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
|
||||
if (view.getVisibility() != desiredVisibility) {
|
||||
view.setVisibility(desiredVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
private static void playerTypeChanged(@NonNull PlayerType playerType) {
|
||||
try {
|
||||
final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN;
|
||||
canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED);
|
||||
|
||||
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
|
||||
setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen);
|
||||
setViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible);
|
||||
|
||||
SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get();
|
||||
setSkipButtonMargins(skipHighlightButton, isWatchFullScreen);
|
||||
setViewVisibility(skipHighlightButton, skipHighlight != null);
|
||||
|
||||
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
|
||||
setSkipButtonMargins(skipSponsorButton, isWatchFullScreen);
|
||||
setViewVisibility(skipSponsorButton, skipSegment != null);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Player type changed failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNewSegmentLayoutMargins(@Nullable NewSegmentLayout layout, boolean fullScreen) {
|
||||
if (layout != null) {
|
||||
setLayoutMargins(layout, fullScreen, layout.defaultBottomMargin, layout.ctaBottomMargin);
|
||||
}
|
||||
}
|
||||
private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) {
|
||||
if (button != null) {
|
||||
setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin);
|
||||
}
|
||||
}
|
||||
private static void setLayoutMargins(@NonNull View view, boolean fullScreen,
|
||||
int defaultBottomMargin, int ctaBottomMargin) {
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams();
|
||||
if (params == null) {
|
||||
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)");
|
||||
return;
|
||||
}
|
||||
params.bottomMargin = fullScreen ? ctaBottomMargin : defaultBottomMargin;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void endOfVideoReached() {
|
||||
try {
|
||||
LogHelper.printDebug(() -> "endOfVideoReached");
|
||||
// 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()) {
|
||||
CreateSegmentButtonController.hide();
|
||||
VotingButtonController.hide();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "endOfVideoReached failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
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 app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
|
||||
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
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) {
|
||||
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.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);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void changeVisibilityImmediate(boolean visible) {
|
||||
changeVisibility(visible, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
*/
|
||||
public static void changeVisibilityNegatedImmediate(boolean visible) {
|
||||
changeVisibility(!visible, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
*/
|
||||
public static void changeVisibility(boolean visible) {
|
||||
changeVisibility(visible, false);
|
||||
}
|
||||
|
||||
public static void changeVisibility(boolean visible, boolean immediate) {
|
||||
try {
|
||||
if (isShowing == visible) return;
|
||||
isShowing = visible;
|
||||
|
||||
ImageView iView = buttonReference.get();
|
||||
if (iView == null) return;
|
||||
|
||||
if (visible) {
|
||||
iView.clearAnimation();
|
||||
if (!shouldBeShown()) {
|
||||
return;
|
||||
}
|
||||
if (!immediate) {
|
||||
iView.startAnimation(fadeIn);
|
||||
}
|
||||
iView.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (iView.getVisibility() == View.VISIBLE) {
|
||||
iView.clearAnimation();
|
||||
if (!immediate) {
|
||||
iView.startAnimation(fadeOut);
|
||||
}
|
||||
iView.setVisibility(View.GONE);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "changeVisibility failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldBeShown() {
|
||||
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean()
|
||||
&& SegmentPlaybackController.currentVideoHasSegments() && !VideoInformation.isAtEndOfVideo();
|
||||
}
|
||||
|
||||
public static void hide() {
|
||||
if (!isShowing) {
|
||||
return;
|
||||
}
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
View v = buttonReference.get();
|
||||
if (v == null) {
|
||||
LogHelper.printDebug(() -> "Cannot hide voting button (value is null)");
|
||||
return;
|
||||
}
|
||||
v.setVisibility(View.GONE);
|
||||
isShowing = false;
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ import app.revanced.integrations.swipecontrols.controller.gesture.PressToSwipeCo
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.core.GestureController
|
||||
import app.revanced.integrations.swipecontrols.misc.Rectangle
|
||||
import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout
|
||||
import app.revanced.integrations.utils.LogHelper.printDebug
|
||||
import app.revanced.integrations.utils.LogHelper.printException
|
||||
import app.revanced.integrations.utils.LogHelper.printInfo
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
@ -121,7 +121,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
*/
|
||||
private fun initialize() {
|
||||
// create controllers
|
||||
printInfo { "initializing swipe controls controllers" }
|
||||
printDebug { "initializing swipe controls controllers" }
|
||||
config = SwipeControlsConfigurationProvider(this)
|
||||
keys = VolumeKeysController(this)
|
||||
audio = createAudioController()
|
||||
@ -157,7 +157,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
* (re) attaches swipe overlays
|
||||
*/
|
||||
private fun reAttachOverlays() {
|
||||
printInfo { "attaching swipe controls overlay" }
|
||||
printDebug{ "attaching swipe controls overlay" }
|
||||
contentRoot.removeView(overlay)
|
||||
contentRoot.addView(overlay)
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ class SwipeZonesController(
|
||||
/**
|
||||
* id for R.id.player_view
|
||||
*/
|
||||
private val playerViewId = ReVancedUtils.getResourceIdByName(host, "id", "player_view")
|
||||
private val playerViewId = ReVancedUtils.getResourceIdentifier(host, "player_view", "id")
|
||||
|
||||
/**
|
||||
* current bounding rectangle of the player
|
||||
|
@ -39,7 +39,7 @@ class SwipeControlsOverlayLayout(
|
||||
|
||||
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
|
||||
return resources.getDrawable(
|
||||
ReVancedUtils.getResourceIdByName(context, "drawable", name),
|
||||
ReVancedUtils.getResourceIdentifier(context, name, "drawable"),
|
||||
context.theme
|
||||
).apply {
|
||||
setTint(config.overlayForegroundColor)
|
||||
|
@ -7,10 +7,18 @@ class Event<T> {
|
||||
private val eventListeners = mutableSetOf<(T) -> Unit>()
|
||||
|
||||
operator fun plusAssign(observer: (T) -> Unit) {
|
||||
addObserver(observer)
|
||||
}
|
||||
|
||||
fun addObserver(observer: (T) -> Unit) {
|
||||
eventListeners.add(observer)
|
||||
}
|
||||
|
||||
operator fun minusAssign(observer: (T) -> Unit) {
|
||||
removeObserver(observer)
|
||||
}
|
||||
|
||||
fun removeObserver(observer: (T) -> Unit) {
|
||||
eventListeners.remove(observer)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -131,12 +129,7 @@ public class LogHelper {
|
||||
String toastMessageToDisplay = (userToastMessage != null)
|
||||
? userToastMessage
|
||||
: outerClassSimpleName + ": " + messageString;
|
||||
ReVancedUtils.runOnMainThread(() -> {
|
||||
Context context = ReVancedUtils.getContext();
|
||||
if (context != null) {
|
||||
Toast.makeText(context, toastMessageToDisplay, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
ReVancedUtils.showToastLong(toastMessageToDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,134 +3,119 @@ package app.revanced.integrations.utils;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.Bidi;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
||||
|
||||
public class ReVancedUtils {
|
||||
|
||||
private static PlayerType env;
|
||||
private static boolean newVideo = false;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static Context context;
|
||||
|
||||
private ReVancedUtils() {
|
||||
} // utility class
|
||||
|
||||
/**
|
||||
* Maximum number of background threads run concurrently
|
||||
*/
|
||||
private static final int SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS = 20;
|
||||
|
||||
/**
|
||||
* General purpose pool for network calls and other background tasks.
|
||||
* All tasks run at max thread priority.
|
||||
*/
|
||||
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
||||
2, // minimum 2 threads always ready to be used
|
||||
2, // 2 threads always ready to go
|
||||
Integer.MAX_VALUE,
|
||||
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
||||
SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<Runnable>(),
|
||||
new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r);
|
||||
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
|
||||
return t;
|
||||
}
|
||||
new SynchronousQueue<>(),
|
||||
r -> { // ThreadFactory
|
||||
Thread t = new Thread(r);
|
||||
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
|
||||
return t;
|
||||
});
|
||||
|
||||
private static void checkIfPoolHasReachedLimit() {
|
||||
if (backgroundThreadPool.getActiveCount() >= SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS) {
|
||||
// Something is wrong. Background threads are piling up and not completing as expected,
|
||||
// or some ReVanced code is submitting an unexpected number of background tasks.
|
||||
LogHelper.printException(() -> "Reached maximum background thread count of "
|
||||
+ SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS + " threads");
|
||||
}
|
||||
}
|
||||
|
||||
public static void runOnBackgroundThread(Runnable task) {
|
||||
public static void runOnBackgroundThread(@NonNull Runnable task) {
|
||||
backgroundThreadPool.execute(task);
|
||||
checkIfPoolHasReachedLimit();
|
||||
}
|
||||
|
||||
public static <T> Future<T> submitOnBackgroundThread(Callable<T> call) {
|
||||
Future<T> future = backgroundThreadPool.submit(call);
|
||||
checkIfPoolHasReachedLimit();
|
||||
return future;
|
||||
@NonNull
|
||||
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
|
||||
return backgroundThreadPool.submit(call);
|
||||
}
|
||||
|
||||
public static boolean containsAny(final String value, final String... targets) {
|
||||
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
||||
for (String string : targets)
|
||||
if (!string.isEmpty() && value.contains(string)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setNewVideo(boolean started) {
|
||||
LogHelper.printDebug(() -> "New video started: " + started);
|
||||
newVideo = started;
|
||||
/**
|
||||
* @return zero, if the resource is not found
|
||||
*/
|
||||
@SuppressLint("DiscouragedApi")
|
||||
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
|
||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
||||
}
|
||||
|
||||
public static boolean isNewVideoStarted() {
|
||||
return newVideo;
|
||||
/**
|
||||
* @return zero, if the resource is not found
|
||||
*/
|
||||
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
|
||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||
}
|
||||
|
||||
public static Integer getResourceIdByName(Context context, String type, String name) {
|
||||
try {
|
||||
Resources res = context.getResources();
|
||||
return res.getIdentifier(name, type, context.getPackageName());
|
||||
} catch (Throwable exception) {
|
||||
LogHelper.printException(() -> "Resource not found.", exception);
|
||||
return null;
|
||||
}
|
||||
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
||||
}
|
||||
|
||||
public static void setPlayerType(PlayerType type) {
|
||||
env = type;
|
||||
@NonNull
|
||||
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
||||
}
|
||||
|
||||
public static PlayerType getPlayerType() {
|
||||
return env;
|
||||
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
||||
}
|
||||
|
||||
public static int getIdentifier(String name, String defType) {
|
||||
Context context = getContext();
|
||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public static Context getContext() {
|
||||
if (context != null) {
|
||||
return context;
|
||||
} else {
|
||||
LogHelper.printException(() -> "Context is null, returning null!");
|
||||
return null;
|
||||
}
|
||||
LogHelper.printException(() -> "Context is null, returning null!");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void setClipboard(String text) {
|
||||
public static void setClipboard(@NonNull String text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
|
||||
public static boolean isTablet(Context context) {
|
||||
public static boolean isTablet() {
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Boolean isRightToLeftTextLayout;
|
||||
/**
|
||||
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
||||
@ -144,16 +129,46 @@ public class ReVancedUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws
|
||||
* Safe to call from any thread
|
||||
*/
|
||||
public static void runOnMainThread(Runnable runnable) {
|
||||
public static void showToastShort(@NonNull String messageToToast) {
|
||||
showToast(messageToToast, Toast.LENGTH_SHORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe to call from any thread
|
||||
*/
|
||||
public static void showToastLong(@NonNull String messageToToast) {
|
||||
showToast(messageToToast, Toast.LENGTH_LONG);
|
||||
}
|
||||
|
||||
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
||||
Objects.requireNonNull(messageToToast);
|
||||
runOnMainThreadNowOrLater(() -> {
|
||||
// cannot use getContext(), otherwise if context is null it will cause infinite recursion of error logging
|
||||
if (context == null) {
|
||||
LogHelper.printDebug(() -> "Cannot show toast (context is null)");
|
||||
} else {
|
||||
LogHelper.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws.
|
||||
*
|
||||
* @see #runOnMainThreadNowOrLater(Runnable)
|
||||
*/
|
||||
public static void runOnMainThread(@NonNull Runnable runnable) {
|
||||
runOnMainThreadDelayed(runnable, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws
|
||||
*/
|
||||
public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) {
|
||||
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
||||
Runnable loggingRunnable = () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
@ -164,10 +179,22 @@ public class ReVancedUtils {
|
||||
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* If called from the main thread, the code is run immediately.<p>
|
||||
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
|
||||
*/
|
||||
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
|
||||
if (isCurrentlyOnMainThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
runOnMainThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the calling thread is on the main thread
|
||||
*/
|
||||
public static boolean currentlyIsOnMainThread() {
|
||||
public static boolean isCurrentlyOnMainThread() {
|
||||
return Looper.getMainLooper().isCurrentThread();
|
||||
}
|
||||
|
||||
@ -175,7 +202,7 @@ public class ReVancedUtils {
|
||||
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
||||
*/
|
||||
public static void verifyOnMainThread() throws IllegalStateException {
|
||||
if (!currentlyIsOnMainThread()) {
|
||||
if (!isCurrentlyOnMainThread()) {
|
||||
throw new IllegalStateException("Must call _on_ the main thread");
|
||||
}
|
||||
}
|
||||
@ -184,8 +211,37 @@ public class ReVancedUtils {
|
||||
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
||||
*/
|
||||
public static void verifyOffMainThread() throws IllegalStateException {
|
||||
if (currentlyIsOnMainThread()) {
|
||||
if (isCurrentlyOnMainThread()) {
|
||||
throw new IllegalStateException("Must call _off_ the main thread");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isNetworkConnected() {
|
||||
NetworkType networkType = getNetworkType();
|
||||
return networkType == NetworkType.MOBILE
|
||||
|| networkType == NetworkType.OTHER;
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // permission already included in YouTube
|
||||
public static NetworkType getNetworkType() {
|
||||
Context networkContext = getContext();
|
||||
if (networkContext == null) {
|
||||
return NetworkType.NONE;
|
||||
}
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
var networkInfo = cm.getActiveNetworkInfo();
|
||||
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
return NetworkType.NONE;
|
||||
}
|
||||
var type = networkInfo.getType();
|
||||
return (type == ConnectivityManager.TYPE_MOBILE)
|
||||
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
|
||||
}
|
||||
|
||||
public enum NetworkType {
|
||||
NONE,
|
||||
MOBILE,
|
||||
OTHER,
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class SharedPrefHelper {
|
||||
public static void saveString(SharedPrefNames prefName, String key, String value) {
|
||||
getPreferences(prefName).edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
public static void saveBoolean(SharedPrefNames prefName, String key, boolean value) {
|
||||
getPreferences(prefName).edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
public static void saveFloat(SharedPrefNames prefName, String key, float value) {
|
||||
getPreferences(prefName).edit().putFloat(key, value).apply();
|
||||
}
|
||||
|
||||
public static void saveInt(SharedPrefNames prefName, String key, int value) {
|
||||
getPreferences(prefName).edit().putInt(key, value).apply();
|
||||
}
|
||||
|
||||
public static void saveLong(SharedPrefNames prefName, String key, long value) {
|
||||
getPreferences(prefName).edit().putLong(key, value).apply();
|
||||
}
|
||||
|
||||
public static String getString(SharedPrefNames prefName, String key, String _default) {
|
||||
return getPreferences(prefName).getString(key, _default);
|
||||
}
|
||||
|
||||
public static boolean getBoolean(SharedPrefNames prefName, String key, boolean _default) {
|
||||
return getPreferences(prefName).getBoolean(key, _default);
|
||||
}
|
||||
|
||||
// region Hack, unknown why required
|
||||
|
||||
public static Long getLong(SharedPrefNames prefName, String key, Long _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(prefName);
|
||||
try {
|
||||
return Long.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getLong(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public static Float getFloat(SharedPrefNames prefName, String key, Float _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(prefName);
|
||||
try {
|
||||
return Float.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getFloat(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public static Integer getInt(SharedPrefNames prefName, String key, Integer _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(prefName);
|
||||
try {
|
||||
return Integer.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getInt(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
public static SharedPreferences getPreferences(SharedPrefNames name) {
|
||||
return Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(name.getName(), Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public enum SharedPrefNames {
|
||||
|
||||
YOUTUBE("youtube"),
|
||||
RYD("ryd"),
|
||||
SPONSOR_BLOCK("sponsor-block"),
|
||||
REVANCED_PREFS("revanced_prefs");
|
||||
|
||||
private final String name;
|
||||
|
||||
SharedPrefNames(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.integrations.sponsorblock;
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@ -9,26 +9,22 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
// should probably move this class into utils package
|
||||
public class StringRef {
|
||||
private static Resources resources;
|
||||
private static String packageName;
|
||||
|
||||
// must use a thread safe map, as this class is used both on and off the main thread
|
||||
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap());
|
||||
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Gets strings reference from shared collection or creates if not exists yet,
|
||||
* this method should be called if you want to get StringRef
|
||||
* Returns a cached instance.
|
||||
* Should be used if the same String could be loaded more than once.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @return String reference that'll resolve to excepted string, may be from cache
|
||||
* @see #sf(String)
|
||||
*/
|
||||
@NonNull
|
||||
public static StringRef sf(@NonNull String id) {
|
||||
public static StringRef sfc(@NonNull String id) {
|
||||
StringRef ref = strings.get(id);
|
||||
if (ref == null) {
|
||||
ref = new StringRef(id);
|
||||
@ -38,18 +34,30 @@ public class StringRef {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string value by string id, shorthand for <code>sf(id).toString()</code>
|
||||
* Creates a new instance, but does not cache the value.
|
||||
* Should be used for Strings that are loaded exactly once.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @see #sfc(String)
|
||||
*/
|
||||
@NonNull
|
||||
public static StringRef sf(@NonNull String id) {
|
||||
return new StringRef(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code>
|
||||
*
|
||||
* @param id string resource name/id
|
||||
* @return String value from string.xml
|
||||
*/
|
||||
@NonNull
|
||||
public static String str(@NonNull String id) {
|
||||
return sf(id).toString();
|
||||
return sfc(id).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string value by string id, shorthand for <code>sf(id).toString()</code> and formats the string
|
||||
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code> and formats the string
|
||||
* with given args.
|
||||
*
|
||||
* @param id string resource name/id
|
||||
@ -61,7 +69,6 @@ public class StringRef {
|
||||
return String.format(str(id), args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a StringRef object that'll not change it's value
|
||||
*
|
@ -4,13 +4,18 @@ public class ThemeHelper {
|
||||
private static int themeValue;
|
||||
|
||||
public static void setTheme(int value) {
|
||||
themeValue = value;
|
||||
LogHelper.printDebug(() -> "Theme value: " + themeValue);
|
||||
if (themeValue != value) {
|
||||
themeValue = value;
|
||||
LogHelper.printDebug(() -> "Theme value: " + themeValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTheme(Object value) {
|
||||
themeValue = ((Enum) value).ordinal();
|
||||
LogHelper.printDebug(() -> "Theme value: " + themeValue);
|
||||
final int newOrdinalValue = ((Enum) value).ordinal();
|
||||
if (themeValue != newOrdinalValue) {
|
||||
themeValue = newOrdinalValue;
|
||||
LogHelper.printDebug(() -> "Theme value: " + themeValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
|
@ -3,7 +3,6 @@ package app.revanced.integrations.videoplayer;
|
||||
import android.support.constraint.ConstraintLayout;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@ -26,22 +25,19 @@ public abstract class BottomControlButton {
|
||||
constraintLayout = (ConstraintLayout) obj;
|
||||
isButtonEnabled = isEnabled;
|
||||
|
||||
ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getIdentifier(viewId, "id"));
|
||||
ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getResourceIdentifier(viewId, "id"));
|
||||
if (imageView == null) {
|
||||
LogHelper.printDebug(() -> "Couldn't find ImageView with id: " + viewId);
|
||||
LogHelper.printException(() -> "Couldn't find ImageView with id: " + viewId);
|
||||
return;
|
||||
}
|
||||
|
||||
imageView.setOnClickListener(onClickListener);
|
||||
|
||||
button = new WeakReference<>(imageView);
|
||||
fadeIn = getAnimation("fade_in");
|
||||
fadeOut = getAnimation("fade_out");
|
||||
|
||||
int fadeDurationFast = getInteger("fade_duration_fast");
|
||||
int fadeDurationScheduled = getInteger("fade_duration_scheduled");
|
||||
fadeIn.setDuration(fadeDurationFast);
|
||||
fadeOut.setDuration(fadeDurationScheduled);
|
||||
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
|
||||
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
|
||||
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
|
||||
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
|
||||
|
||||
isShowing = true;
|
||||
setVisibility(false);
|
||||
} catch (Exception e) {
|
||||
@ -69,11 +65,4 @@ public abstract class BottomControlButton {
|
||||
imageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
private static int getInteger(String str) {
|
||||
return ReVancedUtils.getContext().getResources().getInteger(ReVancedUtils.getIdentifier(str, "integer"));
|
||||
}
|
||||
|
||||
private static Animation getAnimation(String str) {
|
||||
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), ReVancedUtils.getIdentifier(str, "anim"));
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ package app.revanced.integrations.videoplayer;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.integrations.patches.VideoInformation;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.sponsorblock.StringRef;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.StringRef;
|
||||
|
||||
public class DownloadButton extends BottomControlButton {
|
||||
public static DownloadButton instance;
|
||||
@ -46,7 +46,7 @@ public class DownloadButton extends BottomControlButton {
|
||||
|
||||
// If the package is not installed, show the toast
|
||||
if (!packageEnabled) {
|
||||
Toast.makeText(context, downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning"), Toast.LENGTH_LONG).show();
|
||||
ReVancedUtils.showToastLong(downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
package app.revanced.tiktok.settings;
|
||||
|
||||
public enum ReturnType {
|
||||
BOOLEAN, INTEGER, STRING, LONG, FLOAT
|
||||
}
|
@ -1,104 +1,112 @@
|
||||
package app.revanced.tiktok.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.tiktok.settings.SettingsEnum.ReturnType.BOOLEAN;
|
||||
import static app.revanced.tiktok.settings.SettingsEnum.ReturnType.STRING;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.tiktok.utils.LogHelper;
|
||||
import app.revanced.tiktok.utils.ReVancedUtils;
|
||||
import app.revanced.tiktok.utils.SharedPrefHelper;
|
||||
|
||||
public enum SettingsEnum {
|
||||
//TikTok Settings
|
||||
TIK_REMOVE_ADS("tik_remove_ads", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true),
|
||||
TIK_HIDE_LIVE("tik_hide_live", false, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true),
|
||||
TIK_DOWN_PATH("tik_down_path", "DCIM/TikTok", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING),
|
||||
TIK_DOWN_WATERMARK("tik_down_watermark", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN),
|
||||
TIK_SIMSPOOF("tik_simspoof", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true),
|
||||
TIK_SIMSPOOF_ISO("tik_simspoof_iso", "us", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING),
|
||||
TIK_SIMSPOOF_MCCMNC("tik_simspoof_mccmnc", "310160", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING),
|
||||
TIK_SIMSPOOF_OP_NAME("tik_simspoof_op_name", "T-Mobile", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING),
|
||||
TIK_DEBUG("tik_debug", false, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN);
|
||||
TIK_DEBUG("tik_debug", BOOLEAN, FALSE), // must be first value, otherwise logging during loading will not work
|
||||
TIK_REMOVE_ADS("tik_remove_ads", BOOLEAN, TRUE, true),
|
||||
TIK_HIDE_LIVE("tik_hide_live", BOOLEAN, FALSE, true),
|
||||
TIK_DOWN_PATH("tik_down_path", STRING, "DCIM/TikTok"),
|
||||
TIK_DOWN_WATERMARK("tik_down_watermark", BOOLEAN, TRUE),
|
||||
TIK_SIMSPOOF("tik_simspoof", BOOLEAN, TRUE, true),
|
||||
TIK_SIMSPOOF_ISO("tik_simspoof_iso", STRING, "us"),
|
||||
TIK_SIMSPOOF_MCCMNC("tik_simspoof_mccmnc", STRING, "310160"),
|
||||
TIK_SIMSPOOF_OP_NAME("tik_simspoof_op_name", STRING, "T-Mobile");
|
||||
|
||||
static {
|
||||
load();
|
||||
loadAllSettings();
|
||||
}
|
||||
|
||||
private final String path;
|
||||
private final Object defaultValue;
|
||||
private final SharedPrefHelper.SharedPrefNames sharedPref;
|
||||
private final ReturnType returnType;
|
||||
private final boolean rebootApp;
|
||||
private Object value = null;
|
||||
@NonNull
|
||||
public final String path;
|
||||
@NonNull
|
||||
public final Object defaultValue;
|
||||
@NonNull
|
||||
public final SharedPrefCategory sharedPref;
|
||||
@NonNull
|
||||
public final ReturnType returnType;
|
||||
/**
|
||||
* If the app should be rebooted, if this setting is changed
|
||||
*/
|
||||
public final boolean rebootApp;
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
||||
private Object value;
|
||||
|
||||
SettingsEnum(String path, ReturnType returnType, Object defaultValue) {
|
||||
this(path, returnType, defaultValue, SharedPrefCategory.TIKTOK_PREFS, false);
|
||||
}
|
||||
SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp) {
|
||||
this(path, returnType, defaultValue, SharedPrefCategory.TIKTOK_PREFS, rebootApp);
|
||||
}
|
||||
SettingsEnum(@NonNull String path, @NonNull ReturnType returnType, @NonNull Object defaultValue,
|
||||
@NonNull SharedPrefCategory prefName, boolean rebootApp) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.sharedPref = SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = false;
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.sharedPref = prefName;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = false;
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType, Boolean rebootApp) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.sharedPref = prefName;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = rebootApp;
|
||||
}
|
||||
|
||||
private static void load() {
|
||||
Context context = ReVancedUtils.getAppContext();
|
||||
if (context == null) {
|
||||
Log.e("revanced: SettingsEnum", "Context returned null! Setings NOT initialized");
|
||||
} else {
|
||||
try {
|
||||
for (SettingsEnum setting : values()) {
|
||||
Object value = setting.getDefaultValue();
|
||||
|
||||
//LogHelper is not initialized here
|
||||
Log.d("revanced: SettingsEnum", "Loading Setting: " + setting.name());
|
||||
|
||||
switch (setting.getReturnType()) {
|
||||
case FLOAT:
|
||||
value = SharedPrefHelper.getFloat(context, setting.sharedPref, setting.getPath(), (float) setting.getDefaultValue());
|
||||
break;
|
||||
case LONG:
|
||||
value = SharedPrefHelper.getLong(context, setting.sharedPref, setting.getPath(), (long) setting.getDefaultValue());
|
||||
break;
|
||||
case BOOLEAN:
|
||||
value = SharedPrefHelper.getBoolean(context, setting.sharedPref, setting.getPath(), (boolean) setting.getDefaultValue());
|
||||
break;
|
||||
case INTEGER:
|
||||
value = SharedPrefHelper.getInt(context, setting.sharedPref, setting.getPath(), (int) setting.getDefaultValue());
|
||||
break;
|
||||
case STRING:
|
||||
value = SharedPrefHelper.getString(context, setting.sharedPref, setting.getPath(), (String) setting.getDefaultValue());
|
||||
break;
|
||||
default:
|
||||
LogHelper.printException(SettingsEnum.class, "Setting does not have a valid Type. Name is: " + setting.name());
|
||||
break;
|
||||
}
|
||||
setting.setValue(value);
|
||||
|
||||
//LogHelper is not initialized here
|
||||
Log.d("revanced: SettingsEnum", "Loaded Setting: " + setting.name() + " Value: " + value);
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
LogHelper.printException(SettingsEnum.class, "Error during load()!", th);
|
||||
private static void loadAllSettings() {
|
||||
try {
|
||||
Context context = ReVancedUtils.getAppContext();
|
||||
if (context == null) {
|
||||
Log.e("revanced: SettingsEnum", "Context returned null! Settings NOT initialized");
|
||||
return;
|
||||
}
|
||||
for (SettingsEnum setting : values()) {
|
||||
setting.load(context);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(SettingsEnum.class, "Error during load()!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setValue(Object newValue) {
|
||||
this.value = newValue;
|
||||
private void load(Context context) {
|
||||
switch (returnType) {
|
||||
case BOOLEAN:
|
||||
value = sharedPref.getBoolean(context, path, (boolean) defaultValue);
|
||||
break;
|
||||
case INTEGER:
|
||||
value = sharedPref.getInt(context, path, (Integer) defaultValue);
|
||||
break;
|
||||
case LONG:
|
||||
value = sharedPref.getLong(context, path, (Long) defaultValue);
|
||||
break;
|
||||
case FLOAT:
|
||||
value = sharedPref.getFloat(context, path, (Float) defaultValue);
|
||||
break;
|
||||
case STRING:
|
||||
value = sharedPref.getString(context, path, (String) defaultValue);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(name());
|
||||
}
|
||||
|
||||
LogHelper.debug(SettingsEnum.class, "Loaded Setting: " + name() + " Value: " + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, but does _not_ persistently save the value.
|
||||
*
|
||||
* This intentionally is a static method, to deter accidental usage
|
||||
* when {@link #saveValue(Object)} was intended.
|
||||
*/
|
||||
public static void setValue(SettingsEnum setting, Object newValue) {
|
||||
// FIXME: this should validate the parameter matches the return type
|
||||
setting.value = newValue;
|
||||
}
|
||||
|
||||
public void saveValue(Object newValue) {
|
||||
@ -108,48 +116,39 @@ public enum SettingsEnum {
|
||||
return;
|
||||
}
|
||||
|
||||
if (returnType == ReturnType.BOOLEAN) {
|
||||
SharedPrefHelper.saveBoolean(context, sharedPref, path, (Boolean) newValue);
|
||||
if (returnType == BOOLEAN) {
|
||||
sharedPref.saveBoolean(context, path, (Boolean) newValue);
|
||||
} else {
|
||||
SharedPrefHelper.saveString(context, sharedPref, path, newValue + "");
|
||||
sharedPref.saveString(context, path, newValue.toString());
|
||||
}
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public boolean getBoolean() {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
public Long getLong() {
|
||||
public int getInt() {
|
||||
return (Integer) value;
|
||||
}
|
||||
|
||||
public long getLong() {
|
||||
return (Long) value;
|
||||
}
|
||||
|
||||
public Float getFloat() {
|
||||
public float getFloat() {
|
||||
return (Float) value;
|
||||
}
|
||||
|
||||
public Object getDefaultValue() {
|
||||
return defaultValue;
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
public enum ReturnType {
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
STRING,
|
||||
}
|
||||
|
||||
public ReturnType getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
public boolean shouldRebootOnChange() {
|
||||
return rebootApp;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
package app.revanced.tiktok.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public enum SharedPrefCategory {
|
||||
TIKTOK_PREFS("tiktok_revanced");
|
||||
|
||||
@NonNull
|
||||
public final String prefName;
|
||||
|
||||
SharedPrefCategory(@NonNull String prefName) {
|
||||
this.prefName = prefName;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return prefName;
|
||||
}
|
||||
|
||||
private SharedPreferences getPreferences(Context context) {
|
||||
return context.getSharedPreferences(prefName, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void saveBoolean(Context context, String key, boolean value) {
|
||||
getPreferences(context).edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
public void saveString(Context context, String key, String value) {
|
||||
getPreferences(context).edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
public boolean getBoolean(Context context, String key, boolean _default) {
|
||||
return getPreferences(context).getBoolean(key, _default);
|
||||
}
|
||||
|
||||
public Integer getInt(Context context, String key, Integer _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context);
|
||||
try {
|
||||
return Integer.valueOf(sharedPreferences.getString(key, _default.toString()));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getInt(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getLong(Context context, String key, Long _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context);
|
||||
try {
|
||||
return Long.valueOf(sharedPreferences.getString(key, _default.toString()));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getLong(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public Float getFloat(Context context, String key, Float _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context);
|
||||
try {
|
||||
return Float.valueOf(sharedPreferences.getString(key, _default.toString()));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getFloat(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public String getString(Context context, String key, String _default) {
|
||||
return getPreferences(context).getString(key, _default);
|
||||
}
|
||||
|
||||
}
|
@ -21,9 +21,9 @@ import androidx.annotation.Nullable;
|
||||
import com.ss.android.ugc.aweme.splash.SplashActivity;
|
||||
|
||||
import app.revanced.tiktok.settings.SettingsEnum;
|
||||
import app.revanced.tiktok.settings.SharedPrefCategory;
|
||||
import app.revanced.tiktok.settingsmenu.preference.DownloadPathPreference;
|
||||
import app.revanced.tiktok.utils.ReVancedUtils;
|
||||
import app.revanced.tiktok.utils.SharedPrefHelper;
|
||||
|
||||
public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
|
||||
@ -32,9 +32,9 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
|
||||
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
if (!setting.getPath().equals(str)) continue;
|
||||
if (!setting.path.equals(str)) continue;
|
||||
|
||||
if (ReVancedUtils.getAppContext() != null && this.settingsInitialized && setting.shouldRebootOnChange()) {
|
||||
if (ReVancedUtils.getAppContext() != null && this.settingsInitialized && setting.rebootApp) {
|
||||
rebootDialog(getActivity());
|
||||
}
|
||||
}
|
||||
@ -43,7 +43,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS.getName());
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefCategory.TIKTOK_PREFS.prefName);
|
||||
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this.listener);
|
||||
this.Registered = true;
|
||||
|
||||
@ -61,12 +61,14 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
feedFilter.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_REMOVE_ADS.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_REMOVE_ADS.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_REMOVE_ADS.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_REMOVE_ADS.defaultValue);
|
||||
preference.setChecked(SettingsEnum.TIK_REMOVE_ADS.getBoolean());
|
||||
preference.setTitle("Remove feed ads");
|
||||
preference.setSummary("Remove ads from feed.");
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
// FIXME: the value is already saved in the preferences.
|
||||
// instead of saving again, simple call SettingsEnum#setValue()
|
||||
final boolean value = (Boolean) newValue;
|
||||
SettingsEnum.TIK_REMOVE_ADS.saveValue(value);
|
||||
return true;
|
||||
@ -76,8 +78,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
feedFilter.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_HIDE_LIVE.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_HIDE_LIVE.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_HIDE_LIVE.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_HIDE_LIVE.defaultValue);
|
||||
preference.setChecked(SettingsEnum.TIK_HIDE_LIVE.getBoolean());
|
||||
preference.setTitle("Hide livestreams");
|
||||
preference.setSummary("Hide livestreams from feed.");
|
||||
@ -98,8 +100,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
DownloadPathPreference preference = new DownloadPathPreference(context);
|
||||
download.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_DOWN_PATH.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DOWN_PATH.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_DOWN_PATH.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DOWN_PATH.defaultValue);
|
||||
preference.setValue(SettingsEnum.TIK_DOWN_PATH.getString());
|
||||
preference.setTitle("Download path");
|
||||
preference.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + preference.getValue());
|
||||
@ -113,8 +115,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
download.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_DOWN_WATERMARK.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DOWN_WATERMARK.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_DOWN_WATERMARK.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DOWN_WATERMARK.defaultValue);
|
||||
preference.setChecked(SettingsEnum.TIK_DOWN_WATERMARK.getBoolean());
|
||||
preference.setTitle("Remove watermark");
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
@ -134,8 +136,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
simSpoof.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF.defaultValue);
|
||||
preference.setChecked(SettingsEnum.TIK_SIMSPOOF.getBoolean());
|
||||
preference.setTitle("Fake sim card info");
|
||||
preference.setSummary("Bypass regional restriction by fake sim card information.");
|
||||
@ -149,8 +151,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
simSpoof.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_ISO.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_ISO.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_ISO.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_ISO.defaultValue);
|
||||
preference.setText(SettingsEnum.TIK_SIMSPOOF_ISO.getString());
|
||||
preference.setTitle("Country ISO");
|
||||
preference.setSummary("us, uk, jp, ...");
|
||||
@ -164,8 +166,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
simSpoof.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_MCCMNC.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_MCCMNC.defaultValue);
|
||||
preference.setText(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getString());
|
||||
preference.setTitle("Operator mcc+mnc");
|
||||
preference.setSummary("mcc+mnc");
|
||||
@ -179,8 +181,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
EditTextPreference preference = new EditTextPreference(context);
|
||||
simSpoof.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_SIMSPOOF_OP_NAME.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_OP_NAME.defaultValue);
|
||||
preference.setText(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getString());
|
||||
preference.setTitle("Operator name");
|
||||
preference.setSummary("Name of the operator");
|
||||
@ -200,8 +202,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
integration.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.TIK_DEBUG.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DEBUG.getDefaultValue());
|
||||
preference.setKey(SettingsEnum.TIK_DEBUG.path);
|
||||
preference.setDefaultValue(SettingsEnum.TIK_DEBUG.defaultValue);
|
||||
preference.setChecked(SettingsEnum.TIK_DEBUG.getBoolean());
|
||||
preference.setTitle("Enable debug log");
|
||||
preference.setSummary("Show integration debug log.");
|
||||
|
@ -1,82 +0,0 @@
|
||||
package app.revanced.tiktok.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class SharedPrefHelper {
|
||||
public static void saveString(Context context, SharedPrefNames prefName, String key, String value) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
sharedPreferences.edit().putString(key, value).apply();
|
||||
}
|
||||
|
||||
public static void saveBoolean(Context context, SharedPrefNames prefName, String key, Boolean value) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
sharedPreferences.edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
||||
public static String getString(Context context, SharedPrefNames prefName, String key, String _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
return (sharedPreferences.getString(key, _default));
|
||||
}
|
||||
|
||||
public static Boolean getBoolean(Context context, SharedPrefNames prefName, String key, Boolean _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
return (sharedPreferences.getBoolean(key, _default));
|
||||
}
|
||||
|
||||
public static Long getLong(Context context, SharedPrefNames prefName, String key, Long _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
try {
|
||||
return Long.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getLong(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public static Float getFloat(Context context, SharedPrefNames prefName, String key, Float _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
try {
|
||||
return Float.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getFloat(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public static Integer getInt(Context context, SharedPrefNames prefName, String key, Integer _default) {
|
||||
SharedPreferences sharedPreferences = getPreferences(context, prefName);
|
||||
try {
|
||||
return Integer.valueOf(sharedPreferences.getString(key, _default + ""));
|
||||
} catch (ClassCastException ex) {
|
||||
return sharedPreferences.getInt(key, _default);
|
||||
}
|
||||
}
|
||||
|
||||
public static SharedPreferences getPreferences(Context context, SharedPrefNames name) {
|
||||
if (context == null) return null;
|
||||
return context.getSharedPreferences(name.getName(), Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static SharedPreferences getPreferences(Context context, String name) {
|
||||
if (context == null) return null;
|
||||
return context.getSharedPreferences(name, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public enum SharedPrefNames {
|
||||
TIKTOK_PREFS("tiktok_revanced");
|
||||
|
||||
private final String name;
|
||||
|
||||
SharedPrefNames(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package app.revanced.twitch.settings;
|
||||
|
||||
public enum ReturnType {
|
||||
BOOLEAN, INTEGER, STRING, LONG, FLOAT
|
||||
}
|
@ -1,150 +1,163 @@
|
||||
package app.revanced.twitch.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.twitch.settings.SettingsEnum.ReturnType.BOOLEAN;
|
||||
import static app.revanced.twitch.settings.SettingsEnum.ReturnType.STRING;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.twitch.utils.LogHelper;
|
||||
import app.revanced.twitch.utils.ReVancedUtils;
|
||||
|
||||
public enum SettingsEnum {
|
||||
/* Ads */
|
||||
BLOCK_VIDEO_ADS("revanced_block_video_ads", true, ReturnType.BOOLEAN),
|
||||
BLOCK_AUDIO_ADS("revanced_block_audio_ads", true, ReturnType.BOOLEAN),
|
||||
BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", "ttv-lol", ReturnType.STRING),
|
||||
BLOCK_VIDEO_ADS("revanced_block_video_ads", BOOLEAN, TRUE),
|
||||
BLOCK_AUDIO_ADS("revanced_block_audio_ads", BOOLEAN, TRUE),
|
||||
BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", STRING, "ttv-lol"),
|
||||
|
||||
/* Chat */
|
||||
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", "cross-out", ReturnType.STRING),
|
||||
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", STRING, "cross-out"),
|
||||
|
||||
/* Misc */
|
||||
DEBUG_MODE("revanced_debug_mode", false, ReturnType.BOOLEAN, true);
|
||||
DEBUG_MODE("revanced_debug_mode", BOOLEAN, FALSE, true);
|
||||
|
||||
public static final String REVANCED_PREFS = "revanced_prefs";
|
||||
|
||||
private final String path;
|
||||
private final Object defaultValue;
|
||||
private final ReturnType returnType;
|
||||
private final boolean rebootApp;
|
||||
@NonNull
|
||||
public final String path;
|
||||
@NonNull
|
||||
public final ReturnType returnType;
|
||||
@NonNull
|
||||
public final Object defaultValue;
|
||||
/**
|
||||
* If the app should be rebooted, if this setting is changed
|
||||
*/
|
||||
public final boolean rebootApp;
|
||||
|
||||
private Object value = null;
|
||||
private Object value;
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = false;
|
||||
SettingsEnum(String path, ReturnType returnType, Object defaultValue) {
|
||||
this(path, returnType, defaultValue, false);
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType, Boolean rebootApp) {
|
||||
SettingsEnum(@NonNull String path, @NonNull ReturnType returnType, @NonNull Object defaultValue, boolean rebootApp) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.returnType = returnType;
|
||||
this.defaultValue = defaultValue;
|
||||
this.rebootApp = rebootApp;
|
||||
}
|
||||
|
||||
static {
|
||||
load();
|
||||
loadAllSettings();
|
||||
}
|
||||
|
||||
private static void load() {
|
||||
private static void loadAllSettings() {
|
||||
ReVancedUtils.ifContextAttached((context -> {
|
||||
try {
|
||||
SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE);
|
||||
for (SettingsEnum setting : values()) {
|
||||
Object value = setting.getDefaultValue();
|
||||
|
||||
try {
|
||||
switch (setting.getReturnType()) {
|
||||
case BOOLEAN:
|
||||
value = prefs.getBoolean(setting.getPath(), (boolean)setting.getDefaultValue());
|
||||
break;
|
||||
// Numbers are implicitly converted from strings
|
||||
case FLOAT:
|
||||
case LONG:
|
||||
case INTEGER:
|
||||
case STRING:
|
||||
value = prefs.getString(setting.getPath(), setting.getDefaultValue() + "");
|
||||
break;
|
||||
default:
|
||||
LogHelper.error("Setting '%s' does not have a valid type", setting.name());
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ClassCastException ex) {
|
||||
LogHelper.printException("Failed to read value", ex);
|
||||
}
|
||||
|
||||
setting.setValue(value);
|
||||
LogHelper.debug("Loaded setting '%s' with value %s", setting.name(), value);
|
||||
setting.load(prefs);
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
LogHelper.printException("Failed to load settings", th);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException("Failed to load settings", ex);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void setValue(Object newValue) {
|
||||
private void load(SharedPreferences prefs) {
|
||||
try {
|
||||
switch (returnType) {
|
||||
case BOOLEAN:
|
||||
setValue(prefs.getBoolean(path, (Boolean) defaultValue));
|
||||
break;
|
||||
// Numbers are implicitly converted from strings
|
||||
case INTEGER:
|
||||
case LONG:
|
||||
case FLOAT:
|
||||
case STRING:
|
||||
setValue(prefs.getString(path, defaultValue.toString()));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(name());
|
||||
}
|
||||
LogHelper.debug("Loaded setting '%s' with value %s", name(), value);
|
||||
} catch (ClassCastException ex) {
|
||||
LogHelper.printException("Failed to read value", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets, but does _not_ persistently save the value.
|
||||
*
|
||||
* This intentionally is a static method, to deter accidental usage
|
||||
* when {@link #saveValue(Object)} was intended.
|
||||
*/
|
||||
public static void setValue(SettingsEnum setting, Object newValue) {
|
||||
setting.setValue(newValue);
|
||||
}
|
||||
|
||||
private void setValue(Object newValue) {
|
||||
// Implicitly convert strings to numbers depending on the ResultType
|
||||
switch (returnType) {
|
||||
case FLOAT:
|
||||
value = Float.valueOf(newValue + "");
|
||||
value = Float.valueOf(newValue.toString());
|
||||
break;
|
||||
case LONG:
|
||||
value = Long.valueOf(newValue + "");
|
||||
value = Long.valueOf(newValue.toString());
|
||||
break;
|
||||
case INTEGER:
|
||||
value = Integer.valueOf(newValue + "");
|
||||
value = Integer.valueOf(newValue.toString());
|
||||
break;
|
||||
default:
|
||||
case BOOLEAN:
|
||||
case STRING:
|
||||
value = newValue;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(name());
|
||||
}
|
||||
}
|
||||
|
||||
public void saveValue(Object newValue) {
|
||||
ReVancedUtils.ifContextAttached((context) -> {
|
||||
SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE);
|
||||
if (returnType == ReturnType.BOOLEAN) {
|
||||
if (returnType == BOOLEAN) {
|
||||
prefs.edit().putBoolean(path, (Boolean)newValue).apply();
|
||||
} else {
|
||||
prefs.edit().putString(path, newValue + "").apply();
|
||||
prefs.edit().putString(path, newValue.toString()).apply();
|
||||
}
|
||||
value = newValue;
|
||||
setValue(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public boolean getBoolean() {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
public Long getLong() {
|
||||
public int getInt() {
|
||||
return (Integer) value;
|
||||
}
|
||||
|
||||
public long getLong() {
|
||||
return (Long) value;
|
||||
}
|
||||
|
||||
public Float getFloat() {
|
||||
public float getFloat() {
|
||||
return (Float) value;
|
||||
}
|
||||
|
||||
public Object getDefaultValue() {
|
||||
return defaultValue;
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public ReturnType getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
public boolean shouldRebootOnChange() {
|
||||
return rebootApp;
|
||||
public enum ReturnType {
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
STRING,
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ import androidx.annotation.Nullable;
|
||||
import app.revanced.twitch.settings.SettingsEnum;
|
||||
import app.revanced.twitch.utils.LogHelper;
|
||||
import app.revanced.twitch.utils.ReVancedUtils;
|
||||
|
||||
import tv.twitch.android.app.core.LandingActivity;
|
||||
|
||||
public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
@ -41,28 +40,28 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
*/
|
||||
private void syncPreference(@Nullable String key) {
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
if (!setting.getPath().equals(key) && key != null)
|
||||
if (!setting.path.equals(key) && key != null)
|
||||
continue;
|
||||
|
||||
Preference pref = this.findPreference(setting.getPath());
|
||||
LogHelper.debug("Syncing setting '%s' with UI", setting.getPath());
|
||||
Preference pref = this.findPreference(setting.path);
|
||||
LogHelper.debug("Syncing setting '%s' with UI", setting.path);
|
||||
|
||||
if (pref instanceof SwitchPreference) {
|
||||
setting.setValue(((SwitchPreference) pref).isChecked());
|
||||
SettingsEnum.setValue(setting, ((SwitchPreference) pref).isChecked());
|
||||
}
|
||||
else if (pref instanceof EditTextPreference) {
|
||||
setting.setValue(((EditTextPreference) pref).getText());
|
||||
SettingsEnum.setValue(setting, ((EditTextPreference) pref).getText());
|
||||
}
|
||||
else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
listPref.setSummary(listPref.getEntry());
|
||||
setting.setValue(listPref.getValue());
|
||||
SettingsEnum.setValue(setting, listPref.getValue());
|
||||
}
|
||||
else {
|
||||
LogHelper.error("Setting '%s' cannot be handled!", pref);
|
||||
}
|
||||
|
||||
if (ReVancedUtils.getContext() != null && key != null && settingsInitialized && setting.shouldRebootOnChange()) {
|
||||
if (ReVancedUtils.getContext() != null && key != null && settingsInitialized && setting.rebootApp) {
|
||||
rebootDialog(getActivity());
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs = -Xmx2048m
|
||||
android.useAndroidX = true
|
||||
version = 0.102.0
|
||||
version = 0.103.0-dev.7
|
||||
|
Loading…
Reference in New Issue
Block a user