Merge pull request #68 from YTVanced/ryd-integration

This commit is contained in:
caneleex 2022-02-16 16:29:29 +00:00 committed by GitHub
commit 94c8684929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 3049 additions and 398 deletions

View File

@ -1,16 +1,22 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
compileSdkVersion 32
defaultConfig {
applicationId "pl.jakubweg"
minSdkVersion 21
targetSdkVersion 30
applicationId "vanced.integrations"
minSdkVersion 23
targetSdkVersion 31
versionCode 1
versionName "1.0"
multiDexEnabled false
Properties properties = new Properties()
if (rootProject.file("local.properties").exists()) {
properties.load(rootProject.file("local.properties").newDataInputStream())
}
buildConfigField "String", "YT_API_KEY", "\"${properties.getProperty("youtubeAPIKey", "")}\""
}
buildTypes {
@ -26,6 +32,6 @@ android {
}
dependencies {
implementation 'androidx.annotation:annotation:1.2.0'
implementation 'androidx.annotation:annotation:1.3.0'
}

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="pl.jakubweg">
package="vanced.integrations">
</manifest>

View File

@ -0,0 +1,25 @@
package com.google.android.apps.youtube.app.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class SlimMetadataScrollableButtonContainerLayout extends ViewGroup {
public SlimMetadataScrollableButtonContainerLayout(Context context) {
super(context);
}
public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
}

View File

@ -0,0 +1,139 @@
package fi.vanced.libraries.youtube.dialog;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_HINT_SHOWN;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN;
import static pl.jakubweg.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.os.Build;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
import pl.jakubweg.SponsorBlockSettings;
public class Dialogs {
// Inject call from YT to this
public static void showDialogsAtStartup(Activity activity) {
rydFirstRun(activity);
sbFirstRun(activity);
}
private static void rydFirstRun(Activity activity) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
boolean enabled = SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, false);
boolean hintShown = SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, false);
// If RYD is enabled or hint has been shown, exit
if (enabled || hintShown) {
// If RYD is enabled but hint hasn't been shown, mark it as shown
if (enabled && !hintShown) {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
}
return;
}
AlertDialog.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new AlertDialog.Builder(activity, android.R.style.Theme_Material_Dialog_Alert);
} else {
builder = new AlertDialog.Builder(activity);
}
builder.setTitle(str("vanced_ryd"));
builder.setIcon(VancedUtils.getIdentifier("reel_dislike_icon", "drawable"));
builder.setCancelable(false);
builder.setMessage(str("vanced_ryd_firstrun"));
builder.setPositiveButton(str("vanced_enable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, true);
dialog.dismiss();
});
builder.setNegativeButton(str("vanced_disable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, false);
dialog.dismiss();
});
builder.setNeutralButton(str("vanced_learnmore"), null);
AlertDialog dialog = builder.create();
dialog.show();
// Set black background
dialog.getWindow().getDecorView().getBackground().setColorFilter(new LightingColorFilter(0xFF000000, VancedUtils.getIdentifier("ytBrandBackgroundSolid", "color")));
// Set learn more action (set here so clicking it doesn't dismiss the dialog)
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> {
Uri uri = Uri.parse("https://www.returnyoutubedislike.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intent);
});
}
private static void sbFirstRun(Activity activity) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
boolean enabled = SharedPrefUtils.getBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, false);
boolean hintShown = SharedPrefUtils.getBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, false);
// If SB is enabled or hint has been shown, exit
if (enabled || hintShown) {
// If SB is enabled but hint hasn't been shown, mark it as shown
if (enabled && !hintShown) {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
}
return;
}
AlertDialog.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new AlertDialog.Builder(activity, android.R.style.Theme_Material_Dialog_Alert);
} else {
builder = new AlertDialog.Builder(activity);
}
builder.setTitle(str("vanced_sb"));
builder.setIcon(VancedUtils.getIdentifier("ic_sb_logo", "drawable"));
builder.setCancelable(false);
builder.setMessage(str("vanced_sb_firstrun"));
builder.setPositiveButton(str("vanced_enable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, true);
dialog.dismiss();
});
builder.setNegativeButton(str("vanced_disable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, false);
dialog.dismiss();
});
builder.setNeutralButton(str("vanced_learnmore"), null);
AlertDialog dialog = builder.create();
dialog.show();
// Set black background
dialog.getWindow().getDecorView().getBackground().setColorFilter(new LightingColorFilter(0xFF000000, VancedUtils.getIdentifier("ytBrandBackgroundSolid", "color")));
// Set learn more action (set here so clicking it doesn't dismiss the dialog)
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> {
Uri uri = Uri.parse("https://sponsor.ajay.app/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intent);
});
}
}

View File

@ -0,0 +1,29 @@
package fi.vanced.libraries.youtube.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;
}
}

View File

@ -0,0 +1,62 @@
package fi.vanced.libraries.youtube.player;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import static fi.razerman.youtube.XGlobals.debug;
import static pl.jakubweg.StringRef.str;
public class VideoHelpers {
public static final String TAG = "VideoHelpers";
public static void copyVideoUrlToClipboard() {
generateVideoUrl(false);
}
public static void copyVideoUrlWithTimeStampToClipboard() {
generateVideoUrl(true);
}
private static void generateVideoUrl(boolean appendTimeStamp) {
try {
String videoId = VideoInformation.currentVideoId;
if (videoId == null || videoId.isEmpty()) {
if (debug) {
Log.d(TAG, "VideoId was empty");
}
return;
}
String videoUrl = String.format("https://youtu.be/%s", videoId);
if (appendTimeStamp) {
long videoTime = VideoInformation.lastKnownVideoTime;
videoUrl += String.format("?t=%s", (videoTime / 1000));
}
if (debug) {
Log.d(TAG, "Video URL: " + videoUrl);
}
setClipboard(YouTubeTikTokRoot_Application.getAppContext(), videoUrl);
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), str("share_copy_url_success"), Toast.LENGTH_SHORT).show();
}
catch (Exception ex) {
Log.e(TAG, "Couldn't generate video url", ex);
}
}
private static void setClipboard(Context context, String text) {
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(text);
} else {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("link", text);
clipboard.setPrimaryClip(clip);
}
}
}

View File

@ -1,6 +1,86 @@
package fi.vanced.libraries.youtube.player;
import static fi.razerman.youtube.XGlobals.debug;
import android.util.Log;
import fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes;
public class VideoInformation {
private static final String TAG = "VI - VideoInfo";
public static String currentVideoId;
public static Integer dislikeCount;
public static String channelName;
public static long lastKnownVideoTime = -1L;
private static boolean tempInfoSaved = false;
private static String tempVideoId;
private static Integer tempDislikeCount;
// Call hook in the YT code when the video changes
public static void setCurrentVideoId(final String videoId) {
if (videoId == null) {
if (debug) {
Log.d(TAG, "setCurrentVideoId - new id was null - currentVideoId was" + currentVideoId);
}
clearInformation(true);
return;
}
// Restore temporary information that was stored from the last watched video
if (tempInfoSaved) {
restoreTempInformation();
}
if (videoId.equals(currentVideoId)) {
if (debug) {
Log.d(TAG, "setCurrentVideoId - new and current video were equal - " + videoId);
}
return;
}
if (debug) {
Log.d(TAG, "setCurrentVideoId - video id updated from " + currentVideoId + " to " + videoId);
}
currentVideoId = videoId;
// New video
ReturnYouTubeDislikes.newVideoLoaded(videoId);
}
// Call hook in the YT code when the video ends
public static void videoEnded() {
saveTempInformation();
clearInformation(false);
}
// Information is cleared once a video ends
// It's cleared because the setCurrentVideoId isn't called for Shorts
// so Shorts would otherwise use the information from the last watched video
private static void clearInformation(boolean full) {
if (full) {
currentVideoId = null;
dislikeCount = null;
}
channelName = null;
}
// Temporary information is saved once a video ends
// so that if the user watches the same video again,
// the information can be restored without having to fetch again
private static void saveTempInformation() {
tempVideoId = currentVideoId;
tempDislikeCount = dislikeCount;
tempInfoSaved = true;
}
private static void restoreTempInformation() {
currentVideoId = tempVideoId;
dislikeCount = tempDislikeCount;
tempVideoId = null;
tempDislikeCount = null;
tempInfoSaved = false;
}
}

View File

@ -0,0 +1,95 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_HINT_SHOWN;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static pl.jakubweg.StringRef.str;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import fi.vanced.utils.SharedPrefUtils;
public class RYDFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(PREFERENCES_NAME);
final Activity context = this.getActivity();
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
// RYD enable toggle
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_RYD_ENABLED);
preference.setDefaultValue(false);
preference.setChecked(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED));
preference.setTitle(str("vanced_ryd_title"));
preference.setSummary(str("vanced_ryd_summary"));
preference.setOnPreferenceChangeListener((pref, newValue) -> {
final boolean value = (Boolean) newValue;
ReturnYouTubeDislikes.onEnabledChange(value);
return true;
});
}
// Clear hint
if (debug) {
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_RYD_HINT_SHOWN);
preference.setDefaultValue(false);
preference.setChecked(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN));
preference.setTitle("Hint debug");
preference.setSummary("Debug toggle for clearing the hint shown preference");
preference.setOnPreferenceChangeListener((pref, newValue) -> true);
}
// About category
addAboutCategory(context, preferenceScreen);
}
private void addAboutCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
category.setTitle(str("about"));
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle(str("vanced_ryd_attribution_title"));
preference.setSummary(str("vanced_ryd_attribution_summary"));
preference.setOnPreferenceClickListener(pref -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://returnyoutubedislike.com"));
pref.getContext().startActivity(i);
return false;
});
}
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle("GitHub");
preference.setOnPreferenceClickListener(pref -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://github.com/Anarios/return-youtube-dislike"));
pref.getContext().startActivity(i);
return false;
});
}
}
}

View File

@ -0,0 +1,8 @@
package fi.vanced.libraries.youtube.ryd;
public class RYDSettings {
public static final String PREFERENCES_NAME = "ryd";
public static final String PREFERENCES_KEY_USERID = "userId";
public static final String PREFERENCES_KEY_RYD_ENABLED = "ryd-enabled";
public static final String PREFERENCES_KEY_RYD_HINT_SHOWN = "ryd_hint_shown";
}

View File

@ -0,0 +1,67 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_USERID;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static fi.vanced.utils.VancedUtils.getPreferences;
import static fi.vanced.utils.VancedUtils.randomString;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
public class Registration {
private static final String TAG = "VI - RYD - Registration";
private String userId;
private Context context;
public Registration(Context context) {
this.context = context;
}
public String getUserId() {
return userId != null ? userId : fetchUserId();
}
private String fetchUserId() {
try {
if (this.context == null) throw new Exception("Unable to fetch userId because context was null");
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
this.userId = preferences.getString(PREFERENCES_KEY_USERID, null);
if (this.userId == null) {
this.userId = register();
}
}
catch (Exception ex) {
Log.e(TAG, "Unable to fetch the userId from shared preferences", ex);
}
return this.userId;
}
public void saveUserId(String userId) {
try {
if (this.context == null) throw new Exception("Unable to save userId because context was null");
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(PREFERENCES_KEY_USERID, userId).apply();
}
catch (Exception ex) {
Log.e(TAG, "Unable to save the userId in shared preferences", ex);
}
}
private String register() {
String userId = randomString(36);
if (debug) {
Log.d(TAG, "Trying to register the following userId: " + userId);
}
return RYDRequester.register(userId, this);
}
}

View File

@ -0,0 +1,318 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static fi.vanced.utils.VancedUtils.getIdentifier;
import android.content.Context;
import android.icu.text.CompactDecimalFormat;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import java.util.Locale;
import java.util.Objects;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
import fi.vanced.utils.SharedPrefUtils;
public class ReturnYouTubeDislikes {
public static boolean isEnabled;
public static final String TAG = "VI - RYD";
private static View _dislikeView = null;
private static Thread _dislikeFetchThread = null;
private static Thread _votingThread = null;
private static Registration registration;
private static Voting voting;
private static boolean likeActive;
private static boolean dislikeActive;
private static int votingValue = 0; // 1 = like, -1 = dislike, 0 = no vote
private static CompactDecimalFormat compactNumberFormatter;
static {
Context context = YouTubeTikTokRoot_Application.getAppContext();
isEnabled = SharedPrefUtils.getBoolean(Objects.requireNonNull(context), PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, false);
if (isEnabled) {
registration = new Registration(context);
voting = new Voting(context, registration);
}
Locale locale = context.getResources().getConfiguration().locale;
if (debug) {
Log.d(TAG, "locale - " + locale);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
compactNumberFormatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT
);
}
}
public static void onEnabledChange(boolean enabled) {
isEnabled = enabled;
if (registration == null) {
registration = new Registration(YouTubeTikTokRoot_Application.getAppContext());
}
if (voting == null) {
voting = new Voting(YouTubeTikTokRoot_Application.getAppContext(), registration);
}
}
public static void newVideoLoaded(String videoId) {
if (debug) {
Log.d(TAG, "newVideoLoaded - " + videoId);
}
dislikeCount = null;
if (!isEnabled) return;
try {
if (_dislikeFetchThread != null && _dislikeFetchThread.getState() != Thread.State.TERMINATED) {
if (debug) {
Log.d(TAG, "Interrupting the thread. Current state " + _dislikeFetchThread.getState());
}
_dislikeFetchThread.interrupt();
}
}
catch (Exception ex) {
Log.e(TAG, "Error in the dislike fetch thread", ex);
}
_dislikeFetchThread = new Thread(() -> RYDRequester.fetchDislikes(videoId));
_dislikeFetchThread.start();
}
// Call to this needs to be injected in YT code
public static void setLikeTag(View view) {
if (!isEnabled) return;
setTag(view, "like");
}
public static void setLikeTag(View view, boolean active) {
if (!isEnabled) return;
likeActive = active;
if (likeActive) {
votingValue = 1;
}
if (debug) {
Log.d(TAG, "Like tag active " + likeActive);
}
setTag(view, "like");
}
// Call to this needs to be injected in YT code
public static void setDislikeTag(View view) {
if (!isEnabled) return;
_dislikeView = view;
setTag(view, "dislike");
}
public static void setDislikeTag(View view, boolean active) {
if (!isEnabled) return;
dislikeActive = active;
if (dislikeActive) {
votingValue = -1;
}
_dislikeView = view;
if (debug) {
Log.d(TAG, "Dislike tag active " + dislikeActive);
}
setTag(view, "dislike");
}
// Call to this needs to be injected in YT code
public static CharSequence onSetText(View view, CharSequence originalText) {
if (!isEnabled) return originalText;
return handleOnSetText(view, originalText);
}
// Call to this needs to be injected in YT code
public static void onClick(View view, boolean inactive) {
if (!isEnabled) return;
handleOnClick(view, inactive);
}
private static CharSequence handleOnSetText(View view, CharSequence originalText) {
if (!isEnabled) return originalText;
try {
CharSequence tag = (CharSequence) view.getTag();
if (debug) {
Log.d(TAG, "handleOnSetText - " + tag + " - original text - " + originalText);
}
if (tag == null) return originalText;
if (tag == "like") {
return originalText;
}
else if (tag == "dislike") {
return dislikeCount != null ? formatDislikes(dislikeCount) : originalText;
}
}
catch (Exception ex) {
Log.e(TAG, "Error while handling the setText", ex);
}
return originalText;
}
public static void trySetDislikes(String dislikeCount) {
if (!isEnabled) return;
try {
// Try to set normal video dislike count
if (_dislikeView == null) {
if (debug) { Log.d(TAG, "_dislikeView was null"); }
return;
}
View buttonView = _dislikeView.findViewById(getIdentifier("button_text", "id"));
if (buttonView == null) {
if (debug) { Log.d(TAG, "buttonView was null"); }
return;
}
TextView button = (TextView) buttonView;
button.setText(dislikeCount);
if (debug) {
Log.d(TAG, "trySetDislikes - " + dislikeCount);
}
}
catch (Exception ex) {
if (debug) {
Log.e(TAG, "Error while trying to set dislikes text", ex);
}
}
}
private static void handleOnClick(View view, boolean previousState) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
if (!isEnabled || SharedPrefUtils.getBoolean(Objects.requireNonNull(context),"youtube","user_signed_out",true)) return;
try {
String tag = (String) view.getTag();
if (debug) {
Log.d(TAG, "handleOnClick - " + tag + " - previousState - " + previousState);
}
if (tag == null) return;
// If active status was removed, vote should be none
if (previousState) { votingValue = 0; }
if (tag.equals("like")) {
// Like was activated
if (!previousState) { votingValue = 1; likeActive = true; }
else { likeActive = false; }
// Like was activated and dislike was previously activated
if (!previousState && dislikeActive) { dislikeCount--; trySetDislikes(formatDislikes(dislikeCount)); }
dislikeActive = false;
}
else if (tag.equals("dislike")) {
likeActive = false;
// Dislike was activated
if (!previousState) { votingValue = -1; dislikeActive = true; dislikeCount++; }
// Dislike was removed
else { dislikeActive = false; dislikeCount--; }
trySetDislikes(formatDislikes(dislikeCount));
}
else {
// Unknown tag
return;
}
if (debug) {
Log.d(TAG, "New vote status - " + votingValue);
Log.d(TAG, "Like button " + likeActive + " | Dislike button " + dislikeActive);
}
sendVote(votingValue);
}
catch (Exception ex) {
Log.e(TAG, "Error while handling the onClick", ex);
}
}
private static void sendVote(int vote) {
if (!isEnabled) return;
if (debug) {
Log.d(TAG, "sending vote - " + vote + " for video " + currentVideoId);
}
try {
if (_votingThread != null && _votingThread.getState() != Thread.State.TERMINATED) {
if (debug) {
Log.d(TAG, "Interrupting the thread. Current state " + _votingThread.getState());
}
_votingThread.interrupt();
}
}
catch (Exception ex) {
Log.e(TAG, "Error in the voting thread", ex);
}
_votingThread = new Thread(() -> {
try {
boolean result = voting.sendVote(currentVideoId, vote);
if (debug) {
Log.d(TAG, "sendVote status " + result);
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
}
});
_votingThread.start();
}
private static void setTag(View view, String tag) {
if (!isEnabled) return;
try {
if (view == null) {
if (debug) {
Log.d(TAG, "View was empty");
}
return;
}
if (debug) {
Log.d(TAG, "setTag - " + tag);
}
view.setTag(tag);
}
catch (Exception ex) {
Log.e(TAG, "Error while trying to set tag to view", ex);
}
}
public static String formatDislikes(int dislikes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && compactNumberFormatter != null) {
final String formatted = compactNumberFormatter.format(dislikes);
if (debug) {
Log.d(TAG, "Formatting dislikes - " + dislikes + " - " + formatted);
}
return formatted;
}
if (debug) {
Log.d(TAG, "Couldn't format dislikes, using the unformatted count - " + dislikes);
}
return String.valueOf(dislikes);
}
}

View File

@ -0,0 +1,65 @@
package fi.vanced.libraries.youtube.ryd;
import android.util.Base64;
import android.util.Log;
import java.security.MessageDigest;
public class Utils {
private static final String TAG = "VI - RYD - Utils";
public static String solvePuzzle(String challenge, int difficulty) {
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
byte[] buffer = new byte[20];
for (int i = 4; i < 20; i++) {
buffer[i] = decodedChallenge[i - 4];
}
try {
int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
MessageDigest md = MessageDigest.getInstance("SHA-512");
for (int i = 0; i < maxCount; i++) {
buffer[0] = (byte)i;
buffer[1] = (byte)(i >> 8);
buffer[2] = (byte)(i >> 16);
buffer[3] = (byte)(i >> 24);
byte[] messageDigest = md.digest(buffer);
if (countLeadingZeroes(messageDigest) >= difficulty) {
String encode = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
return encode;
}
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to solve puzzle", ex);
}
return null;
}
static int countLeadingZeroes(byte[] uInt8View) {
int zeroes = 0;
int value = 0;
for (int i = 0; i < uInt8View.length; i++) {
value = uInt8View[i] & 0xFF;
if (value == 0) {
zeroes += 8;
} else {
int count = 1;
if (value >>> 4 == 0) {
count += 4;
value <<= 4;
}
if (value >>> 6 == 0) {
count += 2;
value <<= 2;
}
zeroes += count - (value >>> 7);
break;
}
}
return zeroes;
}
}

View File

@ -0,0 +1,28 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import android.content.Context;
import android.util.Log;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
public class Voting {
private static final String TAG = "VI - RYD - Voting";
private Registration registration;
private Context context;
public Voting(Context context, Registration registration) {
this.context = context;
this.registration = registration;
}
public boolean sendVote(String videoId, int vote) {
String userId = registration.getUserId();
if (debug) {
Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId);
}
return RYDRequester.sendVote(videoId, userId, vote);
}
}

View File

@ -0,0 +1,227 @@
package fi.vanced.libraries.youtube.ryd.requests;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount;
import static fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes.TAG;
import static fi.vanced.utils.requests.Requester.parseJson;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import fi.vanced.libraries.youtube.ryd.Registration;
import fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes;
import fi.vanced.libraries.youtube.ryd.Utils;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
public class RYDRequester {
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
private RYDRequester() {}
public static void fetchDislikes(String videoId) {
try {
if (debug) {
Log.d(TAG, "Fetching dislikes for " + videoId);
}
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.GET_DISLIKES, videoId);
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = getJSONObject(connection);
int dislikes = json.getInt("dislikes");
dislikeCount = dislikes;
if (debug) {
Log.d(TAG, "dislikes fetched - " + dislikeCount);
}
// Set the dislikes
new Handler(Looper.getMainLooper()).post(() -> ReturnYouTubeDislikes.trySetDislikes(ReturnYouTubeDislikes.formatDislikes(dislikes)));
}
else if (debug) {
Log.d(TAG, "dislikes fetch response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
dislikeCount = null;
Log.e(TAG, "Failed to fetch dislikes", ex);
}
}
public static String register(String userId, Registration registration) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.GET_REGISTRATION, userId);
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = getJSONObject(connection);
String challenge = json.getString("challenge");
int difficulty = json.getInt("difficulty");
if (debug) {
Log.d(TAG, "Registration challenge - " + challenge + " with difficulty of " + difficulty);
}
// Solve the puzzle
String solution = Utils.solvePuzzle(challenge, difficulty);
if (debug) {
Log.d(TAG, "Registration confirmation solution is " + solution);
}
return confirmRegistration(userId, solution, registration);
}
else if (debug) {
Log.d(TAG, "Registration response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex){
Log.e(TAG, "Failed to register userId", ex);
}
return null;
}
private static String confirmRegistration(String userId, String solution, Registration registration) {
try {
if (debug) {
Log.d(TAG, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
}
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.CONFIRM_REGISTRATION, userId);
applyCommonRequestSettings(connection);
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
String result = parseJson(connection);
if (debug) {
Log.d(TAG, "Registration confirmation result was " + result);
}
if (result.equalsIgnoreCase("true")) {
registration.saveUserId(userId);
if (debug) {
Log.d(TAG, "Registration was successful for user " + userId);
}
return userId;
}
}
else if (debug) {
Log.d(TAG, "Registration confirmation response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to confirm registration", ex);
}
return null;
}
public static boolean sendVote(String videoId, String userId, int vote) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.SEND_VOTE);
applyCommonRequestSettings(connection);
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
JSONObject json = getJSONObject(connection);
String challenge = json.getString("challenge");
int difficulty = json.getInt("difficulty");
if (debug) {
Log.d(TAG, "Vote challenge - " + challenge + " with difficulty of " + difficulty);
}
// Solve the puzzle
String solution = Utils.solvePuzzle(challenge, difficulty);
if (debug) {
Log.d(TAG, "Vote confirmation solution is " + solution);
}
// Confirm vote
return confirmVote(videoId, userId, solution);
}
else if (debug) {
Log.d(TAG, "Vote response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
}
return false;
}
private static boolean confirmVote(String videoId, String userId, String solution) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.CONFIRM_VOTE);
applyCommonRequestSettings(connection);
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
String result = parseJson(connection);
if (debug) {
Log.d(TAG, "Vote confirmation result was " + result);
}
if (result.equalsIgnoreCase("true")) {
if (debug) {
Log.d(TAG, "Vote was successful for user " + userId);
}
return true;
}
}
else if (debug) {
Log.d(TAG, "Vote confirmation response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to confirm vote", ex);
}
return false;
}
// utils
private static void applyCommonRequestSettings(HttpURLConnection connection) throws Exception {
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(5 * 1000);
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(RYD_API_URL, route, params);
}
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return Requester.getJSONObject(connection);
}
}

View File

@ -0,0 +1,16 @@
package fi.vanced.libraries.youtube.ryd.requests;
import static fi.vanced.utils.requests.Route.Method.GET;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class RYDRoutes {
public static final Route SEND_VOTE = new Route(POST,"interact/vote");
public static final Route CONFIRM_VOTE = new Route(POST,"interact/confirmVote");
public static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}");
public static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}");
public static final Route CONFIRM_REGISTRATION = new Route(POST,"puzzle/registration?userId={user_id}");
private RYDRoutes() {}
}

View File

@ -0,0 +1,76 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.libraries.youtube.whitelisting.requests.WhitelistRequester;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class AdButton extends SlimButton {
public static final String TAG = "VI - AdButton - Button";
public AdButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID,
SharedPrefUtils.getBoolean(context, WhitelistType.ADS.getSharedPreferencesName(), WhitelistType.ADS.getPreferenceEnabledName(), false));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_ad_button", "drawable"));
this.button_text.setText(str("action_ads"));
changeEnabled(Whitelist.shouldShowAds());
}
public void changeEnabled(boolean enabled) {
if (debug) {
Log.d(TAG, "changeEnabled " + enabled);
}
this.button_icon.setEnabled(enabled);
}
@Override
public void onClick(View view) {
this.view.setEnabled(false);
if (this.button_icon.isEnabled()) {
removeFromWhitelist();
return;
}
//this.button_icon.setEnabled(!this.button_icon.isEnabled());
addToWhiteList(this.view, this.button_icon);
}
private void removeFromWhitelist() {
try {
Whitelist.removeFromWhitelist(WhitelistType.ADS, this.context, VideoInformation.channelName);
changeEnabled(false);
}
catch (Exception ex) {
Log.e(TAG, "Failed to remove from whitelist", ex);
return;
}
this.view.setEnabled(true);
}
private void addToWhiteList(View view, ImageView buttonIcon) {
new Thread(() -> {
if (debug) {
Log.d(TAG, "Fetching channelId for " + currentVideoId);
}
WhitelistRequester.addChannelToWhitelist(WhitelistType.ADS, view, buttonIcon, this.context);
}).start();
}
}

View File

@ -0,0 +1,36 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import fi.vanced.utils.SharedPrefUtils;
public class ButtonVisibility {
public static Visibility getButtonVisibility(Context context, String key) {
return getButtonVisibility(context, key, "youtube");
}
public static Visibility getButtonVisibility(Context context, String key, String preferenceName) {
String value = SharedPrefUtils.getString(context, preferenceName, 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(Context context, String key) {
return isVisibleInContainer(getButtonVisibility(context, key));
}
public static boolean isVisibleInContainer(Context context, String key, String preferenceName) {
return isVisibleInContainer(getButtonVisibility(context, key, preferenceName));
}
public static boolean isVisibleInContainer(Visibility visibility) {
return visibility == Visibility.BOTH || visibility == Visibility.BUTTON_CONTAINER;
}
}

View File

@ -0,0 +1,28 @@
package fi.vanced.libraries.youtube.ui;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import fi.vanced.libraries.youtube.player.VideoHelpers;
import fi.vanced.utils.VancedUtils;
public class CopyButton extends SlimButton {
public CopyButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_button_list"));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon", "drawable"));
this.button_text.setText(str("action_copy"));
}
@Override
public void onClick(View view) {
VideoHelpers.copyVideoUrlToClipboard();
}
}

View File

@ -0,0 +1,28 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import fi.vanced.libraries.youtube.player.VideoHelpers;
import fi.vanced.utils.VancedUtils;
import static pl.jakubweg.StringRef.str;
public class CopyWithTimestamp extends SlimButton {
public CopyWithTimestamp(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_timestamp_button_list"));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon_with_time", "drawable"));
this.button_text.setText(str("action_tcopy"));
}
@Override
public void onClick(View view) {
VideoHelpers.copyVideoUrlWithTimeStampToClipboard();
}
}

View File

@ -0,0 +1,37 @@
package fi.vanced.libraries.youtube.ui;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.utils.VancedUtils;
import pl.jakubweg.SponsorBlockSettings;
import pl.jakubweg.SponsorBlockUtils;
public class SBBrowserButton extends SlimButton {
private static final String BROWSER_URL = "https://sb.ltn.fi/video/";
public SBBrowserButton(Context context, ViewGroup container) {
super(context, container, SLIM_METADATA_BUTTON_ID,
SponsorBlockUtils.isSBButtonEnabled(context, SponsorBlockSettings.PREFERENCES_KEY_BROWSER_BUTTON));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_sb_browser", "drawable"));
this.button_text.setText(str("action_browser"));
}
@Override
public void onClick(View v) {
Uri uri = Uri.parse(BROWSER_URL + VideoInformation.currentVideoId);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
context.startActivity(intent);
}
}

View File

@ -0,0 +1,76 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.libraries.youtube.whitelisting.requests.WhitelistRequester;
import fi.vanced.utils.VancedUtils;
import pl.jakubweg.SponsorBlockUtils;
public class SBWhitelistButton extends SlimButton {
public static final String TAG = "VI - SBWhitelistButton";
public SBWhitelistButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID,
SponsorBlockUtils.isSBButtonEnabled(context, WhitelistType.SPONSORBLOCK.getPreferenceEnabledName()));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_sb_button", "drawable"));
this.button_text.setText(str("action_segments"));
changeEnabled(Whitelist.isChannelSBWhitelisted());
}
public void changeEnabled(boolean enabled) {
if (debug) {
Log.d(TAG, "changeEnabled " + enabled);
}
this.button_icon.setEnabled(!enabled); // enabled == true -> strikethrough (no segments), enabled == false -> clear (segments)
}
@Override
public void onClick(View view) {
this.view.setEnabled(false);
if (Whitelist.isChannelSBWhitelisted()) {
removeFromWhitelist();
return;
}
//this.button_icon.setEnabled(!this.button_icon.isEnabled());
addToWhiteList(this.view, this.button_icon);
}
private void removeFromWhitelist() {
try {
Whitelist.removeFromWhitelist(WhitelistType.SPONSORBLOCK, this.context, VideoInformation.channelName);
changeEnabled(false);
}
catch (Exception ex) {
Log.e(TAG, "Failed to remove from whitelist", ex);
return;
}
this.view.setEnabled(true);
}
private void addToWhiteList(View view, ImageView buttonIcon) {
new Thread(() -> {
if (debug) {
Log.d(TAG, "Fetching channelId for " + currentVideoId);
}
WhitelistRequester.addChannelToWhitelist(WhitelistType.SPONSORBLOCK, view, buttonIcon, this.context);
}).start();
}
}

View File

@ -0,0 +1,73 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import fi.vanced.utils.VancedUtils;
public abstract class SlimButton implements View.OnClickListener {
private static final String TAG = "VI - Slim - Button";
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 = VancedUtils.getIdentifier("slim_metadata_button", "layout");
}
public SlimButton(Context context, ViewGroup container, int id, boolean visible) {
if (debug) {
Log.d(TAG, "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(VancedUtils.getIdentifier("button_icon", "id"));
button_text = (TextView)view.findViewById(VancedUtils.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) {
Log.e(TAG, "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);
}
}

View File

@ -0,0 +1,141 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_BROWSER_BUTTON;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import com.google.android.apps.youtube.app.ui.SlimMetadataScrollableButtonContainerLayout;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
import pl.jakubweg.SponsorBlockSettings;
public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLayout {
private static final String TAG = "VI - Slim - Container";
private ViewGroup container;
private CopyButton copyButton;
private CopyWithTimestamp copyWithTimestampButton;
public static AdButton adBlockButton;
public static SBWhitelistButton sbWhitelistButton;
private SBBrowserButton sbBrowserButton;
private final Context context;
SharedPreferences.OnSharedPreferenceChangeListener listener;
public SlimButtonContainer(Context context) {
super(context);
this.context = context;
this.initialize(context);
}
public SlimButtonContainer(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.initialize(context);
}
public SlimButtonContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
this.initialize(context);
}
public void initialize(Context context) {
try {
container = this.findViewById(VancedUtils.getIdentifier("button_container_vanced", "id"));
if (container == null) throw new Exception("Unable to initialize the button container because the button_container_vanced couldn't be found");
copyButton = new CopyButton(context, this);
copyWithTimestampButton = new CopyWithTimestamp(context, this);
adBlockButton = new AdButton(context, this);
sbWhitelistButton = new SBWhitelistButton(context, this);
sbBrowserButton = new SBBrowserButton(context, this);
new SponsorBlockVoting(context, this);
addSharedPrefsChangeListener();
}
catch (Exception ex) {
Log.e(TAG, "Unable to initialize the button container", ex);
}
}
private void addSharedPrefsChangeListener() {
listener = (sharedPreferences, key) -> {
try {
if (debug) {
Log.d(TAG, String.format("SharedPreference changed with key %s", key));
}
if ("pref_copy_video_url_button_list".equals(key) && copyButton != null) {
copyButton.setVisible(ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_button_list"));
return;
}
if ("pref_copy_video_url_timestamp_button_list".equals(key) && copyWithTimestampButton != null) {
copyWithTimestampButton.setVisible(ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_timestamp_button_list"));
return;
}
if (PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED.equals(key)) {
if (sbWhitelistButton != null) {
if (SponsorBlockSettings.isSponsorBlockEnabled) {
toggleWhitelistButton();
}
else {
Whitelist.setEnabled(WhitelistType.SPONSORBLOCK, false);
sbWhitelistButton.setVisible(false);
}
}
if (sbBrowserButton != null) {
if (SponsorBlockSettings.isSponsorBlockEnabled) {
toggleBrowserButton();
}
else {
sbBrowserButton.setVisible(false);
}
}
}
if (PREFERENCES_KEY_BROWSER_BUTTON.equals(key) && sbBrowserButton != null) {
toggleBrowserButton();
return;
}
WhitelistType whitelistAds = WhitelistType.ADS;
String adsEnabledPreferenceName = whitelistAds.getPreferenceEnabledName();
if (adsEnabledPreferenceName.equals(key) && adBlockButton != null) {
boolean enabled = SharedPrefUtils.getBoolean(context, whitelistAds.getSharedPreferencesName(), adsEnabledPreferenceName, false);
Whitelist.setEnabled(whitelistAds, enabled);
adBlockButton.setVisible(enabled);
return;
}
if (WhitelistType.SPONSORBLOCK.getPreferenceEnabledName().equals(key) && sbWhitelistButton != null) {
toggleWhitelistButton();
return;
}
}
catch (Exception ex) {
Log.e(TAG, "Error handling shared preference change", ex);
}
};
context.getSharedPreferences(WhitelistType.ADS.getSharedPreferencesName(), Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(listener);
context.getSharedPreferences(WhitelistType.SPONSORBLOCK.getSharedPreferencesName(), Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(listener);
}
private void toggleWhitelistButton() {
WhitelistType whitelistSB = WhitelistType.SPONSORBLOCK;
String sbEnabledPreferenceName = whitelistSB.getPreferenceEnabledName();
boolean enabled = SharedPrefUtils.getBoolean(context, whitelistSB.getSharedPreferencesName(), sbEnabledPreferenceName, false);
Whitelist.setEnabled(whitelistSB, enabled);
sbWhitelistButton.setVisible(enabled);
}
private void toggleBrowserButton() {
sbBrowserButton.setVisible(SharedPrefUtils.getBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_BROWSER_BUTTON, false));
}
}

View File

@ -0,0 +1,28 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import fi.vanced.utils.VancedUtils;
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(VancedUtils.getIdentifier("vanced_sb_voting", "drawable"));
this.button_text.setText("SB Voting");
}
@Override
public void onClick(View view) {
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show();
}
}

View File

@ -0,0 +1,8 @@
package fi.vanced.libraries.youtube.ui;
public enum Visibility {
NONE,
PLAYER,
BUTTON_CONTAINER,
BOTH,
}

View File

@ -0,0 +1,185 @@
package fi.vanced.libraries.youtube.whitelisting;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.channelName;
import static fi.vanced.libraries.youtube.ui.SlimButtonContainer.adBlockButton;
import static fi.vanced.libraries.youtube.ui.SlimButtonContainer.sbWhitelistButton;
import static fi.vanced.utils.VancedUtils.getPreferences;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.utils.ObjectSerializer;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class Whitelist {
private static final String TAG = "VI - Whitelisting";
private static final Map<WhitelistType, ArrayList<ChannelModel>> whitelistMap = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext());
private static final Map<WhitelistType, Boolean> enabledMap = parseEnabledMap(YouTubeTikTokRoot_Application.getAppContext());
private Whitelist() {}
// injected calls
public static boolean shouldShowAds() {
return isWhitelisted(WhitelistType.ADS);
}
public static void setChannelName(String channelName) {
if (debug) {
Log.d(TAG, "channel name set to " + channelName);
}
VideoInformation.channelName = channelName;
if (enabledMap.get(WhitelistType.ADS) && adBlockButton != null) {
adBlockButton.changeEnabled(shouldShowAds());
}
if (enabledMap.get(WhitelistType.SPONSORBLOCK) && sbWhitelistButton != null) {
sbWhitelistButton.changeEnabled(isChannelSBWhitelisted());
}
}
// the rest
public static boolean isChannelSBWhitelisted() {
return isWhitelisted(WhitelistType.SPONSORBLOCK);
}
private static Map<WhitelistType, ArrayList<ChannelModel>> parseWhitelist(Context context) {
if (context == null) {
return Collections.emptyMap();
}
WhitelistType[] whitelistTypes = WhitelistType.values();
Map<WhitelistType, ArrayList<ChannelModel>> whitelistMap = new EnumMap<>(WhitelistType.class);
for (WhitelistType whitelistType : whitelistTypes) {
SharedPreferences preferences = VancedUtils.getPreferences(context, whitelistType.getPreferencesName());
String serializedChannels = preferences.getString("channels", null);
if (serializedChannels == null) {
if (debug) {
Log.d(TAG, String.format("channels string was null for %s whitelisting", whitelistType));
}
whitelistMap.put(whitelistType, new ArrayList<>());
continue;
}
try {
ArrayList<ChannelModel> deserializedChannels = (ArrayList<ChannelModel>) ObjectSerializer.deserialize(serializedChannels);
if (debug) {
Log.d(TAG, serializedChannels);
for (ChannelModel channel : deserializedChannels) {
Log.d(TAG, String.format("Whitelisted channel %s (%s) for type %s", channel.getAuthor(), channel.getChannelId(), whitelistType));
}
}
whitelistMap.put(whitelistType, deserializedChannels);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
return whitelistMap;
}
private static Map<WhitelistType, Boolean> parseEnabledMap(Context context) {
Map<WhitelistType, Boolean> enabledMap = new EnumMap<>(WhitelistType.class);
for (WhitelistType whitelistType : WhitelistType.values()) {
enabledMap.put(whitelistType, SharedPrefUtils.getBoolean(context, whitelistType.getSharedPreferencesName(), whitelistType.getPreferenceEnabledName()));
}
return enabledMap;
}
private static boolean isWhitelisted(WhitelistType whitelistType) {
boolean isEnabled = enabledMap.get(whitelistType);
if (!isEnabled) {
return false;
}
if (channelName == null || channelName.trim().isEmpty()) {
if (debug) {
Log.d(TAG, String.format("Can't check whitelist status for %s because channel name was missing", whitelistType));
}
return false;
}
List<ChannelModel> whitelistedChannels = whitelistMap.get(whitelistType);
for (ChannelModel channel : whitelistedChannels) {
if (channel.getAuthor().equals(channelName)) {
if (debug) {
Log.d(TAG, String.format("Whitelist for channel %s for type %s", channelName, whitelistType));
}
return true;
}
}
return false;
}
public static boolean addToWhitelist(WhitelistType whitelistType, Context context, ChannelModel channel) {
ArrayList<ChannelModel> whitelisted = whitelistMap.get(whitelistType);
for (ChannelModel whitelistedChannel : whitelisted) {
String channelId = channel.getChannelId();
if (whitelistedChannel.getChannelId().equals(channelId)) {
if (debug) {
Log.d(TAG, String.format("Tried whitelisting an existing channel again. Old info (%1$s | %2$s) - New info (%3$s | %4$s)",
whitelistedChannel.getAuthor(), channelId, channelName, channelId));
}
return true;
}
}
whitelisted.add(channel);
return updateWhitelist(whitelistType, whitelisted, context);
}
public static void removeFromWhitelist(WhitelistType whitelistType, Context context, String channelName) {
ArrayList<ChannelModel> channels = whitelistMap.get(whitelistType);
Iterator<ChannelModel> iterator = channels.iterator();
while (iterator.hasNext()) {
ChannelModel channel = iterator.next();
if (channel.getAuthor().equals(channelName)) {
iterator.remove();
break;
}
}
boolean success = updateWhitelist(whitelistType, channels, context);
String friendlyName = whitelistType.getFriendlyName();
if (success) {
Toast.makeText(context, str("vanced_whitelisting_removed", channelName, friendlyName), Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(context, str("vanced_whitelisting_remove_failed", channelName, friendlyName), Toast.LENGTH_SHORT).show();
}
}
private static boolean updateWhitelist(WhitelistType whitelistType, ArrayList<ChannelModel> channels, Context context) {
if (context == null) {
return false;
}
SharedPreferences preferences = getPreferences(context, whitelistType.getPreferencesName());
SharedPreferences.Editor editor = preferences.edit();
try {
editor.putString("channels", ObjectSerializer.serialize(channels));
editor.apply();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void setEnabled(WhitelistType whitelistType, boolean enabled) {
enabledMap.put(whitelistType, enabled);
}
}

View File

@ -0,0 +1,38 @@
package fi.vanced.libraries.youtube.whitelisting;
import static pl.jakubweg.StringRef.str;
import pl.jakubweg.SponsorBlockSettings;
public enum WhitelistType {
ADS("youtube", "vanced_whitelist_ads_enabled"),
SPONSORBLOCK(SponsorBlockSettings.PREFERENCES_NAME, "vanced_whitelist_sb_enabled");
private final String friendlyName;
private final String preferencesName;
private final String sharedPreferencesName;
private final String preferenceEnabledName;
WhitelistType(String sharedPreferencesName, String preferenceEnabledName) {
this.friendlyName = str("vanced_whitelisting_" + name().toLowerCase());
this.sharedPreferencesName = sharedPreferencesName;
this.preferencesName = "whitelist_" + name();
this.preferenceEnabledName = preferenceEnabledName;
}
public String getFriendlyName() {
return friendlyName;
}
public String getSharedPreferencesName() {
return sharedPreferencesName;
}
public String getPreferencesName() {
return preferencesName;
}
public String getPreferenceEnabledName() {
return preferenceEnabledName;
}
}

View File

@ -0,0 +1,100 @@
package fi.vanced.libraries.youtube.whitelisting.requests;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static fi.vanced.libraries.youtube.ui.AdButton.TAG;
import static fi.vanced.utils.VancedUtils.runOnMainThread;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.VancedUtils;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
import vanced.integrations.BuildConfig;
public class WhitelistRequester {
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
private WhitelistRequester() {}
public static void addChannelToWhitelist(WhitelistType whitelistType, View view, ImageView buttonIcon, Context context) {
try {
HttpURLConnection connection = getConnectionFromRoute(WhitelistRoutes.GET_CHANNEL_DETAILS, BuildConfig.YT_API_KEY);
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(2 * 1000);
String versionName = VancedUtils.getVersionName(context);
String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"" + versionName + "\" } }, \"videoId\": \"" + currentVideoId + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
JSONObject json = getJSONObject(connection);
JSONObject videoInfo = json.getJSONObject("videoDetails");
ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId"));
String author = channelModel.getAuthor();
if (debug) {
Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + author);
}
boolean success = Whitelist.addToWhitelist(whitelistType, context, channelModel);
String whitelistTypeName = whitelistType.getFriendlyName();
runOnMainThread(() -> {
if (success) {
buttonIcon.setEnabled(whitelistType != WhitelistType.SPONSORBLOCK);
Toast.makeText(context, str("vanced_whitelisting_added", author, whitelistTypeName), Toast.LENGTH_SHORT).show();
}
else {
buttonIcon.setEnabled(whitelistType == WhitelistType.SPONSORBLOCK);
Toast.makeText(context, str("vanced_whitelisting_add_failed", author, whitelistTypeName), Toast.LENGTH_SHORT).show();
}
view.setEnabled(true);
});
}
else {
if (debug) {
Log.d(TAG, "player fetch response was " + responseCode);
}
runOnMainThread(() -> {
Toast.makeText(context, str("vanced_whitelisting_fetch_failed", responseCode), Toast.LENGTH_SHORT).show();
buttonIcon.setEnabled(true);
view.setEnabled(true);
});
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to fetch channelId", ex);
runOnMainThread(() -> view.setEnabled(true));
}
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(YT_API_URL, route, params);
}
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return Requester.getJSONObject(connection);
}
}

View File

@ -0,0 +1,11 @@
package fi.vanced.libraries.youtube.whitelisting.requests;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class WhitelistRoutes {
public static final Route GET_CHANNEL_DETAILS = new Route(POST, "player?key={api_key}");
private WhitelistRoutes() {}
}

View File

@ -0,0 +1,83 @@
package fi.vanced.utils;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Modifications copyright (C) 2022 Vanced
*/
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class ObjectSerializer {
private static final String TAG = "VI - ObjectSerializer";
public static String serialize(Serializable obj) throws IOException {
if (obj == null) return "";
try {
ByteArrayOutputStream serialObj = new ByteArrayOutputStream();
ObjectOutputStream objStream = new ObjectOutputStream(serialObj);
objStream.writeObject(obj);
objStream.close();
return encodeBytes(serialObj.toByteArray());
} catch (Exception e) {
Log.e(TAG, "Serialization error: " + e.getMessage(), e);
throw new IOException(e);
}
}
public static Object deserialize(String str) throws IOException {
if (str == null || str.length() == 0) return null;
try {
ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str));
ObjectInputStream objStream = new ObjectInputStream(serialObj);
return objStream.readObject();
} catch (Exception e) {
Log.e(TAG, "Deserialization error: " + e.getMessage(), e);
throw new IOException(e);
}
}
public static String encodeBytes(byte[] bytes) {
StringBuffer strBuf = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
strBuf.append((char) (((bytes[i] >> 4) & 0xF) + ((int) 'a')));
strBuf.append((char) (((bytes[i]) & 0xF) + ((int) 'a')));
}
return strBuf.toString();
}
public static byte[] decodeBytes(String str) {
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < str.length(); i+=2) {
char c = str.charAt(i);
bytes[i/2] = (byte) ((c - 'a') << 4);
c = str.charAt(i+1);
bytes[i/2] += (c - 'a');
}
return bytes;
}
}

View File

@ -0,0 +1,43 @@
package fi.vanced.utils;
import android.content.Context;
import android.content.SharedPreferences;
public class SharedPrefUtils {
public static void saveString(Context context, String preferenceName, String key, String value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putString(key, value).apply();
}
public static void saveBoolean(Context context, String preferenceName, String key, Boolean value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putBoolean(key, value).apply();
}
public static void saveInt(Context context, String preferenceName, String key, Integer value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putInt(key, value).apply();
}
public static String getString(Context context, String preferenceName, String key){
return getString(context, preferenceName, key, null);
}
public static String getString(Context context, String preferenceName, String key, String _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getString(key, _default));
}
public static Boolean getBoolean(Context context, String preferenceName, String key){
return getBoolean(context, preferenceName, key, false);
}
public static Boolean getBoolean(Context context, String preferenceName, String key, Boolean _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getBoolean(key, _default));
}
public static Integer getInt(Context context, String preferenceName, String key){
return getInt(context, preferenceName, key, -1);
}
public static Integer getInt(Context context, String preferenceName, String key, Integer _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getInt(key, _default));
}
}

View File

@ -0,0 +1,63 @@
package fi.vanced.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import java.security.SecureRandom;
public class VancedUtils {
private VancedUtils() {}
public static SharedPreferences getPreferences(Context context, String preferencesName) {
if (context == null) return null;
return context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE);
}
public static int getIdentifier(String name, String defType) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
// https://stackoverflow.com/a/157202
static final String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static SecureRandom rnd = new SecureRandom();
public static String randomString(int len){
StringBuilder sb = new StringBuilder(len);
for(int i = 0; i < len; i++)
sb.append(AB.charAt(rnd.nextInt(AB.length())));
return sb.toString();
}
public static int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
if (seq.charAt(i) == c)
count++;
}
return count;
}
public static String getVersionName(Context context) {
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = pInfo.versionName;
return (version);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return ("17.03.35");
}
public static void runOnMainThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
}

View File

@ -0,0 +1,57 @@
package fi.vanced.utils.requests;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class Requester {
private Requester() {}
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
String url = apiUrl + route.compile(params).getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(route.getMethod().name());
return connection;
}
public static String parseJson(HttpURLConnection connection) throws IOException {
return parseJson(connection.getInputStream(), false);
}
public static String parseJson(InputStream inputStream, boolean isError) throws IOException {
StringBuilder jsonBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line);
if (isError)
jsonBuilder.append("\n");
}
inputStream.close();
return jsonBuilder.toString();
}
public static String parseErrorJson(HttpURLConnection connection) throws IOException {
return parseJson(connection.getErrorStream(), true);
}
public static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return new JSONObject(parseJsonAndDisconnect(connection));
}
public static JSONArray getJSONArray(HttpURLConnection connection) throws Exception {
return new JSONArray(parseJsonAndDisconnect(connection));
}
private static String parseJsonAndDisconnect(HttpURLConnection connection) throws IOException {
String json = parseJson(connection);
connection.disconnect();
return json;
}
}

View File

@ -0,0 +1,59 @@
package fi.vanced.utils.requests;
import fi.vanced.utils.VancedUtils;
public class Route {
private final String route;
private final Route.Method method;
private final int paramCount;
public Route(Route.Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = VancedUtils.countMatches(route, '{');
if (paramCount != VancedUtils.countMatches(route, '}'))
throw new IllegalArgumentException("Not enough parameters");
}
public Route.Method getMethod() {
return method;
}
public Route.CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
StringBuilder compiledRoute = new StringBuilder(route);
for (int i = 0; i < paramCount; i++) {
int paramStart = compiledRoute.indexOf("{");
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
return new Route.CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
private final Route baseRoute;
private final String compiledRoute;
private CompiledRoute(Route baseRoute, String compiledRoute) {
this.baseRoute = baseRoute;
this.compiledRoute = compiledRoute;
}
public String getCompiledRoute() {
return compiledRoute;
}
public Route.Method getMethod() {
return baseRoute.method;
}
}
public enum Method {
GET,
POST
}
}

View File

@ -1,5 +1,10 @@
package pl.jakubweg;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import static pl.jakubweg.SponsorBlockUtils.timeWithoutSegments;
import static pl.jakubweg.SponsorBlockUtils.videoHasSegments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@ -22,11 +27,9 @@ import java.util.Timer;
import java.util.TimerTask;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.requests.Requester;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import pl.jakubweg.requests.SBRequester;
@SuppressLint({"LongLogTag"})
public class PlayerController {
@ -64,8 +67,6 @@ public class PlayerController {
return;
}
VideoInformation.currentVideoId = videoId;
Context context = YouTubeTikTokRoot_Application.getAppContext();
if(context == null){
Log.e(TAG, "context is null");
@ -125,7 +126,11 @@ public class PlayerController {
}
public static void executeDownloadSegments(String videoId) {
SponsorSegment[] segments = Requester.getSegments(videoId);
videoHasSegments = false;
timeWithoutSegments = "";
if (Whitelist.isChannelSBWhitelisted())
return;
SponsorSegment[] segments = SBRequester.getSegments(videoId);
Arrays.sort(segments);
if (VERBOSE)
@ -283,7 +288,7 @@ public class PlayerController {
segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED &&
millis - segment.start < 2000) {
// Only skips from the start should count as a view
Requester.sendViewCountRequest(segment);
SBRequester.sendViewCountRequest(segment);
}
}).start();
}

View File

@ -1,17 +1,23 @@
package pl.jakubweg;
import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour;
import static fi.razerman.youtube.XGlobals.debug;
import static pl.jakubweg.SponsorBlockSettings.DEFAULT_API_URL;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_API_URL;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_BROWSER_BUTTON;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_MIN_DURATION;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_NEW_SEGMENT_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_UUID;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_VOTING_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_NAME;
import static pl.jakubweg.SponsorBlockSettings.adjustNewSegmentMillis;
import static pl.jakubweg.SponsorBlockSettings.countSkips;
import static pl.jakubweg.SponsorBlockSettings.minDuration;
import static pl.jakubweg.SponsorBlockSettings.setSeenGuidelines;
import static pl.jakubweg.SponsorBlockSettings.showTimeWithoutSegments;
import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically;
@ -21,29 +27,38 @@ import static pl.jakubweg.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.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
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 java.text.DecimalFormat;
import java.util.ArrayList;
import pl.jakubweg.requests.Requester;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.SharedPrefUtils;
import pl.jakubweg.objects.EditTextListPreference;
import pl.jakubweg.requests.SBRequester;
@SuppressWarnings({"unused", "deprecation"}) // injected
public class SponsorBlockPreferenceFragment 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<>();
@Override
@ -75,6 +90,18 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
});
}
// Clear hint
if (debug) {
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN);
preference.setDefaultValue(false);
preference.setChecked(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN));
preference.setTitle("Hint debug");
preference.setSummary("Debug toggle for clearing the hint shown preference");
preference.setOnPreferenceChangeListener((pref, newValue) -> true);
}
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
@ -122,7 +149,7 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
setSeenGuidelines(context);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/ajayyy/SponsorBlock/wiki/Guidelines"));
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
context.startActivity(intent);
}
@ -143,7 +170,6 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
preferencesToDisableWhenSBDisabled.add(category);
category.setTitle(str("diff_segments"));
String defaultValue = DefaultBehaviour.key;
SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values();
String[] entries = new String[segmentBehaviours.length];
String[] entryValues = new String[segmentBehaviours.length];
@ -156,33 +182,21 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
for (SponsorBlockSettings.SegmentInfo segmentInfo : categories) {
ListPreference preference = new ListPreference(context);
EditTextListPreference preference = new EditTextListPreference(context);
preference.setTitle(segmentInfo.getTitleWithDot());
preference.setSummary(segmentInfo.description.toString());
preference.setKey(segmentInfo.key);
preference.setDefaultValue(defaultValue);
preference.setDefaultValue(segmentInfo.behaviour.key);
preference.setEntries(entries);
preference.setEntryValues(entryValues);
category.addPreference(preference);
}
Preference colorPreference = new Preference(context);
Preference colorPreference = new Preference(context); // TODO remove this after the next major update
screen.addPreference(colorPreference);
colorPreference.setTitle(str("color_change"));
colorPreference.setOnPreferenceClickListener(preference1 -> {
CharSequence[] items = new CharSequence[categories.length];
for (int i = 0; i < items.length; i++) {
items[i] = categories[i].getTitleWithDot();
}
new AlertDialog.Builder(context)
.setTitle(str("color_choose_category"))
.setItems(items, SponsorBlockUtils.categoryColorChangeClickListener)
.show();
return true;
});
colorPreference.setSummary(str("color_change_sum"));
preferencesToDisableWhenSBDisabled.add(colorPreference);
}
@ -197,7 +211,7 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
category.addPreference(preference);
preference.setTitle(str("stats_loading"));
Requester.retrieveUserStats(category, preference);
SBRequester.retrieveUserStats(category, preference);
}
}
@ -278,9 +292,27 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
screen.addPreference(preference);
}
{
Preference preference = new SwitchPreference(context);
preference.setTitle(str("general_whitelisting"));
preference.setSummary(str("general_whitelisting_sum"));
preference.setKey(WhitelistType.SPONSORBLOCK.getPreferenceEnabledName());
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
Preference preference = new SwitchPreference(context);
preference.setTitle(str("general_browser_button"));
preference.setSummary(str("general_browser_button_sum"));
preference.setKey(PREFERENCES_KEY_BROWSER_BUTTON);
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
preference.setTitle(str("general_adjusting"));
preference.setSummary(str("general_adjusting_sum"));
preference.setKey(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP);
@ -289,6 +321,17 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
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.setKey(PREFERENCES_KEY_MIN_DURATION);
preference.setDefaultValue(String.valueOf(minDuration));
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new EditTextPreference(context);
preference.setTitle(str("general_uuid"));
@ -299,6 +342,31 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
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(SponsorBlockSettings.apiUrl);
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();
@ -315,6 +383,48 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
}
}
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();
SharedPreferences preferences = SponsorBlockSettings.getPreferences(context);
switch (which) {
case DialogInterface.BUTTON_NEUTRAL:
preferences.edit().putString(PREFERENCES_KEY_API_URL, DEFAULT_API_URL).apply();
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 {
preferences.edit().putString(PREFERENCES_KEY_API_URL, textAsString).apply();
Toast.makeText(applicationContext, str("api_url_changed"), Toast.LENGTH_SHORT).show();
}
}
break;
}
}
public void setEditTextRef(EditText editText) {
editTextRef = new WeakReference<>(editText);
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SponsorBlockSettings.update(getActivity());

View File

@ -22,7 +22,9 @@ public class SponsorBlockSettings {
public static final String PREFERENCES_KEY_COUNT_SKIPS = "count-skips";
public static final String PREFERENCES_KEY_UUID = "uuid";
public static final String PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP = "new-segment-step-accuracy";
public static final String PREFERENCES_KEY_MIN_DURATION = "sb-min-duration";
public static final String PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED = "sb-enabled";
public static final String PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN = "sb_hint_shown";
public static final String PREFERENCES_KEY_SEEN_GUIDELINES = "sb-seen-gl";
public static final String PREFERENCES_KEY_NEW_SEGMENT_ENABLED = "sb-new-segment-enabled";
public static final String PREFERENCES_KEY_VOTING_ENABLED = "sb-voting-enabled";
@ -30,8 +32,14 @@ public class SponsorBlockSettings {
public static final String PREFERENCES_KEY_SKIPPED_SEGMENTS_TIME = "sb-skipped-segments-time";
public static final String PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS = "sb-length-without-segments";
public static final String PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX = "_color";
public static final String PREFERENCES_KEY_BROWSER_BUTTON = "sb-browser-button";
public static final String PREFERENCES_KEY_IS_VIP = "sb-is-vip";
public static final String PREFERENCES_KEY_LAST_VIP_CHECK = "sb-last-vip-check";
public static final String PREFERENCES_KEY_API_URL = "sb-api-url";
public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.SKIP_AUTOMATICALLY;
public static final String DEFAULT_SERVER_URL = "https://sponsor.ajay.app";
public static final String DEFAULT_API_URL = DEFAULT_SERVER_URL + "/api/";
public static boolean isSponsorBlockEnabled = false;
public static boolean seenGuidelinesPopup = false;
@ -40,8 +48,12 @@ public class SponsorBlockSettings {
public static boolean showToastWhenSkippedAutomatically = true;
public static boolean countSkips = true;
public static boolean showTimeWithoutSegments = true;
public static boolean vip = false;
public static long lastVipCheck = 0;
public static int adjustNewSegmentMillis = 150;
public static float minDuration = 0f;
public static String uuid = "<invalid>";
public static String apiUrl = DEFAULT_API_URL;
public static String sponsorBlockUrlCategories = "[]";
public static int skippedSegments;
public static long skippedTime;
@ -100,9 +112,7 @@ public class SponsorBlockSettings {
SegmentBehaviour behaviour = null;
String value = preferences.getString(segment.key, null);
if (value == null)
behaviour = DefaultBehaviour;
else {
if (value != null) {
for (SegmentBehaviour possibleBehaviour : possibleBehaviours) {
if (possibleBehaviour.key.equals(value)) {
behaviour = possibleBehaviour;
@ -110,10 +120,13 @@ public class SponsorBlockSettings {
}
}
}
if (behaviour == null)
behaviour = DefaultBehaviour;
if (behaviour != null) {
segment.behaviour = behaviour;
}
else {
behaviour = segment.behaviour;
}
segment.behaviour = behaviour;
if (behaviour.showOnTimeBar && segment != SegmentInfo.UNSUBMITTED)
enabledCategories.add(segment.key);
}
@ -132,8 +145,19 @@ public class SponsorBlockSettings {
if (tmp1 != null)
adjustNewSegmentMillis = Integer.parseInt(tmp1);
String minTmp = preferences.getString(PREFERENCES_KEY_MIN_DURATION, null);
if (minTmp != null)
minDuration = Float.parseFloat(minTmp);
countSkips = preferences.getBoolean(PREFERENCES_KEY_COUNT_SKIPS, countSkips);
showTimeWithoutSegments = preferences.getBoolean(PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS, showTimeWithoutSegments);
vip = preferences.getBoolean(PREFERENCES_KEY_IS_VIP, false);
String vipCheckTmp = preferences.getString(PREFERENCES_KEY_LAST_VIP_CHECK, null);
if (vipCheckTmp != null)
lastVipCheck = Long.parseLong(vipCheckTmp);
apiUrl = preferences.getString(PREFERENCES_KEY_API_URL, DEFAULT_API_URL);
uuid = preferences.getString(PREFERENCES_KEY_UUID, null);
if (uuid == null) {
@ -179,13 +203,14 @@ public class SponsorBlockSettings {
}
public enum SegmentInfo {
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), null, 0xFF00d400),
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), null, 0xFF00ffff),
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), null, 0xFF0202ed),
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), null, 0xFFcc00ff),
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), null, 0xFFffff00),
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), null, 0xFFff9900),
PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), null, 0xFF008fd6),
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), DefaultBehaviour, 0xFF00d400),
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), DefaultBehaviour, 0xFF00ffff),
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), DefaultBehaviour, 0xFF0202ed),
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), DefaultBehaviour, 0xFFcc00ff),
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), DefaultBehaviour, 0xFFffff00),
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), DefaultBehaviour, 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"), SegmentBehaviour.IGNORE, 0xFF7300FF),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("skipped_unsubmitted"), StringRef.empty, SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFFFFFFF);
private static final SegmentInfo[] mValuesWithoutUnsubmitted = new SegmentInfo[]{
@ -195,7 +220,8 @@ public class SponsorBlockSettings {
INTERACTION,
SELF_PROMO,
MUSIC_OFFTOPIC,
PREVIEW
PREVIEW,
FILLER
};
private static final Map<String, SegmentInfo> mValuesMap = new HashMap<>(values().length);

View File

@ -10,21 +10,32 @@ import static pl.jakubweg.PlayerController.getLastKnownVideoTime;
import static pl.jakubweg.PlayerController.sponsorSegmentsOfCurrentVideo;
import static pl.jakubweg.SponsorBlockPreferenceFragment.FORMATTER;
import static pl.jakubweg.SponsorBlockPreferenceFragment.SAVED_TEMPLATE;
import static pl.jakubweg.SponsorBlockSettings.DEFAULT_API_URL;
import static pl.jakubweg.SponsorBlockSettings.DEFAULT_SERVER_URL;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_API_URL;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_IS_VIP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_LAST_VIP_CHECK;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_MIN_DURATION;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_UUID;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_NAME;
import static pl.jakubweg.SponsorBlockSettings.apiUrl;
import static pl.jakubweg.SponsorBlockSettings.countSkips;
import static pl.jakubweg.SponsorBlockSettings.getPreferences;
import static pl.jakubweg.SponsorBlockSettings.isSponsorBlockEnabled;
import static pl.jakubweg.SponsorBlockSettings.lastVipCheck;
import static pl.jakubweg.SponsorBlockSettings.minDuration;
import static pl.jakubweg.SponsorBlockSettings.showTimeWithoutSegments;
import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import static pl.jakubweg.SponsorBlockSettings.uuid;
import static pl.jakubweg.SponsorBlockSettings.vip;
import static pl.jakubweg.StringRef.str;
import static pl.jakubweg.requests.Requester.voteForSegment;
import static pl.jakubweg.requests.SBRequester.voteForSegment;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
@ -32,13 +43,11 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.text.Html;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@ -59,9 +68,10 @@ import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import fi.vanced.utils.SharedPrefUtils;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.objects.UserStats;
import pl.jakubweg.requests.Requester;
import pl.jakubweg.requests.SBRequester;
@SuppressWarnings({"LongLogTag"})
public abstract class SponsorBlockUtils {
@ -72,6 +82,7 @@ public abstract class SponsorBlockUtils {
public static boolean videoHasSegments = false;
public static String timeWithoutSegments = "";
private static final int sponsorBtnId = 1234;
private static final String LOCKED_COLOR = "#FFC83D";
public static final View.OnClickListener sponsorBlockBtnListener = v -> {
if (debug) {
Log.d(TAG, "Shield button clicked");
@ -212,21 +223,27 @@ public abstract class SponsorBlockUtils {
final SponsorSegment segment = sponsorSegmentsOfCurrentVideo[which];
final VoteOption[] voteOptions = VoteOption.values();
String[] items = new String[voteOptions.length];
CharSequence[] items = new CharSequence[voteOptions.length];
for (int i = 0; i < voteOptions.length; i++) {
items[i] = voteOptions[i].title;
VoteOption voteOption = voteOptions[i];
String title = voteOption.title;
if (vip && 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());
switch (voteOptions[which1]) {
VoteOption voteOption = voteOptions[which1];
switch (voteOption) {
case UPVOTE:
voteForSegment(segment, VoteOption.UPVOTE, appContext.get(), toastRunnable);
break;
case DOWNVOTE:
voteForSegment(segment, VoteOption.DOWNVOTE, appContext.get(), toastRunnable);
voteForSegment(segment, voteOption, appContext.get());
break;
case CATEGORY_CHANGE:
onNewCategorySelect(segment, context);
@ -235,40 +252,6 @@ public abstract class SponsorBlockUtils {
})
.show();
};
public static final DialogInterface.OnClickListener categoryColorChangeClickListener = (dialog, which) -> {
SponsorBlockSettings.SegmentInfo segmentInfo = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted()[which];
String key = segmentInfo.key + PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
Context context = ((AlertDialog) dialog).getContext();
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT);
editText.setText(formatColorString(segmentInfo.color));
Context applicationContext = context.getApplicationContext();
SharedPreferences preferences = SponsorBlockSettings.getPreferences(context);
new AlertDialog.Builder(context)
.setView(editText)
.setPositiveButton(str("change"), (dialog1, which1) -> {
try {
int color = Color.parseColor(editText.getText().toString());
segmentInfo.setColor(color);
Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show();
preferences.edit().putString(key, formatColorString(color)).apply();
}
catch (Exception ex) {
Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show();
}
})
.setNeutralButton(str("reset"), (dialog1, which1) -> {
int defaultColor = segmentInfo.defaultColor;
segmentInfo.setColor(defaultColor);
Toast.makeText(applicationContext, str("color_reset"), Toast.LENGTH_SHORT).show();
preferences.edit().putString(key, formatColorString(defaultColor)).apply();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
};
private static final Runnable submitRunnable = () -> {
messageToToast = null;
final String uuid = SponsorBlockSettings.uuid;
@ -281,7 +264,7 @@ public abstract class SponsorBlockUtils {
Log.e(TAG, "Unable to submit times, invalid parameters");
return;
}
Requester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
SBRequester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
} catch (Exception e) {
Log.e(TAG, "Unable to submit segment", e);
@ -401,7 +384,7 @@ public abstract class SponsorBlockUtils {
new AlertDialog.Builder(context)
.setTitle(str("new_segment_choose_category"))
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), toastRunnable, values[which].key))
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), values[which].key))
.show();
}
@ -416,7 +399,7 @@ public abstract class SponsorBlockUtils {
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);
SponsorBlockSettings.SegmentInfo.UNSUBMITTED, null, false);
Arrays.sort(segments);
sponsorSegmentsOfCurrentVideo = segments;
@ -485,15 +468,6 @@ public abstract class SponsorBlockUtils {
}
}
public static int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
if (seq.charAt(i) == c)
count++;
}
return count;
}
public static String formatColorString(int color) {
return String.format("#%06X", color);
}
@ -514,7 +488,7 @@ public abstract class SponsorBlockUtils {
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, newUsername) -> {
appContext = new WeakReference<>(context.getApplicationContext());
Requester.setUsername((String) newUsername, toastRunnable);
SBRequester.setUsername((String) newUsername, preference, toastRunnable);
return false;
});
}
@ -597,7 +571,17 @@ public abstract class SponsorBlockUtils {
editor.putBoolean(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP, !settingsJson.getBoolean("dontShowNotice"));
editor.putBoolean(PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS, settingsJson.getBoolean("showTimeWithSkips"));
editor.putBoolean(PREFERENCES_KEY_COUNT_SKIPS, settingsJson.getBoolean("trackViewCount"));
editor.putBoolean(PREFERENCES_KEY_IS_VIP, settingsJson.getBoolean("isVip"));
editor.putString(PREFERENCES_KEY_MIN_DURATION, settingsJson.getString("minDuration"));
editor.putString(PREFERENCES_KEY_UUID, settingsJson.getString("userID"));
editor.putString(PREFERENCES_KEY_LAST_VIP_CHECK, settingsJson.getString("lastIsVipUpdate"));
String serverAddress = settingsJson.getString("serverAddress");
if (serverAddress.equalsIgnoreCase(DEFAULT_SERVER_URL)) {
serverAddress = DEFAULT_API_URL;
}
editor.putString(PREFERENCES_KEY_API_URL, serverAddress);
editor.apply();
Toast.makeText(context, str("settings_import_successful"), Toast.LENGTH_SHORT).show();
@ -633,9 +617,18 @@ public abstract class SponsorBlockUtils {
json.put("dontShowNotice", !showToastWhenSkippedAutomatically);
json.put("barTypes", barTypesObject);
json.put("showTimeWithSkips", showTimeWithoutSegments);
json.put("minDuration", minDuration);
json.put("trackViewCount", countSkips);
json.put("categorySelections", categorySelectionsArray);
json.put("userID", uuid);
json.put("isVip", vip);
json.put("lastIsVipUpdate", lastVipCheck);
String apiAddress = apiUrl;
if (apiAddress.equalsIgnoreCase(DEFAULT_API_URL)) {
apiAddress = DEFAULT_SERVER_URL;
}
json.put("serverAddress", apiAddress);
return json.toString();
}
@ -650,15 +643,22 @@ public abstract class SponsorBlockUtils {
return isSponsorBlockEnabled && setting;
}
public static boolean isSBButtonEnabled(Context context, String key) {
return isSettingEnabled(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, key, false));
}
public enum VoteOption {
UPVOTE(str("vote_upvote")),
DOWNVOTE(str("vote_downvote")),
CATEGORY_CHANGE(str("vote_category"));
UPVOTE(str("vote_upvote"), false),
DOWNVOTE(str("vote_downvote"), true),
CATEGORY_CHANGE(str("vote_category"), true);
public final String title;
public final boolean shouldHighlight;
VoteOption(String title) {
VoteOption(String title, boolean shouldHighlight) {
this.title = title;
this.shouldHighlight = shouldHighlight;
}
}

View File

@ -0,0 +1,123 @@
package pl.jakubweg.objects;
import static pl.jakubweg.SponsorBlockUtils.formatColorString;
import static pl.jakubweg.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 pl.jakubweg.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.PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
}
private void reformatTitle() {
this.setTitle(getCategoryBySelf().getTitleWithDot());
}
}

View File

@ -7,12 +7,14 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
public final long end;
public final SponsorBlockSettings.SegmentInfo category;
public final String UUID;
public final boolean isLocked;
public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID) {
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;
}
@Override
@ -21,6 +23,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
"start=" + start +
", end=" + end +
", category='" + category + '\'' +
", locked=" + isLocked +
'}';
}

View File

@ -1,207 +0,0 @@
package pl.jakubweg.requests;
import static pl.jakubweg.SponsorBlockUtils.timeWithoutSegments;
import static pl.jakubweg.SponsorBlockUtils.videoHasSegments;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import pl.jakubweg.SponsorBlockSettings;
import pl.jakubweg.SponsorBlockUtils;
import pl.jakubweg.SponsorBlockUtils.VoteOption;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.objects.UserStats;
public class Requester {
private static final String SPONSORBLOCK_API_URL = "https://sponsor.ajay.app/api/";
private static final String TIME_TEMPLATE = "%.3f";
private Requester() {}
public static synchronized SponsorSegment[] getSegments(String videoId) {
List<SponsorSegment> segments = new ArrayList<>();
try {
HttpURLConnection connection = getConnectionFromRoute(Route.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
int responseCode = connection.getResponseCode();
videoHasSegments = false;
timeWithoutSegments = "";
if (responseCode == 200) {
JSONArray responseArray = new JSONArray(parseJson(connection));
int length = responseArray.length();
for (int i = 0; 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);
String category = obj.getString("category");
String uuid = obj.getString("UUID");
SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category);
if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) {
SponsorSegment sponsorSegment = new SponsorSegment(start, end, segmentCategory, uuid);
segments.add(sponsorSegment);
}
}
videoHasSegments = true;
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
}
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
return segments.toArray(new SponsorSegment[0]);
}
public static void submitSegments(String videoId, String uuid, float startTime, float endTime, String category, Runnable toastRunnable) {
try {
String start = String.format(Locale.US, TIME_TEMPLATE, startTime);
String end = String.format(Locale.US, TIME_TEMPLATE, endTime);
HttpURLConnection connection = getConnectionFromRoute(Route.SUBMIT_SEGMENTS, videoId, uuid, start, end, category);
int responseCode = connection.getResponseCode();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("submit_succeeded");
break;
case 409:
SponsorBlockUtils.messageToToast = str("submit_failed_duplicate");
break;
case 403:
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden");
break;
case 429:
SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit");
break;
default:
SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void sendViewCountRequest(SponsorSegment segment) {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.VIEWED_SEGMENT, segment.UUID);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, Runnable toastRunnable, String... args) {
try {
String segmentUuid = segment.UUID;
String uuid = SponsorBlockSettings.uuid;
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show();
HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE
? getConnectionFromRoute(Route.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0])
: getConnectionFromRoute(Route.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote);
int responseCode = connection.getResponseCode();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("vote_succeeded");
break;
case 403:
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden");
break;
default:
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) {
if (!SponsorBlockSettings.isSponsorBlockEnabled) {
loadingPreference.setTitle(str("stats_sb_disabled"));
return;
}
new Thread(() -> {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.GET_USER_STATS, SponsorBlockSettings.uuid);
JSONObject json = new JSONObject(parseJson(connection));
connection.disconnect();
UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"),
json.getInt("viewCount"));
SponsorBlockUtils.addUserStats(category, loadingPreference, stats);
}
catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
public static void setUsername(String username, Runnable toastRunnable) {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.CHANGE_USERNAME, SponsorBlockSettings.uuid, username);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
SponsorBlockUtils.messageToToast = str("stats_username_changed");
}
else {
SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage());
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
String url = SPONSORBLOCK_API_URL + route.compile(params).getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(route.getMethod().name());
return connection;
}
private static String parseJson(HttpURLConnection connection) throws IOException {
StringBuilder jsonBuilder = new StringBuilder();
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line);
}
inputStream.close();
return jsonBuilder.toString();
}
}

View File

@ -1,70 +0,0 @@
package pl.jakubweg.requests;
import static pl.jakubweg.requests.Route.Method.GET;
import static pl.jakubweg.requests.Route.Method.POST;
import pl.jakubweg.SponsorBlockUtils;
public class Route {
public static final Route GET_SEGMENTS = new Route(GET, "skipSegments?videoID={video_id}&categories={categories}");
public static final Route VIEWED_SEGMENT = new Route(POST, "viewedVideoSponsorTime?UUID={segment_id}");
public static final Route GET_USER_STATS = new Route(GET, "userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
public static final Route CHANGE_USERNAME = new Route(POST, "setUsername?userID={user_id}&username={username}");
public static final Route SUBMIT_SEGMENTS = new Route(POST, "skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}");
public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
private final String route;
private final Method method;
private final int paramCount;
private Route(Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = SponsorBlockUtils.countMatches(route, '{');
if (paramCount != SponsorBlockUtils.countMatches(route, '}'))
throw new IllegalArgumentException("Not enough parameters");
}
public Method getMethod() {
return method;
}
public CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
StringBuilder compiledRoute = new StringBuilder(route);
for (int i = 0; i < paramCount; i++) {
int paramStart = compiledRoute.indexOf("{");
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
return new CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
private final Route baseRoute;
private final String compiledRoute;
private CompiledRoute(Route baseRoute, String compiledRoute) {
this.baseRoute = baseRoute;
this.compiledRoute = compiledRoute;
}
public String getCompiledRoute() {
return compiledRoute;
}
public Method getMethod() {
return baseRoute.method;
}
}
public enum Method {
GET,
POST
}
}

View File

@ -0,0 +1,236 @@
package pl.jakubweg.requests;
import static android.text.Html.fromHtml;
import static fi.vanced.utils.VancedUtils.runOnMainThread;
import static pl.jakubweg.SponsorBlockUtils.timeWithoutSegments;
import static pl.jakubweg.SponsorBlockUtils.videoHasSegments;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
import pl.jakubweg.PlayerController;
import pl.jakubweg.SponsorBlockSettings;
import pl.jakubweg.SponsorBlockUtils;
import pl.jakubweg.SponsorBlockUtils.VoteOption;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.objects.UserStats;
public class SBRequester {
private static final String TIME_TEMPLATE = "%.3f";
private SBRequester() {}
public static synchronized SponsorSegment[] getSegments(String videoId) {
List<SponsorSegment> segments = new ArrayList<>();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
int responseCode = connection.getResponseCode();
runVipCheck();
if (responseCode == 200) {
JSONArray responseArray = Requester.getJSONArray(connection);
int length = responseArray.length();
for (int i = 0; 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);
long minDuration = (long) (SponsorBlockSettings.minDuration * 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);
}
}
if (!segments.isEmpty()) {
videoHasSegments = true;
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
}
}
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
return segments.toArray(new SponsorSegment[0]);
}
public static void submitSegments(String videoId, String uuid, float startTime, float endTime, String category, Runnable toastRunnable) {
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();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("submit_succeeded");
break;
case 409:
SponsorBlockUtils.messageToToast = str("submit_failed_duplicate");
break;
case 403:
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJson(connection));
break;
case 429:
SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit");
break;
default:
SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
runOnMainThread(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void sendViewCountRequest(SponsorSegment segment) {
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, String... args) {
new Thread(() -> {
try {
String segmentUuid = segment.UUID;
String uuid = SponsorBlockSettings.uuid;
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
runOnMainThread(() -> Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show());
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();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("vote_succeeded");
break;
case 403:
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJson(connection));
break;
default:
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
runOnMainThread(() -> Toast.makeText(context, SponsorBlockUtils.messageToToast, Toast.LENGTH_LONG).show());
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) {
if (!SponsorBlockSettings.isSponsorBlockEnabled) {
loadingPreference.setTitle(str("stats_sb_disabled"));
return;
}
new Thread(() -> {
try {
JSONObject json = getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.uuid);
UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"),
json.getInt("viewCount"));
SponsorBlockUtils.addUserStats(category, loadingPreference, stats);
}
catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
public static void setUsername(String username, EditTextPreference preference, Runnable toastRunnable) {
new Thread(() -> {
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.uuid, 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) {
ex.printStackTrace();
}
}).start();
}
public static void runVipCheck() {
long now = System.currentTimeMillis();
if (now < (SponsorBlockSettings.lastVipCheck + TimeUnit.DAYS.toMillis(3))) {
return;
}
try {
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SponsorBlockSettings.uuid);
boolean vip = json.getBoolean("vip");
SponsorBlockSettings.vip = vip;
SponsorBlockSettings.lastVipCheck = now;
SharedPreferences.Editor edit = SponsorBlockSettings.getPreferences(YouTubeTikTokRoot_Application.getAppContext()).edit();
edit.putString(SponsorBlockSettings.PREFERENCES_KEY_LAST_VIP_CHECK, String.valueOf(now));
edit.putBoolean(SponsorBlockSettings.PREFERENCES_KEY_IS_VIP, vip);
edit.apply();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(SponsorBlockSettings.apiUrl, route, params);
}
private static JSONObject getJSONObject(Route route, String... params) throws Exception {
return Requester.getJSONObject(getConnectionFromRoute(route, params));
}
}

View File

@ -0,0 +1,19 @@
package pl.jakubweg.requests;
import static fi.vanced.utils.requests.Route.Method.GET;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class SBRoutes {
public static final Route IS_USER_VIP = new Route(GET, "isUserVIP?userID={user_id}");
public static final Route GET_SEGMENTS = new Route(GET, "skipSegments?videoID={video_id}&categories={categories}");
public static final Route VIEWED_SEGMENT = new Route(POST, "viewedVideoSponsorTime?UUID={segment_id}");
public static final Route GET_USER_STATS = new Route(GET, "userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
public static final Route CHANGE_USERNAME = new Route(POST, "setUsername?userID={user_id}&username={username}");
public static final Route SUBMIT_SEGMENTS = new Route(POST, "skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}&videoDuration={duration}");
public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
private SBRoutes() {}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="vanced_button_location_entries">
<item>@string/vanced_button_location_entry_none</item>
<item>@string/vanced_button_location_entry_player</item>
<item>@string/vanced_button_location_entry_buttoncontainer</item>
<item>@string/vanced_button_location_entry_both</item>
</string-array>
<string-array name="vanced_button_location_entry_values">
<item>NONE</item>
<item>PLAYER</item>
<item>BUTTON_CONTAINER</item>
<item>BOTH</item>
</string-array>
</resources>

View File

@ -151,10 +151,14 @@
<string name="general_skipcount_sum">This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment</string>
<string name="general_adjusting">Adjusting new segment step</string>
<string name="general_adjusting_sum">This is the number of milliseconds you can move when you use the time adjustment buttons while adding new segment</string>
<string name="general_min_duration">Minimum segment duration</string>
<string name="general_min_duration_sum">Segments shorter than the set value (in seconds) will not be skipped or show in the player</string>
<string name="general_uuid">Your unique user id</string>
<string name="general_uuid_sum">This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you</string>
<string name="settings_ie">Import/Export settings</string>
<string name="settings_ie_sum">This is your entire configuration that is applicable in the desktop extension in JSON. This includes your userID, so be sure to share this wisely.</string>
<string name="general_api_url">Change API URL</string>
<string name="general_api_url_sum">The address SponsorBlock uses to make calls to the server. &lt;b>Don\'t change this unless you know what you\'re doing.&lt;/b></string>
<string name="settings_import_successful">Settings were successfully imported</string>
<string name="settings_import_failed">Failed to import settings</string>
<string name="settings_export_failed">Failed to export settings</string>
@ -173,6 +177,8 @@
<string name="segments_selfpromo_sum">Similar to "sponsor" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with</string>
<string name="segments_nomusic">Music: Non-Music Section</string>
<string name="segments_nomusic_sum">Only for use in music videos. This includes introductions or outros in music videos</string>
<string name="segments_filler">Filler Tangent/Jokes</string>
<string name="segments_filler_sum">Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include segments providing context or background details</string>
<string name="skipped_segment">Skipped a sponsor segment</string>
<string name="skipped_sponsor">Skipped sponsor</string>
<string name="skipped_intermission">Skipped intro</string>
@ -181,6 +187,7 @@
<string name="skipped_selfpromo">Skipped self promotion</string>
<string name="skipped_nomusic">Skipped silence</string>
<string name="skipped_preview">Skipped preview</string>
<string name="skipped_filler">Skipped filler</string>
<string name="skipped_unsubmitted">Skipped unsubmitted segment</string>
<string name="skip_automatically">Skip automatically</string>
<string name="skip_showbutton">Show a skip button</string>
@ -193,14 +200,14 @@
<string name="submit_failed_unknown_error" formatted="false">Unable to submit segments: Status: %d %s</string>
<string name="submit_failed_rate_limit">Can\'t submit the segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="submit_failed_forbidden">Can\'t submit the segment.\nRejected by auto moderator</string>
<string name="submit_failed_forbidden" formatted="false">Can\'t submit the segment.\n\n%s</string>
<string name="submit_failed_duplicate">Can\'t submit the segment.\nAlready exists</string>
<string name="submit_succeeded">Segment submitted successfully</string>
<string name="submit_started">Submitting segment…</string>
<string name="vote_failed_unknown_error" formatted="false">Unable to vote for segment: Status: %d %s</string>
<string name="vote_failed_rate_limit">Can\'t vote for segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="vote_failed_forbidden">Can\'t vote for segment.\nA moderator has decided that this segment is correct</string>
<string name="vote_failed_forbidden" formatted="false">Can\'t vote for segment.\n\n%s</string>
<string name="vote_succeeded">Voted successfully</string>
<string name="vote_started">Voting for segment…</string>
<string name="vote_upvote">Upvote</string>
@ -311,14 +318,18 @@
<string name="general_time_without_sb">Show time without segments</string>
<string name="general_time_without_sb_sum">This time appears in brackets next to the current time. This shows the total video duration minus any segments.</string>
<string name="general_whitelisting">Channel whitelisting</string>
<string name="general_whitelisting_sum">Use the Segments button under the player to whitelist a channel</string>
<string name="general_browser_button">Enable SB Browser button</string>
<string name="general_browser_button_sum">Clicking this button under the player will open sb.ltn.fi where you can see more details about segments.</string>
<string name="segments_preview">Preview/Recap</string>
<string name="segments_preview_sum">Quick recap of previous episodes, or a preview of what\'s coming up later in the current video. Meant for edited together clips, not for spoken summaries.</string>
<string name="stats">Stats</string>
<string name="stats_loading">Loading..</string>
<string name="stats_sb_disabled">SponsorBlock is disabled</string>
<string name="stats_username">Your username: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_username" formatted="false">Your username: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_username_change">Click to change your username</string>
<string name="stats_username_change_unknown_error">Unable to change username: Status: %d %s</string>
<string name="stats_username_change_unknown_error" formatted="false">Unable to change username: Status: %d %s</string>
<string name="stats_username_changed">Username successfully changed</string>
<string name="stats_submissions">Submissions: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_saved">You\'ve saved people from &lt;b&gt;%s&lt;/b&gt; segments.</string>
@ -326,11 +337,56 @@
<string name="stats_self_saved">You\'ve skipped &lt;b&gt;%s&lt;/b&gt; segments.</string>
<string name="stats_self_saved_sum">That\'s &lt;b&gt;%s&lt;/b&gt;.</string>
<string name="minutes">minutes</string>
<string name="color_change">Change colors</string>
<string name="color_change">Are you looking for changing colors?</string>
<string name="color_change_sum">You can now change a category\'s color by clicking on it above.</string>
<string name="color_choose_category">Choose the category</string>
<string name="color_changed">Color changed</string>
<string name="color_reset">Color reset</string>
<string name="color_invalid">Invalid hex code</string>
<string name="change">Change</string>
<string name="reset">Reset</string>
<string name="action_copy">Copy link</string>
<string name="action_tcopy">Timestamp</string>
<string name="action_ads">Ads</string>
<string name="action_segments">Segments</string>
<string name="action_browser">SB Browser</string>
<string name="api_url_changed">API URL changed</string>
<string name="api_url_reset">API URL reset</string>
<string name="api_url_invalid">Provided API URL is invalid</string>
<string name="vanced_video_ad_settings_title">Video ad settings</string>
<string name="vanced_videoadwhitelisting_title">Video ad whitelisting</string>
<string name="vanced_videoadwhitelisting_summary_off">Video ad whitelisting is turned off</string>
<string name="vanced_videoadwhitelisting_summary_on">Video ad whitelisting is turned on. Use the Ads button under the player to whitelist a channel</string>
<string name="vanced_whitelisting_ads">Ads</string>
<string name="vanced_whitelisting_sponsorblock">SponsorBlock</string>
<string name="vanced_whitelisting_added" formatted="false">Channel %s was added to the %s whitelist</string>
<string name="vanced_whitelisting_removed" formatted="false">Channel %s was removed from the %s whitelist</string>
<string name="vanced_whitelisting_add_failed" formatted="false">Failed to add channel %s to the %s whitelist</string>
<string name="vanced_whitelisting_remove_failed" formatted="false">Failed to remove channel %s from the %s whitelist</string>
<string name="vanced_whitelisting_fetch_failed" formatted="false">Failed to retrieve channel details, received code %d</string>
<string name="vanced_button_location_entry_none">Hidden</string>
<string name="vanced_button_location_entry_player">In player</string>
<string name="vanced_button_location_entry_buttoncontainer">Under player (ALPHA)</string>
<string name="vanced_button_location_entry_both">Both</string>
<string name="vanced_ryd_settings_title">Return YouTube Dislike settings</string>
<string name="vanced_ryd_settings_summary">Uses the RYD API</string>
<string name="vanced_ryd_title">Enable RYD (ALPHA)</string>
<string name="vanced_ryd_summary">Switch this on to see the dislike counts again</string>
<string name="vanced_ryd_attribution_title">Return YouTube Dislike Integration</string>
<string name="vanced_ryd_attribution_summary">This integration uses the RYD API from https://returnyoutubedislike.com. Tap to learn more</string>
<string name="xfile_xfenster_tablet_title">Tablet style</string>
<string name="xfile_xfenster_tablet_summary_on">Tablet style is turned on. For example suggested videos are only partially working</string>
<string name="xfile_xfenster_tablet_summary_off">Tablet style is turned off</string>
<string name="vanced_ryd">Return YouTube Dislike</string>
<string name="vanced_ryd_firstrun">Want to enable Return YouTube Dislikes to see dislikes again? Your likes/dislikes will be sent to RYD API (anonymously) after enabling RYD integration. You can enable/disable this in the settings at any time.</string>
<string name="vanced_sb">SponsorBlock</string>
<string name="vanced_sb_firstrun">Are you aware of the SponsorBlock integration in Vanced? With it you can skip sponsored segments in the videos. You can enable/disable this in the settings at any time.</string>
<string name="vanced_learnmore">Learn more</string>
<string name="vanced_disable">Disable</string>
<string name="vanced_enable">Enable</string>
</resources>

View File

@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0'
classpath 'com.android.tools.build:gradle:7.1.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -1,6 +1,6 @@
#Mon Jun 07 19:51:48 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME