mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-12-01 08:12:55 +01:00
feat(youtube/return-youtube-dislike): show dislike as a percentage (#234)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
718c5a75c1
commit
7840bc48ba
@ -2,6 +2,8 @@ package app.revanced.integrations.returnyoutubedislike;
|
||||
|
||||
import android.content.Context;
|
||||
import android.icu.text.CompactDecimalFormat;
|
||||
import android.icu.text.DecimalFormat;
|
||||
import android.icu.text.DecimalFormatSymbols;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableString;
|
||||
|
||||
@ -17,6 +19,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
@ -41,7 +44,7 @@ public class ReturnYouTubeDislike {
|
||||
private static volatile boolean isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
||||
|
||||
/**
|
||||
* Used to guard {@link #currentVideoId} and {@link #dislikeFetchFuture},
|
||||
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture},
|
||||
* as multiple threads access this class.
|
||||
*/
|
||||
private static final Object videoIdLockObject = new Object();
|
||||
@ -50,10 +53,10 @@ public class ReturnYouTubeDislike {
|
||||
private static String currentVideoId;
|
||||
|
||||
/**
|
||||
* Stores the results of the dislike fetch, and used as a barrier to wait until fetch completes
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes
|
||||
*/
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Future<Integer> dislikeFetchFuture;
|
||||
private static Future<RYDVoteData> voteFetchFuture;
|
||||
|
||||
public enum Vote {
|
||||
LIKE(1),
|
||||
@ -73,8 +76,14 @@ public class ReturnYouTubeDislike {
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // number formatter is not thread safe
|
||||
private static CompactDecimalFormat compactNumberFormatter;
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static DecimalFormat dislikePercentageFormatter;
|
||||
|
||||
public static void onEnabledChange(boolean enabled) {
|
||||
isEnabled = enabled;
|
||||
@ -86,9 +95,9 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
}
|
||||
|
||||
private static Future<Integer> getDislikeFetchFuture() {
|
||||
private static Future<RYDVoteData> getVoteFetchFuture() {
|
||||
synchronized (videoIdLockObject) {
|
||||
return dislikeFetchFuture;
|
||||
return voteFetchFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +113,7 @@ public class ReturnYouTubeDislike {
|
||||
currentVideoId = videoId;
|
||||
// no need to wrap the fetchDislike call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call
|
||||
dislikeFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchDislikes(videoId));
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
|
||||
@ -128,19 +137,19 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
// Have to block the current thread until fetching is done
|
||||
// There's no known way to edit the text after creation yet
|
||||
Integer dislikeCount;
|
||||
RYDVoteData votingData;
|
||||
try {
|
||||
dislikeCount = getDislikeFetchFuture().get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||
votingData = getVoteFetchFuture().get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
LogHelper.printDebug(() -> "UI timed out waiting for dislike fetch to complete");
|
||||
return;
|
||||
}
|
||||
if (dislikeCount == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislike count to UI (dislike count not available)");
|
||||
if (votingData == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislike count to UI (RYD data not available)");
|
||||
return;
|
||||
}
|
||||
|
||||
updateDislike(textRef, isSegmentedButton, dislikeCount);
|
||||
updateDislike(textRef, isSegmentedButton, votingData);
|
||||
LogHelper.printDebug(() -> "Updated text on component: " + conversionContextString);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Error while trying to update dislikes text", ex);
|
||||
@ -197,9 +206,11 @@ public class ReturnYouTubeDislike {
|
||||
return userId;
|
||||
}
|
||||
|
||||
private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, int dislikeCount) {
|
||||
private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, RYDVoteData voteData) {
|
||||
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
||||
String newDislikeString = formatDislikeCount(dislikeCount);
|
||||
String newDislikeString = SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
|
||||
? formatDislikePercentage(voteData.dislikePercentage)
|
||||
: formatDislikeCount(voteData.dislikeCount);
|
||||
|
||||
if (isSegmentedButton) { // both likes and dislikes are on a custom segmented button
|
||||
// parse out the like count as a string
|
||||
@ -239,16 +250,16 @@ public class ReturnYouTubeDislike {
|
||||
textRef.set(newSpannableString);
|
||||
}
|
||||
|
||||
private static String formatDislikeCount(int dislikeCount) {
|
||||
private static String formatDislikeCount(long dislikeCount) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String formatted;
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (compactNumberFormatter == null) {
|
||||
if (dislikeCountFormatter == null) {
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
compactNumberFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
}
|
||||
formatted = compactNumberFormatter.format(dislikeCount);
|
||||
formatted = dislikeCountFormatter.format(dislikeCount);
|
||||
}
|
||||
LogHelper.printDebug(() -> "Dislike count: " + dislikeCount + " formatted as: " + formatted);
|
||||
return formatted;
|
||||
@ -257,4 +268,29 @@ public class ReturnYouTubeDislike {
|
||||
// never will be reached, as the oldest supported YouTube app requires Android N or greater
|
||||
return String.valueOf(dislikeCount);
|
||||
}
|
||||
|
||||
private static String formatDislikePercentage(float dislikePercentage) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
String formatted;
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
dislikePercentageFormatter = new DecimalFormat("", new DecimalFormatSymbols(locale));
|
||||
}
|
||||
if (dislikePercentage == 0 || dislikePercentage >= 0.01) { // zero, or at least 1%
|
||||
dislikePercentageFormatter.applyLocalizedPattern("0"); // show only whole percentage points
|
||||
} else { // between (0, 1)%
|
||||
dislikePercentageFormatter.applyLocalizedPattern("0.#"); // show 1 digit precision
|
||||
}
|
||||
final char percentChar = dislikePercentageFormatter.getDecimalFormatSymbols().getPercent();
|
||||
formatted = dislikePercentageFormatter.format(100 * dislikePercentage) + percentChar;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// never will be reached, as the oldest supported YouTube app requires Android N or greater
|
||||
return (int) (100 * dislikePercentage) + "%";
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
package app.revanced.integrations.returnyoutubedislike.requests;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ReturnYouTubeDislike API estimated like/dislike/view counts.
|
||||
*
|
||||
* ReturnYouTubeDislike does not guarantee when the counts are updated.
|
||||
* So these values may lag behind what YouTube shows.
|
||||
*/
|
||||
public final class RYDVoteData {
|
||||
|
||||
public final String videoId;
|
||||
|
||||
/**
|
||||
* Estimated number of views
|
||||
*/
|
||||
public final long viewCount;
|
||||
|
||||
/**
|
||||
* Estimated like count
|
||||
*/
|
||||
public final long likeCount;
|
||||
|
||||
/**
|
||||
* Estimated dislike count
|
||||
*/
|
||||
public final long dislikeCount;
|
||||
|
||||
/**
|
||||
* Estimated percentage of likes for all votes. Value has range of [0, 1]
|
||||
*
|
||||
* A video with 400 positive votes, and 100 negative votes, has a likePercentage of 0.8
|
||||
*/
|
||||
public final float likePercentage;
|
||||
|
||||
/**
|
||||
* Estimated percentage of dislikes for all votes. Value has range of [0, 1]
|
||||
*
|
||||
* A video with 400 positive votes, and 100 negative votes, has a dislikePercentage of 0.2
|
||||
*/
|
||||
public final float dislikePercentage;
|
||||
|
||||
/**
|
||||
* @throws JSONException if JSON parse error occurs, or if the values make no sense (ie: negative values)
|
||||
*/
|
||||
public RYDVoteData(JSONObject json) throws JSONException {
|
||||
Objects.requireNonNull(json);
|
||||
videoId = json.getString("id");
|
||||
viewCount = json.getLong("viewCount");
|
||||
likeCount = json.getLong("likes");
|
||||
dislikeCount = json.getLong("dislikes");
|
||||
if (likeCount < 0 || dislikeCount < 0 || viewCount < 0) {
|
||||
throw new JSONException("Unexpected JSON values: " + json);
|
||||
}
|
||||
likePercentage = (likeCount == 0 ? 0 : (float)likeCount / (likeCount + dislikeCount));
|
||||
dislikePercentage = (dislikeCount == 0 ? 0 : (float)dislikeCount / (likeCount + dislikeCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RYDVoteData{"
|
||||
+ "videoId=" + videoId
|
||||
+ ", viewCount=" + viewCount
|
||||
+ ", likeCount=" + likeCount
|
||||
+ ", dislikeCount=" + dislikeCount
|
||||
+ ", likePercentage=" + likePercentage
|
||||
+ ", dislikePercentage=" + dislikePercentage
|
||||
+ '}';
|
||||
}
|
||||
|
||||
// equals and hashcode is not implemented (currently not needed)
|
||||
}
|
@ -4,6 +4,7 @@ import android.util.Base64;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -26,7 +27,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
|
||||
|
||||
/**
|
||||
* Default connection and response timeout for {@link #fetchDislikes(String)}
|
||||
* Default connection and response timeout for {@link #fetchVotes(String)}
|
||||
*/
|
||||
private static final int API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS = 5000;
|
||||
|
||||
@ -128,11 +129,10 @@ public class ReturnYouTubeDislikeApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of dislikes.
|
||||
* Returns NULL if fetch failed, or a rate limit is in effect.
|
||||
* @return NULL if fetch failed, or if a rate limit is in effect.
|
||||
*/
|
||||
@Nullable
|
||||
public static Integer fetchDislikes(String videoId) {
|
||||
public static RYDVoteData fetchVotes(String videoId) {
|
||||
ReVancedUtils.verifyOffMainThread();
|
||||
Objects.requireNonNull(videoId);
|
||||
try {
|
||||
@ -161,10 +161,14 @@ public class ReturnYouTubeDislikeApi {
|
||||
}
|
||||
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
|
||||
JSONObject json = Requester.getJSONObject(connection); // also disconnects
|
||||
Integer fetchedDislikeCount = json.getInt("dislikes");
|
||||
LogHelper.printDebug(() -> "Fetched video: " + videoId
|
||||
+ " dislikes: " + fetchedDislikeCount);
|
||||
return fetchedDislikeCount;
|
||||
try {
|
||||
RYDVoteData votingData = new RYDVoteData(json);
|
||||
LogHelper.printDebug(() -> "Voting data fetched: " + votingData);
|
||||
return votingData;
|
||||
} catch (JSONException ex) {
|
||||
LogHelper.printException(() -> "Failed to parse video: " + videoId + " json: " + json, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
LogHelper.printDebug(() -> "Failed to fetch dislikes for video: " + videoId
|
||||
+ " response code was: " + responseCode);
|
||||
|
@ -115,6 +115,7 @@ public enum SettingsEnum {
|
||||
// RYD settings
|
||||
RYD_USER_ID("ryd_userId", null, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.STRING),
|
||||
RYD_ENABLED("ryd_enabled", true, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
|
||||
RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
|
||||
|
||||
// SponsorBlock settings
|
||||
SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
|
||||
|
@ -3,7 +3,6 @@ package app.revanced.integrations.settingsmenu;
|
||||
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@ -18,65 +17,89 @@ import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||
|
||||
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
|
||||
|
||||
/**
|
||||
* If ReturnYouTubeDislike is enabled
|
||||
*/
|
||||
private SwitchPreference enabledPreference;
|
||||
|
||||
/**
|
||||
* If dislikes are shown as percentage
|
||||
*/
|
||||
private SwitchPreference percentagePreference;
|
||||
|
||||
private void updateUIState() {
|
||||
final boolean rydIsEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
||||
final boolean dislikePercentageEnabled = SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean();
|
||||
|
||||
enabledPreference.setSummary(rydIsEnabled
|
||||
? str("revanced_ryd_enable_summary_on")
|
||||
: str("revanced_ryd_enable_summary_off"));
|
||||
|
||||
percentagePreference.setSummary(dislikePercentageEnabled
|
||||
? str("revanced_ryd_dislike_percentage_summary_on")
|
||||
: str("revanced_ryd_dislike_percentage_summary_off"));
|
||||
percentagePreference.setEnabled(rydIsEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.RYD.getName());
|
||||
|
||||
final Activity context = this.getActivity();
|
||||
|
||||
Activity context = this.getActivity();
|
||||
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
|
||||
setPreferenceScreen(preferenceScreen);
|
||||
|
||||
// RYD enable toggle
|
||||
{
|
||||
SwitchPreference preference = new SwitchPreference(context);
|
||||
preferenceScreen.addPreference(preference);
|
||||
preference.setKey(SettingsEnum.RYD_ENABLED.getPath());
|
||||
preference.setDefaultValue(SettingsEnum.RYD_ENABLED.getDefaultValue());
|
||||
preference.setChecked(SettingsEnum.RYD_ENABLED.getBoolean());
|
||||
preference.setTitle(str("revanced_ryd_title"));
|
||||
preference.setSummary(str("revanced_ryd_summary"));
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
final boolean value = (Boolean) newValue;
|
||||
ReturnYouTubeDislike.onEnabledChange(value);
|
||||
SettingsEnum.RYD_ENABLED.saveValue(value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
enabledPreference = new SwitchPreference(context);
|
||||
enabledPreference.setKey(SettingsEnum.RYD_ENABLED.getPath());
|
||||
enabledPreference.setDefaultValue(SettingsEnum.RYD_ENABLED.getDefaultValue());
|
||||
enabledPreference.setChecked(SettingsEnum.RYD_ENABLED.getBoolean());
|
||||
enabledPreference.setTitle(str("revanced_ryd_enable_title"));
|
||||
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
final boolean rydIsEnabled = (Boolean) newValue;
|
||||
SettingsEnum.RYD_ENABLED.saveValue(rydIsEnabled);
|
||||
ReturnYouTubeDislike.onEnabledChange(rydIsEnabled);
|
||||
|
||||
updateUIState();
|
||||
return true;
|
||||
});
|
||||
preferenceScreen.addPreference(enabledPreference);
|
||||
|
||||
percentagePreference = new SwitchPreference(context);
|
||||
percentagePreference.setKey(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getPath());
|
||||
percentagePreference.setDefaultValue(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getDefaultValue());
|
||||
percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean());
|
||||
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
|
||||
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue((Boolean)newValue);
|
||||
|
||||
updateUIState();
|
||||
return true;
|
||||
});
|
||||
preferenceScreen.addPreference(percentagePreference);
|
||||
|
||||
updateUIState();
|
||||
|
||||
|
||||
// About category
|
||||
addAboutCategory(context, preferenceScreen);
|
||||
|
||||
PreferenceCategory aboutCategory = new PreferenceCategory(context);
|
||||
aboutCategory.setTitle(str("about"));
|
||||
preferenceScreen.addPreference(aboutCategory);
|
||||
|
||||
// ReturnYouTubeDislike Website
|
||||
|
||||
Preference aboutWebsitePreference = new Preference(context);
|
||||
aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title"));
|
||||
aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary"));
|
||||
aboutWebsitePreference.setOnPreferenceClickListener(pref -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://returnyoutubedislike.com"));
|
||||
pref.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
preferenceScreen.addPreference(aboutWebsitePreference);
|
||||
}
|
||||
|
||||
private void addAboutCategory(Context context, PreferenceScreen screen) {
|
||||
PreferenceCategory category = new PreferenceCategory(context);
|
||||
screen.addPreference(category);
|
||||
category.setTitle(str("about"));
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
screen.addPreference(preference);
|
||||
preference.setTitle(str("revanced_ryd_attribution_title"));
|
||||
preference.setSummary(str("revanced_ryd_attribution_summary"));
|
||||
preference.setOnPreferenceClickListener(pref -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://returnyoutubedislike.com"));
|
||||
pref.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
Preference preference = new Preference(context);
|
||||
screen.addPreference(preference);
|
||||
preference.setTitle("GitHub");
|
||||
preference.setOnPreferenceClickListener(pref -> {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse("https://github.com/Anarios/return-youtube-dislike"));
|
||||
pref.getContext().startActivity(i);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user