implemented SponsorBlock

This commit is contained in:
polymorphicshade 2024-01-20 17:55:31 -07:00
parent 8adedec08f
commit b088c109a5
55 changed files with 2952 additions and 77 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
"identityHash": "7dcdec7a500be9088f7a9a4767292b41",
"entities": [
{
"tableName": "subscriptions",
@ -58,10 +58,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
],
"autoGenerate": true
]
},
"indices": [
{
@ -107,10 +107,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [
{
@ -209,10 +209,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
],
"autoGenerate": true
]
},
"indices": [
{
@ -252,11 +252,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"access_date"
],
"autoGenerate": false
]
},
"indices": [
{
@ -301,10 +301,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": [
@ -351,10 +351,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
],
"autoGenerate": true
]
},
"indices": [
{
@ -393,11 +393,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"playlist_id",
"join_index"
],
"autoGenerate": false
]
},
"indices": [
{
@ -493,10 +493,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
],
"autoGenerate": true
]
},
"indices": [
{
@ -539,11 +539,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"stream_id",
"subscription_id"
],
"autoGenerate": false
]
},
"indices": [
{
@ -611,10 +611,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
],
"autoGenerate": true
]
},
"indices": [
{
@ -647,11 +647,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group_id",
"subscription_id"
],
"autoGenerate": false
]
},
"indices": [
{
@ -707,10 +707,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"subscription_id"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": [
@ -726,12 +726,32 @@
]
}
]
},
{
"tableName": "sponsorblock_whitelist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uploader` TEXT NOT NULL, PRIMARY KEY(`uploader`))",
"fields": [
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uploader"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7dcdec7a500be9088f7a9a4767292b41')"
]
}
}

View file

@ -817,7 +817,8 @@ public class RouterActivity extends AppCompatActivity {
inFlight(true);
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
disposables.add(ExtractorHelper.getStreamInfo(getContext(), currentServiceId,
currentUrl, true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(this::pleaseWait)
@ -837,7 +838,8 @@ public class RouterActivity extends AppCompatActivity {
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
inFlight(true);
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
disposables.add(ExtractorHelper.getStreamInfo(getContext(), currentServiceId,
currentUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(this::pleaseWait)
@ -970,7 +972,8 @@ public class RouterActivity extends AppCompatActivity {
switch (choice.linkType) {
case STREAM:
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
single = ExtractorHelper.getStreamInfo(
this, choice.serviceId, choice.url, false);
userAction = UserAction.REQUESTED_STREAM;
break;
case CHANNEL:

View file

@ -22,6 +22,8 @@ import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
import org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistDAO;
import org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry;
import org.schabi.newpipe.database.stream.dao.StreamDAO;
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
import org.schabi.newpipe.database.stream.model.StreamEntity;
@ -36,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class,
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class
FeedLastUpdatedEntity.class, SponsorBlockWhitelistEntry.class
},
version = DB_VER_7
)
@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract FeedGroupDAO feedGroupDAO();
public abstract SubscriptionDAO subscriptionDAO();
public abstract SponsorBlockWhitelistDAO sponsorBlockWhitelistDAO();
}

View file

@ -0,0 +1,35 @@
package org.schabi.newpipe.database.sponsorblock.dao;
import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.SPONSORBLOCK_WHITELIST_TABLE;
import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.UPLOADER;
import androidx.room.Dao;
import androidx.room.Query;
import org.schabi.newpipe.database.BasicDAO;
import java.util.List;
import io.reactivex.rxjava3.core.Flowable;
@Dao
public abstract class SponsorBlockWhitelistDAO implements BasicDAO<SponsorBlockWhitelistEntry> {
@Override
@Query("SELECT * FROM " + SPONSORBLOCK_WHITELIST_TABLE)
public abstract Flowable<List<SponsorBlockWhitelistEntry>> getAll();
@Override
@Query("DELETE FROM " + SPONSORBLOCK_WHITELIST_TABLE)
public abstract int deleteAll();
@Override
public Flowable<List<SponsorBlockWhitelistEntry>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + SPONSORBLOCK_WHITELIST_TABLE + " WHERE " + UPLOADER + " = :uploader")
public abstract int deleteByUploader(String uploader);
@Query("SELECT 1 FROM " + SPONSORBLOCK_WHITELIST_TABLE + " WHERE " + UPLOADER + " = :uploader")
public abstract boolean exists(String uploader);
}

View file

@ -0,0 +1,33 @@
package org.schabi.newpipe.database.sponsorblock.dao;
import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.SPONSORBLOCK_WHITELIST_TABLE;
import static org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry.UPLOADER;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
@Entity(tableName = SPONSORBLOCK_WHITELIST_TABLE,
primaryKeys = {UPLOADER}
)
public class SponsorBlockWhitelistEntry {
public static final String SPONSORBLOCK_WHITELIST_TABLE = "sponsorblock_whitelist";
public static final String UPLOADER = "uploader";
@NonNull
@ColumnInfo(name = UPLOADER)
private String uploader;
public SponsorBlockWhitelistEntry(final @NonNull String uploader) {
this.uploader = uploader;
}
@NonNull
public String getUploader() {
return uploader;
}
public void setUploader(final @NonNull String uploader) {
this.uploader = uploader;
}
}

View file

@ -76,6 +76,10 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockExtractorHelper;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@ -86,11 +90,14 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.sponsorblock.SponsorBlockFragment;
import org.schabi.newpipe.fragments.list.sponsorblock.SponsorBlockFragmentListener;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
@ -110,12 +117,13 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.SponsorBlockMode;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.ArrayList;
import java.util.Iterator;
@ -128,6 +136,7 @@ import java.util.function.Consumer;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
@ -136,7 +145,8 @@ public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo>
implements BackPressable,
PlayerServiceExtendedEventListener,
OnKeyDownListener {
OnKeyDownListener,
SponsorBlockFragmentListener {
public static final String KEY_SWITCHING_PLAYERS = "switching_players";
private static final float MAX_OVERLAY_ALPHA = 0.9f;
@ -156,6 +166,7 @@ public final class VideoDetailFragment
private static final String COMMENTS_TAB_TAG = "COMMENTS";
private static final String RELATED_TAB_TAG = "NEXT VIDEO";
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String SPONSOR_BLOCK_TAB_TAG = "SPONSOR_BLOCK TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
@ -164,6 +175,7 @@ public final class VideoDetailFragment
private boolean showComments;
private boolean showRelatedItems;
private boolean showDescription;
private boolean showSponsorBlock;
private String selectedTabTag;
@AttrRes
@NonNull
@ -175,18 +187,25 @@ public final class VideoDetailFragment
private int lastAppBarVerticalOffset = Integer.MAX_VALUE; // prevents useless updates
private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener =
(sharedPreferences, key) -> {
if (getString(R.string.show_comments_key).equals(key)) {
showComments = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (getString(R.string.show_next_video_key).equals(key)) {
showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (getString(R.string.show_description_key).equals(key)) {
showDescription = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
}
};
this::onSharedPreferencesChanged;
private Disposable workerIsWhitelisted;
private void onSharedPreferencesChanged(final SharedPreferences sharedPreferences,
final String key) {
if (getString(R.string.show_comments_key).equals(key)) {
showComments = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (getString(R.string.show_next_video_key).equals(key)) {
showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (getString(R.string.show_description_key).equals(key)) {
showDescription = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (getString(R.string.sponsor_block_enable_key).equals(key)) {
showSponsorBlock = sharedPreferences.getBoolean(key, false);
tabSettingsChanged = true;
}
}
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@ -212,6 +231,8 @@ public final class VideoDetailFragment
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable
private Disposable positionSubscriber = null;
private Disposable submitSegmentSubscriber;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
@ -230,6 +251,7 @@ public final class VideoDetailFragment
private PlayerService playerService;
private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
private SponsorBlockDataManager sponsorBlockDataManager;
/*//////////////////////////////////////////////////////////////////////////
// Service management
@ -309,6 +331,7 @@ public final class VideoDetailFragment
showComments = prefs.getBoolean(getString(R.string.show_comments_key), true);
showRelatedItems = prefs.getBoolean(getString(R.string.show_next_video_key), true);
showDescription = prefs.getBoolean(getString(R.string.show_description_key), true);
showSponsorBlock = prefs.getBoolean(getString(R.string.sponsor_block_enable_key), false);
selectedTabTag = prefs.getString(
getString(R.string.stream_info_selected_tab_key), COMMENTS_TAB_TAG);
prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
@ -326,6 +349,8 @@ public final class VideoDetailFragment
activity.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
settingsContentObserver);
sponsorBlockDataManager = new SponsorBlockDataManager(requireContext());
}
@Override
@ -426,6 +451,17 @@ public final class VideoDetailFragment
binding = null;
}
@Override
public void onDetach() {
super.onDetach();
if (submitSegmentSubscriber != null) {
submitSegmentSubscriber.dispose();
}
if (workerIsWhitelisted != null) {
workerIsWhitelisted.dispose();
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -552,10 +588,10 @@ public final class VideoDetailFragment
}));
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
openBackgroundPlayer(true)
openBackgroundPlayer(true)
));
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
openPopupPlayer(true)
openPopupPlayer(true)
));
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
NavigationHelper.openDownloads(activity)));
@ -838,7 +874,7 @@ public final class VideoDetailFragment
private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
currentWorker = ExtractorHelper.getStreamInfo(getContext(), serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
@ -901,6 +937,13 @@ public final class VideoDetailFragment
tabContentDescriptions.add(R.string.description_tab_description);
}
if (showSponsorBlock) {
// temp empty fragment. will be updated in handleResult
pageAdapter.addFragment(EmptyFragment.newInstance(false), SPONSOR_BLOCK_TAB_TAG);
tabIcons.add(R.drawable.ic_sponsor_block_enable);
tabContentDescriptions.add(R.string.sponsor_block_tab_description);
}
if (pageAdapter.getCount() == 0) {
pageAdapter.addFragment(EmptyFragment.newInstance(true), EMPTY_TAB_TAG);
}
@ -949,6 +992,13 @@ public final class VideoDetailFragment
pageAdapter.updateItem(DESCRIPTION_TAB_TAG, new DescriptionFragment(info));
}
if (showSponsorBlock) {
final SponsorBlockFragment sponsorBlockFragment = new SponsorBlockFragment(info);
sponsorBlockFragment.setListener(this);
pageAdapter.updateItem(SPONSOR_BLOCK_TAB_TAG, sponsorBlockFragment);
}
binding.viewPager.setVisibility(View.VISIBLE);
// make sure the tab layout is visible
updateTabLayoutVisibility();
@ -1243,6 +1293,9 @@ public final class VideoDetailFragment
playerUi.removeViewFromParent();
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
playerUi.setupVideoSurfaceIfNeeded();
if (currentInfo != null) {
playerUi.onMarkSeekbarRequested(currentInfo);
}
}
});
});
@ -1786,6 +1839,9 @@ public final class VideoDetailFragment
return;
}
getSponsorBlockFragment().ifPresent(
fragment -> fragment.setCurrentProgress(currentProgress));
if (player.getPlayQueue().getItem().getUrl().equals(url)) {
updatePlaybackProgress(currentProgress, duration);
}
@ -1793,6 +1849,25 @@ public final class VideoDetailFragment
@Override
public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) {
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean isSponsorBlockEnabled =
prefs.getBoolean(getString(R.string.sponsor_block_enable_key), false);
if (player != null && isSponsorBlockEnabled) {
workerIsWhitelisted = sponsorBlockDataManager.isWhiteListed(info.getUploaderName())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(isWhitelisted -> {
final SponsorBlockMode mode = isWhitelisted
? SponsorBlockMode.DISABLED
: SponsorBlockMode.ENABLED;
player.setSponsorBlockMode(mode);
getSponsorBlockFragment().ifPresent(
fragment -> fragment.setSponsorBlockMode(mode));
});
}
final StackItem item = findQueueInStack(queue);
if (item != null) {
// When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue)
@ -2451,4 +2526,147 @@ public final class VideoDetailFragment
lastStableBottomSheetState = newState;
}
}
private Optional<SponsorBlockFragment> getSponsorBlockFragment() {
final int sponsorBlockTabPos = pageAdapter.getItemPositionByTitle(SPONSOR_BLOCK_TAB_TAG);
if (sponsorBlockTabPos < 0) {
return Optional.empty();
}
final Fragment fragment = pageAdapter.getItem(sponsorBlockTabPos);
if (fragment instanceof SponsorBlockFragment sponsorBlockFragment) {
return Optional.of(sponsorBlockFragment);
} else {
return Optional.empty();
}
}
@Override
public void onSkippingEnabledChanged(final boolean newValue) {
if (player == null) {
return;
}
player.setSponsorBlockMode(newValue
? SponsorBlockMode.ENABLED
: SponsorBlockMode.DISABLED);
}
@Override
public void onRequestNewPendingSegment(final int startTime, final int endTime) {
if (currentInfo == null) {
return;
}
if (player == null) {
return;
}
currentInfo.removeSponsorBlockSegment("TEMP");
final SponsorBlockSegment segment = new SponsorBlockSegment(
"TEMP",
startTime,
endTime,
SponsorBlockCategory.PENDING,
SponsorBlockAction.SKIP);
currentInfo.addSponsorBlockSegment(segment);
player.UIs().get(MainPlayerUi.class).ifPresent(
playerUi -> playerUi.onMarkSeekbarRequested(currentInfo));
getSponsorBlockFragment().ifPresent(SponsorBlockFragment::refreshSponsorBlockSegments);
}
@Override
public void onRequestClearPendingSegment() {
if (currentInfo == null) {
return;
}
if (player == null) {
return;
}
currentInfo.removeSponsorBlockSegment("TEMP");
player.UIs().get(MainPlayerUi.class).ifPresent(
playerUi -> playerUi.onMarkSeekbarRequested(currentInfo));
getSponsorBlockFragment().ifPresent(SponsorBlockFragment::refreshSponsorBlockSegments);
}
@Override
public void onRequestSubmitPendingSegment(final SponsorBlockSegment newSegment) {
if (currentInfo == null) {
return;
}
if (player == null) {
return;
}
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String apiUrl = prefs.getString(context
.getString(R.string.sponsor_block_api_url_key), null);
if (apiUrl == null || apiUrl.isEmpty()) {
return;
}
submitSegmentSubscriber = Single.fromCallable(() ->
SponsorBlockExtractorHelper.submitSponsorBlockSegment(
currentInfo,
newSegment,
""))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> {
if (response.responseCode() != 200) {
String message = response.responseMessage();
if (message.equals("")) {
message = "Error " + response.responseCode();
}
Toast.makeText(context,
message,
Toast.LENGTH_SHORT).show();
return;
}
currentInfo.removeSponsorBlockSegment("TEMP");
currentInfo.addSponsorBlockSegment(newSegment);
player.UIs().get(MainPlayerUi.class).ifPresent(
playerUi -> playerUi.onMarkSeekbarRequested(currentInfo));
getSponsorBlockFragment().ifPresent(
SponsorBlockFragment::clearPendingSegment);
new AlertDialog
.Builder(context)
.setMessage(R.string.sponsor_block_upload_success_message)
.setPositiveButton(R.string.ok, (d, w) -> d.dismiss())
.show();
}, throwable -> {
if (throwable instanceof NullPointerException) {
return;
}
ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.USER_REPORT,
"Submit SponsorBlock segment"));
});
}
@Override
public void onSeekToRequested(final long positionMillis) {
if (player == null) {
return;
}
player.seekTo(positionMillis);
}
}

View file

@ -0,0 +1,333 @@
package org.schabi.newpipe.fragments.list.sponsorblock;
import static org.schabi.newpipe.util.TimeUtils.millisecondsToString;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentSponsorBlockBinding;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager;
import org.schabi.newpipe.util.SponsorBlockHelper;
import org.schabi.newpipe.util.SponsorBlockMode;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SponsorBlockFragment
extends BaseFragment
implements CompoundButton.OnCheckedChangeListener,
SponsorBlockSegmentListAdapterListener {
private StreamInfo streamInfo;
FragmentSponsorBlockBinding binding;
private Integer markedStartTime = null;
private Integer markedEndTime = null;
private SponsorBlockSegmentListAdapter segmentListAdapter;
private int currentProgress = -1;
private @Nullable SponsorBlockFragmentListener sponsorBlockFragmentListener;
private SponsorBlockDataManager sponsorBlockDataManager;
private Disposable workerIsWhitelisted;
private Disposable workerAddToWhitelisted;
private Disposable workerRemoveFromWhitelisted;
private SponsorBlockMode sponsorBlockMode = SponsorBlockMode.ENABLED;
public SponsorBlockFragment() {
}
public SponsorBlockFragment(@NonNull final StreamInfo streamInfo) {
this.streamInfo = streamInfo;
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sponsorBlockDataManager = new SponsorBlockDataManager(getContext());
}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
if (streamInfo == null) {
return;
}
segmentListAdapter = new SponsorBlockSegmentListAdapter(context, this);
segmentListAdapter.setItems(streamInfo.getSponsorBlockSegments());
}
@Override
public void onDetach() {
super.onDetach();
if (workerIsWhitelisted != null) {
workerIsWhitelisted.dispose();
}
if (workerAddToWhitelisted != null) {
workerAddToWhitelisted.dispose();
}
if (workerRemoveFromWhitelisted != null) {
workerRemoveFromWhitelisted.dispose();
}
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
if (sponsorBlockDataManager != null) {
sponsorBlockDataManager = new SponsorBlockDataManager(getContext());
}
binding = FragmentSponsorBlockBinding.inflate(inflater, container, false);
binding.sponsorBlockControlsMarkSegmentStart.setOnClickListener(v ->
doMarkPendingSegment(true));
binding.sponsorBlockControlsMarkSegmentEnd.setOnClickListener(v ->
doMarkPendingSegment(false));
binding.sponsorBlockControlsSegmentStart.setOnClickListener(v ->
doPendingSegmentSeek(true));
binding.sponsorBlockControlsSegmentEnd.setOnClickListener(v ->
doPendingSegmentSeek(false));
binding.sponsorBlockControlsClearSegment.setOnClickListener(v ->
doClearPendingSegment());
binding.sponsorBlockControlsSubmitSegment.setOnClickListener(v ->
doSubmitPendingSegment());
binding.segmentList.setAdapter(segmentListAdapter);
workerIsWhitelisted = sponsorBlockDataManager.isWhiteListed(streamInfo.getUploaderName())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(isWhitelisted -> {
binding.channelIsWhitelistedSwitch.setChecked(isWhitelisted);
binding.skippingIsEnabledSwitch.setChecked(
!isWhitelisted && sponsorBlockMode == SponsorBlockMode.ENABLED);
binding.skippingIsEnabledSwitch.setOnCheckedChangeListener(this);
binding.channelIsWhitelistedSwitch.setOnCheckedChangeListener(this);
});
return binding.getRoot();
}
@Override
public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) {
if (buttonView.getId() == R.id.skipping_is_enabled_switch) {
if (sponsorBlockFragmentListener != null) {
sponsorBlockFragmentListener.onSkippingEnabledChanged(isChecked);
}
} else if (buttonView.getId() == R.id.channel_is_whitelisted_switch) {
final Context context = requireContext();
final String toastText;
if (isChecked) {
toastText = context.getString(
R.string.sponsor_block_uploader_added_to_whitelist_toast);
workerAddToWhitelisted =
sponsorBlockDataManager.addToWhitelist(streamInfo.getUploaderName())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show();
}, error -> {
// TODO
});
} else {
toastText = context.getString(
R.string.sponsor_block_uploader_removed_from_whitelist_toast);
workerRemoveFromWhitelisted =
sponsorBlockDataManager.removeFromWhitelist(streamInfo.getUploaderName())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show();
}, error -> {
// TODO
});
}
binding.skippingIsEnabledSwitch.setChecked(false);
binding.skippingIsEnabledSwitch.setEnabled(!isChecked);
}
}
public void setListener(final SponsorBlockFragmentListener listener) {
sponsorBlockFragmentListener = listener;
}
public void setSponsorBlockMode(@NonNull final SponsorBlockMode mode) {
sponsorBlockMode = mode;
}
public void setCurrentProgress(final int progress) {
currentProgress = progress;
}
@SuppressLint("SetTextI18n")
public void clearPendingSegment() {
markedStartTime = null;
markedEndTime = null;
binding.sponsorBlockControlsSegmentStart.setText("00:00:00");
binding.sponsorBlockControlsSegmentEnd.setText("00:00:00");
if (sponsorBlockFragmentListener != null) {
sponsorBlockFragmentListener.onRequestClearPendingSegment();
}
}
public void refreshSponsorBlockSegments() {
if (segmentListAdapter == null) {
return;
}
segmentListAdapter.setItems(streamInfo.getSponsorBlockSegments());
}
private void doMarkPendingSegment(final boolean isStart) {
if (currentProgress < 0) {
return;
}
if (isStart) {
if (markedEndTime != null && currentProgress > markedEndTime) {
Toast.makeText(getContext(),
getString(R.string.sponsor_block_invalid_start_toast),
Toast.LENGTH_SHORT).show();
return;
}
markedStartTime = currentProgress;
} else {
if (markedStartTime != null && currentProgress < markedStartTime) {
Toast.makeText(getContext(),
getString(R.string.sponsor_block_invalid_end_toast),
Toast.LENGTH_SHORT).show();
return;
}
markedEndTime = currentProgress;
}
if (markedStartTime != null) {
binding.sponsorBlockControlsSegmentStart.setText(
millisecondsToString(markedStartTime));
}
if (markedEndTime != null) {
binding.sponsorBlockControlsSegmentEnd.setText(
millisecondsToString(markedEndTime));
}
if (markedStartTime != null && markedEndTime != null) {
if (sponsorBlockFragmentListener != null) {
sponsorBlockFragmentListener.onRequestNewPendingSegment(
markedStartTime, markedEndTime);
}
}
final String message = isStart
? getString(R.string.sponsor_block_marked_start_toast)
: getString(R.string.sponsor_block_marked_end_toast);
Toast.makeText(getContext(),
message,
Toast.LENGTH_SHORT).show();
}
@SuppressLint("SetTextI18n")
private void doClearPendingSegment() {
new AlertDialog
.Builder(requireContext())
.setMessage(R.string.sponsor_block_clear_marked_segment_prompt)
.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss())
.setPositiveButton(R.string.yes, (dialog, which) -> {
clearPendingSegment();
dialog.dismiss();
})
.show();
}
private void doPendingSegmentSeek(final boolean isStart) {
if (isStart && markedStartTime != null) {
onSkipToTimestampRequested((long) markedStartTime);
} else if (markedEndTime != null) {
onSkipToTimestampRequested((long) markedEndTime);
}
}
private void doSubmitPendingSegment() {
final Context context = requireContext();
if (markedStartTime == null || markedEndTime == null) {
Toast.makeText(context,
getString(R.string.sponsor_block_missing_times_toast),
Toast.LENGTH_SHORT).show();
return;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.sponsor_block_select_a_category);
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
builder.setItems(new String[]{
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.SPONSOR),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.INTRO),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.OUTRO),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.INTERACTION),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.HIGHLIGHT),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.SELF_PROMO),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.NON_MUSIC),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.PREVIEW),
SponsorBlockHelper.convertCategoryToFriendlyName(
context, SponsorBlockCategory.FILLER)
}, (dialog, which) -> {
final SponsorBlockCategory category = SponsorBlockCategory.values()[which];
final SponsorBlockAction action = category == SponsorBlockCategory.HIGHLIGHT
? SponsorBlockAction.POI
: SponsorBlockAction.SKIP;
final SponsorBlockSegment newSegment =
new SponsorBlockSegment(
"", markedStartTime, markedEndTime, category, action);
if (sponsorBlockFragmentListener != null) {
sponsorBlockFragmentListener.onRequestSubmitPendingSegment(newSegment);
}
dialog.dismiss();
});
builder.show();
}
@Override
public void onSkipToTimestampRequested(final long positionMillis) {
if (sponsorBlockFragmentListener != null) {
sponsorBlockFragmentListener.onSeekToRequested(positionMillis);
}
}
}

View file

@ -0,0 +1,11 @@
package org.schabi.newpipe.fragments.list.sponsorblock;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
public interface SponsorBlockFragmentListener {
void onSkippingEnabledChanged(boolean newValue);
void onRequestNewPendingSegment(int startTime, int endTime);
void onRequestClearPendingSegment();
void onRequestSubmitPendingSegment(SponsorBlockSegment newSegment);
void onSeekToRequested(long positionMillis);
}

View file

@ -0,0 +1,294 @@
package org.schabi.newpipe.fragments.list.sponsorblock;
import static org.schabi.newpipe.util.TimeUtils.millisecondsToString;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockExtractorHelper;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
import org.schabi.newpipe.util.SponsorBlockHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SponsorBlockSegmentListAdapter extends
RecyclerView.Adapter<SponsorBlockSegmentListAdapter.SponsorBlockSegmentItemViewHolder> {
private final Context context;
private ArrayList<SponsorBlockSegment> sponsorBlockSegments = new ArrayList<>();
private final SponsorBlockSegmentListAdapterListener listener;
public SponsorBlockSegmentListAdapter(final Context context,
final SponsorBlockSegmentListAdapterListener listener) {
this.context = context;
this.listener = listener;
}
public void setItems(final SponsorBlockSegment[] items) {
if (items == null) {
sponsorBlockSegments.clear();
} else {
sponsorBlockSegments = new ArrayList<>(Arrays.asList(items));
}
// find the first "highlight" segment (if it exists) and move it to the top
if (sponsorBlockSegments.size() > 0) {
final Optional<SponsorBlockSegment> highlightSegment =
sponsorBlockSegments
.stream()
.filter(x -> x.category == SponsorBlockCategory.HIGHLIGHT)
.findFirst();
if (highlightSegment.isPresent()) {
sponsorBlockSegments.remove(highlightSegment.get());
sponsorBlockSegments.add(0, highlightSegment.get());
}
}
notifyDataSetChanged();
}
@NonNull
@Override
public SponsorBlockSegmentListAdapter.SponsorBlockSegmentItemViewHolder onCreateViewHolder(
@NonNull final ViewGroup parent, final int viewType) {
final View itemView = LayoutInflater
.from(context)
.inflate(R.layout.list_segments_item, parent, false);
return new SponsorBlockSegmentItemViewHolder(itemView, listener);
}
@Override
public void onBindViewHolder(
@NonNull final SponsorBlockSegmentListAdapter.SponsorBlockSegmentItemViewHolder holder,
final int position) {
final SponsorBlockSegment sponsorBlockSegment = sponsorBlockSegments.get(position);
holder.updateFrom(sponsorBlockSegment);
}
@Override
public int getItemCount() {
return sponsorBlockSegments.size();
}
public static class SponsorBlockSegmentItemViewHolder extends RecyclerView.ViewHolder {
private final View itemSegmentColorView;
private final ImageView itemSegmentSkipToHighlight;
private final TextView itemSegmentNameTextView;
private final TextView itemSegmentStartTimeTextView;
private final TextView itemSegmentEndTimeTextView;
private final ImageView itemSegmentVoteUpImageView;
private final ImageView itemSegmentVoteDownImageView;
private Disposable voteSubscriber;
private String segmentUuid;
private boolean isVoting;
private boolean hasUpVoted;
private boolean hasDownVoted;
private boolean hasResetVote;
private SponsorBlockSegment currentSponsorBlockSegment;
public SponsorBlockSegmentItemViewHolder(
@NonNull final View itemView,
final SponsorBlockSegmentListAdapterListener listener) {
super(itemView);
itemSegmentColorView = itemView.findViewById(R.id.item_segment_color_view);
itemSegmentSkipToHighlight = itemView.findViewById(R.id.item_segment_skip_to_highlight);
itemSegmentSkipToHighlight.setOnClickListener(v -> {
if (currentSponsorBlockSegment != null && listener != null) {
listener.onSkipToTimestampRequested(
(long) currentSponsorBlockSegment.startTime);
}
});
itemSegmentNameTextView = itemView.findViewById(
R.id.item_segment_category_name_textview);
itemSegmentStartTimeTextView = itemView.findViewById(
R.id.item_segment_start_time_textview);
itemSegmentStartTimeTextView.setOnClickListener(v -> {
if (currentSponsorBlockSegment != null && listener != null) {
listener.onSkipToTimestampRequested(
(long) currentSponsorBlockSegment.startTime);
}
});
itemSegmentEndTimeTextView = itemView.findViewById(R.id.item_segment_end_time_textview);
itemSegmentEndTimeTextView.setOnClickListener(v -> {
if (currentSponsorBlockSegment != null && listener != null) {
listener.onSkipToTimestampRequested((long) currentSponsorBlockSegment.endTime);
}
});
// voting:
// 1 = up
// 0 = down
// 20 = reset
itemSegmentVoteUpImageView =
itemView.findViewById(R.id.item_segment_vote_up_imageview);
itemSegmentVoteUpImageView.setOnClickListener(v -> vote(1));
itemSegmentVoteUpImageView.setOnLongClickListener(v -> {
vote(20);
return true;
});
itemSegmentVoteDownImageView =
itemView.findViewById(R.id.item_segment_vote_down_imageview);
itemSegmentVoteDownImageView.setOnClickListener(v -> vote(0));
itemSegmentVoteDownImageView.setOnLongClickListener(v -> {
vote(20);
return true;
});
}
private void updateFrom(final SponsorBlockSegment sponsorBlockSegment) {
currentSponsorBlockSegment = sponsorBlockSegment;
final Context context = itemView.getContext();
// uuid
segmentUuid = sponsorBlockSegment.uuid;
// category color
final Integer segmentColor =
SponsorBlockHelper.convertCategoryToColor(
sponsorBlockSegment.category, context);
if (segmentColor != null) {
itemSegmentColorView.setBackgroundColor(segmentColor);
}
// skip to highlight
if (sponsorBlockSegment.category == SponsorBlockCategory.HIGHLIGHT) {
itemSegmentColorView.setVisibility(View.GONE);
itemSegmentSkipToHighlight.setVisibility(View.VISIBLE);
} else {
itemSegmentColorView.setVisibility(View.VISIBLE);
itemSegmentSkipToHighlight.setVisibility(View.GONE);
}
// category name
final String friendlyCategoryName =
SponsorBlockHelper.convertCategoryToFriendlyName(
context, sponsorBlockSegment.category);
itemSegmentNameTextView.setText(friendlyCategoryName);
// from
final String startText = millisecondsToString(sponsorBlockSegment.startTime);
itemSegmentStartTimeTextView.setText(startText);
// to
final String endText = millisecondsToString(sponsorBlockSegment.endTime);
itemSegmentEndTimeTextView.setText(endText);
if (sponsorBlockSegment.category == SponsorBlockCategory.PENDING
|| sponsorBlockSegment.uuid.equals("TEMP")
|| sponsorBlockSegment.uuid.equals("")) {
itemSegmentVoteUpImageView.setVisibility(View.INVISIBLE);
itemSegmentVoteDownImageView.setVisibility(View.INVISIBLE);
}
}
private void vote(final int value) {
if (segmentUuid == null) {
return;
}
if (isVoting) {
return;
}
if (voteSubscriber != null) {
voteSubscriber.dispose();
}
// these 3 checks prevent the user from continuously spamming votes
// (not entirely sure if we need this)
if (value == 0 && hasDownVoted) {
return;
}
if (value == 1 && hasUpVoted) {
return;
}
if (value == 20 && hasResetVote) {
return;
}
final Context context = itemView.getContext();
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String apiUrl = prefs.getString(context
.getString(R.string.sponsor_block_api_url_key), null);
if (apiUrl == null || apiUrl.isEmpty()) {
return;
}
voteSubscriber = Single.fromCallable(() -> {
isVoting = true;
return SponsorBlockExtractorHelper.submitSponsorBlockSegmentVote(
segmentUuid, apiUrl, value);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> {
isVoting = false;
String toastMessage;
if (response.responseCode() != 200) {
toastMessage = response.responseMessage();
if (toastMessage.equals("")) {
toastMessage = "Error " + response.responseCode();
}
} else if (value == 0) {
hasDownVoted = true;
hasUpVoted = false;
hasResetVote = false;
toastMessage = context.getString(
R.string.sponsor_block_segment_voted_down_toast);
} else if (value == 1) {
hasDownVoted = false;
hasUpVoted = true;
hasResetVote = false;
toastMessage = context.getString(
R.string.sponsor_block_segment_voted_up_toast);
} else if (value == 20) {
hasDownVoted = false;
hasUpVoted = false;
hasResetVote = true;
toastMessage = context.getString(
R.string.sponsor_block_segment_reset_vote_toast);
} else {
return;
}
Toast.makeText(context,
toastMessage,
Toast.LENGTH_SHORT).show();
}, throwable -> {
if (throwable instanceof NullPointerException) {
return;
}
ErrorUtil.showSnackbar(context,
new ErrorInfo(throwable, UserAction.SUBSCRIPTION_UPDATE,
"Submit vote for SponsorBlock segment"));
});
}
}
}

View file

@ -0,0 +1,5 @@
package org.schabi.newpipe.fragments.list.sponsorblock;
public interface SponsorBlockSegmentListAdapterListener {
void onSkipToTimestampRequested(long positionMillis);
}

View file

@ -59,6 +59,7 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class HistoryRecordManager {
private final Context context;
private final AppDatabase database;
private final StreamDAO streamTable;
private final StreamHistoryDAO streamHistoryTable;
@ -69,6 +70,7 @@ public class HistoryRecordManager {
private final String streamHistoryKey;
public HistoryRecordManager(final Context context) {
this.context = context;
database = NewPipeDatabase.getInstance(context);
streamTable = database.streamDAO();
streamHistoryTable = database.streamHistoryDAO();
@ -103,6 +105,7 @@ public class HistoryRecordManager {
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
if (info.getDuration() < 0) {
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
context,
info.getServiceId(),
info.getUrl(),
false
@ -235,7 +238,7 @@ public class HistoryRecordManager {
///////////////////////////////////////////////////////
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
return queueItem.getStream(context)
.map(info -> streamTable.upsert(new StreamEntity(info)))
.flatMapPublisher(streamStateTable::getState)
.firstElement()

View file

@ -0,0 +1,42 @@
package org.schabi.newpipe.local.sponsorblock;
import android.content.Context;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistDAO;
import org.schabi.newpipe.database.sponsorblock.dao.SponsorBlockWhitelistEntry;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SponsorBlockDataManager {
private final SponsorBlockWhitelistDAO sponsorBlockWhitelistTable;
public SponsorBlockDataManager(final Context context) {
final AppDatabase database = NewPipeDatabase.getInstance(context);
sponsorBlockWhitelistTable = database.sponsorBlockWhitelistDAO();
}
public Maybe<Long> addToWhitelist(final String uploader) {
return Maybe.fromCallable(() -> {
final SponsorBlockWhitelistEntry entry = new SponsorBlockWhitelistEntry(uploader);
return sponsorBlockWhitelistTable.insert(entry);
}).subscribeOn(Schedulers.io());
}
public Completable removeFromWhitelist(final String uploader) {
return Completable.fromAction(() -> sponsorBlockWhitelistTable.deleteByUploader(uploader));
}
public Single<Boolean> isWhiteListed(final String uploader) {
return Single.fromCallable(() -> sponsorBlockWhitelistTable.exists(uploader))
.subscribeOn(Schedulers.io());
}
public Completable clearWhitelist() {
return Completable.fromAction(sponsorBlockWhitelistTable::deleteAll);
}
}

View file

@ -46,6 +46,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.SponsorBlockHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
@ -229,9 +230,12 @@ public final class PlayQueueActivity extends AppCompatActivity
} else {
onQueueUpdate(player.getPlayQueue());
buildComponents();
if (player != null) {
player.setActivityListener(PlayQueueActivity.this);
}
player.setActivityListener(PlayQueueActivity.this);
player.getCurrentStreamInfo().ifPresent(info ->
SponsorBlockHelper.markSegments(
getApplicationContext(),
queueControlBinding.seekBar,
info));
}
}
};

View file

@ -57,6 +57,7 @@ import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -69,6 +70,7 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.source.MediaSource;
@ -86,6 +88,8 @@ import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockAction;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@ -118,6 +122,8 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SponsorBlockMode;
import org.schabi.newpipe.util.SponsorBlockHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
@ -262,7 +268,9 @@ public final class Player implements PlaybackListener, Listener {
private final SharedPreferences prefs;
@NonNull
private final HistoryRecordManager recordManager;
private SponsorBlockMode sponsorBlockMode = SponsorBlockMode.DISABLED;
private final SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener;
/*//////////////////////////////////////////////////////////////////////////
// Constructor
@ -273,6 +281,25 @@ public final class Player implements PlaybackListener, Listener {
this.service = service;
context = service;
prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean isSponsorBlockEnabled = prefs.getBoolean(
context.getString(R.string.sponsor_block_enable_key), false);
setSponsorBlockMode(isSponsorBlockEnabled
? SponsorBlockMode.ENABLED
: SponsorBlockMode.DISABLED);
preferenceChangeListener =
(sharedPreferences, key) -> {
if (context.getString(R.string.sponsor_block_enable_key).equals(key)) {
setSponsorBlockMode(sharedPreferences.getBoolean(key, false)
? SponsorBlockMode.ENABLED
: SponsorBlockMode.DISABLED);
}
};
prefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener);
recordManager = new HistoryRecordManager(context);
setupBroadcastReceiver();
@ -637,7 +664,7 @@ public final class Player implements PlaybackListener, Listener {
}
if (playQueue != null) {
playQueueManager = new MediaSourceManager(this, playQueue);
playQueueManager = new MediaSourceManager(context, this, playQueue);
}
}
@ -923,12 +950,61 @@ public final class Player implements PlaybackListener, Listener {
}
public void triggerProgressUpdate() {
triggerProgressUpdate(false);
}
public void triggerProgressUpdate(final boolean isRewind) {
if (exoPlayerIsNull()) {
return;
}
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
onUpdateProgress(
currentProgress,
(int) simpleExoPlayer.getDuration(),
simpleExoPlayer.getBufferedPercentage());
triggerCheckForSponsorBlockSegments(currentProgress, isRewind);
}
private void triggerCheckForSponsorBlockSegments(final int currentProgress,
final boolean isRewind) {
if (sponsorBlockMode != SponsorBlockMode.ENABLED || !isPrepared) {
return;
}
getSkippableSponsorBlockSegment(currentProgress).ifPresent(sponsorBlockSegment -> {
int skipTarget = isRewind
? (int) Math.ceil((sponsorBlockSegment.startTime)) - 1
: (int) Math.ceil((sponsorBlockSegment.endTime));
if (skipTarget < 0) {
skipTarget = 0;
}
// temporarily force EXACT seek parameters to prevent infinite skip looping
final SeekParameters seekParams = simpleExoPlayer.getSeekParameters();
simpleExoPlayer.setSeekParameters(SeekParameters.EXACT);
seekTo(skipTarget);
simpleExoPlayer.setSeekParameters(seekParams);
if (prefs.getBoolean(
context.getString(R.string.sponsor_block_notifications_key), false)) {
final String toastText =
SponsorBlockHelper.convertCategoryToSkipMessage(
context, sponsorBlockSegment.category);
Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
}
if (DEBUG) {
Log.d("SPONSOR_BLOCK", "Skipped segment: currentProgress = ["
+ currentProgress + "], skipped to = [" + skipTarget + "]");
}
});
}
private Disposable getProgressUpdateDisposable() {
@ -1706,7 +1782,7 @@ public final class Player implements PlaybackListener, Listener {
public void fastForward() {
if (DEBUG) {
Log.d(TAG, "fastRewind() called");
Log.d(TAG, "fastForward() called");
}
seekBy(retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate();
@ -1717,7 +1793,7 @@ public final class Player implements PlaybackListener, Listener {
Log.d(TAG, "fastRewind() called");
}
seekBy(-retrieveSeekDurationFromPreferences(this));
triggerProgressUpdate();
triggerProgressUpdate(true);
}
//endregion
@ -2302,6 +2378,41 @@ public final class Player implements PlaybackListener, Listener {
return Optional.ofNullable(fragmentListener);
}
public SponsorBlockMode getSponsorBlockMode() {
return sponsorBlockMode;
}
public void setSponsorBlockMode(final SponsorBlockMode mode) {
sponsorBlockMode = mode;
}
public Optional<SponsorBlockSegment> getSkippableSponsorBlockSegment(final int progress) {
return getCurrentStreamInfo().map(info -> {
final SponsorBlockSegment[] sponsorBlockSegments = info.getSponsorBlockSegments();
if (sponsorBlockSegments == null) {
return null;
}
for (final SponsorBlockSegment sponsorBlockSegment : sponsorBlockSegments) {
if (sponsorBlockSegment.action != SponsorBlockAction.SKIP) {
continue;
}
if (progress < sponsorBlockSegment.startTime) {
continue;
}
if (progress > sponsorBlockSegment.endTime) {
continue;
}
return sponsorBlockSegment;
}
return null;
});
}
/**
* @return the user interfaces connected with the player
*/

View file

@ -0,0 +1,5 @@
package org.schabi.newpipe.player;
public interface PlayerListener {
void onPlayerPrepared(Player player);
}

View file

@ -1,5 +1,11 @@
package org.schabi.newpipe.player.playback;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
@ -38,11 +44,6 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
public class MediaSourceManager {
@NonNull
private final String TAG = "MediaSourceManager@" + hashCode();
@ -69,6 +70,8 @@ public class MediaSourceManager {
*/
private static final int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull
private final Context context;
@NonNull
private final PlaybackListener playbackListener;
@NonNull
@ -125,14 +128,16 @@ public class MediaSourceManager {
private final Handler removeMediaSourceHandler = new Handler();
public MediaSourceManager(@NonNull final PlaybackListener listener,
public MediaSourceManager(@NonNull final Context context,
@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, 400L,
this(context, listener, playQueue, 400L,
/*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
private MediaSourceManager(@NonNull final Context context,
@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final long loadDebounceMillis,
final long playbackNearEndGapMillis,
@ -146,6 +151,7 @@ public class MediaSourceManager {
+ " ms] for them to be useful.");
}
this.context = context;
this.playbackListener = listener;
this.playQueue = playQueue;
@ -420,7 +426,7 @@ public class MediaSourceManager {
}
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
return stream.getStream()
return stream.getStream(context)
.map(streamInfo -> Optional
.ofNullable(playbackListener.sourceOf(stream, streamInfo))
.<ManagedMediaSource>flatMap(source ->

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.player.playqueue;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -122,8 +124,8 @@ public class PlayQueueItem implements Serializable {
}
@NonNull
public Single<StreamInfo> getStream() {
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
public Single<StreamInfo> getStream(final Context context) {
return ExtractorHelper.getStreamInfo(context, this.serviceId, this.url, false)
.subscribeOn(Schedulers.io())
.doOnError(throwable -> error = throwable);
}

View file

@ -127,6 +127,9 @@ public abstract class PlayerUi {
public void onPrepared() {
}
public void onMarkSeekbarRequested(@NonNull final StreamInfo streamInfo) {
}
public void onBlocked() {
}

View file

@ -82,6 +82,7 @@ import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SponsorBlockHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
@ -797,6 +798,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
binding.playbackSpeed.setText(formatSpeed(player.getPlaybackSpeed()));
}
@Override
public void onMarkSeekbarRequested(@NonNull final StreamInfo streamInfo) {
SponsorBlockHelper.markSegments(context, binding.playbackSeekBar, streamInfo);
}
@Override
public void onBlocked() {
super.onBlocked();

View file

@ -63,6 +63,8 @@ public final class NewPipeSettings {
PreferenceManager.setDefaultValues(context, R.xml.player_notification_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.sponsor_block_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.sponsor_block_category_settings, true);
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);

View file

@ -41,6 +41,8 @@ public final class SettingsResourceRegistry {
add(UpdateSettingsFragment.class, R.xml.update_settings);
add(VideoAudioSettingsFragment.class, R.xml.video_audio_settings);
add(ExoPlayerSettingsFragment.class, R.xml.exoplayer_settings);
add(SponsorBlockSettingsFragment.class, R.xml.sponsor_block_settings);
add(SponsorBlockCategoriesSettingsFragment.class, R.xml.sponsor_block_category_settings);
}
private SettingRegistryEntry add(

View file

@ -0,0 +1,154 @@
package org.schabi.newpipe.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.SwitchPreference;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.custom.EditColorPreference;
public class SponsorBlockCategoriesSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResourceRegistry();
final Preference allOnPreference =
findPreference(getString(R.string.sponsor_block_category_all_on_key));
allOnPreference.setOnPreferenceClickListener(p -> {
final SwitchPreference sponsorCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_sponsor_key));
final SwitchPreference introCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_intro_key));
final SwitchPreference outroCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_outro_key));
final SwitchPreference interactionCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_interaction_key));
final SwitchPreference highlightCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_highlight_key));
final SwitchPreference selfPromoCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_self_promo_key));
final SwitchPreference nonMusicCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_non_music_key));
final SwitchPreference previewCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_preview_key));
final SwitchPreference fillerCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_filler_key));
sponsorCategoryPreference.setChecked(true);
introCategoryPreference.setChecked(true);
outroCategoryPreference.setChecked(true);
interactionCategoryPreference.setChecked(true);
highlightCategoryPreference.setChecked(true);
selfPromoCategoryPreference.setChecked(true);
nonMusicCategoryPreference.setChecked(true);
previewCategoryPreference.setChecked(true);
fillerCategoryPreference.setChecked(true);
return true;
});
final Preference allOffPreference =
findPreference(getString(R.string.sponsor_block_category_all_off_key));
allOffPreference.setOnPreferenceClickListener(p -> {
final SwitchPreference sponsorCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_sponsor_key));
final SwitchPreference introCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_intro_key));
final SwitchPreference outroCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_outro_key));
final SwitchPreference interactionCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_interaction_key));
final SwitchPreference highlightCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_highlight_key));
final SwitchPreference selfPromoCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_self_promo_key));
final SwitchPreference nonMusicCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_non_music_key));
final SwitchPreference previewCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_preview_key));
final SwitchPreference fillerCategoryPreference =
findPreference(getString(R.string.sponsor_block_category_filler_key));
sponsorCategoryPreference.setChecked(false);
introCategoryPreference.setChecked(false);
outroCategoryPreference.setChecked(false);
interactionCategoryPreference.setChecked(false);
highlightCategoryPreference.setChecked(false);
selfPromoCategoryPreference.setChecked(false);
nonMusicCategoryPreference.setChecked(false);
previewCategoryPreference.setChecked(false);
fillerCategoryPreference.setChecked(false);
return true;
});
final Preference resetPreference =
findPreference(getString(R.string.sponsor_block_category_reset_key));
resetPreference.setOnPreferenceClickListener(p -> {
new AlertDialog.Builder(p.getContext())
.setMessage(R.string.sponsor_block_confirm_reset_colors)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final SharedPreferences.Editor editor =
getPreferenceManager()
.getSharedPreferences()
.edit();
setColorPreference(editor,
R.string.sponsor_block_category_sponsor_color_key,
R.color.sponsor_segment);
setColorPreference(editor,
R.string.sponsor_block_category_intro_color_key,
R.color.intro_segment);
setColorPreference(editor,
R.string.sponsor_block_category_outro_color_key,
R.color.outro_segment);
setColorPreference(editor,
R.string.sponsor_block_category_interaction_color_key,
R.color.interaction_segment);
setColorPreference(editor,
R.string.sponsor_block_category_highlight_color_key,
R.color.highlight_segment);
setColorPreference(editor,
R.string.sponsor_block_category_self_promo_color_key,
R.color.self_promo_segment);
setColorPreference(editor,
R.string.sponsor_block_category_non_music_color_key,
R.color.non_music_segment);
setColorPreference(editor,
R.string.sponsor_block_category_preview_color_key,
R.color.preview_segment);
setColorPreference(editor,
R.string.sponsor_block_category_filler_color_key,
R.color.filler_segment);
setColorPreference(editor,
R.string.sponsor_block_category_pending_color_key,
R.color.pending_segment);
editor.apply();
})
.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss())
.show();
return true;
});
}
private void setColorPreference(final SharedPreferences.Editor editor,
@StringRes final int resId,
@ColorRes final int colorId) {
final String colorStr = "#" + Integer.toHexString(getResources().getColor(colorId));
editor.putString(getString(resId), colorStr);
final EditColorPreference colorPreference = findPreference(getString(resId));
colorPreference.setText(colorStr);
}
}

View file

@ -0,0 +1,86 @@
package org.schabi.newpipe.settings;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.sponsorblock.SponsorBlockDataManager;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SponsorBlockSettingsFragment extends BasePreferenceFragment {
private SponsorBlockDataManager sponsorBlockDataManager;
private Disposable workerClearWhitelist;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sponsorBlockDataManager = new SponsorBlockDataManager(getContext());
}
@Override
public void onDetach() {
super.onDetach();
if (workerClearWhitelist != null) {
workerClearWhitelist.dispose();
}
}
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResourceRegistry();
final Preference sponsorBlockWebsitePreference =
findPreference(getString(R.string.sponsor_block_home_page_key));
assert sponsorBlockWebsitePreference != null;
sponsorBlockWebsitePreference.setOnPreferenceClickListener((Preference p) -> {
final Intent i = new Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.sponsor_block_homepage_url)));
startActivity(i);
return true;
});
final Preference sponsorBlockPrivacyPreference =
findPreference(getString(R.string.sponsor_block_privacy_key));
assert sponsorBlockPrivacyPreference != null;
sponsorBlockPrivacyPreference.setOnPreferenceClickListener((Preference p) -> {
final Intent i = new Intent(Intent.ACTION_VIEW,
Uri.parse(getString(R.string.sponsor_block_privacy_policy_url)));
startActivity(i);
return true;
});
final Preference sponsorBlockClearWhitelistPreference =
findPreference(getString(R.string.sponsor_block_clear_whitelist_key));
assert sponsorBlockClearWhitelistPreference != null;
sponsorBlockClearWhitelistPreference.setOnPreferenceClickListener((Preference p) -> {
new AlertDialog.Builder(p.getContext())
.setMessage(R.string.sponsor_block_confirm_clear_whitelist)
.setPositiveButton(R.string.yes, (dialog, which) -> {
workerClearWhitelist =
sponsorBlockDataManager.clearWhitelist()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Toast.makeText(p.getContext(),
R.string.sponsor_block_whitelist_cleared_toast,
Toast.LENGTH_SHORT).show();
}, error -> {
// TODO
});
})
.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss())
.show();
return true;
});
}
}

View file

@ -0,0 +1,81 @@
package org.schabi.newpipe.settings.custom;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import org.schabi.newpipe.R;
public class EditColorPreference extends EditTextPreference
implements Preference.OnPreferenceChangeListener {
private PreferenceViewHolder viewHolder;
public EditColorPreference(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public EditColorPreference(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public EditColorPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
public EditColorPreference(final Context context) {
super(context);
init();
}
private void init() {
setWidgetLayoutResource(R.layout.preference_edit_color);
setOnPreferenceChangeListener(this);
}
@Override
public void onBindViewHolder(final PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
viewHolder = holder;
final String colorStr =
getPreferenceManager()
.getSharedPreferences()
.getString(getKey(), null);
if (colorStr == null) {
return;
}
final int color = Color.parseColor(colorStr);
final View view = viewHolder.findViewById(R.id.sponsor_block_segment_color_view);
view.setBackgroundColor(color);
}
@Override
public boolean onPreferenceChange(final Preference preference, final Object newValue) {
try {
final int color = Color.parseColor((String) newValue);
final View view = viewHolder.findViewById(R.id.sponsor_block_segment_color_view);
view.setBackgroundColor(color);
return true;
} catch (final Exception e) {
Toast.makeText(getContext(), R.string.invalid_color_toast, Toast.LENGTH_SHORT).show();
return false;
}
}
}

View file

@ -0,0 +1,104 @@
package org.schabi.newpipe.settings.custom;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import org.schabi.newpipe.R;
public class SponsorBlockApiUrlPreference extends Preference {
public SponsorBlockApiUrlPreference(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSetInitialValue(@Nullable final Object defaultValue) {
// apparently this is how you're supposed to respect default values for a custom preference
persistString(getPersistedString((String) defaultValue));
}
@Nullable
@Override
protected Object onGetDefaultValue(@NonNull final TypedArray a, final int index) {
return a.getString(index);
}
@Override
protected void onClick() {
super.onClick();
final Context context = getContext();
final String apiUrl = getPersistedString(null);
final View alertDialogView = LayoutInflater.from(context)
.inflate(R.layout.dialog_sponsor_block_api_url, null);
final EditText editText = alertDialogView.findViewById(R.id.api_url_edit);
editText.setText(apiUrl);
editText.setOnFocusChangeListener((v, hasFocus) -> editText.post(() -> {
final InputMethodManager inputMethodManager = (InputMethodManager) context
.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager
.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}));
editText.requestFocus();
alertDialogView.findViewById(R.id.icon_api_url_help)
.setOnClickListener(v -> {
final Uri privacyPolicyUri = Uri.parse(context
.getString(R.string.sponsor_block_privacy_policy_url));
final View helpDialogView = LayoutInflater.from(context)
.inflate(R.layout.dialog_sponsor_block_api_url_help, null);
final View privacyPolicyButton = helpDialogView
.findViewById(R.id.sponsor_block_privacy_policy_button);
privacyPolicyButton.setOnClickListener(v1 -> {
final Intent i = new Intent(Intent.ACTION_VIEW, privacyPolicyUri);
context.startActivity(i);
});
new AlertDialog.Builder(context)
.setView(helpDialogView)
.setPositiveButton("Use Official", (dialog, which) -> {
editText.setText(context
.getString(R.string.sponsor_block_default_api_url));
dialog.dismiss();
})
.setNeutralButton("Close", (dialog, which) -> dialog.dismiss())
.create()
.show();
});
final AlertDialog alertDialog =
new AlertDialog.Builder(context)
.setView(alertDialogView)
.setTitle(context.getString(R.string.sponsor_block_api_url_title))
.setPositiveButton("OK", (dialog, which) -> {
final String newValue = editText.getText().toString();
if (!newValue.isEmpty()) {
final SharedPreferences.Editor editor =
getPreferenceManager().getSharedPreferences().edit();
editor.putString(getKey(), newValue);
editor.apply();
callChangeListener(newValue);
}
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel())
.create();
alertDialog.show();
}
}

View file

@ -23,6 +23,7 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
@ -47,6 +48,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockApiSettings;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
@ -110,11 +112,16 @@ public final class ExtractorHelper {
});
}
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url,
public static Single<StreamInfo> getStreamInfo(final Context context,
final int serviceId,
final String url,
final boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, InfoItem.InfoType.STREAM,
Single.fromCallable(() -> StreamInfo.getInfo(NewPipe.getService(serviceId), url)));
Single.fromCallable(() -> StreamInfo.getInfo(
NewPipe.getService(serviceId),
url,
buildSponsorBlockApiSettings(context))));
}
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url,
@ -334,4 +341,49 @@ public final class ExtractorHelper {
return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
}
}
private static @Nullable SponsorBlockApiSettings buildSponsorBlockApiSettings(
final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean isSponsorBlockEnabled = prefs.getBoolean(context
.getString(R.string.sponsor_block_enable_key), false);
if (!isSponsorBlockEnabled) {
return null;
}
final SponsorBlockApiSettings result = new SponsorBlockApiSettings();
result.apiUrl =
prefs.getString(context.getString(R.string.sponsor_block_api_url_key), null);
result.includeSponsorCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_sponsor_key), false);
result.includeIntroCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_intro_key), false);
result.includeOutroCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_outro_key), false);
result.includeInteractionCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_interaction_key), false);
result.includeHighlightCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_highlight_key), false);
result.includeSelfPromoCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_self_promo_key), false);
result.includeMusicCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_non_music_key), false);
result.includePreviewCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_preview_key), false);
result.includeFillerCategory =
prefs.getBoolean(context
.getString(R.string.sponsor_block_category_filler_key), false);
return result;
}
}

View file

@ -102,7 +102,7 @@ public final class SparseItemUtil {
@NonNull final String url,
final Consumer<StreamInfo> callback) {
Toast.makeText(context, R.string.loading_stream_details, Toast.LENGTH_SHORT).show();
ExtractorHelper.getStreamInfo(serviceId, url, false)
ExtractorHelper.getStreamInfo(context, serviceId, url, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {

View file

@ -0,0 +1,190 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockCategory;
import org.schabi.newpipe.extractor.sponsorblock.SponsorBlockSegment;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.views.MarkableSeekBar;
import org.schabi.newpipe.views.SeekBarMarker;
public final class SponsorBlockHelper {
private SponsorBlockHelper() {
}
public static Integer convertCategoryToColor(
final SponsorBlockCategory category,
final Context context
) {
final String key;
final String colorStr;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
switch (category) {
case SPONSOR -> {
key = context.getString(R.string.sponsor_block_category_sponsor_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.sponsor_segment)
: Color.parseColor(colorStr);
}
case INTRO -> {
key = context.getString(R.string.sponsor_block_category_intro_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.intro_segment)
: Color.parseColor(colorStr);
}
case OUTRO -> {
key = context.getString(R.string.sponsor_block_category_outro_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.outro_segment)
: Color.parseColor(colorStr);
}
case INTERACTION -> {
key = context.getString(R.string.sponsor_block_category_interaction_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.interaction_segment)
: Color.parseColor(colorStr);
}
case HIGHLIGHT -> {
key = context.getString(R.string.sponsor_block_category_highlight_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.highlight_segment)
: Color.parseColor(colorStr);
}
case SELF_PROMO -> {
key = context.getString(R.string.sponsor_block_category_self_promo_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.self_promo_segment)
: Color.parseColor(colorStr);
}
case NON_MUSIC -> {
key = context.getString(R.string.sponsor_block_category_non_music_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.non_music_segment)
: Color.parseColor(colorStr);
}
case PREVIEW -> {
key = context.getString(R.string.sponsor_block_category_preview_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.preview_segment)
: Color.parseColor(colorStr);
}
case FILLER -> {
key = context.getString(R.string.sponsor_block_category_filler_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.filler_segment)
: Color.parseColor(colorStr);
}
case PENDING -> {
key = context.getString(R.string.sponsor_block_category_pending_color_key);
colorStr = prefs.getString(key, null);
return colorStr == null
? context.getResources().getColor(R.color.pending_segment)
: Color.parseColor(colorStr);
}
}
return null;
}
public static void markSegments(
final Context context,
final MarkableSeekBar seekBar,
@NonNull final StreamInfo streamInfo
) {
seekBar.clearMarkers();
final SponsorBlockSegment[] sponsorBlockSegments = streamInfo.getSponsorBlockSegments();
if (sponsorBlockSegments == null) {
return;
}
for (final SponsorBlockSegment sponsorBlockSegment : sponsorBlockSegments) {
final Integer color = convertCategoryToColor(
sponsorBlockSegment.category, context);
// if null, then this category should not be marked
if (color == null) {
continue;
}
// duration is in seconds, we need milliseconds
final long length = streamInfo.getDuration() * 1000;
final SeekBarMarker seekBarMarker =
new SeekBarMarker(sponsorBlockSegment.startTime, sponsorBlockSegment.endTime,
length, color);
seekBar.seekBarMarkers.add(seekBarMarker);
}
seekBar.drawMarkers();
}
public static String convertCategoryToFriendlyName(final Context context,
final SponsorBlockCategory category) {
return switch (category) {
case SPONSOR -> context.getString(
R.string.sponsor_block_category_sponsor);
case INTRO -> context.getString(
R.string.sponsor_block_category_intro);
case OUTRO -> context.getString(
R.string.sponsor_block_category_outro);
case INTERACTION -> context.getString(
R.string.sponsor_block_category_interaction);
case HIGHLIGHT -> context.getString(
R.string.sponsor_block_category_highlight);
case SELF_PROMO -> context.getString(
R.string.sponsor_block_category_self_promo);
case NON_MUSIC -> context.getString(
R.string.sponsor_block_category_non_music);
case PREVIEW -> context.getString(
R.string.sponsor_block_category_preview);
case FILLER -> context.getString(
R.string.sponsor_block_category_filler);
case PENDING -> context.getString(
R.string.sponsor_block_category_pending);
};
}
public static String convertCategoryToSkipMessage(final Context context,
final SponsorBlockCategory category) {
return switch (category) {
case SPONSOR -> context
.getString(R.string.sponsor_block_skip_sponsor_toast);
case INTRO -> context
.getString(R.string.sponsor_block_skip_intro_toast);
case OUTRO -> context
.getString(R.string.sponsor_block_skip_outro_toast);
case INTERACTION -> context
.getString(R.string.sponsor_block_skip_interaction_toast);
case HIGHLIGHT -> ""; // this should never happen
case SELF_PROMO -> context
.getString(R.string.sponsor_block_skip_self_promo_toast);
case NON_MUSIC -> context
.getString(R.string.sponsor_block_skip_non_music_toast);
case PREVIEW -> context
.getString(R.string.sponsor_block_skip_preview_toast);
case FILLER -> context
.getString(R.string.sponsor_block_skip_filler_toast);
case PENDING -> context
.getString(R.string.sponsor_block_skip_pending_toast);
};
}
}

View file

@ -0,0 +1,7 @@
package org.schabi.newpipe.util;
public enum SponsorBlockMode {
DISABLED,
ENABLED,
IGNORE
}

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.util;
import java.util.Locale;
public final class TimeUtils {
private TimeUtils() {
}
public static String millisecondsToString(final double milliseconds) {
final int seconds = (int) (milliseconds / 1000) % 60;
final int minutes = (int) ((milliseconds / (1000 * 60)) % 60);
final int hours = (int) ((milliseconds / (1000 * 60 * 60)) % 24);
return String.format(Locale.getDefault(),
"%02d:%02d:%02d", hours, minutes, seconds);
}
}

View file

@ -154,7 +154,7 @@ public final class InternalUrlsHandler {
}
final Single<StreamInfo> single =
ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
ExtractorHelper.getStreamInfo(context, service.getServiceId(), cleanUrl, false);
disposables.add(single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {

View file

@ -24,8 +24,6 @@ import android.view.KeyEvent;
import android.view.ViewTreeObserver;
import android.widget.SeekBar;
import androidx.appcompat.widget.AppCompatSeekBar;
import org.schabi.newpipe.util.DeviceUtils;
/**
@ -33,7 +31,7 @@ import org.schabi.newpipe.util.DeviceUtils;
* (onStartTrackingTouch/onStopTrackingTouch), so existing code does not need to be changed to
* work with it.
*/
public final class FocusAwareSeekBar extends AppCompatSeekBar {
public final class FocusAwareSeekBar extends MarkableSeekBar {
private NestedListener listener;
private ViewTreeObserver treeObserver;

View file

@ -0,0 +1,109 @@
package org.schabi.newpipe.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatSeekBar;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.R;
import java.util.ArrayList;
public class MarkableSeekBar extends AppCompatSeekBar {
public ArrayList<SeekBarMarker> seekBarMarkers = new ArrayList<>();
private Drawable originalProgressDrawable;
public MarkableSeekBar(final Context context) {
super(context);
}
public MarkableSeekBar(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MarkableSeekBar(final Context context,
final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setProgressDrawable(final Drawable d) {
super.setProgressDrawable(d);
// stored for when we draw (and potentially re-draw) markers
originalProgressDrawable = d;
}
@Override
protected void onSizeChanged(final int w, final int h, final int oldW, final int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
// re-draw markers since the progress bar may have a different width
drawMarkers();
}
public void drawMarkers() {
if (seekBarMarkers.size() == 0) {
return;
}
// Markers are drawn like so:
//
// - LayerDrawable (original drawable for the SeekBar)
// - GradientDrawable (background)
// - ScaleDrawable (secondaryProgress)
// - ScaleDrawable (progress)
// - LayerDrawable (we add our markers in a sub-LayerDrawable)
// - Drawable (marker)
// - Drawable (marker)
// - Drawable (marker)
// - etc...
final int width = getMeasuredWidth() - (getPaddingStart() + getPaddingEnd());
LayerDrawable layerDrawable = (LayerDrawable) originalProgressDrawable;
final ArrayList<Drawable> markerDrawables = new ArrayList<>();
markerDrawables.add(layerDrawable);
for (final SeekBarMarker seekBarMarker : seekBarMarkers) {
@SuppressLint("PrivateResource")
final Drawable markerDrawable =
ContextCompat.getDrawable(
getContext(),
R.drawable.abc_scrubber_primary_mtrl_alpha);
final PorterDuffColorFilter colorFilter =
new PorterDuffColorFilter(seekBarMarker.color, PorterDuff.Mode.SRC_IN);
assert markerDrawable != null;
markerDrawable.setColorFilter(colorFilter);
markerDrawables.add(markerDrawable);
}
layerDrawable = new LayerDrawable(markerDrawables.toArray(new Drawable[0]));
for (int i = 1; i < layerDrawable.getNumberOfLayers(); i++) {
final SeekBarMarker seekBarMarker = seekBarMarkers.get(i - 1);
final int l = (int) (width * seekBarMarker.percentStart);
final int r = (int) (width * (1.0 - seekBarMarker.percentEnd));
layerDrawable.setLayerInset(i, l, 0, r, 0);
}
super.setProgressDrawable(layerDrawable);
}
public void clearMarkers() {
seekBarMarkers.clear();
super.setProgressDrawable(originalProgressDrawable);
}
}

View file

@ -0,0 +1,26 @@
package org.schabi.newpipe.views;
public class SeekBarMarker {
public double startTime;
public double endTime;
public double percentStart;
public double percentEnd;
public int color;
public SeekBarMarker(final double startTime,
final double endTime,
final long maxTime,
final int color) {
this.startTime = startTime;
this.endTime = endTime;
this.percentStart = ((startTime / maxTime) * 100.0) / 100.0;
this.percentEnd = ((endTime / maxTime) * 100.0) / 100.0;
this.color = color;
}
public SeekBarMarker(final double percentStart, final double percentEnd, final int color) {
this.percentStart = percentStart;
this.percentEnd = percentEnd;
this.color = color;
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M17.77,3.77l-1.77,-1.77l-10,10l10,10l1.77,-1.77l-8.23,-8.23z" />
</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="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6.23,20.23l1.77,1.77l10,-10l-10,-10l-1.77,1.77l8.23,8.23z" />
</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="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z" />
</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="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z" />
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,22.7994C11.55,22.7994 11.1,22.7094 10.74,22.4394 4.89,18.8394 1.29,12.6294 1.2,5.7894 1.2,4.8894 1.65,3.9894 2.46,3.5394 8.4,0.3894 15.6,0.3894 21.54,3.6294 22.35,3.9894 22.8,4.8894 22.8,5.7894 22.71,12.6294 19.11,18.8394 13.35,22.4394 12.9,22.7094 12.45,22.7994 12,22.7994ZM12,1.9194c-3.15,0 -6.3,0.81 -9.18,2.34 -0.54,0.27 -0.9,0.9 -0.9,1.53 0.09,6.57 3.51,12.51 9.18,16.02 0.54,0.36 1.26,0.36 1.8,0C18.57,18.3894 21.9,12.3594 22.08,5.7894 22.08,5.1594 21.72,4.5294 21.18,4.2594 18.3,2.7294 15.15,1.9194 12,1.9194Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M20.73,4.9794C15.24,2.0994 8.76,2.0994 3.27,4.9794 3,5.1594 2.82,5.4294 2.82,5.7894c0.09,6.48 3.51,12.06 8.73,15.3 0.27,0.18 0.63,0.18 0.9,0 5.13,-3.15 8.64,-8.82 8.73,-15.3C21.18,5.4294 21,5.1594 20.73,4.9794ZM12,15.8694c-2.79,0 -4.95,-2.25 -4.95,-4.95 0,-2.7 2.25,-4.95 4.95,-4.95 2.7,0 4.95,2.25 4.95,4.95 0,2.79 -2.16,4.95 -4.95,4.95z"/>
<path
android:fillColor="#FF000000"
android:pathData="m15.15,13.4394c0.54,-0.72 0.9,-1.53 0.9,-2.52 0,-2.25 -1.8,-4.05 -4.05,-4.05 -0.9,0 -1.8,0.36 -2.52,0.9z"/>
<path
android:fillColor="#FF000000"
android:pathData="m8.85,8.4894c-0.54,0.72 -0.9,1.53 -0.9,2.52 0,2.25 1.8,4.05 4.05,4.05 0.9,0 1.8,-0.36 2.52,-0.9z"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,22.7994C11.55,22.7994 11.1,22.7094 10.74,22.4394 4.89,18.8394 1.29,12.6294 1.2,5.7894 1.2,4.8894 1.65,3.9894 2.46,3.5394 8.4,0.3894 15.6,0.3894 21.54,3.6294 22.35,3.9894 22.8,4.8894 22.8,5.7894 22.71,12.6294 19.11,18.8394 13.35,22.4394 12.9,22.7094 12.45,22.7994 12,22.7994ZM12,1.9194c-3.15,0 -6.3,0.81 -9.18,2.34 -0.54,0.27 -0.9,0.9 -0.9,1.53 0.09,6.57 3.51,12.51 9.18,16.02 0.54,0.36 1.26,0.36 1.8,0C18.57,18.3894 21.9,12.3594 22.08,5.7894 22.08,5.1594 21.72,4.5294 21.18,4.2594 18.3,2.7294 15.15,1.9194 12,1.9194Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M20.73,4.9794C15.24,2.0994 8.76,2.0994 3.27,4.9794 3,5.1594 2.82,5.4294 2.82,5.7894c0.09,6.48 3.51,12.06 8.73,15.3 0.27,0.18 0.63,0.18 0.9,0 5.13,-3.15 8.64,-8.82 8.73,-15.3C21.18,5.4294 21,5.1594 20.73,4.9794ZM9.66,15.1494L9.66,6.7794l7.29,4.23z"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,22.7994C11.55,22.7994 11.1,22.7094 10.74,22.4394 4.89,18.8394 1.29,12.6294 1.2,5.7894 1.2,4.8894 1.65,3.9894 2.46,3.5394 8.4,0.3894 15.6,0.3894 21.54,3.6294 22.35,3.9894 22.8,4.8894 22.8,5.7894 22.71,12.6294 19.11,18.8394 13.35,22.4394 12.9,22.7094 12.45,22.7994 12,22.7994ZM12,1.9194c-3.15,0 -6.3,0.81 -9.18,2.34 -0.54,0.27 -0.9,0.9 -0.9,1.53 0.09,6.57 3.51,12.51 9.18,16.02 0.54,0.36 1.26,0.36 1.8,0C18.57,18.3894 21.9,12.3594 22.08,5.7894 22.08,5.1594 21.72,4.5294 21.18,4.2594 18.3,2.7294 15.15,1.9194 12,1.9194Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M20.73,4.9794C15.24,2.0994 8.76,2.0994 3.27,4.9794 3,5.1594 2.82,5.4294 2.82,5.7894c0.09,6.48 3.51,12.06 8.73,15.3 0.27,0.18 0.63,0.18 0.9,0 5.13,-3.15 8.64,-8.82 8.73,-15.3C21.18,5.4294 21,5.1594 20.73,4.9794ZM12,15.4194c0,0 -4.5,-3.6 -4.5,-5.94 0,-1.53 0.99,-2.43 2.25,-2.43 1.08,0 2.25,1.17 2.25,1.17 0,0 1.08,-1.17 2.25,-1.17 1.26,0 2.25,0.81 2.25,2.43 0,2.34 -4.5,5.94 -4.5,5.94z"/>
</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="@color/defaultIconTint"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z" />
</vector>

View file

@ -280,7 +280,7 @@
tools:ignore="HardcodedText"
tools:text="1:06:29" />
<androidx.appcompat.widget.AppCompatSeekBar
<org.schabi.newpipe.views.FocusAwareSeekBar
android:id="@+id/seek_bar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"

View file

@ -116,7 +116,7 @@
tools:text="1:06:29" />
<androidx.appcompat.widget.AppCompatSeekBar
<org.schabi.newpipe.views.FocusAwareSeekBar
android:id="@+id/seek_bar"
style="@style/Widget.AppCompat.SeekBar"
android:layout_width="0dp"

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clickable="false"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:paddingTop="@dimen/video_item_search_padding">
<EditText
android:id="@+id/api_url_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:saveEnabled="true"
android:inputType="textUri"
android:maxLines="1"
android:importantForAutofill="no"
android:hint="https://domain.com/api/"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/icon_api_url_help"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:clickable="true"
android:focusable="true"
app:srcCompat="@drawable/ic_help"
android:background="?attr/selectableItemBackground"
android:tint="?attr/colorAccent"/>
</LinearLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clickable="false"
android:paddingLeft="@dimen/video_item_search_padding"
android:paddingRight="@dimen/video_item_search_padding"
android:paddingTop="@dimen/video_item_search_padding">
<TextView
android:id="@+id/api_url_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginBottom="6dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:text="@string/sponsor_block_api_url_help_text"/>
<Button
android:id="@+id/sponsor_block_privacy_policy_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/sponsor_block_privacy_policy_text"/>
</LinearLayout>

View file

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
tools:context=".fragments.list.sponsorblock.SponsorBlockFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/sponsor_block_controls_mark_segment_start"
android:layout_width="0dp"
android:layout_height="@dimen/detail_control_height"
android:layout_weight="1.4"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/sponsor_block_mark_the_start_of_a_segment"
android:drawableTop="@drawable/ic_chevron_right"
android:focusable="true"
android:gravity="center"
android:paddingVertical="@dimen/detail_control_padding"
android:text="@string/start"
android:textSize="@dimen/detail_control_text_size" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.4">
</Space>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/sponsor_block_controls_mark_segment_end"
android:layout_width="0dp"
android:layout_height="@dimen/detail_control_height"
android:layout_weight="1.4"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/sponsor_block_mark_the_end_of_a_segment"
android:drawableTop="@drawable/ic_chevron_left"
android:focusable="true"
android:gravity="center"
android:paddingVertical="@dimen/detail_control_padding"
android:text="@string/end"
android:textSize="@dimen/detail_control_text_size" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.4" >
</Space>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:layout_gravity="center"
android:gravity="center_horizontal">
<TextView
android:id="@+id/sponsor_block_controls_segment_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:textAlignment="center"
android:text="00:00:00"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:textAlignment="center"
android:text="-"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/sponsor_block_controls_segment_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:textAlignment="center"
android:text="00:00:00"
tools:ignore="HardcodedText" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.4" >
</Space>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/sponsor_block_controls_clear_segment"
android:layout_width="0dp"
android:layout_height="@dimen/detail_control_height"
android:layout_weight="1.4"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/sponsor_block_clear_segment"
android:drawableTop="@drawable/ic_delete"
android:focusable="true"
android:gravity="center"
android:paddingVertical="@dimen/detail_control_padding"
android:text="@string/clear"
android:textSize="@dimen/detail_control_text_size" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="0.4">
</Space>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/sponsor_block_controls_submit_segment"
android:layout_width="0dp"
android:layout_height="@dimen/detail_control_height"
android:layout_weight="1.4"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/sponsor_block_submit_segment"
android:drawableTop="@drawable/ic_upload"
android:focusable="true"
android:gravity="center"
android:paddingVertical="@dimen/detail_control_padding"
android:text="@string/submit"
android:textSize="@dimen/detail_control_text_size" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="16dp"
android:background="?android:attr/listDivider" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/skipping_is_enabled_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/sponsor_block_skip_marked_segments" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/channel_is_whitelisted_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/sponsor_block_whitelist_channel" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="16dp"
android:background="?android:attr/listDivider" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/segment_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/list_segments_item"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal">
<!-- segment color -->
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center">
<!-- skip to highlight -->
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_segment_skip_to_highlight"
android:layout_width="20dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/description_skip_to_highlight"
android:scaleType="fitCenter"
android:src="@drawable/ic_fast_forward" />
<!-- segment type (color) -->
<View
android:id="@+id/item_segment_color_view"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_centerInParent="true"
android:background="@color/interaction_segment" />
</RelativeLayout>
<!-- segment name -->
<TextView
android:id="@+id/item_segment_category_name_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_gravity="center"
android:textAlignment="center"
tools:text="Intermission/Intro Animation" />
<!-- start time, end time -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:id="@+id/item_segment_start_time_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:textAlignment="center"
tools:text="00:00:00" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:textAlignment="center"
android:text="-" />
<TextView
android:id="@+id/item_segment_end_time_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:textAlignment="center"
tools:text="99:99:99" />
</LinearLayout>
<!-- vote up -->
<ImageView
android:id="@+id/item_segment_vote_up_imageview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/description_up_vote_segment"
android:scaleType="fitCenter"
android:src="@drawable/ic_thumb_up" />
<!-- vote down -->
<ImageView
android:id="@+id/item_segment_vote_down_imageview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/description_down_vote_segment"
android:scaleType="fitCenter"
android:src="@drawable/ic_thumb_down" />
</LinearLayout>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sponsor_block_segment_color_view"
android:layout_width="98dp"
android:layout_height="6dp"
android:background="@color/black"/>

View file

@ -84,4 +84,16 @@
<color name="black">#000</color>
<!-- default SponsorBlock segment colors -->
<color name="sponsor_segment">#00d400</color>
<color name="intro_segment">#00ffff</color>
<color name="outro_segment">#0202ed</color>
<color name="interaction_segment">#cc00ff</color>
<color name="highlight_segment">#ff1983</color>
<color name="self_promo_segment">#ffff00</color>
<color name="non_music_segment">#ff9900</color>
<color name="preview_segment">#008fd6</color>
<color name="filler_segment">#7300ff</color>
<color name="pending_segment">#ffffff</color>
</resources>

View file

@ -1466,4 +1466,36 @@
<item>@string/image_quality_medium_key</item>
<item>@string/image_quality_high_key</item>
</string-array>
<!-- SponsorBlock -->
<string name="sponsor_block_home_page_key" translatable="false">sponsor_block_home_page</string>
<string name="sponsor_block_enable_key" translatable="false">sponsor_block_enable</string>
<string name="sponsor_block_api_url_key" translatable="false">sponsor_block_api_url</string>
<string name="sponsor_block_notifications_key" translatable="false">sponsor_block_notifications</string>
<string name="sponsor_block_privacy_key" translatable="false">sponsor_block_privacy</string>
<string name="sponsor_block_categories_key" translatable="false">sponsor_block_categories</string>
<string name="sponsor_block_category_reset_key" translatable="false">sponsor_block_category_reset</string>
<string name="sponsor_block_category_all_on_key" translatable="false">sponsor_block_category_all_on</string>
<string name="sponsor_block_category_all_off_key" translatable="false">sponsor_block_category_all_off</string>
<string name="sponsor_block_category_sponsor_key" translatable="false">sponsor_block_category_sponsor</string>
<string name="sponsor_block_category_sponsor_color_key" translatable="false">sponsor_block_category_sponsor_color</string>
<string name="sponsor_block_category_intro_key" translatable="false">sponsor_block_category_intro</string>
<string name="sponsor_block_category_intro_color_key" translatable="false">sponsor_block_category_intro_color</string>
<string name="sponsor_block_category_outro_key" translatable="false">sponsor_block_category_outro</string>
<string name="sponsor_block_category_outro_color_key" translatable="false">sponsor_block_category_outro_color</string>
<string name="sponsor_block_category_interaction_key" translatable="false">sponsor_block_category_interaction</string>
<string name="sponsor_block_category_interaction_color_key" translatable="false">sponsor_block_category_interaction_color</string>
<string name="sponsor_block_category_highlight_key" translatable="false">sponsor_block_category_highlight</string>
<string name="sponsor_block_category_highlight_color_key" translatable="false">sponsor_block_category_highlight_color</string>
<string name="sponsor_block_category_self_promo_key" translatable="false">sponsor_block_category_self_promo</string>
<string name="sponsor_block_category_self_promo_color_key" translatable="false">sponsor_block_category_self_promo_color</string>
<string name="sponsor_block_category_non_music_key" translatable="false">sponsor_block_category_music</string>
<string name="sponsor_block_category_non_music_color_key" translatable="false">sponsor_block_category_music_color</string>
<string name="sponsor_block_category_preview_key" translatable="false">sponsor_block_category_preview</string>
<string name="sponsor_block_category_preview_color_key" translatable="false">sponsor_block_category_preview_color</string>
<string name="sponsor_block_category_filler_key" translatable="false">sponsor_block_category_filler</string>
<string name="sponsor_block_category_filler_color_key" translatable="false">sponsor_block_category_filler_color</string>
<string name="sponsor_block_category_pending_key" translatable="false">sponsor_block_category_pending_key</string>
<string name="sponsor_block_category_pending_color_key" translatable="false">sponsor_block_category_pending_color_key</string>
<string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string>
</resources>

View file

@ -273,6 +273,7 @@
<string name="comments_tab_description">Comments</string>
<string name="related_items_tab_description">Related items</string>
<string name="description_tab_description">Description</string>
<string name="sponsor_block_tab_description">SponsorBlock</string>
<string name="search_no_results">No results</string>
<string name="empty_list_subtitle">Nothing here but crickets</string>
<string name="import_subscriptions_hint">Import or export subscriptions from the 3-dot menu</string>
@ -838,4 +839,94 @@
<string name="share_playlist_with_list">Share URL list</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="share_playlist_content_details">%1$s\n%2$s</string>
<!-- SponsorBlock -->
<string name="sponsor_block">SponsorBlock</string>
<string name="sponsor_block_category_sponsor">Sponsor</string>
<string name="sponsor_block_category_intro">Intermission/Intro Animation</string>
<string name="sponsor_block_category_outro">Endcards/Credits</string>
<string name="sponsor_block_category_interaction">Interaction Reminders (Subscribe)</string>
<string name="sponsor_block_category_highlight">Highlight</string>
<string name="sponsor_block_category_self_promo">Unpaid/Self Promotion</string>
<string name="sponsor_block_category_non_music">Music: Non-Music Section</string>
<string name="sponsor_block_category_preview">Preview/Recap</string>
<string name="sponsor_block_category_filler">Filler Tangent/Jokes</string>
<string name="sponsor_block_category_pending">Pending</string>
<string name="sponsor_block_home_page_title">View Website</string>
<string name="sponsor_block_home_page_summary">View the official SponsorBlock website.</string>
<string name="sponsor_block_enable_title">Enable SponsorBlock</string>
<string name="sponsor_block_enable_summary">Use the SponsorBlock API to automatically skip sponsors in videos. This currently only works for YouTube videos.</string>
<string name="sponsor_block_api_url_title">API Url</string>
<string name="sponsor_block_api_url_summary">The url to use when querying the SponsorBlock API. This must be set for SponsorBlock to work.</string>
<string name="sponsor_block_notifications_title">Notify when sponsors are skipped</string>
<string name="sponsor_block_notifications_summary">Show a toast notification when a sponsor is automatically skipped.</string>
<string name="sponsor_block_privacy_title">View Privacy Policy</string>
<string name="sponsor_block_privacy_summary">View SponsorBlock\'s privacy policy.</string>
<string name="sponsor_block_api_url_help_text">This is the URL that will be queried when the application needs to know which parts of a video to skip.\n\nYou can set the official URL by clicking the \'Use Official\' option below, though it is highly recommended you view SponsorBlock\'s privacy policy before you do.</string>
<string name="sponsor_block_privacy_policy_text">SponsorBlock Privacy Policy</string>
<string name="sponsor_block_homepage_url">https://sponsor.ajay.app/</string>
<string name="sponsor_block_default_api_url">https://sponsor.ajay.app/api/</string>
<string name="sponsor_block_privacy_policy_url">https://gist.github.com/ajayyy/aa9f8ded2b573d4f73a3ffa0ef74f796</string>
<string name="sponsor_block_skip_sponsor_toast">Skipped sponsor</string>
<string name="sponsor_block_skip_intro_toast">Skipped intermission/intro</string>
<string name="sponsor_block_skip_outro_toast">Skipped endcards/credits</string>
<string name="sponsor_block_skip_interaction_toast">Skipped interaction reminder</string>
<string name="sponsor_block_skip_self_promo_toast">Skipped unpaid/self promo</string>
<string name="sponsor_block_skip_non_music_toast">Skipped non-music</string>
<string name="sponsor_block_skip_preview_toast">Skipped preview/recap</string>
<string name="sponsor_block_skip_filler_toast">Skipped filler</string>
<string name="sponsor_block_skip_pending_toast">Skipped pending segment</string>
<string name="sponsor_block_toggle_skipping">Toggle skipping sponsors</string>
<string name="sponsor_block_clear_whitelist_title">Clear Whitelist</string>
<string name="sponsor_block_clear_whitelist_summary">Clear the list of uploaders SponsorBlock will ignore.</string>
<string name="sponsor_block_enabled_toast">SponsorBlock enabled</string>
<string name="sponsor_block_disabled_toast">SponsorBlock disabled</string>
<string name="sponsor_block_whitelist_cleared_toast">Whitelist cleared</string>
<string name="sponsor_block_uploader_added_to_whitelist_toast">Channel added to whitelist</string>
<string name="sponsor_block_uploader_removed_from_whitelist_toast">Channel removed from whitelist</string>
<string name="sponsor_block_confirm_clear_whitelist">Are you sure you want to clear the whitelist?</string>
<string name="sponsor_block_confirm_reset_colors">Are you sure you want to reset the category colors?</string>
<string name="sponsor_block_segment_voted_up_toast">Up-voted segment</string>
<string name="sponsor_block_segment_voted_down_toast">Down-voted segment</string>
<string name="sponsor_block_segment_reset_vote_toast">Voting reset for segment</string>
<string name="sponsor_block_invalid_start_toast">Invalid start location</string>
<string name="sponsor_block_invalid_end_toast">Invalid end location</string>
<string name="sponsor_block_marked_start_toast">Marked start of segment</string>
<string name="sponsor_block_marked_end_toast">Marked end of segment</string>
<string name="sponsor_block_missing_times_toast">Missing start/end time(s)</string>
<string name="sponsor_block_select_a_category">Select a category</string>
<string name="sponsor_block_clear_marked_segment_prompt">Are you sure you want to clear your new segment?</string>
<string name="sponsor_block_upload_success_message">Your segment has been submitted.\n\nIt might take some time for your submission to appear in the video the next time you open it.</string>
<string name="sponsor_block_mark_the_start_of_a_segment">Mark the start of a segment</string>
<string name="sponsor_block_mark_the_end_of_a_segment">Mark the end of a segment</string>
<string name="sponsor_block_clear_segment">Clear segment</string>
<string name="sponsor_block_submit_segment">Submit segment</string>
<string name="sponsor_block_skip_marked_segments">Skip marked segments</string>
<string name="sponsor_block_whitelist_channel">Whitelist channel</string>
<string name="settings_category_sponsor_block_title">SponsorBlock (Third-Party Service)</string>
<string name="settings_category_sponsor_block_categories_quick_actions">Quick Actions</string>
<string name="settings_category_sponsor_block_categories_title">SponsorBlock Categories</string>
<string name="settings_category_sponsor_block_categories_summary">Customize which video segments to skip, along with their color markings on the seek bar.</string>
<string name="settings_category_sponsor_block_categories_reset_colors_title">Reset Colors</string>
<string name="settings_category_sponsor_block_categories_all_colors_on_title">All On</string>
<string name="settings_category_sponsor_block_categories_all_colors_off_title">All Off</string>
<string name="settings_category_sponsor_block_category_enable_title">Enable</string>
<string name="settings_category_sponsor_block_category_color">Seek Bar Color</string>
<string name="settings_category_sponsor_block_category_sponsor_summary">Paid promotion, paid referrals and direct advertisements. Not for self-promotion or free shoutouts to causes/creators/websites/products they like.</string>
<string name="settings_category_sponsor_block_category_intro_summary">An interval without actual content. Could be a pause, static frame, repeating animation. This should not be used for transitions containing information or be used on music videos.</string>
<string name="settings_category_sponsor_block_category_outro_summary">Credits or when the YouTube endcards appear. Not for spoken conclusions. This should not include useful content. This should not be used on music videos.</string>
<string name="settings_category_sponsor_block_category_interaction_summary">When there is a short reminder to like, subscribe or follow them in the middle of content. If it is long or about something specific, it should be under self promotion instead.</string>
<string name="settings_category_sponsor_block_category_highlight_summary">The part of the video that most people are looking for. Similar to "Video starts at x" comments.</string>
<string name="settings_category_sponsor_block_category_self_promo_summary">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="settings_category_sponsor_block_category_non_music_summary">Only for use in music videos. This includes introductions or outros in music videos.</string>
<string name="settings_category_sponsor_block_category_preview_summary">Quick recap of previous episodes, or a preview of what\'s coming up later in the current video. Meant for edited together clips, not for spoken summaries.</string>
<string name="settings_category_sponsor_block_category_filler_summary">This is for tangential scenes added only for filler or humor that are not required to understand the main content of the video.</string>
<string name="settings_category_sponsor_block_category_pending_summary">Represents a new segment ready to be submitted.</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="submit">Submit</string>
<string name="end">End</string>
<string name="invalid_color_toast">Invalid color</string>
<string name="description_up_vote_segment">Up-vote segment</string>
<string name="description_down_vote_segment">Down-vote segment</string>
<string name="description_skip_to_highlight">Skip to highlight</string>
</resources>

View file

@ -47,6 +47,12 @@
android:title="@string/settings_category_updates_title"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.SponsorBlockSettingsFragment"
android:icon="@drawable/ic_sponsor_block_enable"
android:title="@string/sponsor_block"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
android:icon="@drawable/ic_bug_report"

View file

@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/settings_category_sponsor_block_categories_title">
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/settings_category_sponsor_block_categories_quick_actions">
<Preference
app:iconSpaceReserved="false"
android:key="@string/sponsor_block_category_all_on_key"
android:title="@string/settings_category_sponsor_block_categories_all_colors_on_title"/>
<Preference
app:iconSpaceReserved="false"
android:key="@string/sponsor_block_category_all_off_key"
android:title="@string/settings_category_sponsor_block_categories_all_colors_off_title"/>
<Preference
app:iconSpaceReserved="false"
android:key="@string/sponsor_block_category_reset_key"
android:title="@string/settings_category_sponsor_block_categories_reset_colors_title"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_sponsor">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_sponsor_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_sponsor_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_sponsor_key"
android:defaultValue="@color/sponsor_segment"
android:key="@string/sponsor_block_category_sponsor_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_intro">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_intro_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_intro_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_intro_key"
android:defaultValue="@color/intro_segment"
android:key="@string/sponsor_block_category_intro_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_outro">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_outro_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_outro_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_outro_key"
android:defaultValue="@color/outro_segment"
android:key="@string/sponsor_block_category_outro_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_interaction">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_interaction_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_interaction_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_interaction_key"
android:defaultValue="@color/interaction_segment"
android:key="@string/sponsor_block_category_interaction_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_highlight">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_highlight_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_highlight_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_highlight_key"
android:defaultValue="@color/highlight_segment"
android:key="@string/sponsor_block_category_highlight_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_self_promo">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_self_promo_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_self_promo_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_self_promo_key"
android:defaultValue="@color/self_promo_segment"
android:key="@string/sponsor_block_category_self_promo_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_non_music">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_non_music_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_non_music_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_non_music_key"
android:defaultValue="@color/non_music_segment"
android:key="@string/sponsor_block_category_non_music_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_preview">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_preview_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_preview_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_preview_key"
android:defaultValue="@color/preview_segment"
android:key="@string/sponsor_block_category_preview_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_filler">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_filler_summary"
android:selectable="false"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_category_filler_key"
android:title="@string/settings_category_sponsor_block_category_enable_title"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_category_filler_key"
android:defaultValue="@color/filler_segment"
android:key="@string/sponsor_block_category_filler_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/sponsor_block_category_pending">
<Preference
app:iconSpaceReserved="false"
android:summary="@string/settings_category_sponsor_block_category_pending_summary"
android:selectable="false"/>
<org.schabi.newpipe.settings.custom.EditColorPreference
app:iconSpaceReserved="false"
android:defaultValue="@color/pending_segment"
android:key="@string/sponsor_block_category_pending_color_key"
android:title="@string/settings_category_sponsor_block_category_color"/>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/sponsor_block">
<Preference
app:iconSpaceReserved="false"
android:key="@string/sponsor_block_home_page_key"
android:summary="@string/sponsor_block_home_page_summary"
android:title="@string/sponsor_block_home_page_title"/>
<Preference
app:iconSpaceReserved="false"
android:key="@string/sponsor_block_privacy_key"
android:summary="@string/sponsor_block_privacy_summary"
android:title="@string/sponsor_block_privacy_title"/>
<PreferenceCategory
android:layout="@layout/settings_category_header_layout"
android:title="@string/settings">
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="true"
android:key="@string/sponsor_block_enable_key"
android:summary="@string/sponsor_block_enable_summary"
android:title="@string/sponsor_block_enable_title"/>
<org.schabi.newpipe.settings.custom.SponsorBlockApiUrlPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_enable_key"
android:defaultValue="@string/sponsor_block_default_api_url"
android:key="@string/sponsor_block_api_url_key"
android:summary="@string/sponsor_block_api_url_summary"
android:title="@string/sponsor_block_api_url_title"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_enable_key"
android:defaultValue="true"
android:key="@string/sponsor_block_notifications_key"
android:summary="@string/sponsor_block_notifications_summary"
android:title="@string/sponsor_block_notifications_title"/>
<PreferenceScreen
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_enable_key"
android:fragment="org.schabi.newpipe.settings.SponsorBlockCategoriesSettingsFragment"
android:key="@string/sponsor_block_categories_key"
android:title="@string/settings_category_sponsor_block_categories_title"
android:summary="@string/settings_category_sponsor_block_categories_summary"/>
<Preference
app:iconSpaceReserved="false"
android:dependency="@string/sponsor_block_enable_key"
android:key="@string/sponsor_block_clear_whitelist_key"
android:summary="@string/sponsor_block_clear_whitelist_summary"
android:title="@string/sponsor_block_clear_whitelist_title"/>
</PreferenceCategory>
</PreferenceScreen>