Merge pull request #3065 from GradyClark/dev
Added the ability to remove all watched videos from a local playlist
This commit is contained in:
commit
2db0d63c97
|
@ -49,6 +49,13 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||||
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||||
|
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE
|
||||||
|
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||||
|
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||||
|
+ " ORDER BY " + STREAM_ID + " ASC")
|
||||||
|
public abstract Flowable<List<StreamHistoryEntry>> getHistorySortedById();
|
||||||
|
|
||||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
|
||||||
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
@ -120,6 +120,10 @@ public class HistoryRecordManager {
|
||||||
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
|
||||||
|
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() {
|
public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() {
|
||||||
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
|
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,15 @@ package org.schabi.newpipe.local.playlist;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
@ -24,11 +28,14 @@ import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
@ -39,15 +46,18 @@ import org.schabi.newpipe.util.StreamDialogEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
import io.reactivex.Flowable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.disposables.Disposables;
|
import io.reactivex.disposables.Disposables;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
@ -71,6 +81,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
private View headerPlayAllButton;
|
private View headerPlayAllButton;
|
||||||
private View headerPopupButton;
|
private View headerPopupButton;
|
||||||
private View headerBackgroundButton;
|
private View headerBackgroundButton;
|
||||||
|
|
||||||
private ItemTouchHelper itemTouchHelper;
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
private LocalPlaylistManager playlistManager;
|
private LocalPlaylistManager playlistManager;
|
||||||
|
@ -83,6 +94,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
private AtomicBoolean isLoadingComplete;
|
private AtomicBoolean isLoadingComplete;
|
||||||
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
||||||
private AtomicBoolean isModified;
|
private AtomicBoolean isModified;
|
||||||
|
/* Is the playlist currently being processed to remove watched videos */
|
||||||
|
private boolean isRemovingWatched = false;
|
||||||
|
|
||||||
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
|
||||||
LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||||
|
@ -244,6 +257,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
saveImmediate();
|
saveImmediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||||
|
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||||
|
}
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
inflater.inflate(R.menu.menu_local_playlist, menu);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
@ -331,6 +354,122 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_item_remove_watched:
|
||||||
|
if (!isRemovingWatched) {
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(R.string.remove_watched_popup_warning)
|
||||||
|
.setTitle(R.string.remove_watched_popup_title)
|
||||||
|
.setPositiveButton(R.string.yes,
|
||||||
|
(DialogInterface d, int id) -> removeWatchedStreams(false))
|
||||||
|
.setNeutralButton(
|
||||||
|
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
||||||
|
(DialogInterface d, int id) -> removeWatchedStreams(true))
|
||||||
|
.setNegativeButton(R.string.cancel,
|
||||||
|
(DialogInterface d, int id) -> d.cancel())
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||||
|
if (isRemovingWatched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRemovingWatched = true;
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.map((List<PlaylistStreamEntry> playlist) -> {
|
||||||
|
// Playlist data
|
||||||
|
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
|
||||||
|
|
||||||
|
// History data
|
||||||
|
final HistoryRecordManager recordManager
|
||||||
|
= new HistoryRecordManager(getContext());
|
||||||
|
final Iterator<StreamHistoryEntry> historyIter = recordManager
|
||||||
|
.getStreamHistorySortedById().blockingFirst().iterator();
|
||||||
|
|
||||||
|
// Remove Watched, Functionality data
|
||||||
|
final List<PlaylistStreamEntry> notWatchedItems = new ArrayList<>();
|
||||||
|
boolean thumbnailVideoRemoved = false;
|
||||||
|
|
||||||
|
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
|
||||||
|
final ArrayList<Long> historyStreamIds = new ArrayList<>();
|
||||||
|
while (historyIter.hasNext()) {
|
||||||
|
historyStreamIds.add(historyIter.next().getStreamId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removePartiallyWatched) {
|
||||||
|
while (playlistIter.hasNext()) {
|
||||||
|
final PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||||
|
int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||||
|
playlistItem.getStreamId());
|
||||||
|
|
||||||
|
if (indexInHistory < 0) {
|
||||||
|
notWatchedItems.add(playlistItem);
|
||||||
|
} else if (!thumbnailVideoRemoved
|
||||||
|
&& playlistManager.getPlaylistThumbnail(playlistId)
|
||||||
|
.equals(playlistItem.getStreamEntity().getThumbnailUrl())) {
|
||||||
|
thumbnailVideoRemoved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final Iterator<StreamStateEntity> streamStatesIter = recordManager
|
||||||
|
.loadLocalStreamStateBatch(playlist).blockingGet().iterator();
|
||||||
|
|
||||||
|
while (playlistIter.hasNext()) {
|
||||||
|
PlaylistStreamEntry playlistItem = playlistIter.next();
|
||||||
|
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||||
|
playlistItem.getStreamId());
|
||||||
|
|
||||||
|
final boolean hasState = streamStatesIter.next() != null;
|
||||||
|
if (indexInHistory < 0 || hasState) {
|
||||||
|
notWatchedItems.add(playlistItem);
|
||||||
|
} else if (!thumbnailVideoRemoved
|
||||||
|
&& playlistManager.getPlaylistThumbnail(playlistId)
|
||||||
|
.equals(playlistItem.getStreamEntity().getThumbnailUrl())) {
|
||||||
|
thumbnailVideoRemoved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flowable.just(notWatchedItems, thumbnailVideoRemoved);
|
||||||
|
})
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(flow -> {
|
||||||
|
final List<PlaylistStreamEntry> notWatchedItems =
|
||||||
|
(List<PlaylistStreamEntry>) flow.blockingFirst();
|
||||||
|
final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast();
|
||||||
|
|
||||||
|
itemListAdapter.clearStreamItemList();
|
||||||
|
itemListAdapter.addItems(notWatchedItems);
|
||||||
|
saveChanges();
|
||||||
|
|
||||||
|
|
||||||
|
if (thumbnailVideoRemoved) {
|
||||||
|
updateThumbnailUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
final long videoCount = itemListAdapter.getItemsList().size();
|
||||||
|
setVideoCount(videoCount);
|
||||||
|
if (videoCount == 0) {
|
||||||
|
showEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
isRemovingWatched = false;
|
||||||
|
}, this::onError));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResult(@NonNull final List<PlaylistStreamEntry> result) {
|
public void handleResult(@NonNull final List<PlaylistStreamEntry> result) {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/playlist_stream_count">
|
android:layout_below="@id/playlist_stream_count">
|
||||||
|
|
||||||
<include layout="@layout/playlist_control"/>
|
<include layout="@layout/playlist_control" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_remove_watched"
|
||||||
|
android:title="@string/remove_watched"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
</menu>
|
|
@ -601,6 +601,10 @@
|
||||||
<string name="choose_instance_prompt">Choose an instance</string>
|
<string name="choose_instance_prompt">Choose an instance</string>
|
||||||
<string name="app_language_title">App language</string>
|
<string name="app_language_title">App language</string>
|
||||||
<string name="systems_language">System default</string>
|
<string name="systems_language">System default</string>
|
||||||
|
<string name="remove_watched">Remove watched</string>
|
||||||
|
<string name="remove_watched_popup_title">Remove watched videos?</string>
|
||||||
|
<string name="remove_watched_popup_warning">Videos that have been watched before and after being added to the playlist will be removed.\nAre you sure? This cannot be undone!</string>
|
||||||
|
<string name="remove_watched_popup_yes_and_partially_watched_videos">Yes, and partially watched videos</string>
|
||||||
<string name="new_seek_duration_toast">Due to ExoPlayer constraints the seek duration was set to %d seconds</string>
|
<string name="new_seek_duration_toast">Due to ExoPlayer constraints the seek duration was set to %d seconds</string>
|
||||||
<!-- Time duration plurals -->
|
<!-- Time duration plurals -->
|
||||||
<plurals name="seconds">
|
<plurals name="seconds">
|
||||||
|
|
Loading…
Reference in New Issue