mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-11-12 17:44:27 +00:00
Fix playlist not clearing when navigating back (#8888)
This commit is contained in:
1 parent
1d1f801f54
commit
2acfbab5f4
2 files changed
+167
-110
No files matched your search
@@ -17,7 +17,6 @@ import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -31,6 +30,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -40,7 +40,6 @@ 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.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
@@ -55,7 +54,6 @@ import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
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.util.DeviceUtils;
|
||||
@@ -72,13 +70,11 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
|
||||
implements PlaylistControlViewHolder, DebounceSavable {
|
||||
@@ -97,6 +93,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
private LocalPlaylistManager playlistManager;
|
||||
private LocalPlaylistViewModel viewModel;
|
||||
private Subscription databaseSubscription;
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
@@ -105,8 +102,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
private AtomicBoolean isLoadingComplete;
|
||||
/** Used to debounce saving playlist edits to disk. */
|
||||
private DebounceSaver debounceSaver;
|
||||
/** Flag to prevent simultaneous rewrites of the playlist. */
|
||||
private boolean isRewritingPlaylist = false;
|
||||
|
||||
/**
|
||||
* The pager adapter that the fragment is created from when it is used as frontpage, i.e.
|
||||
@@ -130,6 +125,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
super.onCreate(savedInstanceState);
|
||||
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(LocalPlaylistViewModel.class);
|
||||
viewModel.getWorkState().observe(this, state -> {
|
||||
if (state instanceof LocalPlaylistViewModel.WorkState.Loading) {
|
||||
showLoading();
|
||||
} else {
|
||||
hideLoading();
|
||||
if (state instanceof LocalPlaylistViewModel.WorkState.Error error) {
|
||||
showError(new ErrorInfo(error.getThrowable(), UserAction.REQUESTED_BOOKMARK,
|
||||
"Playlist operation"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
disposables = new CompositeDisposable();
|
||||
|
||||
isLoadingComplete = new AtomicBoolean();
|
||||
@@ -369,13 +377,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||
createRenameDialog();
|
||||
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||
if (!isRewritingPlaylist) {
|
||||
openRemoveWatchedConfirmationDialog();
|
||||
}
|
||||
openRemoveWatchedConfirmationDialog();
|
||||
} else if (item.getItemId() == R.id.menu_item_remove_duplicates) {
|
||||
if (!isRewritingPlaylist) {
|
||||
openRemoveDuplicatesDialog();
|
||||
}
|
||||
openRemoveDuplicatesDialog();
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -421,80 +425,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||
if (isRewritingPlaylist) {
|
||||
return;
|
||||
}
|
||||
isRewritingPlaylist = true;
|
||||
showLoading();
|
||||
|
||||
final var recordManager = new HistoryRecordManager(getContext());
|
||||
final var historyIdsMaybe = recordManager.getStreamHistorySortedById()
|
||||
.firstElement()
|
||||
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
|
||||
.map(historyList -> historyList.stream().map(StreamHistoryEntry::getStreamId)
|
||||
.collect(Collectors.toList()));
|
||||
final var streamsMaybe = playlistManager.getPlaylistStreams(playlistId)
|
||||
.firstElement()
|
||||
.zipWith(historyIdsMaybe, (playlist, historyStreamIds) -> {
|
||||
// Remove Watched, Functionality data
|
||||
final List<PlaylistStreamEntry> itemsToKeep = new ArrayList<>();
|
||||
final boolean isThumbnailPermanent = playlistManager
|
||||
.getIsPlaylistThumbnailPermanent(playlistId);
|
||||
boolean thumbnailVideoRemoved = false;
|
||||
|
||||
final var streamStates = recordManager
|
||||
.loadLocalStreamStateBatch(playlist).blockingGet();
|
||||
|
||||
for (int i = 0; i < playlist.size(); i++) {
|
||||
final var playlistItem = playlist.get(i);
|
||||
final var streamStateEntity = streamStates.get(i);
|
||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||
playlistItem.getStreamId());
|
||||
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
||||
|
||||
if (indexInHistory < 0 // stream is not in history
|
||||
// stream is in history but the streamStateEntity is null
|
||||
// if the stream was played for less than 5 seconds, see
|
||||
// StreamStateEntity#PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
|| streamStateEntity == null
|
||||
|| (!removePartiallyWatched
|
||||
&& !streamStateEntity.isFinished(duration))) {
|
||||
itemsToKeep.add(playlistItem);
|
||||
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
|
||||
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
|
||||
== playlistItem.getStreamEntity().getUid()) {
|
||||
thumbnailVideoRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Pair<>(itemsToKeep, thumbnailVideoRemoved);
|
||||
});
|
||||
|
||||
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(flow -> {
|
||||
final List<PlaylistStreamEntry> itemsToKeep = flow.first;
|
||||
final boolean thumbnailVideoRemoved = flow.second;
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
itemListAdapter.addItems(itemsToKeep);
|
||||
debounceSaver.setHasChangesToSave();
|
||||
saveImmediate();
|
||||
|
||||
if (thumbnailVideoRemoved) {
|
||||
updateThumbnailUrl();
|
||||
}
|
||||
|
||||
final long videoCount = itemListAdapter.getItemsList().size();
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
if (videoCount == 0) {
|
||||
showEmptyState();
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
isRewritingPlaylist = false;
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Removing watched videos, partially watched=" + removePartiallyWatched))));
|
||||
viewModel.removeWatchedStreams(playlistId, removePartiallyWatched);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -633,29 +564,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
}
|
||||
|
||||
private void removeDuplicatesInPlaylist() {
|
||||
if (isRewritingPlaylist) {
|
||||
return;
|
||||
}
|
||||
isRewritingPlaylist = true;
|
||||
showLoading();
|
||||
|
||||
final var streamsMaybe = playlistManager
|
||||
.getDistinctPlaylistStreams(playlistId).firstElement();
|
||||
|
||||
|
||||
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(itemsToKeep -> {
|
||||
itemListAdapter.clearStreamItemList();
|
||||
itemListAdapter.addItems(itemsToKeep);
|
||||
setStreamCountAndOverallDuration(itemListAdapter.getItemsList());
|
||||
debounceSaver.setHasChangesToSave();
|
||||
saveImmediate();
|
||||
|
||||
hideLoading();
|
||||
isRewritingPlaylist = false;
|
||||
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
|
||||
"Removing duplicated streams"))));
|
||||
viewModel.removeDuplicates(playlistId);
|
||||
}
|
||||
|
||||
private void deleteItem(final PlaylistStreamEntry item) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.local.playlist
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Pair
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.Collections
|
||||
import org.schabi.newpipe.NewPipeDatabase
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager
|
||||
|
||||
class LocalPlaylistViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val playlistManager = LocalPlaylistManager(NewPipeDatabase.getInstance(application))
|
||||
private val recordManager = HistoryRecordManager(application)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val _workState = MutableLiveData<WorkState>(WorkState.Idle)
|
||||
val workState: LiveData<WorkState> = _workState
|
||||
|
||||
sealed class WorkState {
|
||||
object Idle : WorkState()
|
||||
object Loading : WorkState()
|
||||
data class Error(val throwable: Throwable) : WorkState()
|
||||
object Success : WorkState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes watched streams from the playlist.
|
||||
* This operation is performed in the background and survives fragment destruction
|
||||
* as it is not disposed when the ViewModel is cleared (to fix #8888).
|
||||
*/
|
||||
fun removeWatchedStreams(playlistId: Long, removePartiallyWatched: Boolean) {
|
||||
_workState.value = WorkState.Loading
|
||||
|
||||
val historyIdsMaybe = recordManager.streamHistorySortedById
|
||||
.firstElement()
|
||||
.map { historyList ->
|
||||
historyList.map { it.streamId }
|
||||
}
|
||||
|
||||
val streamsMaybe = playlistManager.getPlaylistStreams(playlistId)
|
||||
.firstElement()
|
||||
.zipWith(historyIdsMaybe) { playlist, historyStreamIds ->
|
||||
val itemsToKeep = mutableListOf<PlaylistStreamEntry>()
|
||||
val isThumbnailPermanent = playlistManager.getIsPlaylistThumbnailPermanent(playlistId)
|
||||
var thumbnailVideoRemoved = false
|
||||
|
||||
val streamStates = recordManager.loadLocalStreamStateBatch(playlist).blockingGet()
|
||||
|
||||
for (i in playlist.indices) {
|
||||
val playlistItem = playlist[i]
|
||||
val streamStateEntity = streamStates[i]
|
||||
val indexInHistory = Collections.binarySearch(historyStreamIds, playlistItem.streamId)
|
||||
val duration = playlistItem.toStreamInfoItem().duration
|
||||
|
||||
if (indexInHistory < 0 || streamStateEntity == null ||
|
||||
(!removePartiallyWatched && !streamStateEntity.isFinished(duration))
|
||||
) {
|
||||
itemsToKeep.add(playlistItem)
|
||||
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved &&
|
||||
playlistManager.getPlaylistThumbnailStreamId(playlistId) ==
|
||||
playlistItem.streamEntity.uid
|
||||
) {
|
||||
thumbnailVideoRemoved = true
|
||||
}
|
||||
}
|
||||
|
||||
Pair(itemsToKeep, thumbnailVideoRemoved)
|
||||
}
|
||||
|
||||
val disposable = streamsMaybe
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMapCompletable { flow ->
|
||||
val itemsToKeep = flow.first
|
||||
val thumbnailVideoRemoved = flow.second
|
||||
val streamIds = itemsToKeep.map { it.streamId }
|
||||
|
||||
playlistManager.updateJoin(playlistId, streamIds)
|
||||
.andThen(
|
||||
Completable.fromAction {
|
||||
if (thumbnailVideoRemoved) {
|
||||
updateThumbnailUrl(playlistId, itemsToKeep)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ _workState.value = WorkState.Success },
|
||||
{ _workState.value = WorkState.Error(it) }
|
||||
)
|
||||
|
||||
disposables.add(disposable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate streams from the playlist.
|
||||
*/
|
||||
fun removeDuplicates(playlistId: Long) {
|
||||
_workState.value = WorkState.Loading
|
||||
|
||||
val disposable = playlistManager.getDistinctPlaylistStreams(playlistId)
|
||||
.firstElement()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMapCompletable { itemsToKeep ->
|
||||
val streamIds = itemsToKeep.map { it.streamId }
|
||||
playlistManager.updateJoin(playlistId, streamIds)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ _workState.value = WorkState.Success },
|
||||
{ _workState.value = WorkState.Error(it) }
|
||||
)
|
||||
|
||||
disposables.add(disposable)
|
||||
}
|
||||
|
||||
private fun updateThumbnailUrl(playlistId: Long, itemsToKeep: List<PlaylistStreamEntry>) {
|
||||
if (playlistManager.getIsPlaylistThumbnailPermanent(playlistId)) {
|
||||
return
|
||||
}
|
||||
|
||||
val thumbnailStreamId = if (itemsToKeep.isNotEmpty()) {
|
||||
itemsToKeep[0].streamEntity.uid
|
||||
} else {
|
||||
PlaylistEntity.DEFAULT_THUMBNAIL_ID
|
||||
}
|
||||
|
||||
playlistManager.changePlaylistThumbnail(playlistId, thumbnailStreamId, false).blockingGet()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
// We deliberately do not clear disposables here to allow pending
|
||||
// database mutations to finish even if the user leaves the screen.
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user