diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 7daf21e6e..c716a2d91 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -49,6 +49,13 @@ public abstract class StreamHistoryDAO implements HistoryDAO> 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> getHistorySortedById(); + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index ba90ae05a..96a385ca8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -120,6 +120,10 @@ public class HistoryRecordManager { return streamHistoryTable.getHistory().subscribeOn(Schedulers.io()); } + public Flowable> getStreamHistorySortedById() { + return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); + } + public Flowable> getStreamStatistics() { return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index d430afa5c..485d3f391 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -2,11 +2,15 @@ package org.schabi.newpipe.local.playlist; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; @@ -24,11 +28,14 @@ import org.reactivestreams.Subscription; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; 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.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemDialog; 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.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; @@ -39,15 +46,18 @@ import org.schabi.newpipe.util.StreamDialogEntry; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import icepick.State; +import io.reactivex.Flowable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposables; +import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -71,6 +81,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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 playlist) -> { + // Playlist data + final Iterator playlistIter = playlist.iterator(); + + // History data + final HistoryRecordManager recordManager + = new HistoryRecordManager(getContext()); + final Iterator historyIter = recordManager + .getStreamHistorySortedById().blockingFirst().iterator(); + + // Remove Watched, Functionality data + final List notWatchedItems = new ArrayList<>(); + boolean thumbnailVideoRemoved = false; + + // already sorted by ^ getStreamHistorySortedById(), binary search can be used + final ArrayList 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 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 notWatchedItems = + (List) 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 public void handleResult(@NonNull final List result) { super.handleResult(result); diff --git a/app/src/main/res/layout/local_playlist_header.xml b/app/src/main/res/layout/local_playlist_header.xml index 420da04ee..04fe32ab0 100644 --- a/app/src/main/res/layout/local_playlist_header.xml +++ b/app/src/main/res/layout/local_playlist_header.xml @@ -50,7 +50,7 @@ android:layout_height="wrap_content" android:layout_below="@id/playlist_stream_count"> - + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_local_playlist.xml b/app/src/main/res/menu/menu_local_playlist.xml new file mode 100644 index 000000000..fdca9b31f --- /dev/null +++ b/app/src/main/res/menu/menu_local_playlist.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43142989c..385bfce82 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -601,6 +601,10 @@ Choose an instance App language System default + Remove watched + Remove watched videos? + Videos that have been watched before and after being added to the playlist will be removed.\nAre you sure? This cannot be undone! + Yes, and partially watched videos Due to ExoPlayer constraints the seek duration was set to %d seconds