diff --git a/app/build.gradle b/app/build.gradle index a08ece6..f6f39a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,12 +5,19 @@ android { buildToolsVersion "30.0.2" defaultConfig { - applicationId "pl.jakubweg" + applicationId "vanced.integrations" minSdkVersion 21 targetSdkVersion 30 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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 430ba65..7609a74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ + package="vanced.integrations"> \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java b/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java deleted file mode 100644 index 161e26d..0000000 --- a/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java +++ /dev/null @@ -1,237 +0,0 @@ -package fi.vanced.libraries.youtube.ads; - -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.utils.VancedUtils.getPreferences; -import static fi.vanced.utils.VancedUtils.parseJson; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; - -import org.json.JSONObject; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.Iterator; - -import fi.razerman.youtube.XGlobals; -import fi.vanced.libraries.youtube.player.ChannelModel; -import fi.vanced.libraries.youtube.player.VideoInformation; -import fi.vanced.utils.ObjectSerializer; -import fi.vanced.utils.SharedPrefUtils; - -public class VideoAds { - public static final String TAG = "VI - VideoAds"; - public static final String PREFERENCES_NAME = "channel-whitelist"; - public static boolean isEnabled; - private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1"; - private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey"; - private static ArrayList whiteList; - private static Thread fetchThread = null; - - static { - whiteList = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext()); - isEnabled = SharedPrefUtils.getBoolean(YouTubeTikTokRoot_Application.getAppContext(), "youtube", "vanced_videoadwhitelisting_enabled", false); - } - - // Call to this needs to be injected in YT code - public static void setChannelName(String channelName) { - if (debug) { - Log.d(TAG, "channel name set to " + channelName); - } - VideoInformation.channelName = channelName; - - if (!isEnabled) return; - - if (adBlockButton != null) { - adBlockButton.changeEnabled(getShouldShowAds()); - } - } - - // Call to this needs to be injected in YT code (CURRENTLY NOT USED) - public static void newVideoLoaded(String videoId) { - if (debug) { - Log.d(TAG, "newVideoLoaded - " + videoId); - } - - try { - if (fetchThread != null && fetchThread.getState() != Thread.State.TERMINATED) { - if (debug) { - Log.d(TAG, "Interrupting the thread. Current state " + fetchThread.getState()); - } - fetchThread.interrupt(); - } - } - catch (Exception ex) { - Log.e(TAG, "Error in the fetch thread", ex); - } - - fetchThread = new Thread(() -> { - try { - if (debug) { - Log.d(TAG, "Fetching channelId for " + videoId); - } - HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json; utf-8"); - connection.setRequestProperty("Accept", "application/json"); - connection.setDoOutput(true); - connection.setConnectTimeout(2 * 1000); - - // TODO: Actually fetch the version - String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + videoId + "\"}"; - try(OutputStream os = connection.getOutputStream()) { - byte[] input = jsonInputString.getBytes("utf-8"); - os.write(input, 0, input.length); - } - if (connection.getResponseCode() == 200) { - JSONObject json = new JSONObject(parseJson(connection)); - JSONObject videoInfo = json.getJSONObject("videoDetails"); - ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId")); - if (debug) { - Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor()); - } - } - else if (debug) { - Log.d(TAG, "player fetch response was " + connection.getResponseCode()); - } - } - catch (Exception ex) { - Log.e(TAG, "Failed to fetch channelId", ex); - return; - } - }); - - fetchThread.start(); - } - - public static boolean getShouldShowAds() { - if (!isEnabled) return false; - - if (channelName == null || channelName.isEmpty() || channelName.trim().isEmpty()) { - if (XGlobals.debug) { - Log.d(TAG, "getShouldShowAds skipped because channelId was null"); - } - - return false; - } - - for (ChannelModel channelModel: whiteList) { - if (channelModel.getAuthor().equals(channelName)) { - if (XGlobals.debug) { - Log.d(TAG, "Video ad whitelist for " + channelName); - } - - return true; - } - } - - return false; - } - - public static boolean addToWhitelist(Context context, String channelName, String channelId) { - try { - // Check that the channel doesn't exist already (can happen if for example the channel changes the name) - // If it exists, remove it - Iterator iterator = whiteList.iterator(); - while(iterator.hasNext()) - { - ChannelModel value = iterator.next(); - if (value.getChannelId().equals(channelId)) - { - if (XGlobals.debug) { - Log.d(TAG, String.format("Tried whitelisting an existing channel again. Old info (%1$s | %2$s) - New info (%3$s | %4$s)", - value.getAuthor(), value.getChannelId(), channelName, channelId)); - } - iterator.remove(); - break; - } - } - - whiteList.add(new ChannelModel(channelName, channelId)); - updateWhitelist(context); - return true; - } - catch (Exception ex) { - Log.d(TAG, "Unable to add " + channelName + " with id " + channelId + " to whitelist"); - } - - return false; - } - - public static boolean removeFromWhitelist(Context context, String channelName) { - try { - //whiteList.removeIf(x -> x.getAuthor().equals(channelName)); // Requires Android N - - Iterator iterator = whiteList.iterator(); - while(iterator.hasNext()) - { - ChannelModel value = iterator.next(); - if (value.getAuthor().equals(channelName)) - { - iterator.remove(); - break; - } - } - updateWhitelist(context); - return true; - } - catch (Exception ex) { - Log.d(TAG, "Unable to remove " + channelName + " from whitelist"); - } - - return false; - } - - private static void updateWhitelist(Context context) { - if (context == null) return; - - SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); - SharedPreferences.Editor editor = preferences.edit(); - - try { - editor.putString("channels", ObjectSerializer.serialize(whiteList)); - } catch (IOException e) { - e.printStackTrace(); - } - - editor.apply(); - } - - private static ArrayList parseWhitelist(Context context) { - if (context == null) return new ArrayList<>(); - - SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); - try { - String channels = preferences.getString("channels", null); - if (channels == null) { - if (debug) { - Log.d(TAG, "channels string was null for ad whitelisting"); - } - - return new ArrayList<>(); - } - - ArrayList channelModels = (ArrayList) ObjectSerializer.deserialize(channels); - if (debug) { - Log.d(TAG, channels); - for (ChannelModel channelModel: channelModels) { - Log.d(TAG, "Ad whitelisted " + channelModel.getAuthor() + " with id of " + channelModel.getChannelId()); - } - } - - return channelModels; - } catch (IOException e) { - e.printStackTrace(); - } - - return new ArrayList<>(); - } -} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/dialog/Dialogs.java b/app/src/main/java/fi/vanced/libraries/youtube/dialog/Dialogs.java new file mode 100644 index 0000000..c22fc15 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/dialog/Dialogs.java @@ -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); + }); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDFragment.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDFragment.java index 93b1cc9..617e1c1 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDFragment.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDFragment.java @@ -1,6 +1,8 @@ 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; @@ -44,6 +46,18 @@ public class RYDFragment extends PreferenceFragment { }); } + // 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); } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDSettings.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDSettings.java index fda4808..aa436a0 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDSettings.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/RYDSettings.java @@ -4,4 +4,5 @@ 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"; } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java index 9f2edd2..7d40835 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java @@ -4,19 +4,13 @@ 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.parseJson; import static fi.vanced.utils.VancedUtils.randomString; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; -import org.json.JSONObject; - -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import fi.vanced.libraries.youtube.ryd.requests.RYDRequester; public class Registration { private static final String TAG = "VI - RYD - Registration"; @@ -50,7 +44,7 @@ public class Registration { return this.userId; } - private void saveUserId(String userId) { + public void saveUserId(String userId) { try { if (this.context == null) throw new Exception("Unable to save userId because context was null"); @@ -64,87 +58,10 @@ public class Registration { } private String register() { - try { - // Generate a new userId - String userId = randomString(36); - if (debug) { - Log.d(TAG, "Trying to register the following userId: " + userId); - } - - // Get the registration challenge - HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection(); - connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); - connection.setConnectTimeout(5 * 1000); - if (connection.getResponseCode() == 200) { - JSONObject json = new JSONObject(parseJson(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); - } - else if (debug) { - Log.d(TAG, "Registration response was " + connection.getResponseCode()); - } + String userId = randomString(36); + if (debug) { + Log.d(TAG, "Trying to register the following userId: " + userId); } - catch (Exception ex) { - Log.e(TAG, "Failed to register userId", ex); - } - - return null; - } - - public String confirmRegistration(String userId, String solution) { - try { - if (debug) { - Log.d(TAG, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution); - } - - // Confirm registration - HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection(); - confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); - confirmationCon.setRequestMethod("POST"); - confirmationCon.setRequestProperty("Content-Type", "application/json"); - confirmationCon.setRequestProperty("Accept", "application/json"); - confirmationCon.setDoOutput(true); - confirmationCon.setConnectTimeout(5 * 1000); - - String jsonInputString = "{\"solution\": \"" + solution + "\"}"; - try(OutputStream os = confirmationCon.getOutputStream()) { - byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); - os.write(input, 0, input.length); - } - if (confirmationCon.getResponseCode() == 200) { - String result = parseJson(confirmationCon); - if (debug) { - Log.d(TAG, "Registration confirmation result was " + result); - } - - if (result.equalsIgnoreCase("true")) { - 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 " + confirmationCon.getResponseCode()); - } - } - catch (Exception ex) { - Log.e(TAG, "Failed to confirm registration", ex); - } - - return null; + return RYDRequester.register(userId, this); } } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java index e182953..d8f2a1e 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java @@ -2,37 +2,28 @@ 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 static fi.vanced.utils.VancedUtils.parseJson; import android.content.Context; import android.icu.text.CompactDecimalFormat; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.TextView; -import android.widget.Toast; import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; import java.util.Locale; -import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount; - +import fi.vanced.libraries.youtube.ryd.requests.RYDRequester; import fi.vanced.utils.SharedPrefUtils; public class ReturnYouTubeDislikes { - public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com"; public static boolean isEnabled; - private static final String TAG = "VI - RYD"; + public static final String TAG = "VI - RYD"; private static View _dislikeView = null; private static Thread _dislikeFetchThread = null; private static Thread _votingThread = null; @@ -96,39 +87,7 @@ public class ReturnYouTubeDislikes { Log.e(TAG, "Error in the dislike fetch thread", ex); } - _dislikeFetchThread = new Thread(() -> { - try { - if (debug) { - Log.d(TAG, "Fetching dislikes for " + videoId); - } - HttpURLConnection connection = (HttpURLConnection) new URL(RYD_API_URL + "/votes?videoId=" + videoId).openConnection(); - connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); - connection.setConnectTimeout(5 * 1000); - if (connection.getResponseCode() == 200) { - JSONObject json = new JSONObject(parseJson(connection)); - dislikeCount = json.getInt("dislikes"); - if (debug) { - Log.d(TAG, "dislikes fetched - " + dislikeCount); - } - - // Set the dislikes - new Handler(Looper.getMainLooper()).post(new Runnable () { - @Override - public void run () { - trySetDislikes(formatDislikes(dislikeCount)); - } - }); - } - else if (debug) { - Log.d(TAG, "dislikes fetch response was " + connection.getResponseCode()); - } - } - catch (Exception ex) { - dislikeCount = null; - Log.e(TAG, "Failed to fetch dislikes", ex); - return; - } - }); + _dislikeFetchThread = new Thread(() -> RYDRequester.fetchDislikes(videoId)); _dislikeFetchThread.start(); } @@ -211,7 +170,7 @@ public class ReturnYouTubeDislikes { return originalText; } - private static void trySetDislikes(String dislikeCount) { + public static void trySetDislikes(String dislikeCount) { if (!isEnabled) return; try { @@ -280,8 +239,6 @@ public class ReturnYouTubeDislikes { Log.d(TAG, "Like button " + likeActive + " | Dislike button " + dislikeActive); } - Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Voting value: " + votingValue, Toast.LENGTH_SHORT).show(); - sendVote(votingValue); } catch (Exception ex) { @@ -345,7 +302,7 @@ public class ReturnYouTubeDislikes { } } - private static String formatDislikes(int dislikes) { + 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) { diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java index 9ee92c2..955d9b3 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java @@ -1,13 +1,14 @@ package fi.vanced.libraries.youtube.ryd; -import android.util.Log; import android.util.Base64; +import android.util.Log; + import java.security.MessageDigest; public class Utils { private static final String TAG = "VI - RYD - Utils"; - static String solvePuzzle(String challenge, int difficulty) { + public static String solvePuzzle(String challenge, int difficulty) { byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP); byte[] buffer = new byte[20]; diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java index fd53bf5..f023b42 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java @@ -1,17 +1,11 @@ package fi.vanced.libraries.youtube.ryd; import static fi.razerman.youtube.XGlobals.debug; -import static fi.vanced.utils.VancedUtils.parseJson; import android.content.Context; import android.util.Log; -import org.json.JSONObject; - -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import fi.vanced.libraries.youtube.ryd.requests.RYDRequester; public class Voting { private static final String TAG = "VI - RYD - Voting"; @@ -25,96 +19,10 @@ public class Voting { } public boolean sendVote(String videoId, int vote) { - try { - String userId = registration.getUserId(); - if (debug) { - Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId); - } - - // Send the vote - HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/vote").openConnection(); - 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); - 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 = new JSONObject(parseJson(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(userId, videoId, solution); - } - else if (debug) { - Log.d(TAG, "Vote response was " + connection.getResponseCode()); - } + String userId = registration.getUserId(); + if (debug) { + Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId); } - catch (Exception ex) { - Log.e(TAG, "Failed to send vote", ex); - } - - return false; - } - - public boolean confirmVote(String userId, String videoId, String solution) { - try { - if (debug) { - Log.d(TAG, "Trying to confirm vote for video: " + videoId + " with solution " + solution + " and userId: " + userId); - } - - // Confirm vote - HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/confirmVote").openConnection(); - confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); - confirmationCon.setRequestMethod("POST"); - confirmationCon.setRequestProperty("Content-Type", "application/json"); - confirmationCon.setRequestProperty("Accept", "application/json"); - confirmationCon.setDoOutput(true); - confirmationCon.setConnectTimeout(5 * 1000); - - String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; - try(OutputStream os = confirmationCon.getOutputStream()) { - byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); - os.write(input, 0, input.length); - } - if (confirmationCon.getResponseCode() == 200) { - String result = parseJson(confirmationCon); - 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 " + confirmationCon.getResponseCode()); - } - } - catch (Exception ex) { - Log.e(TAG, "Failed to send vote", ex); - } - - return false; + return RYDRequester.sendVote(videoId, userId, vote); } } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRequester.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRequester.java new file mode 100644 index 0000000..9777f33 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRequester.java @@ -0,0 +1,226 @@ +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); + dislikeCount = json.getInt("dislikes"); + if (debug) { + Log.d(TAG, "dislikes fetched - " + dislikeCount); + } + + // Set the dislikes + new Handler(Looper.getMainLooper()).post(() -> ReturnYouTubeDislikes.trySetDislikes(ReturnYouTubeDislikes.formatDislikes(dislikeCount))); + } + 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRoutes.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRoutes.java new file mode 100644 index 0000000..27378e7 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/requests/RYDRoutes.java @@ -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() {} +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java deleted file mode 100644 index 94fdac5..0000000 --- a/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java +++ /dev/null @@ -1,136 +0,0 @@ -package fi.vanced.libraries.youtube.ui; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.Toast; - -import static fi.razerman.youtube.XGlobals.debug; -import static fi.vanced.libraries.youtube.ads.VideoAds.getShouldShowAds; -import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId; -import static fi.vanced.utils.VancedUtils.parseJson; -import static pl.jakubweg.StringRef.str; - -import org.json.JSONObject; - -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -import fi.vanced.libraries.youtube.ads.VideoAds; -import fi.vanced.libraries.youtube.player.ChannelModel; -import fi.vanced.libraries.youtube.player.VideoInformation; -import fi.vanced.utils.SharedPrefUtils; -import fi.vanced.utils.VancedUtils; - -public class AdBlock extends SlimButton { - private static final String TAG = "VI - AdBlock - Button"; - private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1"; - private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey"; - - public AdBlock(Context context, ViewGroup container) { - super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, SharedPrefUtils.getBoolean(context, "youtube", "vanced_videoadwhitelisting_enabled", false)); - - initialize(); - } - - private void initialize() { - this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_ad_button", "drawable")); - this.button_text.setText(str("action_ads")); - changeEnabled(getShouldShowAds()); - } - - 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 { - VideoAds.removeFromWhitelist(this.context, VideoInformation.channelName); - this.button_icon.setEnabled(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(() -> { - try { - if (debug) { - Log.d(TAG, "Fetching channelId for " + currentVideoId); - } - HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json; utf-8"); - connection.setRequestProperty("Accept", "application/json"); - connection.setDoOutput(true); - connection.setConnectTimeout(2 * 1000); - - // TODO: Actually fetch the version - String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + currentVideoId + "\"}"; - try(OutputStream os = connection.getOutputStream()) { - byte[] input = jsonInputString.getBytes("utf-8"); - os.write(input, 0, input.length); - } - if (connection.getResponseCode() == 200) { - JSONObject json = new JSONObject(parseJson(connection)); - JSONObject videoInfo = json.getJSONObject("videoDetails"); - ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId")); - if (debug) { - Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor()); - } - - boolean success = VideoAds.addToWhitelist(this.context, channelModel.getAuthor(), channelModel.getChannelId()); - new Handler(Looper.getMainLooper()).post(() -> { - if (success) { - buttonIcon.setEnabled(true); - Toast.makeText(context, "Channel " + channelModel.getAuthor() + " whitelisted", Toast.LENGTH_SHORT).show(); - } - else { - buttonIcon.setEnabled(false); - Toast.makeText(context, "Channel " + channelModel.getAuthor() + " failed to whitelist", Toast.LENGTH_SHORT).show(); - } - - view.setEnabled(true); - }); - } - else { - if (debug) { - Log.d(TAG, "player fetch response was " + connection.getResponseCode()); - } - - buttonIcon.setEnabled(false); - this.view.setEnabled(true); - } - } - catch (Exception ex) { - Log.e(TAG, "Failed to fetch channelId", ex); - this.view.setEnabled(true); - return; - } - }).start(); - } -} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/AdButton.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/AdButton.java new file mode 100644 index 0000000..1e3a5e9 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/AdButton.java @@ -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(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SBWhitelistButton.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SBWhitelistButton.java new file mode 100644 index 0000000..c759ee6 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SBWhitelistButton.java @@ -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 SBWhitelistButton extends SlimButton { + public static final String TAG = "VI - SBWhitelistButton"; + + public SBWhitelistButton(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, + SharedPrefUtils.getBoolean(context, WhitelistType.SPONSORBLOCK.getSharedPreferencesName(), WhitelistType.SPONSORBLOCK.getPreferenceEnabledName(), false)); + + 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(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java index 5b8781d..12ae3e8 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java @@ -10,7 +10,8 @@ import android.view.ViewGroup; import com.google.android.apps.youtube.app.ui.SlimMetadataScrollableButtonContainerLayout; -import fi.vanced.libraries.youtube.ads.VideoAds; +import fi.vanced.libraries.youtube.whitelisting.Whitelist; +import fi.vanced.libraries.youtube.whitelisting.WhitelistType; import fi.vanced.utils.SharedPrefUtils; import fi.vanced.utils.VancedUtils; @@ -19,7 +20,8 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa private ViewGroup container; private CopyButton copyButton; private CopyWithTimestamp copyWithTimestampButton; - public static AdBlock adBlockButton; + public static AdButton adBlockButton; + public static SBWhitelistButton sbWhitelistButton; private final Context context; SharedPreferences.OnSharedPreferenceChangeListener listener; @@ -48,8 +50,8 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa copyButton = new CopyButton(context, this); copyWithTimestampButton = new CopyWithTimestamp(context, this); - adBlockButton = new AdBlock(context, this); - new SponsorBlock(context, this); + adBlockButton = new AdButton(context, this); + sbWhitelistButton = new SBWhitelistButton(context, this); new SponsorBlockVoting(context, this); addSharedPrefsChangeListener(); @@ -73,9 +75,20 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa copyWithTimestampButton.setVisible(ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_timestamp_button_list")); return; } - if ("vanced_videoadwhitelisting_enabled".equals(key) && adBlockButton != null) { - VideoAds.isEnabled = SharedPrefUtils.getBoolean(context, "youtube", "vanced_videoadwhitelisting_enabled", false); - adBlockButton.setVisible(VideoAds.isEnabled); + 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; + } + WhitelistType whitelistSB = WhitelistType.SPONSORBLOCK; + String sbEnabledPreferenceName = whitelistSB.getPreferenceEnabledName(); + if (sbEnabledPreferenceName.equals(key) && sbWhitelistButton != null) { + boolean enabled = SharedPrefUtils.getBoolean(context, whitelistSB.getSharedPreferencesName(), sbEnabledPreferenceName, false); + Whitelist.setEnabled(whitelistSB, enabled); + sbWhitelistButton.setVisible(enabled); return; } } @@ -84,7 +97,9 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa } }; - context.getSharedPreferences("youtube", Context.MODE_PRIVATE) + context.getSharedPreferences(WhitelistType.ADS.getSharedPreferencesName(), Context.MODE_PRIVATE) + .registerOnSharedPreferenceChangeListener(listener); + context.getSharedPreferences(WhitelistType.SPONSORBLOCK.getSharedPreferencesName(), Context.MODE_PRIVATE) .registerOnSharedPreferenceChangeListener(listener); } } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java deleted file mode 100644 index 47132d5..0000000 --- a/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java +++ /dev/null @@ -1,31 +0,0 @@ -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 android.widget.Toast; - -import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; - -import fi.vanced.libraries.youtube.player.VideoHelpers; -import fi.vanced.utils.VancedUtils; - -public class SponsorBlock extends SlimButton { - public SponsorBlock(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_logo", "drawable")); - this.button_text.setText("SB"); - } - - @Override - public void onClick(View view) { - Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show(); - } -} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/Whitelist.java b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/Whitelist.java new file mode 100644 index 0000000..21a9ab0 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/Whitelist.java @@ -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> whitelistMap = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext()); + private static final Map 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> parseWhitelist(Context context) { + if (context == null) { + return Collections.emptyMap(); + } + WhitelistType[] whitelistTypes = WhitelistType.values(); + Map> 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 deserializedChannels = (ArrayList) 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 parseEnabledMap(Context context) { + Map 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 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 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 channels = whitelistMap.get(whitelistType); + Iterator 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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/WhitelistType.java b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/WhitelistType.java new file mode 100644 index 0000000..5b6ddf8 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/WhitelistType.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRequester.java b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRequester.java new file mode 100644 index 0000000..8f1bbe4 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRequester.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRoutes.java b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRoutes.java new file mode 100644 index 0000000..609105b --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/whitelisting/requests/WhitelistRoutes.java @@ -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() {} +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/utils/VancedUtils.java b/app/src/main/java/fi/vanced/utils/VancedUtils.java index e9b0c78..0333f64 100644 --- a/app/src/main/java/fi/vanced/utils/VancedUtils.java +++ b/app/src/main/java/fi/vanced/utils/VancedUtils.java @@ -2,35 +2,24 @@ 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.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; 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 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(); - } - public static int getIdentifier(String name, String defType) { Context context = YouTubeTikTokRoot_Application.getAppContext(); return context.getResources().getIdentifier(name, defType, context.getPackageName()); @@ -46,4 +35,29 @@ public class VancedUtils { 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); + } } \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/utils/requests/Requester.java b/app/src/main/java/fi/vanced/utils/requests/Requester.java new file mode 100644 index 0000000..99ddee6 --- /dev/null +++ b/app/src/main/java/fi/vanced/utils/requests/Requester.java @@ -0,0 +1,51 @@ +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()); + } + + public static String parseJson(InputStream inputStream) throws IOException { + StringBuilder jsonBuilder = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line).append("\n"); + } + inputStream.close(); + return jsonBuilder.toString(); + } + + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/utils/requests/Route.java b/app/src/main/java/fi/vanced/utils/requests/Route.java new file mode 100644 index 0000000..ff82722 --- /dev/null +++ b/app/src/main/java/fi/vanced/utils/requests/Route.java @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/PlayerController.java b/app/src/main/java/pl/jakubweg/PlayerController.java index 6039bc8..256278a 100644 --- a/app/src/main/java/pl/jakubweg/PlayerController.java +++ b/app/src/main/java/pl/jakubweg/PlayerController.java @@ -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 { @@ -123,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) @@ -281,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(); } diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java b/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java index 5f42a49..b88bfb7 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java @@ -1,17 +1,20 @@ package pl.jakubweg; -import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour; +import static fi.razerman.youtube.XGlobals.debug; import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP; 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; @@ -26,7 +29,6 @@ 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; @@ -38,7 +40,10 @@ import android.widget.Toast; 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 { @@ -75,6 +80,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); @@ -143,7 +160,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 +172,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 +201,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 +282,18 @@ 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); + } + { 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 +302,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")); diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java b/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java index 5a617d0..9fb8501 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java @@ -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"; @@ -41,6 +43,7 @@ public class SponsorBlockSettings { public static boolean countSkips = true; public static boolean showTimeWithoutSegments = true; public static int adjustNewSegmentMillis = 150; + public static float minDuration = 0f; public static String uuid = ""; public static String sponsorBlockUrlCategories = "[]"; public static int skippedSegments; @@ -100,9 +103,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 +111,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,6 +136,10 @@ 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); @@ -179,13 +187,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 +204,8 @@ public class SponsorBlockSettings { INTERACTION, SELF_PROMO, MUSIC_OFFTOPIC, - PREVIEW + PREVIEW, + FILLER }; private static final Map mValuesMap = new HashMap<>(values().length); diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java b/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java index 8ac1e0a..dff5295 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java @@ -12,19 +12,21 @@ import static pl.jakubweg.SponsorBlockPreferenceFragment.FORMATTER; import static pl.jakubweg.SponsorBlockPreferenceFragment.SAVED_TEMPLATE; 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_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.countSkips; import static pl.jakubweg.SponsorBlockSettings.getPreferences; import static pl.jakubweg.SponsorBlockSettings.isSponsorBlockEnabled; +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.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 +34,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; @@ -61,7 +61,7 @@ import java.util.TimeZone; 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 { @@ -223,10 +223,10 @@ public abstract class SponsorBlockUtils { appContext = new WeakReference<>(context.getApplicationContext()); switch (voteOptions[which1]) { case UPVOTE: - voteForSegment(segment, VoteOption.UPVOTE, appContext.get(), toastRunnable); + voteForSegment(segment, VoteOption.UPVOTE, appContext.get()); break; case DOWNVOTE: - voteForSegment(segment, VoteOption.DOWNVOTE, appContext.get(), toastRunnable); + voteForSegment(segment, VoteOption.DOWNVOTE, appContext.get()); break; case CATEGORY_CHANGE: onNewCategorySelect(segment, context); @@ -235,40 +235,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 +247,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 +367,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(); } @@ -485,15 +451,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 +471,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,6 +554,7 @@ 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.putString(PREFERENCES_KEY_MIN_DURATION, settingsJson.getString("minDuration")); editor.putString(PREFERENCES_KEY_UUID, settingsJson.getString("userID")); editor.apply(); @@ -633,6 +591,7 @@ 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); diff --git a/app/src/main/java/pl/jakubweg/objects/EditTextListPreference.java b/app/src/main/java/pl/jakubweg/objects/EditTextListPreference.java new file mode 100644 index 0000000..80b8565 --- /dev/null +++ b/app/src/main/java/pl/jakubweg/objects/EditTextListPreference.java @@ -0,0 +1,97 @@ +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.InputType; +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)); + builder.setView(mEditText); + + 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(); + }); + 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(); + } + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/requests/Route.java b/app/src/main/java/pl/jakubweg/requests/Route.java deleted file mode 100644 index ce86b95..0000000 --- a/app/src/main/java/pl/jakubweg/requests/Route.java +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/requests/Requester.java b/app/src/main/java/pl/jakubweg/requests/SBRequester.java similarity index 54% rename from app/src/main/java/pl/jakubweg/requests/Requester.java rename to app/src/main/java/pl/jakubweg/requests/SBRequester.java index 2b7884c..0356c51 100644 --- a/app/src/main/java/pl/jakubweg/requests/Requester.java +++ b/app/src/main/java/pl/jakubweg/requests/SBRequester.java @@ -1,12 +1,13 @@ 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.os.Handler; -import android.os.Looper; +import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.widget.Toast; @@ -14,44 +15,45 @@ 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 fi.vanced.utils.requests.Requester; +import fi.vanced.utils.requests.Route; 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 { +public class SBRequester { private static final String SPONSORBLOCK_API_URL = "https://sponsor.ajay.app/api/"; private static final String TIME_TEMPLATE = "%.3f"; - private Requester() {} + private SBRequester() {} public static synchronized SponsorSegment[] getSegments(String videoId) { List segments = new ArrayList<>(); try { - HttpURLConnection connection = getConnectionFromRoute(Route.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories); int responseCode = connection.getResponseCode(); - videoHasSegments = false; - timeWithoutSegments = ""; if (responseCode == 200) { - JSONArray responseArray = new JSONArray(parseJson(connection)); + JSONArray responseArray = Requester.getJSONArray(connection); int length = responseArray.length(); for (int i = 0; i < length; i++) { - JSONObject obj = ((JSONObject) responseArray.get(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"); @@ -61,8 +63,10 @@ public class Requester { segments.add(sponsorSegment); } } - videoHasSegments = true; - timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0])); + if (!segments.isEmpty()) { + videoHasSegments = true; + timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0])); + } } connection.disconnect(); } @@ -76,7 +80,7 @@ public class Requester { 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); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, videoId, uuid, start, end, category); int responseCode = connection.getResponseCode(); switch (responseCode) { @@ -87,7 +91,7 @@ public class Requester { SponsorBlockUtils.messageToToast = str("submit_failed_duplicate"); break; case 403: - SponsorBlockUtils.messageToToast = str("submit_failed_forbidden"); + SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseJson(connection.getErrorStream())); break; case 429: SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit"); @@ -96,7 +100,7 @@ public class Requester { SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage()); break; } - new Handler(Looper.getMainLooper()).post(toastRunnable); + runOnMainThread(toastRunnable); connection.disconnect(); } catch (Exception ex) { @@ -106,7 +110,7 @@ public class Requester { public static void sendViewCountRequest(SponsorSegment segment) { try { - HttpURLConnection connection = getConnectionFromRoute(Route.VIEWED_SEGMENT, segment.UUID); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID); connection.disconnect(); } catch (Exception ex) { @@ -114,36 +118,38 @@ public class Requester { } } - 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); + 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); - Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show(); + runOnMainThread(() -> 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(); + 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"); - break; - default: - SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage()); - break; + switch (responseCode) { + case 200: + SponsorBlockUtils.messageToToast = str("vote_succeeded"); + break; + case 403: + SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseJson(connection.getErrorStream())); + 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(); } - new Handler(Looper.getMainLooper()).post(toastRunnable); - connection.disconnect(); - } - catch (Exception ex) { - ex.printStackTrace(); - } + catch (Exception ex) { + ex.printStackTrace(); + } + }).start(); } public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) { @@ -154,9 +160,7 @@ public class Requester { new Thread(() -> { try { - HttpURLConnection connection = getConnectionFromRoute(Route.GET_USER_STATS, SponsorBlockSettings.uuid); - JSONObject json = new JSONObject(parseJson(connection)); - connection.disconnect(); + 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); @@ -167,41 +171,38 @@ public class Requester { }).start(); } - public static void setUsername(String username, Runnable toastRunnable) { - try { - HttpURLConnection connection = getConnectionFromRoute(Route.CHANGE_USERNAME, SponsorBlockSettings.uuid, username); - int responseCode = connection.getResponseCode(); + 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"); + 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(); } - else { - SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage()); + catch (Exception ex) { + ex.printStackTrace(); } - new Handler(Looper.getMainLooper()).post(toastRunnable); - connection.disconnect(); - } - catch (Exception ex) { - ex.printStackTrace(); - } + }).start(); } + // helpers + 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; + return Requester.getConnectionFromRoute(SPONSORBLOCK_API_URL, route, params); } - 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(); + private static JSONObject getJSONObject(Route route, String... params) throws Exception { + return Requester.getJSONObject(getConnectionFromRoute(route, params)); } } \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/requests/SBRoutes.java b/app/src/main/java/pl/jakubweg/requests/SBRoutes.java new file mode 100644 index 0000000..840e6b1 --- /dev/null +++ b/app/src/main/java/pl/jakubweg/requests/SBRoutes.java @@ -0,0 +1,18 @@ +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 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 SBRoutes() {} +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e19765a..5d1f352 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,6 +151,8 @@ 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 Adjusting new segment step This is the number of milliseconds you can move when you use the time adjustment buttons while adding new segment + Minimum segment duration + Segments shorter than the set value (in seconds) will not be skipped or show in the player Your unique user id 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 Import/Export settings @@ -173,6 +175,8 @@ Similar to "sponsor" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with Music: Non-Music Section Only for use in music videos. This includes introductions or outros in music videos + Filler/Tangent + 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 Skipped a sponsor segment Skipped sponsor Skipped intro @@ -181,6 +185,7 @@ Skipped self promotion Skipped silence Skipped preview + Skipped filler Skipped unsubmitted segment Skip automatically Show a skip button @@ -193,14 +198,14 @@ Unable to submit segments: Status: %d %s Can\'t submit the segment.\nRate Limited (Too many from the same user or IP) - Can\'t submit the segment.\nRejected by auto moderator + Can\'t submit the segment.\n\n%s Can\'t submit the segment.\nAlready exists Segment submitted successfully Submitting segment… Unable to vote for segment: Status: %d %s Can\'t vote for segment.\nRate Limited (Too many from the same user or IP) - Can\'t vote for segment.\nA moderator has decided that this segment is correct + Can\'t vote for segment.\n\n%s Voted successfully Voting for segment… Upvote @@ -311,14 +316,16 @@ Show time without segments This time appears in brackets next to the current time. This shows the total video duration minus any segments. + Channel whitelisting + Use the Segments button under the player to whitelist a channel Preview/Recap 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. Stats Loading.. SponsorBlock is disabled - Your username: <b>%s</b> + Your username: <b>%s</b> Click to change your username - Unable to change username: Status: %d %s + Unable to change username: Status: %d %s Username successfully changed Submissions: <b>%s</b> You\'ve saved people from <b>%s</b> segments. @@ -326,7 +333,8 @@ You\'ve skipped <b>%s</b> segments. That\'s <b>%s</b>. minutes - Change colors + Are you looking for changing colors? + You can now change a category\'s color by clicking on it above. Choose the category Color changed Color reset @@ -334,22 +342,42 @@ Change Reset - Copy - TCopy + Copy link + Timestamp Ads + Segments Video ad settings Video ad whitelisting Video ad whitelisting is turned off - Video ad whitelisting is turned on. Use the ad button under the player to whitelist a channel + Video ad whitelisting is turned on. Use the Ads button under the player to whitelist a channel + Ads + SponsorBlock + Channel %s was added to the %s whitelist + Channel %s was removed from the %s whitelist + Failed to add channel %s to the %s whitelist + Failed to remove channel %s from the %s whitelist + Failed to retrieve channel details, received code %d Hidden In player Under player Both - RYD settings + Return YouTube Dislike settings Uses the RYD API Enable RYD Switch this on to see the dislike counts again Return YouTube Dislike Integration This integration uses the RYD API from https://returnyoutubedislike.com. Tap to learn more + + Tablet style + Tablet style is turned on. For example suggested videos are only partially working + Tablet style is turned off + + Return YouTube Dislike + 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. + SponsorBlock + 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. + Learn more + Disable + Enable diff --git a/build.gradle b/build.gradle index b36aed2..4a22cbf 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:7.0.4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files