Initial commit

This commit is contained in:
jakweg 2020-08-24 17:47:57 +02:00
commit ac2e3937ce
17 changed files with 2252 additions and 0 deletions

View File

@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="pl.jakubweg">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
tools:ignore="AllowBackup" />
</manifest>

View File

@ -0,0 +1,19 @@
package pl.jakubweg;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
public class Helper {
public static String getStringByName(Context context, String name) {
try {
Resources res = context.getResources();
return res.getString(res.getIdentifier(name, "string", context.getPackageName()));
} catch (Throwable exception) {
Log.e("XGlobals", "Resource not found.", exception);
return "";
}
}
}

View File

@ -0,0 +1,98 @@
package pl.jakubweg;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.lang.reflect.Field;
// invoke-static {p0}, Lpl/jakubweg/InjectedPlugin;->inject(Landroid/content/Context;)V
// invoke-static {}, Lpl/jakubweg/InjectedPlugin;->printSomething()V
// InlineTimeBar
public class InjectedPlugin {
private static final String TAG = "jakubweg.InjectedPlugin";
public static void printSomething() {
Log.d(TAG, "printSomething called");
}
public static void printObject(Object o, int recursive) {
if (o == null)
Log.d(TAG, "Printed object is null");
else {
Log.d(TAG, "Printed object ("
+ o.getClass().getName()
+ ") = " + o.toString());
for (Field field : o.getClass().getDeclaredFields()) {
if (field.getType().isPrimitive())
continue;
field.setAccessible(true);
try {
Object value = field.get(o);
try {
// if ("java.lang.String".equals(field.getType().getName()))
Log.d(TAG, "Field: " + field.toString() + " has value " + value);
} catch (Exception e) {
Log.d(TAG, "Field: " + field.toString() + " has value that thrown an exception in toString method");
}
if (recursive > 0 && value != null && !value.getClass().isPrimitive())
printObject(value, recursive - 1);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
public static void printObject(Object o) {
printObject(o, 0);
}
public static void printObject(int o) {
printObject(Integer.valueOf(o));
}
public static void printObject(float o) {
printObject(Float.valueOf(o));
}
public static void printObject(long o) {
printObject(Long.valueOf(o));
}
public static void printStackTrace() {
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
Log.d(TAG, "Printing stack trace:");
for (StackTraceElement element : stackTrace) {
Log.d(TAG, element.toString());
}
}
public static void printViewStack(final View view, int spaces) {
StringBuilder builder = new StringBuilder(spaces);
for (int i = 0; i < spaces; i++) {
builder.append('-');
}
String spacesStr = builder.toString();
if (view == null) {
Log.i(TAG, spacesStr + "Null view");
return;
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
Log.i(TAG, spacesStr + "View group: " + view);
int childCount = group.getChildCount();
Log.i(TAG, spacesStr + "Children count: " + childCount);
for (int i = 0; i < childCount; i++) {
printViewStack(group.getChildAt(i), spaces + 1);
}
} else {
Log.i(TAG, spacesStr + "Normal view: " + view);
}
}
}

View File

@ -0,0 +1,139 @@
package pl.jakubweg;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import java.lang.ref.WeakReference;
public class NewSegmentHelperLayout extends LinearLayout implements View.OnClickListener {
private static final int rewindBtnId = 1235;
private static final int forwardBtnId = 1236;
private static final int publishBtnId = 1237;
private static final int hideBtnId = 1238;
private static final int markLocationBtnId = 1239;
private static final int previewBtnId = 1240;
private static final int editByHandBtnId = 1241;
private static WeakReference<NewSegmentHelperLayout> INSTANCE = new WeakReference<>(null);
private static boolean isShown = false;
private final int padding;
private final int iconSize;
private final int rippleEffectId;
private final String packageName;
@SuppressLint({"DefaultLocale", "SetTextI18n"})
public NewSegmentHelperLayout(Context context) {
super(context);
INSTANCE = new WeakReference<>(this);
isShown = false;
setVisibility(GONE);
packageName = context.getPackageName();
padding = (int) SkipSegmentView.convertDpToPixel(4f, context);
iconSize = (int) SkipSegmentView.convertDpToPixel(40f, context);
TypedValue rippleEffect = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true);
rippleEffectId = rippleEffect.resourceId;
setOrientation(VERTICAL);
@SuppressLint("RtlHardcoded")
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.START | Gravity.LEFT | Gravity.CENTER_VERTICAL
);
this.setBackgroundColor(0x66000000);
this.bringToFront();
this.setLayoutParams(layoutParams);
this.setPadding(padding, padding, padding, padding);
final LinearLayout topLayout = new LinearLayout(context);
final LinearLayout bottomLayout = new LinearLayout(context);
topLayout.setOrientation(HORIZONTAL);
bottomLayout.setOrientation(HORIZONTAL);
this.addView(topLayout);
this.addView(bottomLayout);
topLayout.addView(createTextViewBtn(rewindBtnId, "player_fast_rewind"));
topLayout.addView(createTextViewBtn(forwardBtnId, "player_fast_forward"));
topLayout.addView(createTextViewBtn(markLocationBtnId, "ic_sb_adjust"));
bottomLayout.addView(createTextViewBtn(previewBtnId, "ic_sb_compare"));
bottomLayout.addView(createTextViewBtn(editByHandBtnId, "ic_sb_edit"));
bottomLayout.addView(createTextViewBtn(publishBtnId, "ic_sb_publish"));
// bottomLayout.addView(createTextViewBtn(hideBtnId,"btn_close_light"));
}
public static void show() {
if (isShown) return;
isShown = true;
NewSegmentHelperLayout i = INSTANCE.get();
if (i == null) return;
i.setVisibility(VISIBLE);
i.bringToFront();
i.requestLayout();
i.invalidate();
}
public static void hide() {
if (!isShown) return;
isShown = false;
NewSegmentHelperLayout i = INSTANCE.get();
if (i != null)
i.setVisibility(GONE);
}
public static void toggle() {
if (isShown) hide();
else show();
}
private View createTextViewBtn(int id, String drawableName) {
int drawableId = getResources().getIdentifier(drawableName, "drawable", packageName);
final ImageView view = new ImageView(getContext());
view.setPadding(padding, padding, padding, padding);
view.setLayoutParams(new LayoutParams(iconSize, iconSize, 1));
view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
view.setImageResource(drawableId);
view.setId(id);
view.setClickable(true);
view.setFocusable(true);
view.setBackgroundResource(rippleEffectId);
view.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case forwardBtnId:
PlayerController.skipRelativeMilliseconds(SponsorBlockSettings.adjustNewSegmentMillis);
break;
case rewindBtnId:
PlayerController.skipRelativeMilliseconds(-SponsorBlockSettings.adjustNewSegmentMillis);
break;
case markLocationBtnId:
SponsorBlockUtils.onMarkLocationClicked(getContext());
break;
case publishBtnId:
SponsorBlockUtils.onPublishClicked(getContext());
break;
case previewBtnId:
SponsorBlockUtils.onPreviewClicked(getContext());
break;
case editByHandBtnId:
SponsorBlockUtils.onEditByHandClicked(getContext());
break;
case hideBtnId:
hide();
break;
}
}
}

View File

@ -0,0 +1,489 @@
package pl.jakubweg;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
@SuppressLint({"LongLogTag"})
public class PlayerController {
public static final String TAG = "jakubweg.PlayerController";
public static final boolean VERBOSE = false;
@SuppressWarnings("PointlessBooleanExpression")
public static final boolean VERBOSE_DRAW_OPTIONS = false && VERBOSE;
private static final Timer sponsorTimer = new Timer("sponsor-skip-timer");
public static WeakReference<Activity> playerActivity = new WeakReference<>(null);
public static SponsorSegment[] sponsorSegmentsOfCurrentVideo;
private static WeakReference<Object> currentPlayerController = new WeakReference<>(null);
private static Method setMillisecondMethod;
private static long allowNextSkipRequestTime = 0L;
private static String currentVideoId;
private static long currentVideoLength = 1L;
private static long lastKnownVideoTime = -1L;
private static final Runnable findAndSkipSegmentRunnable = new Runnable() {
@Override
public void run() {
// Log.d(TAG, "findAndSkipSegmentRunnable");
findAndSkipSegment(false);
}
};
private static float sponsorBarLeft = 1f;
private static float sponsorBarRight = 1f;
private static float sponsorBarThickness = 2f;
private static TimerTask skipSponsorTask = null;
public static String getCurrentVideoId() {
return currentVideoId;
}
public static void setCurrentVideoId(final String videoId) {
if (videoId == null) {
Log.d(TAG, "setCurrentVideoId: videoId is null");
return;
}
if (!SponsorBlockSettings.isSponsorBlockEnabled) {
currentVideoId = null;
return;
}
if (Looper.myLooper() != Looper.getMainLooper()) // check if thread is not main
return;
if (videoId.equals(currentVideoId))
return;
currentVideoId = videoId;
sponsorSegmentsOfCurrentVideo = null;
if (VERBOSE)
Log.d(TAG, "setCurrentVideoId: videoId=" + videoId);
sponsorTimer.schedule(new TimerTask() {
@Override
public void run() {
executeDownloadSegments(currentVideoId, false);
}
}, 0);
}
/**
* Called when creating some kind of youtube internal player controlled, every time when new video starts to play
*/
public static void onCreate(final Object o) {
// "Plugin.printStackTrace();
if (o == null) {
Log.e(TAG, "onCreate called with null object");
return;
}
if (VERBOSE)
Log.i(TAG, String.format("onCreate called with object %s on thread %s", o.toString(), Thread.currentThread().toString()));
try {
setMillisecondMethod = o.getClass().getMethod("a", Long.TYPE);
setMillisecondMethod.setAccessible(true);
lastKnownVideoTime = 0;
currentVideoLength = 1;
currentPlayerController = new WeakReference<>(o);
SkipSegmentView.hide();
NewSegmentHelperLayout.hide();
// add image button when starting new video
Activity activity = playerActivity.get();
if (activity != null)
SponsorBlockUtils.addImageButton(activity, 5);
} catch (Exception e) {
Log.e(TAG, "Exception while initializing skip method", e);
}
}
public static void executeDownloadSegments(String videoId, boolean ignoreCache) {
SponsorSegment[] segments = SponsorBlockUtils.getSegmentsForVideo(videoId, ignoreCache);
Arrays.sort(segments);
if (VERBOSE)
for (SponsorSegment segment : segments) {
Log.v(TAG, "Detected segment: " + segment.toString());
}
sponsorSegmentsOfCurrentVideo = segments;
// new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable);
}
/**
* Works in 14.x, waits some time of object to me filled with data,
* No longer used, i've found another way to get faster videoId
*/
@Deprecated
public static void asyncGetVideoLinkFromObject(final Object o) {
// code no longer used
// if (currentVideoLink != null) {
// if (VERBOSE)
// Log.w(TAG, "asyncGetVideoLinkFromObject: currentVideoLink != null probably share button was clicked");
// return;
// }
//
// new Thread(new Runnable() {
// @Override
// public void run() {
// try {
// // It used to be "b" in 14.x version, it's "a" in 15.x
// Field b = o.getClass().getDeclaredField("b");
//
// int attempts = 0;
// String videoUrl = null;
// while (true) {
// Object objLink = b.get(o);
// if (objLink == null) {
// if (VERBOSE)
// Log.e(TAG, "asyncGetVideoLinkFromObject: objLink is null");
// } else {
// videoUrl = objLink.toString();
// if (videoUrl.isEmpty())
// videoUrl = null;
// }
//
// if (videoUrl != null)
// break;
//
// if (attempts++ > 5) {
// Log.w(TAG, "asyncGetVideoLinkFromObject: attempts++ > 5");
// return;
// }
// Thread.sleep(50);
// }
//
// if (currentVideoLink == null) {
// currentVideoLink = videoUrl;
// if (VERBOSE)
// Log.d(TAG, "asyncGetVideoLinkFromObject: link set to " + videoUrl);
//
// executeDownloadSegments(substringVideoIdFromLink(videoUrl), false);
// }
//
// } catch (Exception e) {
// Log.e(TAG, "Cannot get link from object", e);
// }
// }
// }).start();
//
// Activity activity = playerActivity.get();
// if (activity != null)
// SponsorBlockUtils.addImageButton(activity);
}
/**
* Called when it's time to update the UI with new second, about once per second, only when playing, also in background
*/
public static void setCurrentVideoTime(long millis) {
if (VERBOSE)
Log.v(TAG, "setCurrentVideoTime: current video time: " + millis);
if (!SponsorBlockSettings.isSponsorBlockEnabled) return;
lastKnownVideoTime = millis;
if (millis <= 0) return;
//findAndSkipSegment(false);
SponsorSegment[] segments = sponsorSegmentsOfCurrentVideo;
if (segments == null || segments.length == 0) return;
final long START_TIMER_BEFORE_SEGMENT_MILLIS = 1200;
final long startTimerAtMillis = millis + START_TIMER_BEFORE_SEGMENT_MILLIS;
for (final SponsorSegment segment : segments) {
if (segment.start > millis) {
if (segment.start > startTimerAtMillis)
break; // it's more then START_TIMER_BEFORE_SEGMENT_MILLIS far away
if (!segment.category.behaviour.skip)
break;
if (skipSponsorTask == null) {
if (VERBOSE)
Log.d(TAG, "Scheduling skipSponsorTask");
skipSponsorTask = new TimerTask() {
@Override
public void run() {
skipSponsorTask = null;
lastKnownVideoTime = segment.start + 1;
new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable);
}
};
sponsorTimer.schedule(skipSponsorTask, segment.start - millis);
} else {
if (VERBOSE)
Log.d(TAG, "skipSponsorTask is already scheduled...");
}
break;
}
if (segment.end < millis)
continue;
// we are in the segment!
if (segment.category.behaviour.skip) {
sendViewRequestAsync(millis, segment);
skipSegment(segment, false);
break;
} else {
SkipSegmentView.show();
return;
}
}
SkipSegmentView.hide();
}
private static void sendViewRequestAsync(final long millis, final SponsorSegment segment) {
new Thread(new Runnable() {
@Override
public void run() {
if (SponsorBlockSettings.countSkips &&
segment.category != SponsorBlockSettings.SegmentInfo.Preview &&
millis - segment.start < 2000) {
// Only skips from the start should count as a view
SponsorBlockUtils.sendViewCountRequest(segment);
}
}
}).start();
}
/**
* Called very high frequency (once every about 100ms), also in background. It sometimes triggers when a video is paused (couple times in the row with the same value)
*/
public static void setCurrentVideoTimeHighPrecision(final long millis) {
if (lastKnownVideoTime > 0)
lastKnownVideoTime = millis;
else
setCurrentVideoTime(millis);
}
public static long getLastKnownVideoTime() {
return lastKnownVideoTime;
}
/**
* Called before onDraw method on time bar object, sets video length in millis
*/
public static void setVideoLength(final long length) {
if (VERBOSE_DRAW_OPTIONS)
Log.d(TAG, "setVideoLength: length=" + length);
currentVideoLength = length;
}
public static void setSponsorBarAbsoluteLeft(final Rect rect) {
setSponsorBarAbsoluteLeft(rect.left);
}
public static void setSponsorBarAbsoluteLeft(final float left) {
if (VERBOSE_DRAW_OPTIONS)
Log.d(TAG, String.format("setSponsorBarLeft: left=%.2f", left));
sponsorBarLeft = left;
}
public static void setSponsorBarAbsoluteRight(final Rect rect) {
setSponsorBarAbsoluteRight(rect.right);
}
public static void setSponsorBarAbsoluteRight(final float right) {
if (VERBOSE_DRAW_OPTIONS)
Log.d(TAG, String.format("setSponsorBarRight: right=%.2f", right));
sponsorBarRight = right;
}
public static void setSponsorBarThickness(final int thickness) {
setSponsorBarThickness((float) thickness);
}
public static void setSponsorBarThickness(final float thickness) {
if (VERBOSE_DRAW_OPTIONS)
Log.d(TAG, String.format("setSponsorBarThickness: thickness=%.2f", thickness));
sponsorBarThickness = thickness;
}
public static void onSkipSponsorClicked() {
if (VERBOSE)
Log.d(TAG, "Skip segment clicked");
findAndSkipSegment(true);
}
public static void addSkipSponsorView15(final View view) {
playerActivity = new WeakReference<>((Activity) view.getContext());
if (VERBOSE)
Log.d(TAG, "addSkipSponsorView15: view=" + view.toString());
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
final ViewGroup viewGroup = (ViewGroup) ((ViewGroup) view).getChildAt(2);
Activity context = ((Activity) viewGroup.getContext());
viewGroup.addView(new SkipSegmentView(context));
viewGroup.addView(new NewSegmentHelperLayout(context));
SponsorBlockUtils.addImageButton(context, 40);
}
}, 500);
}
public static void addSkipSponsorView14(final View view) {
playerActivity = new WeakReference<>((Activity) view.getContext());
if (VERBOSE)
Log.d(TAG, "addSkipSponsorView14: view=" + view.toString());
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
final ViewGroup viewGroup = (ViewGroup) view.getParent();
Activity activity = (Activity) viewGroup.getContext();
viewGroup.addView(new SkipSegmentView(activity));
viewGroup.addView(new NewSegmentHelperLayout(activity));
// add image button when creating new activity
SponsorBlockUtils.addImageButton(activity, 5);
// InjectedPlugin.printViewStack(viewGroup, 0);
// SponsorBlockUtils.addImageButton(activity);
}
}, 500);
}
/**
* Called when it's time to draw time bar
*/
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
if (sponsorBarThickness < 0.1) return;
if (sponsorSegmentsOfCurrentVideo == null) return;
final float thicknessDiv2 = sponsorBarThickness / 2;
final float top = posY - thicknessDiv2;
final float bottom = posY + thicknessDiv2;
final float absoluteLeft = sponsorBarLeft;
final float absoluteRight = sponsorBarRight;
final float tmp1 = 1f / (float) currentVideoLength * (absoluteRight - absoluteLeft);
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
float left = segment.start * tmp1 + absoluteLeft;
float right = segment.end * tmp1 + absoluteLeft;
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}
}
// private final static Pattern videoIdRegex = Pattern.compile(".*\\.be\\/([A-Za-z0-9_\\-]{0,50}).*");
public static String substringVideoIdFromLink(String link) {
return link.substring(link.lastIndexOf('/') + 1);
}
public static void skipRelativeMilliseconds(int millisRelative) {
skipToMillisecond(lastKnownVideoTime + millisRelative);
}
public static void skipToMillisecond(long millisecond) {
// in 15.x if sponsor clip hits the end, then it crashes the app, because of too many function invocations
// I put this block so that skip can be made only once per some time
long now = System.currentTimeMillis();
if (now < allowNextSkipRequestTime) {
if (VERBOSE)
Log.w(TAG, "skipToMillisecond: to fast, slow down, because you'll fail");
return;
}
allowNextSkipRequestTime = now + 100;
if (setMillisecondMethod == null) {
Log.e(TAG, "setMillisecondMethod is null");
return;
}
final Object currentObj = currentPlayerController.get();
if (currentObj == null) {
Log.e(TAG, "currentObj is null (might have been collected by GC)");
return;
}
if (VERBOSE)
Log.d(TAG, String.format("Requesting skip to millis=%d on thread %s", millisecond, Thread.currentThread().toString()));
final long finalMillisecond = millisecond;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
if (VERBOSE)
Log.i(TAG, "Skipping to millis=" + finalMillisecond);
lastKnownVideoTime = finalMillisecond;
setMillisecondMethod.invoke(currentObj, finalMillisecond);
} catch (Exception e) {
Log.e(TAG, "Cannot skip to millisecond", e);
}
}
});
}
private static void findAndSkipSegment(boolean wasClicked) {
if (sponsorSegmentsOfCurrentVideo == null)
return;
final long millis = lastKnownVideoTime;
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
if (segment.start > millis)
break;
if (segment.end < millis)
continue;
SkipSegmentView.show();
if (!(segment.category.behaviour.skip || wasClicked))
return;
sendViewRequestAsync(millis, segment);
skipSegment(segment, wasClicked);
break;
}
SkipSegmentView.hide();
}
private static void skipSegment(SponsorSegment segment, boolean wasClicked) {
// if (lastSkippedSegment == segment) return;
// lastSkippedSegment = segment;
if (VERBOSE)
Log.d(TAG, "Skipping segment: " + segment.toString());
if (SponsorBlockSettings.showToastWhenSkippedAutomatically && !wasClicked)
SkipSegmentView.notifySkipped(segment);
skipToMillisecond(segment.end + 2);
SkipSegmentView.hide();
if (segment.category == SponsorBlockSettings.SegmentInfo.Preview) {
SponsorSegment[] newSegments = new SponsorSegment[sponsorSegmentsOfCurrentVideo.length - 1];
int i = 0;
for (SponsorSegment sponsorSegment : sponsorSegmentsOfCurrentVideo) {
if (sponsorSegment != segment)
newSegments[i++] = sponsorSegment;
}
sponsorSegmentsOfCurrentVideo = newSegments;
}
}
}

View File

@ -0,0 +1,95 @@
package pl.jakubweg;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import static pl.jakubweg.Helper.getStringByName;
import static pl.jakubweg.PlayerController.VERBOSE;
@SuppressLint({"RtlHardcoded", "SetTextI18n", "LongLogTag"})
public class SkipSegmentView extends TextView implements View.OnClickListener {
public static final String TAG = "jakubweg.SkipSegmentView";
private static boolean isVisible = false;
private static WeakReference<SkipSegmentView> view = new WeakReference<>(null);
private static SponsorSegment lastNotifiedSegment;
public SkipSegmentView(Context context) {
super(context);
isVisible = false;
setVisibility(GONE);
view = new WeakReference<>(this);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.END | Gravity.RIGHT | Gravity.CENTER_VERTICAL
);
this.setLayoutParams(layoutParams);
this.setBackgroundColor(0x66000000);
// this.setBackgroundColor(Color.MAGENTA);
this.setTextColor(0xFFFFFFFF);
int padding = (int) convertDpToPixel(4, context);
setPadding(padding, padding, padding, padding);
this.setText("" + getStringByName(context, "tap_skip"));
setOnClickListener(this);
}
public static void show() {
if (isVisible) return;
SkipSegmentView view = SkipSegmentView.view.get();
if (VERBOSE)
Log.d(TAG, "show; view=" + view);
if (view != null) {
view.setVisibility(VISIBLE);
view.bringToFront();
view.requestLayout();
view.invalidate();
}
isVisible = true;
}
public static void hide() {
if (!isVisible) return;
SkipSegmentView view = SkipSegmentView.view.get();
if (VERBOSE)
Log.d(TAG, "hide; view=" + view);
if (view != null)
view.setVisibility(GONE);
isVisible = false;
}
public static void notifySkipped(SponsorSegment segment) {
if (segment == lastNotifiedSegment) {
if (VERBOSE)
Log.d(TAG, "notifySkipped; segment == lastNotifiedSegment");
return;
}
lastNotifiedSegment = segment;
String skipMessage = segment.category.skipMessage;
SkipSegmentView view = SkipSegmentView.view.get();
if (VERBOSE)
Log.d(TAG, String.format("notifySkipped; view=%s, message=%s", view, skipMessage));
if (view != null)
Toast.makeText(view.getContext(), skipMessage, Toast.LENGTH_SHORT).show();
}
public static float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
@Override
public void onClick(View v) {
PlayerController.onSkipSponsorClicked();
}
}

View File

@ -0,0 +1,249 @@
package pl.jakubweg;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.text.InputType;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
import static pl.jakubweg.Helper.getStringByName;
import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_CACHE_SEGMENTS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_NEW_SEGMENT_ENABLED;
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_NAME;
import static pl.jakubweg.SponsorBlockSettings.adjustNewSegmentMillis;
import static pl.jakubweg.SponsorBlockSettings.cacheEnabled;
import static pl.jakubweg.SponsorBlockSettings.countSkips;
import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically;
import static pl.jakubweg.SponsorBlockSettings.uuid;
public class SponsorBlockPreferenceFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
private ArrayList<Preference> preferencesToDisableWhenSBDisabled = new ArrayList<>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(PREFERENCES_NAME);
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
Activity context = this.getActivity();
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED);
preference.setDefaultValue(SponsorBlockSettings.isSponsorBlockEnabled);
preference.setChecked(SponsorBlockSettings.isSponsorBlockEnabled);
preference.setTitle(getStringByName(context, "enable_sb"));
preference.setSummary(getStringByName(context, "enable_sb_sum"));
preference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
enableCategoriesIfNeeded(((Boolean) newValue));
return true;
}
});
}
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_NEW_SEGMENT_ENABLED);
preference.setDefaultValue(SponsorBlockSettings.isAddNewSegmentEnabled);
preference.setChecked(SponsorBlockSettings.isAddNewSegmentEnabled);
preference.setTitle(getStringByName(context, "enable_segmadding"));
preference.setSummary(getStringByName(context, "enable_segmadding_sum"));
preferencesToDisableWhenSBDisabled.add(preference);
}
addGeneralCategory(context, preferenceScreen);
addSegmentsCategory(context, preferenceScreen);
addAboutCategory(context, preferenceScreen);
enableCategoriesIfNeeded(SponsorBlockSettings.isSponsorBlockEnabled);
}
private void enableCategoriesIfNeeded(boolean enabled) {
for (Preference preference : preferencesToDisableWhenSBDisabled)
preference.setEnabled(enabled);
}
@Override
public void onDestroy() {
super.onDestroy();
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
private void addSegmentsCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
preferencesToDisableWhenSBDisabled.add(category);
category.setTitle(getStringByName(context, "diff_segments"));
String defaultValue = DefaultBehaviour.key;
SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values();
String[] entries = new String[segmentBehaviours.length];
String[] entryValues = new String[segmentBehaviours.length];
for (int i = 0, segmentBehavioursLength = segmentBehaviours.length; i < segmentBehavioursLength; i++) {
SponsorBlockSettings.SegmentBehaviour behaviour = segmentBehaviours[i];
entries[i] = behaviour.name;
entryValues[i] = behaviour.key;
}
for (SponsorBlockSettings.SegmentInfo segmentInfo : SponsorBlockSettings.SegmentInfo.valuesWithoutPreview()) {
ListPreference preference = new ListPreference(context);
preference.setTitle(segmentInfo.getTitleWithDot());
preference.setSummary(segmentInfo.description);
preference.setKey(segmentInfo.key);
preference.setDefaultValue(defaultValue);
preference.setEntries(entries);
preference.setEntryValues(entryValues);
category.addPreference(preference);
}
}
private void addAboutCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
category.setTitle("About");
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle(getStringByName(context, "about_api"));
preference.setSummary(getStringByName(context, "about_api_sum"));
preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("http://sponsor.ajay.app"));
preference.getContext().startActivity(i);
return false;
}
});
}
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle(getStringByName(context, "about_madeby"));
}
}
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
final PreferenceCategory category = new PreferenceCategory(context);
preferencesToDisableWhenSBDisabled.add(category);
screen.addPreference(category);
category.setTitle(getStringByName(context, "general"));
{
Preference preference = new SwitchPreference(context);
preference.setTitle(getStringByName(context, "general_skiptoast"));
preference.setSummary(getStringByName(context, "general_skiptoast_sum"));
preference.setKey(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP);
preference.setDefaultValue(showToastWhenSkippedAutomatically);
preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Toast.makeText(preference.getContext(), getStringByName(context, "skipped_segment"), Toast.LENGTH_SHORT).show();
return false;
}
});
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
Preference preference = new SwitchPreference(context);
preference.setTitle(getStringByName(context, "general_skipcount"));
preference.setSummary(getStringByName(context, "general_skipcount_sum"));
preference.setKey(PREFERENCES_KEY_COUNT_SKIPS);
preference.setDefaultValue(countSkips);
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
preference.setTitle(getStringByName(context, "general_adjusting"));
preference.setSummary(getStringByName(context, "general_adjusting_sum"));
preference.setKey(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP);
preference.setDefaultValue(String.valueOf(adjustNewSegmentMillis));
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new EditTextPreference(context);
preference.setTitle(getStringByName(context, "general_uuid"));
preference.setSummary(getStringByName(context, "general_uuid_sum"));
preference.setKey(PREFERENCES_KEY_UUID);
preference.setDefaultValue(uuid);
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new SwitchPreference(context);
preference.setTitle(getStringByName(context, "general_cache"));
preference.setSummary(getStringByName(context, "general_cache_sum"));
preference.setKey(PREFERENCES_KEY_CACHE_SEGMENTS);
preference.setDefaultValue(cacheEnabled);
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new Preference(context);
preference.setTitle(getStringByName(context, "general_cache_clear"));
preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
File cacheDirectory = SponsorBlockSettings.cacheDirectory;
if (cacheDirectory != null) {
for (File file : cacheDirectory.listFiles()) {
if (!file.delete())
return false;
}
Toast.makeText(getActivity(), getStringByName(context, "done"), Toast.LENGTH_SHORT).show();
}
return false;
}
});
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SponsorBlockSettings.update(getActivity());
}
}

View File

@ -0,0 +1,215 @@
package pl.jakubweg;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Paint;
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;
import java.util.UUID;
import static pl.jakubweg.Helper.getStringByName;
public class SponsorBlockSettings {
public static final String CACHE_DIRECTORY_NAME = "sponsor-block-segments-1";
public static final String PREFERENCES_NAME = "sponsor-block";
public static final String PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP = "show-toast";
public static final String PREFERENCES_KEY_COUNT_SKIPS = "count-skips";
public static final String PREFERENCES_KEY_UUID = "uuid";
public static final String PREFERENCES_KEY_CACHE_SEGMENTS = "cache-enabled";
public static final String PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP = "new-segment-step-accuracy";
public static final String PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED = "sb-enabled";
public static final String PREFERENCES_KEY_NEW_SEGMENT_ENABLED = "sb-new-segment-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 SegmentBehaviour DefaultBehaviour = SegmentBehaviour.SkipAutomatically;
public static boolean isSponsorBlockEnabled = false;
public static boolean isAddNewSegmentEnabled = false;
public static boolean showToastWhenSkippedAutomatically = true;
public static boolean countSkips = true;
public static boolean cacheEnabled = true;
public static int adjustNewSegmentMillis = 150;
public static String uuid = "<invalid>";
public static File cacheDirectory;
static Context context;
private static String sponsorBlockUrlCategories = "[]";
public SponsorBlockSettings(Context context) {
SponsorBlockSettings.context = context;
}
public static String getSponsorBlockUrlWithCategories(String videoId) {
return sponsorBlockSkipSegmentsUrl + "?videoID=" + videoId + "&categories=" + sponsorBlockUrlCategories;
}
public static String getSponsorBlockViewedUrl(String UUID) {
return sponsorBlockViewedUrl + "?UUID=" + UUID;
}
public static void update(Context context) {
if (context == null) return;
File directory = cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY_NAME);
if (!directory.mkdirs() && !directory.exists()) {
Log.e("jakubweg.Settings", "Unable to create cache directory");
cacheDirectory = null;
}
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
isSponsorBlockEnabled = preferences.getBoolean(PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, isSponsorBlockEnabled);
if (!isSponsorBlockEnabled) {
SkipSegmentView.hide();
NewSegmentHelperLayout.hide();
SponsorBlockUtils.hideButton();
PlayerController.sponsorSegmentsOfCurrentVideo = null;
} else if (isAddNewSegmentEnabled) {
SponsorBlockUtils.showButton();
}
isAddNewSegmentEnabled = preferences.getBoolean(PREFERENCES_KEY_NEW_SEGMENT_ENABLED, isAddNewSegmentEnabled);
if (!isAddNewSegmentEnabled) {
NewSegmentHelperLayout.hide();
SponsorBlockUtils.hideButton();
} else {
SponsorBlockUtils.showButton();
}
SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values();
final ArrayList<String> enabledCategories = new ArrayList<>(possibleBehaviours.length);
for (SegmentInfo segment : SegmentInfo.valuesWithoutPreview()) {
SegmentBehaviour behaviour = null;
String value = preferences.getString(segment.key, null);
if (value == null)
behaviour = DefaultBehaviour;
else {
for (SegmentBehaviour possibleBehaviour : possibleBehaviours) {
if (possibleBehaviour.key.equals(value)) {
behaviour = possibleBehaviour;
break;
}
}
}
if (behaviour == null)
behaviour = DefaultBehaviour;
segment.behaviour = behaviour;
if (behaviour.showOnTimeBar)
enabledCategories.add(segment.key);
}
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22]";
if (enabledCategories.size() == 0)
sponsorBlockUrlCategories = "[]";
else
sponsorBlockUrlCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
showToastWhenSkippedAutomatically = preferences.getBoolean(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP, showToastWhenSkippedAutomatically);
cacheEnabled = preferences.getBoolean(PREFERENCES_KEY_CACHE_SEGMENTS, true);
adjustNewSegmentMillis = Integer.parseInt(preferences
.getString(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP,
String.valueOf(adjustNewSegmentMillis)));
uuid = preferences.getString(PREFERENCES_KEY_UUID, null);
if (uuid == null) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())
.replace("-", "");
preferences.edit().putString(PREFERENCES_KEY_UUID, uuid).apply();
}
}
public enum SegmentBehaviour {
SkipAutomatically("skip", getStringByName(context, "skip_automatically"), true, true),
ManualSkip("manual-skip", getStringByName(context, "skip_showbutton"), false, true),
Ignore("ignore", getStringByName(context, "skip_ignore"), false, false);
public final String key;
public final String name;
public final boolean skip;
public final boolean showOnTimeBar;
SegmentBehaviour(String key,
String name,
boolean skip,
boolean showOnTimeBar) {
this.key = key;
this.name = name;
this.skip = skip;
this.showOnTimeBar = showOnTimeBar;
}
}
public enum SegmentInfo {
Sponsor("sponsor", getStringByName(context, "segments_sponsor"), getStringByName(context, "skipped_sponsor"), getStringByName(context, "segments_sponsor_sum"), null, 0xFF00d400),
Intro("intro", getStringByName(context, "segments_intermission"), getStringByName(context, "skipped_intermission"), getStringByName(context, "segments_intermission_sum"), null, 0xFF00ffff),
Outro("outro", getStringByName(context, "segments_endcard"), getStringByName(context, "skipped_endcard"), getStringByName(context, "segments_endcards_sum"), null, 0xFF0202ed),
Interaction("interaction", getStringByName(context, "segments_subscribe"), getStringByName(context, "skipped_subscribe"), getStringByName(context, "segments_subscribe_sum"), null, 0xFFcc00ff),
SelfPromo("selfpromo", getStringByName(context, "segments_selfpromo"), getStringByName(context, "skipped_selfpromo"), getStringByName(context, "segments_selfpromo_sum"), null, 0xFFffff00),
MusicOfftopic("music_offtopic", getStringByName(context, "segments_music"), getStringByName(context, "skipped_music"), getStringByName(context, "segments_music_sum"), null, 0xFFff9900),
Preview("preview", "", getStringByName(context, "skipped_preview"), "", SegmentBehaviour.SkipAutomatically, 0xFF000000),
;
private static SegmentInfo[] mValuesWithoutPreview = new SegmentInfo[]{
Sponsor,
Intro,
Outro,
Interaction,
SelfPromo,
MusicOfftopic
};
private static Map<String, SegmentInfo> mValuesMap = new HashMap<>(7);
static {
for (SegmentInfo value : valuesWithoutPreview())
mValuesMap.put(value.key, value);
}
public final String key;
public final String title;
public final String skipMessage;
public final String description;
public final int color;
public final Paint paint;
public SegmentBehaviour behaviour;
private CharSequence lazyTitleWithDot;
SegmentInfo(String key,
String title,
String skipMessage,
String description,
SegmentBehaviour behaviour,
int color) {
this.key = key;
this.title = title;
this.skipMessage = skipMessage;
this.description = description;
this.behaviour = behaviour;
this.color = color & 0xFFFFFF;
paint = new Paint();
paint.setColor(color);
}
public static SegmentInfo[] valuesWithoutPreview() {
return mValuesWithoutPreview;
}
public static SegmentInfo byCategoryKey(String key) {
return mValuesMap.get(key);
}
public CharSequence getTitleWithDot() {
return (lazyTitleWithDot == null) ?
lazyTitleWithDot = Html.fromHtml(String.format("<font color=\"#%06X\">⬤</font> %s", color, title))
: lazyTitleWithDot;
}
}
}

View File

@ -0,0 +1,648 @@
package pl.jakubweg;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
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.sponsorBlockSkipSegmentsUrl;
@SuppressWarnings({"LongLogTag"})
public abstract class SponsorBlockUtils {
public static final String TAG = "jakubweg.SponsorBlockUtils";
public static final String DATE_FORMAT = "HH:mm:ss.SSS";
@SuppressLint("SimpleDateFormat")
public static final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
private static final int sponsorBtnId = 1234;
private static final View.OnClickListener sponsorBlockBtnListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
NewSegmentHelperLayout.toggle();
}
};
private static int shareBtnId = -1;
private static long newSponsorSegmentDialogShownMillis;
private static long newSponsorSegmentStartMillis = -1;
private static long newSponsorSegmentEndMillis = -1;
private static final DialogInterface.OnClickListener newSponsorSegmentDialogListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = ((AlertDialog) dialog).getContext();
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// start
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
Toast.makeText(context.getApplicationContext(), "Start of the segment set", Toast.LENGTH_LONG).show();
break;
case DialogInterface.BUTTON_POSITIVE:
// end
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
Toast.makeText(context.getApplicationContext(), "End of the segment set", Toast.LENGTH_SHORT).show();
break;
}
dialog.dismiss();
}
};
private static SponsorBlockSettings.SegmentInfo newSponsorBlockSegmentType;
private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockSettings.SegmentInfo.valuesWithoutPreview()[which];
boolean enableButton;
if (!segmentType.behaviour.showOnTimeBar) {
Toast.makeText(
((AlertDialog) dialog).getContext().getApplicationContext(),
"You've disabled this category in the settings, so can't submit it",
Toast.LENGTH_SHORT).show();
enableButton = false;
} else {
Toast.makeText(
((AlertDialog) dialog).getContext().getApplicationContext(),
segmentType.description,
Toast.LENGTH_SHORT).show();
newSponsorBlockSegmentType = segmentType;
enableButton = true;
}
((AlertDialog) dialog)
.getButton(DialogInterface.BUTTON_POSITIVE)
.setEnabled(enableButton);
}
};
private static final DialogInterface.OnClickListener segmentReadyDialogButtonListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
NewSegmentHelperLayout.hide();
Context context = ((AlertDialog) dialog).getContext();
dialog.dismiss();
SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutPreview();
CharSequence[] titles = new CharSequence[values.length];
for (int i = 0; i < values.length; i++) {
// titles[i] = values[i].title;
titles[i] = values[i].getTitleWithDot();
}
newSponsorBlockSegmentType = null;
new AlertDialog.Builder(context)
.setTitle("Choose the segment category")
.setSingleChoiceItems(titles, -1, segmentTypeListener)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener)
.show()
.getButton(DialogInterface.BUTTON_POSITIVE)
.setEnabled(false);
}
};
private static WeakReference<Context> appContext = new WeakReference<>(null);
private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
Context context = ((AlertDialog) dialog).getContext().getApplicationContext();
Toast.makeText(context, "Submitting segment...", Toast.LENGTH_SHORT).show();
appContext = new WeakReference<>(context);
new Thread(submitRunnable).start();
}
};
private static boolean isShown = false;
private static WeakReference<ImageView> sponsorBlockBtn = new WeakReference<>(null);
private static String messageToToast = "";
private static EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener();
private static final DialogInterface.OnClickListener editByHandDialogListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = ((AlertDialog) dialog).getContext();
final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which;
final EditText textView = new EditText(context);
textView.setHint(DATE_FORMAT);
if (isStart) {
if (newSponsorSegmentStartMillis >= 0)
textView.setText(dateFormatter.format(new Date(newSponsorSegmentStartMillis)));
} else {
if (newSponsorSegmentEndMillis >= 0)
textView.setText(dateFormatter.format(new Date(newSponsorSegmentEndMillis)));
}
editByHandSaveDialogListener.settingStart = isStart;
editByHandSaveDialogListener.editText = new WeakReference<>(textView);
new AlertDialog.Builder(context)
.setTitle("Time of the " + (isStart ? "start" : "end") + " of the segment")
.setView(textView)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton("now", editByHandSaveDialogListener)
.setPositiveButton(android.R.string.ok, editByHandSaveDialogListener)
.show();
dialog.dismiss();
}
};
private static Runnable toastRunnable = new Runnable() {
@Override
public void run() {
Context context = appContext.get();
if (context != null && messageToToast != null)
Toast.makeText(context, messageToToast, Toast.LENGTH_LONG).show();
}
};
private static final Runnable submitRunnable = new Runnable() {
@Override
public void run() {
messageToToast = null;
final String uuid = SponsorBlockSettings.uuid;
final long start = newSponsorSegmentStartMillis;
final long end = newSponsorSegmentEndMillis;
final String videoId = getCurrentVideoId();
final SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockUtils.newSponsorBlockSegmentType;
try {
if (start < 0 || end < 0 || start >= end || segmentType == null || videoId == null || uuid == null) {
Log.e(TAG, "Unable to submit times, invalid parameters");
return;
}
URL url = new URL(String.format(Locale.US,
sponsorBlockSkipSegmentsUrl + "?videoID=%s&userID=%s&startTime=%.3f&endTime=%.3f&category=%s",
videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key));
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
switch (connection.getResponseCode()) {
default:
messageToToast = "Unable to submit segments: Status: " + connection.getResponseCode() + " " + connection.getResponseMessage();
break;
case 429:
messageToToast = "Can't submit the segment.\nRate Limit (Too many for the same user or IP)";
break;
case 403:
messageToToast = "Can't submit the segment.\nRejected by auto moderator";
break;
case 409:
messageToToast = "Duplicate";
break;
case 200:
messageToToast = "Segment submitted successfully";
break;
}
Log.i(TAG, "Segment submitted with status: " + connection.getResponseCode() + ", " + messageToToast);
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
} catch (Exception e) {
Log.e(TAG, "Unable to submit segment", e);
}
if (videoId != null)
PlayerController.executeDownloadSegments(videoId, true);
}
};
static {
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private SponsorBlockUtils() {
}
public static void showButton() {
if (isShown) return;
isShown = true;
View i = sponsorBlockBtn.get();
if (i == null) return;
i.setVisibility(VISIBLE);
i.bringToFront();
i.requestLayout();
i.invalidate();
}
public static void hideButton() {
if (!isShown) return;
isShown = false;
View i = sponsorBlockBtn.get();
if (i != null)
i.setVisibility(GONE);
}
@SuppressLint("LongLogTag")
public static void addImageButton(final Activity activity, final int attemptsWhenFail) {
if (VERBOSE)
Log.d(TAG, "addImageButton activity=" + activity + ",attemptsWhenFail=" + attemptsWhenFail);
if (activity == null)
return;
final View existingSponsorBtn = activity.findViewById(sponsorBtnId);
if (existingSponsorBtn != null) {
if (VERBOSE)
Log.d(TAG, "addImageButton: sponsorBtn exists");
if (SponsorBlockSettings.isAddNewSegmentEnabled)
showButton();
return;
}
String packageName = activity.getPackageName();
Resources R = activity.getResources();
shareBtnId = R.getIdentifier("player_share_button", "id", packageName);
// final int addToBtnId = R.getIdentifier("player_addto_button", "id", packageName);
final int addToBtnId = R.getIdentifier("live_chat_overlay_button", "id", packageName);
int titleViewId = R.getIdentifier("player_video_title_view", "id", packageName);
// final int iconId = R.getIdentifier("player_fast_forward", "drawable", packageName);
final int iconId = R.getIdentifier("ic_sb_logo", "drawable", packageName);
final View addToBtn = activity.findViewById(addToBtnId);
final ImageView shareBtn = activity.findViewById(shareBtnId);
final TextView titleView = activity.findViewById(titleViewId);
if (addToBtn == null || shareBtn == null || titleView == null) {
if (VERBOSE)
Log.e(TAG, String.format("one of following is null: addToBtn=%s shareBtn=%s titleView=%s",
addToBtn, shareBtn, titleView));
if (attemptsWhenFail > 0)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (VERBOSE)
Log.i(TAG, "Retrying addImageButton");
addImageButton(PlayerController.playerActivity.get(), attemptsWhenFail - 1);
}
}, 5000);
return;
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
Class<?> touchImageViewClass = Class.forName("com.google.android.libraries.youtube.common.ui.TouchImageView");
Constructor<?> constructor = touchImageViewClass.getConstructor(Context.class);
final ImageView instance = ((ImageView) constructor.newInstance(activity));
instance.setImageResource(iconId);
instance.setId(sponsorBtnId);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(shareBtn.getLayoutParams());
layoutParams.addRule(RelativeLayout.LEFT_OF, addToBtnId);
instance.setLayoutParams(layoutParams);
((ViewGroup) shareBtn.getParent()).addView(instance, 0);
instance.setPadding(shareBtn.getPaddingLeft(),
shareBtn.getPaddingTop(),
shareBtn.getPaddingRight(),
shareBtn.getPaddingBottom());
RelativeLayout.LayoutParams titleViewLayoutParams = (RelativeLayout.LayoutParams) titleView.getLayoutParams();
titleViewLayoutParams.addRule(RelativeLayout.START_OF, sponsorBtnId);
titleView.requestLayout();
instance.setClickable(true);
instance.setFocusable(true);
Drawable.ConstantState constantState = shareBtn.getBackground().mutate().getConstantState();
if (constantState != null)
instance.setBackground(constantState.newDrawable());
instance.setOnClickListener(sponsorBlockBtnListener);
sponsorBlockBtn = new WeakReference<>(instance);
isShown = true;
if (!SponsorBlockSettings.isAddNewSegmentEnabled)
hideButton();
if (VERBOSE)
Log.i(TAG, "Image Button added");
} catch (Exception e) {
Log.e(TAG, "Error while adding button", e);
}
}
});
}
@SuppressLint("DefaultLocale")
public static void onMarkLocationClicked(Context context) {
newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime();
new AlertDialog.Builder(context)
.setTitle("New Sponsor Block segment")
.setMessage(String.format("Set %02d:%02d:%04d as a start or end of new segment?",
newSponsorSegmentDialogShownMillis / 60000,
newSponsorSegmentDialogShownMillis / 1000 % 60,
newSponsorSegmentDialogShownMillis % 1000))
.setNeutralButton("Cancel", null)
.setNegativeButton("Start", newSponsorSegmentDialogListener)
.setPositiveButton("End", newSponsorSegmentDialogListener)
.show();
}
@SuppressLint("DefaultLocale")
public static void onPublishClicked(Context context) {
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000;
long start = (newSponsorSegmentStartMillis) / 1000;
long end = (newSponsorSegmentEndMillis) / 1000;
new AlertDialog.Builder(context)
.setTitle("Is it right?")
.setMessage(String.format("The segment lasts from %02d:%02d to %02d:%02d (%d minutes %02d seconds)\nIs it ready to submit?",
start / 60, start % 60,
end / 60, end % 60,
length / 60, length % 60))
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener)
.show();
} else {
Toast.makeText(context, "Mark two locations on the time bar first", Toast.LENGTH_SHORT).show();
}
}
@SuppressLint("DefaultLocale")
public static void onPreviewClicked(Context context) {
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
Toast t = Toast.makeText(context, "Preview", Toast.LENGTH_SHORT);
t.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP, t.getXOffset(), t.getYOffset());
t.show();
PlayerController.skipToMillisecond(newSponsorSegmentStartMillis - 3000);
final SponsorSegment[] original = PlayerController.sponsorSegmentsOfCurrentVideo;
final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1);
segments[segments.length - 1] = new SponsorSegment(newSponsorSegmentStartMillis, newSponsorSegmentEndMillis,
SponsorBlockSettings.SegmentInfo.Preview, null);
Arrays.sort(segments);
sponsorSegmentsOfCurrentVideo = segments;
} else {
Toast.makeText(context, "Mark two locations on the time bar first", Toast.LENGTH_SHORT).show();
}
}
@SuppressLint("DefaultLocale")
public static void onEditByHandClicked(Context context) {
new AlertDialog.Builder(context)
.setTitle("Edit time of new segment by hand")
.setMessage("Do you want to edit time of the start or the end of the segment?")
.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton("start", editByHandDialogListener)
.setPositiveButton("end", editByHandDialogListener)
.show();
}
public static void notifyShareBtnVisibilityChanged(View v) {
if (v.getId() != shareBtnId || !SponsorBlockSettings.isAddNewSegmentEnabled) return;
// if (VERBOSE)
// Log.d(TAG, "VISIBILITY CHANGED of view " + v);
ImageView sponsorBtn = sponsorBlockBtn.get();
if (sponsorBtn != null) {
sponsorBtn.setVisibility(v.getVisibility());
}
}
public synchronized static SponsorSegment[] getSegmentsForVideo(String videoId, boolean ignoreCache) {
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
int usageCounter = 0;
if (!ignoreCache && SponsorBlockSettings.cacheEnabled) {
File cacheDirectory = SponsorBlockSettings.cacheDirectory;
if (cacheDirectory == null) {
Log.w(TAG, "Cache directory is null, cannot read");
} else {
File file = new File(cacheDirectory, videoId);
try {
RandomAccessFile rwd = new RandomAccessFile(file, "rw");
rwd.seek(0);
usageCounter = rwd.readInt();
long now = System.currentTimeMillis();
long savedTimestamp = rwd.readLong();
int segmentsSize = rwd.readInt();
byte maxDaysCache;
if (usageCounter < 2)
maxDaysCache = 0;
else if (usageCounter < 5 || segmentsSize == 0)
maxDaysCache = 2;
else if (usageCounter < 10)
maxDaysCache = 5;
else
maxDaysCache = 10;
if (VERBOSE)
Log.d(TAG, String.format("Read cache data about segments, counter=%d, timestamp=%d, now=%d, maxCacheDays=%s, segmentsSize=%d",
usageCounter, savedTimestamp, now, maxDaysCache, segmentsSize));
if (savedTimestamp + (((long) maxDaysCache) * 24 * 60 * 60 * 1000) > now) {
if (VERBOSE)
Log.d(TAG, "getSegmentsForVideo: cacheHonored videoId=" + videoId);
SponsorSegment[] segments = new SponsorSegment[segmentsSize];
for (int i = 0; i < segmentsSize; i++) {
segments[i] = SponsorSegment.readFrom(rwd);
}
rwd.seek(0);
rwd.writeInt(usageCounter + 1);
rwd.close();
if (VERBOSE)
Log.d(TAG, "getSegmentsForVideo: reading from cache and updating usageCounter finished");
return segments;
} else {
if (VERBOSE)
Log.d(TAG, "getSegmentsForVideo: cache of video " + videoId + " was not honored, fallback to downloading...");
}
} catch (FileNotFoundException | EOFException ignored) {
if (VERBOSE)
Log.e(TAG, "FileNotFoundException | EOFException ignored");
} catch (Exception e) {
//noinspection ResultOfMethodCallIgnored
file.delete();
Log.e(TAG, "Error while reading cached segments", e);
}
}
}
ArrayList<SponsorSegment> sponsorSegments = new ArrayList<>();
try {
if (VERBOSE)
Log.i(TAG, "Trying to download segments for videoId=" + videoId);
URL url = new URL(SponsorBlockSettings.getSponsorBlockUrlWithCategories(videoId));
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
switch (connection.getResponseCode()) {
default:
Log.e(TAG, "Unable to download segments: Status: " + connection.getResponseCode() + " " + connection.getResponseMessage());
break;
case 404:
Log.w(TAG, "No segments for this video (ERR404)");
break;
case 200:
if (VERBOSE)
Log.i(TAG, "Received status 200 OK, parsing response...");
StringBuilder stringBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
connection.getInputStream().close();
JSONArray responseArray = new JSONArray(stringBuilder.toString());
int length = responseArray.length();
for (int i = 0; i < length; i++) {
JSONObject obj = ((JSONObject) responseArray.get(i));
JSONArray segments = obj.getJSONArray("segment");
long start = (long) (segments.getDouble(0) * 1000);
long end = (long) (segments.getDouble(1) * 1000);
String category = obj.getString("category");
String UUID = obj.getString("UUID");
SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category);
if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) {
SponsorSegment segment = new SponsorSegment(start, end, segmentCategory, UUID);
sponsorSegments.add(segment);
}
}
if (VERBOSE)
Log.v(TAG, "Parsing done");
break;
}
connection.disconnect();
if (SponsorBlockSettings.cacheEnabled) {
File cacheDirectory = SponsorBlockSettings.cacheDirectory;
if (cacheDirectory == null) {
Log.w(TAG, "Cache directory is null");
} else {
File file = new File(cacheDirectory, videoId);
try {
DataOutputStream stream = new DataOutputStream(new FileOutputStream(file));
stream.writeInt(usageCounter + 1);
stream.writeLong(System.currentTimeMillis());
stream.writeInt(sponsorSegments.size());
for (SponsorSegment segment : sponsorSegments) {
segment.writeTo(stream);
}
stream.close();
} catch (Exception e) {
//noinspection ResultOfMethodCallIgnored
file.delete();
Log.e(TAG, "Unable to write segments to file", e);
}
}
}
} catch (Exception e) {
Log.e(TAG, "download segments failed", e);
}
return sponsorSegments.toArray(new SponsorSegment[0]);
}
public static void sendViewCountRequest(SponsorSegment segment) {
try {
URL url = new URL(SponsorBlockSettings.getSponsorBlockViewedUrl(segment.UUID));
Log.d("sponsorblock", "requesting: " + url.getPath());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.getInputStream().close();
connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
public boolean settingStart;
public WeakReference<EditText> editText;
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
final EditText editText = this.editText.get();
if (editText == null) return;
Context context = ((AlertDialog) dialog).getContext();
try {
long time = (which == DialogInterface.BUTTON_NEUTRAL) ?
getLastKnownVideoTime() :
(Objects.requireNonNull(dateFormatter.parse(editText.getText().toString())).getTime());
if (settingStart)
newSponsorSegmentStartMillis = Math.max(time, 0);
else
newSponsorSegmentEndMillis = time;
if (which == DialogInterface.BUTTON_NEUTRAL)
editByHandDialogListener.onClick(dialog, settingStart ?
DialogInterface.BUTTON_NEGATIVE :
DialogInterface.BUTTON_POSITIVE);
else
Toast.makeText(context.getApplicationContext(), "Done", Toast.LENGTH_SHORT).show();
} catch (ParseException e) {
Toast.makeText(context.getApplicationContext(), "Cannot parse this time 😔", Toast.LENGTH_LONG).show();
}
}
}
}

View File

@ -0,0 +1,49 @@
package pl.jakubweg;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class SponsorSegment implements Comparable<SponsorSegment> {
public final long start;
public final long end;
public final SponsorBlockSettings.SegmentInfo category;
public final String UUID;
public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID) {
this.start = start;
this.end = end;
this.category = category;
this.UUID = UUID;
}
public static SponsorSegment readFrom(RandomAccessFile stream) throws IOException {
long start = stream.readLong();
long end = stream.readLong();
String categoryName = stream.readUTF();
String UUID = stream.readUTF();
SponsorBlockSettings.SegmentInfo category = SponsorBlockSettings.SegmentInfo.valueOf(categoryName);
return new SponsorSegment(start, end, category, UUID);
}
@Override
public String toString() {
return "SegmentInfo{" +
"start=" + start +
", end=" + end +
", category='" + category + '\'' +
'}';
}
@Override
public int compareTo(SponsorSegment o) {
return (int) (this.start - o.start);
}
public void writeTo(DataOutputStream stream) throws IOException {
stream.writeLong(start);
stream.writeLong(end);
stream.writeUTF(category.name());
stream.writeUTF(UUID);
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h5v2h2L12,1h-2v2zM10,18L5,18l5,-6v6zM19,3h-5v2h5v13l-5,-6v9h5c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX=".8"
android:scaleY=".8">
<path
android:fillColor="#FFFFFF"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
</group>
<path
android:fillColor="#FFFFFF"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM19,11c0,4.52 -2.98,8.69 -7,9.93 -4.02,-1.24 -7,-5.41 -7,-9.93L5,6.3l7,-3.11 7,3.11L19,14.17z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M5,5c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1L6,4c-0.55,0 -1,0.45 -1,1zM7.41,14L9,14v5c0,0.55 0.45,1 1,1h4c0.55,0 1,-0.45 1,-1v-5h1.59c0.89,0 1.34,-1.08 0.71,-1.71L12.71,7.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l-4.59,4.59c-0.63,0.63 -0.19,1.71 0.7,1.71z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="xfile_about_summary">"
- xfileFIN (Mods, Theming, Support)
- Laura (Theming, Support)
- ZaneZam (Publishing, Support)
- KevinX8 (Neko, Support)"</string>
<string name="xfile_about_title">About</string>
<string name="xfile_auto_repeat_bg_summary_off">Auto repeat in background is off</string>
<string name="xfile_auto_repeat_bg_summary_on">Auto repeat in background is on</string>
<string name="xfile_auto_repeat_bg_title">Auto repeat in background</string>
<string name="xfile_auto_repeat_linked_summary_off">Auto repeat is not linked to Autoplay toggle</string>
<string name="xfile_auto_repeat_linked_summary_on">Auto repeat is linked to Autoplay toggle (Autoplay off = Auto repeat on)</string>
<string name="xfile_auto_repeat_linked_title">Auto repeat linked to Autoplay</string>
<string name="xfile_auto_repeat_summary_off">Auto repeat is off</string>
<string name="xfile_auto_repeat_summary_on">Auto repeat is on</string>
<string name="xfile_auto_repeat_title">Auto repeat</string>
<string name="xfile_branding_watermark_summary_off">Video watermark is hidden</string>
<string name="xfile_branding_watermark_summary_on">Video watermark is shown</string>
<string name="xfile_branding_watermark_title">Video watermark</string>
<string name="xfile_buffer_summary">ExoPlayer v2 has to be enabled for buffer settings</string>
<string name="xfile_buffer_title">Buffer settings</string>
<string name="xfile_cast_button_summary_off">Cast button is hidden</string>
<string name="xfile_cast_button_summary_on">Cast button is shown</string>
<string name="xfile_cast_button_title">Cast button</string>
<string name="xfile_codec_override_title">Codec override</string>
<string name="xfile_current_override_manufacturer">Overrided manufacturer</string>
<string name="xfile_current_override_model">Overrided model</string>
<string name="xfile_debug_summary_off">Extra debug logging is disabled</string>
<string name="xfile_debug_summary_on">Extra debug logging is enabled</string>
<string name="xfile_debug_title">Debug mode</string>
<string name="xfile_default_codec_summary">Tap to set your device\'s default codec</string>
<string name="xfile_default_codec_title">Default codec</string>
<string name="xfile_discord_summary">Tap to join Vanced on Discord</string>
<string name="xfile_discord_title">Discord Server</string>
<string name="xfile_exoplayerv2_warning_summary">ExoPlayer v2 is experimental. DO NOT report errors happened when ExoPlayer v2 is enabled</string>
<string name="xfile_exoplayerv2_warning_title">Warning</string>
<string name="xfile_hardware_hdr_summary">Tap to enable hardware HDR</string>
<string name="xfile_hardware_hdr_title" translatable="false">Samsung Galaxy S8+</string>
<string name="xfile_hdr_full_brightness_summary_off">Video brightness will follow your device\'s brightness on HDR landscape videos</string>
<string name="xfile_hdr_full_brightness_summary_on">Video brightness is set to max on HDR landscape videos</string>
<string name="xfile_hdr_full_brightness_title">HDR Max brightness</string>
<string name="xfile_hiddenmenu_needed">taps needed to enable hidden setting</string>
<string name="xfile_hiddenmenu_open">No need, hidden setting has already been enabled</string>
<string name="xfile_hiddenmenu_opened">Hidden setting has been enabled</string>
<string name="xfile_info_cards_summary_off">Info cards are hidden</string>
<string name="xfile_info_cards_summary_on">Info cards are shown</string>
<string name="xfile_info_cards_title">Info cards</string>
<string name="xfile_layout_settings_title">Layout settings</string>
<string name="xfile_maximum_buffer_summary">"The maximum duration of media that the player will attempt to buffer (in milliseconds)
Default: 120000"</string>
<string name="xfile_maximum_buffer_title">Maximum buffer</string>
<string name="xfile_minimized_video_type_summary">Select the preferred minimized video type</string>
<string name="xfile_minimized_video_type_title">Minimized video type</string>
<string name="xfile_miniplayer_style_video">Video only</string>
<string name="xfile_miniplayer_style_video_controls">Video with media controls</string>
<string name="xfile_misc_title">Misc</string>
<string name="xfile_override_resolution_summary_off">Video resolution is being overridden to max</string>
<string name="xfile_override_resolution_summary_on">Video resolution is following your device screen resolution</string>
<string name="xfile_override_resolution_title">Max resolution</string>
<string name="xfile_playback_start_summary">"The duration of media that must be buffered for playback to start or resume following a user action such as seeking (in milliseconds)
Default: 2500"</string>
<string name="xfile_playback_start_title">Playback start</string>
<string name="xfile_preferred_video_quality_mobile_summary">Select preferred video resolution on Cellular Network</string>
<string name="xfile_preferred_video_quality_mobile_title">Preferred video quality Cellular</string>
<string name="xfile_preferred_video_quality_wifi_summary">Select preferred video resolution on Wi-Fi Network</string>
<string name="xfile_preferred_video_quality_wifi_title">Preferred video quality Wi-Fi</string>
<string name="xfile_preferred_video_speed_summary">Select preferred video speed</string>
<string name="xfile_preferred_video_speed_title">Preferred video speed</string>
<string name="xfile_rebuffer_summary">"The duration of media that must be buffered for playback to resume after a rebuffer (in milliseconds). A rebuffer is defined to be caused by buffer depletion rather than a user action
Default: 5000"</string>
<string name="xfile_rebuffer_title">Rebuffer</string>
<string name="xfile_settings">Vanced settings</string>
<string name="xfile_software_hdr_summary">Tap to enable software HDR</string>
<string name="xfile_software_hdr_title" translatable="false">Google Pixel XL</string>
<string name="xfile_suggestion_summary_off">End screens are hidden</string>
<string name="xfile_suggestion_summary_on">End screens are shown</string>
<string name="xfile_suggestion_title">End screens</string>
<string name="xfile_support_summary">Support links</string>
<string name="xfile_support_title">Support</string>
<string name="xfile_video_settings_title">Video settings</string>
<string name="xfile_vp9_summary">Tap to start forcing the VP9 codec</string>
<string name="xfile_vp9_summary_off">VP9 codec not enabled</string>
<string name="xfile_vp9_summary_on">VP9 codec enabled for supported devices, disable if you encounter stuttering/slowness in videos</string>
<string name="xfile_vp9_title">VP9 codec</string>
<string name="xfile_xda_summary">Tap to open the XDA post</string>
<string name="xfile_xda_title">XDA thread</string>
<string name="xfile_new_actionbar_title">Wide search bar</string>
<string name="xfile_new_actionbar_summary_off">Search bar style is defined by the app</string>
<string name="xfile_new_actionbar_summary_on">Forcing wide search bar</string>
<string name="xfile_zoom_to_fit_vertical_title">Dynamic player</string>
<string name="xfile_zoom_to_fit_vertical_summary_off">Dynamic player is defined automatically</string>
<string name="xfile_zoom_to_fit_vertical_summary_on">Dynamic player is forced on square and vertical videos</string>
<string name="xfile_about_theme_summary">New official theme toggle is in the General settings. This theme toggle is \"Developer\" toggle.</string>
<string name="xfile_about_theme_title">Theme info</string>
<string name="xfile_accessibility_seek_buttons_summary_off">Accessibility controls aren\'t displayed in the player</string>
<string name="xfile_accessibility_seek_buttons_summary_on">Accessibility controls are displayed in the player</string>
<string name="xfile_accessibility_seek_buttons_title">Accessibility player</string>
<string name="xfile_auto_captions_summary_off">Captions aren\'t enabled automatically at 0% volume </string>
<string name="xfile_auto_captions_summary_on">Captions are enabled automatically at 0% volume</string>
<string name="xfile_auto_captions_title">Auto captions</string>
<string name="xfile_swipe_padding_top_summary">Amount of pixels excluded from swiping at the top of the display to prevent swipe controls when dragging down notifications</string>
<string name="xfile_swipe_padding_top_title">Swipe padding</string>
<string name="xfile_swipe_threshold_summary">Amount of pixels you have to swipe until detecting starts to prevent unintended swiping</string>
<string name="xfile_swipe_threshold_title">Swipe threshold</string>
<string name="xfile_xfenster_brightness_summary_off">Swipe controls for brightness are disabled</string>
<string name="xfile_xfenster_brightness_summary_on">Swipe controls for brightness are enabled</string>
<string name="xfile_xfenster_brightness_title">Swipe controls for Brightness</string>
<string name="xfile_xfenster_screen_summary">Swipe controls for Brightness and Volume</string>
<string name="xfile_xfenster_title">Swipe controls</string>
<string name="xfile_xfenster_volume_summary_off">Swipe controls for volume are disabled</string>
<string name="xfile_xfenster_volume_summary_on">Swipe controls for volume are enabled</string>
<string name="xfile_xfenster_volume_title">Swipe controls for Volume</string>
<string name="xfile_website_summary">Tap to open our website</string>
<string name="xfile_website_title">Vanced website</string>
<string name="xfile_home_ads_summary_off">Home ads are hidden</string>
<string name="xfile_home_ads_summary_on">Home ads are shown</string>
<string name="xfile_home_ads_title">Home ads (Experimental)</string>
<string name="xfile_reel_summary_off">Stories are hidden</string>
<string name="xfile_reel_summary_on">Stories are shown</string>
<string name="xfile_reel_title">YouTube stories (Experimental)</string>
<string name="xfile_ad_settings_title">Ad settings</string>
<string name="xfile_credit_summary">Credits for people who have contributed</string>
<string name="xfile_credit_title">Credits</string>
<string name="souramoo_summary">Home ads removing enhancement and showed other kinds of debugging methods</string>
<string name="souramoo_title" translatable="false">souramoo</string>
<string name="bawm_summary">SponsorBlock implementation</string>
<string name="bawm_title" translatable="false">JakubWeg</string>
<string name="enable_sb">Enable Sponsor Block (Beta)</string>
<string name="enable_sb_sum">Switch this on for very cool sponsor segments skipping</string>
<string name="enable_segmadding">Enable new segment adding</string>
<string name="enable_segmadding_sum">Switch this on to enable experimental segment adding (has button visibility issues).</string>
<string name="diff_segments">What to do with different segments</string>
<string name="general">General</string>
<string name="general_skiptoast">Show a toast when skipping segment automatically</string>
<string name="general_skiptoast_sum">Click to see an example toast</string>
<string name="general_skipcount">Skip count tracking</string>
<string name="general_skipcount_sum">This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment.</string>
<string name="general_adjusting">Adjusting new segment step</string>
<string name="general_adjusting_sum">This is a number of milliseconds you can move when clicking buttons when adding new segment</string>
<string name="general_uuid">Your unique user id</string>
<string name="general_uuid_sum">This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you</string>
<string name="general_cache">Cache segments locally</string>
<string name="general_cache_sum">Frequently watched videos (eg. music videos) may store segments on this device to make skipping segments faster</string>
<string name="general_cache_clear">Clear sponsor block segments cache</string>
<string name="segments_sponsor">Sponsor</string>
<string name="segments_sponsor_sum">Paid promotion, paid referrals and direct advertisements</string>
<string name="segments_intermission">Intermission/Intro Animation</string>
<string name="segments_intermission_sum">An interval without actual content. Could be a pause, static frame, repeating animation</string>
<string name="segments_endcards">Endcards/Credits</string>
<string name="segments_endcards_sum">Credits or when the YouTube endcards appear. Not spoken conclusions</string>
<string name="segments_subscribe">Interaction Reminder (Subscribe)</string>
<string name="segments_subscribe_sum">When there is a short reminder to like, subscribe or follow them in the middle of content</string>
<string name="segments_selfpromo">Unpaid/Self Promotion</string>
<string name="segments_selfpromo_sum">Similar to "sponsor" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with</string>
<string name="segments_nomusic">Music: Non-Music Section</string>
<string name="segments_nomusic_sum">Only for use in music videos. This includes introductions or outros in music videos</string>
<string name="skipped_segment">Skipped a sponsor segment</string>
<string name="skipped_sponsor">Skipped sponsor</string>
<string name="skipped_intermission">Skipped intro</string>
<string name="skipped_endcard">Skipped outro</string>
<string name="skipped_subscribe">Skipped annoying reminder</string>
<string name="skipped_selfpromo">Skipped self promotion</string>
<string name="skipped_nomusic">Skipped silence</string>
<string name="skipped_preview">Skipped preview</string>
<string name="skip_automatically">Just skip, automatically</string>
<string name="skip_showbutton">Show skip button</string>
<string name="skip_ignore">Don\'t do anything</string>
<string name="about">About</string>
<string name="about_api">This app uses API from Sponsor Block</string>
<string name="about_api_sum">Click to learn more at: sponsor.ajay.app</string>
<string name="about_madeby">Integration made by JakubWeg</string>
<string name="tap_skip">Tap to skip</string>
<string name="app_name" />
</resources>