SponsorBlock: Init

Added a setting which, when enabled, skips parts of the video according to
the sponsor times SponsorBlock's API returns for the video.
This commit is contained in:
polymorphicshade 2020-03-08 15:00:13 -06:00
parent 94ecf9a081
commit c69ce3253f
24 changed files with 606 additions and 4 deletions

View file

@ -0,0 +1,174 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.schabi.newpipe.util.SponsorTimeInfo;
import org.schabi.newpipe.util.TimeFrame;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class SponsorBlockApiTask extends AsyncTask<String, Void, JSONObject> {
private static final Application app = App.getApp();
private static final String sponsorBlockApiUrl = "https://api.sponsor.ajay.app/api/";
private static final int timeoutPeriod = 30;
private static final String TAG = SponsorBlockApiTask.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private OkHttpClient client;
// api methods
public SponsorTimeInfo getVideoSponsorTimes(String url) throws ExecutionException, InterruptedException, JSONException {
String videoId = parseIdFromUrl(url);
String apiSuffix = "getVideoSponsorTimes?videoID=" + videoId;
JSONObject obj = execute(apiSuffix).get();
JSONArray arrayObj = obj.getJSONArray("sponsorTimes");
SponsorTimeInfo result = new SponsorTimeInfo();
for (int i = 0; i < arrayObj.length(); i++) {
JSONArray subArrayObj = arrayObj.getJSONArray(i);
double startTime = subArrayObj.getDouble(0) * 1000;
double endTime = subArrayObj.getDouble(1) * 1000;
TimeFrame timeFrame = new TimeFrame(startTime, endTime);
result.timeFrames.add(timeFrame);
}
return result;
}
public void postVideoSponsorTimes(String url, double startTime, double endTime, String userId) {
if (userId == null) {
userId = getRandomUserId();
}
double dStartTime = startTime / 1000.0;
double dEndTime = endTime / 1000.0;
String videoId = parseIdFromUrl(url);
String apiSuffix = "postVideoSponsorTimes?videoID=" + videoId + "&startTime=" + dStartTime + "&endTime=" + dEndTime + "&userID=" + userId;
execute(apiSuffix);
}
// task methods
@Override
protected JSONObject doInBackground(String... strings) {
if (isCancelled() || !isConnected()) return null;
try {
if (client == null) {
client = getUnsafeOkHttpClient()
.newBuilder()
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
.build();
}
Request request = new Request.Builder()
.url(sponsorBlockApiUrl + strings[0])
.build();
Response response = client.newCall(request).execute();
ResponseBody responseBody = response.body();
return responseBody == null
? null
: new JSONObject(responseBody.string());
}
catch (Exception ex) {
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
}
return null;
}
// helper methods
private boolean isConnected() {
ConnectivityManager cm = (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected();
}
private OkHttpClient getUnsafeOkHttpClient() throws NoSuchAlgorithmException, KeyManagementException {
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
}
};
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
return new OkHttpClient
.Builder()
.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0])
.hostnameVerifier((hostname, session) -> true)
.build();
}
private String parseIdFromUrl(String youTubeUrl) {
String pattern = "(?<=youtu.be/|watch\\?v=|/videos/|embed/)[^#&?]*";
Pattern compiledPattern = Pattern.compile(pattern);
Matcher matcher = compiledPattern.matcher(youTubeUrl);
if (matcher.find()) {
return matcher.group();
}
else {
return null;
}
}
private String getRandomUserId() {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder salt = new StringBuilder();
Random random = new Random();
while (salt.length() < 36) {
int index = (int) (random.nextFloat() * chars.length());
salt.append(chars.charAt(index));
}
return salt.toString();
}
}

View file

@ -34,6 +34,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
@ -54,9 +56,11 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.App;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.R;
import org.schabi.newpipe.SponsorBlockApiTask;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.AudioReactor;
@ -74,6 +78,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.SponsorTimeInfo;
import java.io.IOException;
@ -109,6 +114,11 @@ public abstract class BasePlayer implements
public static final int STATE_PAUSED_SEEK = 127;
public static final int STATE_COMPLETED = 128;
@NonNull
private final SharedPreferences mPrefs;
private SponsorTimeInfo sponsorTimeInfo;
/*//////////////////////////////////////////////////////////////////////////
// Intent
//////////////////////////////////////////////////////////////////////////*/
@ -219,6 +229,8 @@ public abstract class BasePlayer implements
this.loadControl = new LoadController();
this.renderFactory = new DefaultRenderersFactory(context);
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
}
public void setup() {
@ -648,11 +660,37 @@ public abstract class BasePlayer implements
if (simpleExoPlayer == null) {
return;
}
int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
onUpdateProgress(
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
currentProgress,
(int) simpleExoPlayer.getDuration(),
simpleExoPlayer.getBufferedPercentage()
);
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_enable), false) && sponsorTimeInfo != null) {
int skipTo = sponsorTimeInfo.getSponsorEndTimeFromProgress(currentProgress);
if (skipTo == 0) {
return;
}
seekTo(skipTo);
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_notifications), false)) {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notification_channel_id))
.setOngoing(false)
.setSmallIcon(R.drawable.ic_sponsor_block)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.settings_category_sponsorblock))
.setContentText(context.getString(R.string.sponsorblock_skipped_sponsor) + " \uD83D\uDC4D");
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(App.getApp());
notificationManager.notify(0, notificationBuilder.build());
}
if (DEBUG)
Log.d("SPONSOR_BLOCK", "Skipped sponsor: currentProgress = [" + currentProgress + "], skipped to = [" + skipTo + "]");
}
}
private Disposable getProgressReactor() {
@ -1015,6 +1053,15 @@ public abstract class BasePlayer implements
initThumbnail(info.getThumbnailUrl());
registerView();
if (mPrefs.getBoolean(context.getString(R.string.sponsorblock_enable), false)) {
try {
sponsorTimeInfo = new SponsorBlockApiTask().getVideoSponsorTimes(getVideoUrl());
}
catch (Exception e) {
Log.e("SPONSOR_BLOCK", "Error getting video sponsor times.", e);
}
}
}
@Override
@ -1546,4 +1593,12 @@ public abstract class BasePlayer implements
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
}
public SponsorTimeInfo getSponsorTimeInfo() {
return sponsorTimeInfo;
}
public void setSponsorTimeInfo(SponsorTimeInfo sponsorTimeInfo) {
this.sponsorTimeInfo = sponsorTimeInfo;
}
}

View file

@ -20,6 +20,7 @@
package org.schabi.newpipe.player;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@ -70,6 +71,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.SponsorBlockApiTask;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
@ -87,10 +89,15 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.SponsorTimeInfo;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
import org.schabi.newpipe.util.TimeFrame;
import org.schabi.newpipe.views.MarkableSeekBar;
import org.schabi.newpipe.views.SeekBarMarker;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
@ -129,6 +136,8 @@ public final class MainVideoPlayer extends AppCompatActivity
private ContentObserver rotationObserver;
private int customSponsorStartTime;
/*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@ -548,6 +557,7 @@ public final class MainVideoPlayer extends AppCompatActivity
private ImageButton switchPopupButton;
private ImageButton switchBackgroundButton;
private ImageButton muteButton;
private ImageButton submitSponsorTimesButton;
private RelativeLayout windowRootLayout;
private View secondaryControls;
@ -585,6 +595,17 @@ public final class MainVideoPlayer extends AppCompatActivity
this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation);
this.switchBackgroundButton = view.findViewById(R.id.switchBackground);
this.muteButton = view.findViewById(R.id.switchMute);
this.submitSponsorTimesButton = rootView.findViewById(R.id.submitSponsorTimes);
this.submitSponsorTimesButton.setTag(false);
this.submitSponsorTimesButton.setOnLongClickListener(v -> {
onSponsorBlockButtonLongClicked();
return true;
});
if (!defaultPreferences.getBoolean(getString(R.string.sponsorblock_enable), false)) {
this.submitSponsorTimesButton.setVisibility(View.GONE);
}
this.switchPopupButton = view.findViewById(R.id.switchPopup);
this.queueLayout = findViewById(R.id.playQueuePanel);
@ -634,6 +655,7 @@ public final class MainVideoPlayer extends AppCompatActivity
toggleOrientationButton.setOnClickListener(this);
switchBackgroundButton.setOnClickListener(this);
muteButton.setOnClickListener(this);
submitSponsorTimesButton.setOnClickListener(this);
switchPopupButton.setOnClickListener(this);
getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> {
@ -814,6 +836,97 @@ public final class MainVideoPlayer extends AppCompatActivity
setMuteButton(muteButton, playerImpl.isMuted());
}
private void onSponsorBlockButtonClicked() {
if (DEBUG) Log.d(TAG, "onSponsorBlockButtonClicked() called");
if (playerImpl.getPlayer() == null) return;
if ((boolean) submitSponsorTimesButton.getTag()) {
TimeFrame customTimeFrame = new TimeFrame(customSponsorStartTime, simpleExoPlayer.getCurrentPosition());
customTimeFrame.tag = "custom";
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
if (sponsorTimeInfo == null) {
sponsorTimeInfo = new SponsorTimeInfo();
setSponsorTimeInfo(sponsorTimeInfo);
}
sponsorTimeInfo.timeFrames.add(customTimeFrame);
SeekBarMarker marker = new SeekBarMarker(customTimeFrame.startTime, customTimeFrame.endTime, (int) simpleExoPlayer.getDuration(), Color.BLUE);
marker.tag = "custom";
MarkableSeekBar markableSeekBar = (MarkableSeekBar) getPlaybackSeekBar();
markableSeekBar.seekBarMarkers.add(marker);
markableSeekBar.invalidate();
submitSponsorTimesButton.setTag(false);
submitSponsorTimesButton.setImageResource(R.drawable.ic_sponsor_block);
}
else {
customSponsorStartTime = (int) simpleExoPlayer.getCurrentPosition();
submitSponsorTimesButton.setTag(true);
submitSponsorTimesButton.setImageResource(R.drawable.ic_sponsor_block_stop);
}
}
private void onSponsorBlockButtonLongClicked() {
if (DEBUG) Log.d(TAG, "onSponsorBlockButtonLongClicked() called");
if (playerImpl.getPlayer() == null) return;
ArrayList<SeekBarMarker> customMarkers = new ArrayList<>();
ArrayList<TimeFrame> customTimeFrames = new ArrayList<>();
for (SeekBarMarker marker : ((MarkableSeekBar) getPlaybackSeekBar()).seekBarMarkers) {
if (marker.tag == "custom") {
customMarkers.add(marker);
}
}
if (customMarkers.size() == 0) {
return;
}
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
if (sponsorTimeInfo != null) {
for (TimeFrame timeFrame : sponsorTimeInfo.timeFrames) {
if (timeFrame.tag == "custom") {
customTimeFrames.add(timeFrame);
}
}
}
new AlertDialog
.Builder(context)
.setMessage("Submit " + customMarkers.size() + " sponsor time segment(s)?")
.setPositiveButton("Yes", (dialog, id) -> {
String username = defaultPreferences.getString(getString(R.string.sponsorblock_username), null);
for (TimeFrame timeFrame : customTimeFrames) {
try {
new SponsorBlockApiTask().postVideoSponsorTimes(getVideoUrl(), timeFrame.startTime, timeFrame.endTime, username);
}
catch (Exception e) {
Log.e("SPONSOR_BLOCK", "Error getting video sponsor times.", e);
}
}
})
.setNegativeButton("No", null)
.setNeutralButton("Clear", (dialog, id) -> {
for (SeekBarMarker marker : customMarkers) {
((MarkableSeekBar) getPlaybackSeekBar()).seekBarMarkers.remove(marker);
}
if (sponsorTimeInfo != null) {
for (TimeFrame timeFrame : customTimeFrames) {
sponsorTimeInfo.timeFrames.remove(timeFrame);
}
}
})
.create()
.show();
}
@Override
public void onClick(final View v) {
@ -845,7 +958,11 @@ public final class MainVideoPlayer extends AppCompatActivity
onPlayBackgroundButtonClicked();
} else if (v.getId() == muteButton.getId()) {
onMuteUnmuteButtonClicked();
} else if (v.getId() == closeButton.getId()) {
} else if (v.getId() == submitSponsorTimesButton.getId()) {
onSponsorBlockButtonClicked();
}else if (v.getId() == closeButton.getId()) {
onPlaybackShutdown();
return;
} else if (v.getId() == kodiButton.getId()) {

View file

@ -69,6 +69,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.SponsorTimeInfo;
import org.schabi.newpipe.util.TimeFrame;
import org.schabi.newpipe.views.MarkableSeekBar;
import org.schabi.newpipe.views.SeekBarMarker;
import java.util.ArrayList;
import java.util.List;
@ -621,6 +625,8 @@ public abstract class VideoPlayer extends BasePlayer
super.onPrepared(playWhenReady);
tryMarkSponsorTimes();
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler
@ -628,6 +634,28 @@ public abstract class VideoPlayer extends BasePlayer
}
}
private void tryMarkSponsorTimes() {
SponsorTimeInfo sponsorTimeInfo = getSponsorTimeInfo();
if (sponsorTimeInfo == null) {
return;
}
if (!(playbackSeekBar instanceof MarkableSeekBar)) {
return;
}
MarkableSeekBar markableSeekBar = (MarkableSeekBar) playbackSeekBar;
for (TimeFrame timeFrame : sponsorTimeInfo.timeFrames) {
SeekBarMarker seekBarMarker = new SeekBarMarker(timeFrame.startTime, timeFrame.endTime, (int) simpleExoPlayer.getDuration(), Color.GREEN);
markableSeekBar.seekBarMarkers.add(seekBarMarker);
markableSeekBar.invalidate();
Log.d("SPONSOR_BLOCK", "Progress bar marker: " + seekBarMarker.percentStart + ", " + seekBarMarker.percentEnd);
}
}
@Override
public void destroy() {
super.destroy();

View file

@ -6,6 +6,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
@ -138,6 +139,20 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true;
});
Preference sponsorblockStatusPreference = findPreference(getString(R.string.sponsorblock_status));
sponsorblockStatusPreference.setOnPreferenceClickListener((Preference p) -> {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://status.sponsor.ajay.app/"));
startActivity(i);
return true;
});
Preference sponsorblockLeaderboardsPreference = findPreference(getString(R.string.sponsorblock_leaderboards));
sponsorblockLeaderboardsPreference.setOnPreferenceClickListener((Preference p) -> {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://api.sponsor.ajay.app/stats"));
startActivity(i);
return true;
});
}
@Override

View file

@ -0,0 +1,27 @@
package org.schabi.newpipe.util;
import java.util.ArrayList;
public class SponsorTimeInfo {
public ArrayList<TimeFrame> timeFrames = new ArrayList<>();
public int getSponsorEndTimeFromProgress(int progress) {
if (timeFrames == null) {
return 0;
}
for (TimeFrame timeFrames : timeFrames) {
if (progress < timeFrames.startTime) {
continue;
}
if (progress > timeFrames.endTime) {
continue;
}
return (int) Math.ceil((timeFrames.endTime));
}
return 0;
}
}

View file

@ -0,0 +1,12 @@
package org.schabi.newpipe.util;
public class TimeFrame {
public double startTime;
public double endTime;
public Object tag;
public TimeFrame(double startTime, double endTime) {
this.startTime = startTime;
this.endTime = endTime;
}
}

View file

@ -0,0 +1,50 @@
package org.schabi.newpipe.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatSeekBar;
import java.util.ArrayList;
public class MarkableSeekBar extends AppCompatSeekBar {
public ArrayList<SeekBarMarker> seekBarMarkers = new ArrayList<>();
private RectF markerRect = new RectF();
public MarkableSeekBar(Context context) {
super(context);
}
public MarkableSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MarkableSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
Drawable progressDrawable = getProgressDrawable();
Rect progressDrawableBounds = progressDrawable.getBounds();
int width = getMeasuredWidth() - (getPaddingStart() + getPaddingEnd());
int height = progressDrawable.getIntrinsicHeight();
for (int i = 0; i < seekBarMarkers.size(); i++) {
SeekBarMarker marker = seekBarMarkers.get(i);
markerRect.left = width - (float) Math.floor(width * (1.0 - marker.percentStart)) + getPaddingStart();
markerRect.top = progressDrawableBounds.bottom - height - 1;
markerRect.right = width - (float) Math.ceil(width * (1.0 - marker.percentEnd)) + getPaddingStart();
markerRect.bottom = progressDrawableBounds.bottom;
canvas.drawRect(markerRect, marker.paint);
}
}
}

View file

@ -0,0 +1,33 @@
package org.schabi.newpipe.views;
import android.graphics.Paint;
public class SeekBarMarker {
public double startTime;
public double endTime;
public double percentStart;
public double percentEnd;
public Paint paint;
public Object tag;
public SeekBarMarker(double startTime, double endTime, int maxTime, int color) {
this.startTime = startTime;
this.endTime = endTime;
this.percentStart = Math.round((startTime / maxTime) * 100.0) / 100.0;
this.percentEnd = Math.round((endTime / maxTime) * 100.0) / 100.0;
initPaint(color);
}
public SeekBarMarker(double percentStart, double percentEnd, int color) {
this.percentStart = percentStart;
this.percentEnd = percentEnd;
initPaint(color);
}
private void initPaint(int color) {
this.paint = new Paint();
this.paint.setColor(color);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -413,6 +413,23 @@
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/switch_to_background"
tools:ignore="RtlHardcoded" />
<ImageButton
android:id="@+id/submitSponsorTimes"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchMute"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:src="@drawable/ic_sponsor_block"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/submit_sponsor_times"
tools:ignore="RtlHardcoded"/>
</RelativeLayout>
<LinearLayout
@ -438,7 +455,7 @@
tools:text="1:06:29"/>
<org.schabi.newpipe.views.FocusAwareSeekBar
<org.schabi.newpipe.views.MarkableSeekBar
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"

View file

@ -406,6 +406,22 @@
android:contentDescription="@string/switch_to_background"
tools:ignore="RtlHardcoded" />
<ImageButton
android:id="@+id/submitSponsorTimes"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_toLeftOf="@id/switchMute"
android:layout_centerVertical="true"
android:clickable="true"
android:focusable="true"
android:padding="5dp"
android:scaleType="fitXY"
android:src="@drawable/ic_sponsor_block"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/submit_sponsor_times"
tools:ignore="RtlHardcoded"/>
</RelativeLayout>
<LinearLayout
@ -431,7 +447,7 @@
tools:text="1:06:29"/>
<androidx.appcompat.widget.AppCompatSeekBar
<org.schabi.newpipe.views.MarkableSeekBar
android:id="@+id/playbackSeekBar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"

View file

@ -223,6 +223,22 @@
<string name="downloads_storage_ask" translatable="false">downloads_storage_ask</string>
<string name="storage_use_saf" translatable="false">storage_use_saf</string>
<string name="sponsorblock_enable">skip_sponsors</string>
<string name="sponsorblock_enable_title">Skip Sponsors</string>
<string name="sponsorblock_enable_summary">Use the SponsorBlock API to automatically skip sponsors in videos.</string>
<string name="sponsorblock_notifications">sponsorblock_notifications</string>
<string name="sponsorblock_notifications_title">Notify when sponsors are skipped</string>
<string name="sponsorblock_notifications_summary">Send a notification to your device when a sponsor is automatically skipped.</string>
<string name="sponsorblock_username">sponsorblock_username</string>
<string name="sponsorblock_username_title">Set Username</string>
<string name="sponsorblock_username_summary">Set a username used when submitting sponsor times. Leave empty to use a default randomized username.</string>
<string name="sponsorblock_status">sponsorblock_status</string>
<string name="sponsorblock_status_title">View Online Status</string>
<string name="sponsorblock_status_summary">View the online status of the SponsorBlock servers.</string>
<string name="sponsorblock_leaderboards">sponsorblock_leaderboards</string>
<string name="sponsorblock_leaderboards_title">View Leaderboards</string>
<string name="sponsorblock_leaderboards_summary">View the online leaderboards for SponsorBlock.</string>
<!-- FileName Downloads -->
<string name="settings_file_charset_key" translatable="false">file_rename_charset</string>
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>

View file

@ -129,6 +129,7 @@
<string name="settings_category_other_title">Other</string>
<string name="settings_category_debug_title">Debug</string>
<string name="settings_category_updates_title">Updates</string>
<string name="settings_category_sponsorblock">SponsorBlock (beta)</string>
<string name="background_player_playing_toast">Playing in background</string>
<string name="popup_playing_toast">Playing in popup mode</string>
<string name="background_player_append">Queued on background player</string>
@ -181,6 +182,7 @@
<string name="switch_to_background">Switch to Background</string>
<string name="switch_to_popup">Switch to Popup</string>
<string name="switch_to_main">Switch to Main</string>
<string name="submit_sponsor_times">Submit Sponsor Times</string>
<string name="import_data_title">Import database</string>
<string name="export_data_title">Export database</string>
<string name="import_data_summary">Overrides your current history and subscriptions</string>
@ -609,6 +611,7 @@
<string name="remove_watched_popup_warning">Videos that have been watched before and after being added to the playlist will be removed.\nAre you sure? This cannot be undone!</string>
<string name="remove_watched_popup_yes_and_partially_watched_videos">Yes, and partially watched videos</string>
<string name="new_seek_duration_toast">Due to ExoPlayer constraints the seek duration was set to %d seconds</string>
<string name="sponsorblock_skipped_sponsor">Sponsor skipped</string>
<!-- Time duration plurals -->
<plurals name="seconds">
<item quantity="one">%d second</item>

View file

@ -117,4 +117,43 @@
android:summary="@string/feed_use_dedicated_fetch_method_summary"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/settings_category_sponsorblock">
<SwitchPreference
android:defaultValue="false"
android:key="@string/sponsorblock_enable"
android:summary="@string/sponsorblock_enable_summary"
android:title="@string/sponsorblock_enable_title"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="true"
android:dependency="@string/sponsorblock_enable"
android:key="@string/sponsorblock_notifications"
android:summary="@string/sponsorblock_notifications_summary"
android:title="@string/sponsorblock_notifications_title"
app:iconSpaceReserved="false" />
<EditTextPreference
android:dependency="@string/sponsorblock_enable"
android:key="@string/sponsorblock_username"
android:summary="@string/sponsorblock_username_summary"
android:title="@string/sponsorblock_username_title"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/sponsorblock_status"
android:summary="@string/sponsorblock_status_summary"
android:title="@string/sponsorblock_status_title"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/sponsorblock_leaderboards"
android:summary="@string/sponsorblock_leaderboards_summary"
android:title="@string/sponsorblock_leaderboards_title"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>