From 03f6e496b9de13def155a5fd8e153bad8bdde381 Mon Sep 17 00:00:00 2001 From: caneleex Date: Thu, 22 Apr 2021 18:43:14 +0200 Subject: [PATCH] add support for voting --- .../SponsorBlockPreferenceFragment.java | 14 +- .../pl/jakubweg/SponsorBlockSettings.java | 26 ++- .../java/pl/jakubweg/SponsorBlockUtils.java | 156 +++++++++++++++++- .../main/java/pl/jakubweg/VotingButton.java | 133 +++++++++++++++ app/src/main/res/drawable/ic_sb_voting.xml | 5 + app/src/main/res/values/strings.xml | 12 ++ 6 files changed, 331 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/pl/jakubweg/VotingButton.java create mode 100644 app/src/main/res/drawable/ic_sb_voting.xml diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java b/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java index 595e001d..8e44e38b 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java @@ -18,7 +18,6 @@ import android.preference.SwitchPreference; import android.text.InputType; import android.widget.Toast; -import java.io.File; import java.util.ArrayList; import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour; @@ -28,10 +27,10 @@ import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_NEW_SEGMENT_ENABL 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_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.getPreferences; import static pl.jakubweg.SponsorBlockSettings.setSeenGuidelines; import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically; import static pl.jakubweg.SponsorBlockSettings.uuid; @@ -104,6 +103,17 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement }); } + { + SwitchPreference preference = new SwitchPreference(context); + preferenceScreen.addPreference(preference); + preference.setTitle(str("enable_voting")); + preference.setSummary(str("enable_voting_sum")); + preference.setKey(PREFERENCES_KEY_VOTING_ENABLED); + preference.setDefaultValue(SponsorBlockSettings.isVotingEnabled); + preference.setChecked(SponsorBlockSettings.isVotingEnabled); + preferencesToDisableWhenSBDisabled.add(preference); + } + addGeneralCategory(context, preferenceScreen); addSegmentsCategory(context, preferenceScreen); addAboutCategory(context, preferenceScreen); diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java b/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java index 1aaf2b25..91c47e1d 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java @@ -7,7 +7,6 @@ import android.text.Html; import android.text.TextUtils; import android.util.Log; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -25,8 +24,10 @@ public class SponsorBlockSettings { public static final String PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED = "sb-enabled"; 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"; public static final String sponsorBlockSkipSegmentsUrl = "https://sponsor.ajay.app/api/skipSegments"; public static final String sponsorBlockViewedUrl = "https://sponsor.ajay.app/api/viewedVideoSponsorTime"; + public static final String sponsorBlockVoteUrl = "https://sponsor.ajay.app/api/voteOnSponsorTime"; public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.SkipAutomatically; @@ -34,6 +35,7 @@ public class SponsorBlockSettings { public static boolean isSponsorBlockEnabled = false; public static boolean seenGuidelinesPopup = false; public static boolean isAddNewSegmentEnabled = false; + public static boolean isVotingEnabled = true; public static boolean showToastWhenSkippedAutomatically = true; public static boolean countSkips = true; public static int adjustNewSegmentMillis = 150; @@ -54,6 +56,14 @@ public class SponsorBlockSettings { return sponsorBlockViewedUrl + "?UUID=" + UUID; } + public static String getSponsorBlockVoteUrl(String uuid, String userId, int type) { + return sponsorBlockVoteUrl + "?UUID=" + uuid + "&userID=" + userId + "&type=" + type; + } + + public static String getSponsorBlockVoteUrl(String uuid, String userId, String category) { + return sponsorBlockVoteUrl + "?UUID=" + uuid + "&userID=" + userId + "&category=" + category; + } + public static SharedPreferences getPreferences(Context context) { return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); } @@ -73,20 +83,26 @@ public class SponsorBlockSettings { if (!isSponsorBlockEnabled) { SkipSegmentView.hide(); NewSegmentHelperLayout.hide(); - SponsorBlockUtils.hideButton(); + SponsorBlockUtils.hideShieldButton(); PlayerController.sponsorSegmentsOfCurrentVideo = null; } else if (/*isAddNewSegmentEnabled*/false) { - SponsorBlockUtils.showButton(); + SponsorBlockUtils.showShieldButton(); } isAddNewSegmentEnabled = preferences.getBoolean(PREFERENCES_KEY_NEW_SEGMENT_ENABLED, isAddNewSegmentEnabled); if (!/*isAddNewSegmentEnabled*/false) { NewSegmentHelperLayout.hide(); - SponsorBlockUtils.hideButton(); + SponsorBlockUtils.hideShieldButton(); } else { - SponsorBlockUtils.showButton(); + SponsorBlockUtils.showShieldButton(); } + isVotingEnabled = preferences.getBoolean(PREFERENCES_KEY_VOTING_ENABLED, isVotingEnabled); + if (!isVotingEnabled) + SponsorBlockUtils.hideVoteButton(); + else + SponsorBlockUtils.showVoteButton(); + SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values(); final ArrayList enabledCategories = new ArrayList<>(possibleBehaviours.length); for (SegmentInfo segment : SegmentInfo.valuesWithoutPreview()) { diff --git a/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java b/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java index fea969bf..84d89bb4 100644 --- a/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java +++ b/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java @@ -6,6 +6,8 @@ import android.content.Context; import android.content.DialogInterface; import android.os.Handler; import android.os.Looper; +import android.text.Html; +import android.text.Spanned; import android.util.Log; import android.view.View; import android.widget.EditText; @@ -37,7 +39,9 @@ import static pl.jakubweg.PlayerController.VERBOSE; import static pl.jakubweg.PlayerController.getCurrentVideoId; import static pl.jakubweg.PlayerController.getLastKnownVideoTime; import static pl.jakubweg.PlayerController.sponsorSegmentsOfCurrentVideo; +import static pl.jakubweg.SponsorBlockSettings.getSponsorBlockVoteUrl; import static pl.jakubweg.SponsorBlockSettings.sponsorBlockSkipSegmentsUrl; +import static pl.jakubweg.SponsorBlockSettings.uuid; import static pl.jakubweg.StringRef.str; @SuppressWarnings({"LongLogTag"}) @@ -56,6 +60,15 @@ public abstract class SponsorBlockUtils { NewSegmentHelperLayout.toggle(); } }; + public static final View.OnClickListener voteButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (debug) { + Log.d(TAG, "Vote button clicked"); + } + SponsorBlockUtils.onVotingClicked(v.getContext()); + } + }; private static int shareBtnId = -1; private static long newSponsorSegmentDialogShownMillis; private static long newSponsorSegmentStartMillis = -1; @@ -145,8 +158,10 @@ public abstract class SponsorBlockUtils { new Thread(submitRunnable).start(); } }; - private static boolean isShown = false; + private static boolean isShieldShown = false; + private static boolean isVoteShown = false; private static WeakReference sponsorBlockBtn = new WeakReference<>(null); + private static WeakReference votingBtn = new WeakReference<>(null); private static String messageToToast = ""; private static EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener(); private static final DialogInterface.OnClickListener editByHandDialogListener = new DialogInterface.OnClickListener() { @@ -180,6 +195,36 @@ public abstract class SponsorBlockUtils { dialog.dismiss(); } }; + private static final DialogInterface.OnClickListener segmentVoteClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final Context context = ((AlertDialog) dialog).getContext(); + final SponsorSegment segment = sponsorSegmentsOfCurrentVideo[which]; + + new AlertDialog.Builder(context) // negative and positive are switched for more intuitive order + .setNegativeButton(str("vote_upvote"), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show(); + voteForSegment(segment, true, null); + } + }) + .setPositiveButton(str("vote_downvote"), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show(); + voteForSegment(segment, false, null); + } + }) + .setNeutralButton(str("vote_category"), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onNewCategorySelect(segment, context); + } + }) + .show(); + } + }; private static Runnable toastRunnable = new Runnable() { @Override public void run() { @@ -250,9 +295,9 @@ public abstract class SponsorBlockUtils { private SponsorBlockUtils() { } - public static void showButton() { - if (isShown) return; - isShown = true; + public static void showShieldButton() { + if (isShieldShown) return; + isShieldShown = true; View i = sponsorBlockBtn.get(); if (i == null) return; i.setVisibility(VISIBLE); @@ -261,14 +306,33 @@ public abstract class SponsorBlockUtils { i.invalidate(); } - public static void hideButton() { - if (!isShown) return; - isShown = false; + public static void hideShieldButton() { + if (!isShieldShown) return; + isShieldShown = false; View i = sponsorBlockBtn.get(); if (i != null) i.setVisibility(GONE); } + public static void showVoteButton() { + if (isVoteShown) return; + isVoteShown = true; + View i = votingBtn.get(); + if (i == null) return; + i.setVisibility(VISIBLE); + i.bringToFront(); + i.requestLayout(); + i.invalidate(); + } + + public static void hideVoteButton() { + if (!isVoteShown) return; + isVoteShown = false; + View i = votingBtn.get(); + if (i != null) + i.setVisibility(GONE); + } + @SuppressLint("DefaultLocale") public static void onMarkLocationClicked(Context context) { newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime(); @@ -305,6 +369,43 @@ public abstract class SponsorBlockUtils { } } + public static void onVotingClicked(final Context context) { + if (sponsorSegmentsOfCurrentVideo == null || sponsorSegmentsOfCurrentVideo.length == 0) // prevent crashing or empty dialog + return; + CharSequence[] titles = new CharSequence[sponsorSegmentsOfCurrentVideo.length]; + for (int i = 0; i < sponsorSegmentsOfCurrentVideo.length; i++) { + SponsorSegment segment = sponsorSegmentsOfCurrentVideo[i]; + + String start = dateFormatter.format(new Date(segment.start)); + String end = dateFormatter.format(new Date(segment.end)); + Spanned html = Html.fromHtml(String.format(" %s
%s to %s", + segment.category.color, segment.category.title, start, end)); + titles[i] = html; + } + + new AlertDialog.Builder(context) + .setItems(titles, segmentVoteClickListener) + .show(); + } + + private static void onNewCategorySelect(final SponsorSegment segment, Context context) { + final SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutPreview(); + CharSequence[] titles = new CharSequence[values.length]; + for (int i = 0; i < values.length; i++) { + titles[i] = values[i].getTitleWithDot(); + } + + new AlertDialog.Builder(context) + .setTitle(str("new_segment_choose_category")) + .setItems(titles, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + voteForSegment(segment, false, values[which].key); + } + }) + .show(); + } + @SuppressLint("DefaultLocale") public static void onPreviewClicked(Context context) { if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { @@ -416,7 +517,46 @@ public abstract class SponsorBlockUtils { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); - connection.getInputStream().close(); + connection.disconnect(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void voteForSegment(SponsorSegment segment, boolean upvote, String category) { + messageToToast = null; + try { + String voteUrl = category == null + ? getSponsorBlockVoteUrl(segment.UUID, uuid, upvote ? 1 : 0) + : getSponsorBlockVoteUrl(segment.UUID, uuid, category); + URL url = new URL(voteUrl); + + Log.d("sponsorblock", "requesting: " + url.getPath()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + + switch (connection.getResponseCode()) { + default: + messageToToast = String.format(str("vote_failed_unknown_error"), connection.getResponseCode(), connection.getResponseMessage()); + break; + case 429: + messageToToast = str("vote_failed_rate_limit"); + break; + case 403: + messageToToast = str("vote_failed_forbidden"); + break; + case 409: + messageToToast = str("vote_failed_duplicate"); + break; + case 200: + messageToToast = str("vote_succeeded"); + break; + } + + Log.i(TAG, "Voted for segment with status: " + connection.getResponseCode() + ", " + messageToToast); + new Handler(Looper.getMainLooper()).post(toastRunnable); + connection.disconnect(); } catch (IOException e) { e.printStackTrace(); diff --git a/app/src/main/java/pl/jakubweg/VotingButton.java b/app/src/main/java/pl/jakubweg/VotingButton.java new file mode 100644 index 00000000..1b87eeb1 --- /dev/null +++ b/app/src/main/java/pl/jakubweg/VotingButton.java @@ -0,0 +1,133 @@ +package pl.jakubweg; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import java.lang.ref.WeakReference; + +import static fi.razerman.youtube.XGlobals.debug; + +public class VotingButton { + static String TAG = "VOTING"; + static RelativeLayout _youtubeControlsLayout; + static WeakReference _votingButton = new WeakReference<>(null); + static int fadeDurationFast; + static int fadeDurationScheduled; + static Animation fadeIn; + static Animation fadeOut; + static boolean isShowing; + + public static void initialize(Object viewStub) { + try { + if(debug){ + Log.d(TAG, "initializing voting button"); + } + + _youtubeControlsLayout = (RelativeLayout) viewStub; + initButtonVisibilitySettings(); + + ImageView imageView = (ImageView)_youtubeControlsLayout + .findViewById(getIdentifier("voting_button", "id")); + + if (debug && imageView == null){ + Log.d(TAG, "Couldn't find imageView with tag \"voting_button\""); + } + if (imageView == null) return; + imageView.setOnClickListener(SponsorBlockUtils.voteButtonListener); + _votingButton = new WeakReference<>(imageView); + + // Animations + fadeDurationFast = getInteger("fade_duration_fast"); + fadeDurationScheduled = getInteger("fade_duration_scheduled"); + fadeIn = getAnimation("fade_in"); + fadeIn.setDuration(fadeDurationFast); + fadeOut = getAnimation("fade_out"); + fadeOut.setDuration(fadeDurationScheduled); + isShowing = true; + changeVisibilityImmediate(false); + } + catch (Exception ex) { + Log.e(TAG, "Unable to set RelativeLayout", ex); + } + } + + public static void changeVisibilityImmediate(boolean visible) { + changeVisibility(visible, true); + } + + public static void changeVisibilityNegatedImmediate(boolean visible) { + changeVisibility(!visible, true); + } + + public static void changeVisibility(boolean visible) { + changeVisibility(visible, false); + } + + public static void changeVisibility(boolean visible, boolean immediate) { + if (isShowing == visible) return; + isShowing = visible; + + ImageView iView = _votingButton.get(); + if (_youtubeControlsLayout == null || iView == null) return; + + if (visible && shouldBeShown()) { + if (debug) { + Log.d(TAG, "Fading in"); + } + iView.setVisibility(View.VISIBLE); + if (!immediate) + iView.startAnimation(fadeIn); + return; + } + + if (iView.getVisibility() == View.VISIBLE) { + if (debug) { + Log.d(TAG, "Fading out"); + } + if (!immediate) + iView.startAnimation(fadeOut); + iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE); + } + } + + private static boolean shouldBeShown() { + return SponsorBlockSettings.isVotingEnabled && SponsorBlockSettings.isSponsorBlockEnabled; + } + + private static void initButtonVisibilitySettings() { + Context context = YouTubeTikTokRoot_Application.getAppContext(); + if(context == null){ + Log.e(TAG, "context is null"); + SponsorBlockSettings.isSponsorBlockEnabled = false; + SponsorBlockSettings.isVotingEnabled = false; + return; + } + + SharedPreferences sharedPreferences = context.getSharedPreferences(SponsorBlockSettings.PREFERENCES_NAME, Context.MODE_PRIVATE); + SponsorBlockSettings.isSponsorBlockEnabled = sharedPreferences.getBoolean(SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, false); + SponsorBlockSettings.isVotingEnabled = sharedPreferences.getBoolean(SponsorBlockSettings.PREFERENCES_KEY_VOTING_ENABLED, false); + } + + //region Helpers + private static int getIdentifier(String name, String defType) { + Context context = YouTubeTikTokRoot_Application.getAppContext(); + return context.getResources().getIdentifier(name, defType, context.getPackageName()); + } + + private static int getInteger(String name) { + return YouTubeTikTokRoot_Application.getAppContext().getResources().getInteger(getIdentifier(name, "integer")); + } + + private static Animation getAnimation(String name) { + return AnimationUtils.loadAnimation(YouTubeTikTokRoot_Application.getAppContext(), getIdentifier(name, "anim")); + } + //endregion +} diff --git a/app/src/main/res/drawable/ic_sb_voting.xml b/app/src/main/res/drawable/ic_sb_voting.xml new file mode 100644 index 00000000..61638035 --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_voting.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d7fa4bd..e7ca3cf8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,6 +143,8 @@ Switch this on for very cool sponsor segments skipping Enable new segment adding Switch this on to enable experimental segment adding (has button visibility issues). + Enable voting + Switch this on to enable voting. What to do with different segments General Show a toast when skipping segment automatically @@ -188,6 +190,16 @@ 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.\nDuplicate + Voted successfully + Voting for segment… + Upvote + Downvote + Change category + Choose the segment category You\'ve disabled this category in the settings, enable it to be able to submit New Sponsor Block segment