NewPipe/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java

557 lines
21 KiB
Java
Raw Normal View History

package org.schabi.newpipe.local.bookmark;
2024-03-29 16:59:28 +00:00
import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists;
2022-04-14 08:59:52 +00:00
2022-12-03 08:52:04 +00:00
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.util.Log;
2022-04-15 12:44:54 +00:00
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
2019-10-04 12:59:08 +00:00
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
2019-10-04 12:59:08 +00:00
import androidx.fragment.app.FragmentManager;
2022-04-14 08:59:52 +00:00
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment;
2022-04-15 15:19:24 +00:00
import org.schabi.newpipe.local.holder.LocalBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.debounce.DebounceSavable;
import org.schabi.newpipe.util.debounce.DebounceSaver;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
2022-04-15 12:44:54 +00:00
import java.util.ArrayList;
import java.util.List;
2022-04-15 12:44:54 +00:00
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
2020-10-31 20:55:45 +00:00
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
2022-04-17 06:53:02 +00:00
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void>
implements DebounceSavable {
2022-04-15 12:44:54 +00:00
2022-04-14 08:59:52 +00:00
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State
Parcelable itemsListState;
private Subscription databaseSubscription;
private CompositeDisposable disposables = new CompositeDisposable();
private LocalPlaylistManager localPlaylistManager;
private RemotePlaylistManager remotePlaylistManager;
2022-04-14 08:59:52 +00:00
private ItemTouchHelper itemTouchHelper;
2022-04-17 06:53:02 +00:00
/* Have the bookmarked playlists been fully loaded from db */
2022-04-15 12:44:54 +00:00
private AtomicBoolean isLoadingComplete;
2022-04-17 06:53:02 +00:00
/* Gives enough time to avoid interrupting user sorting operations */
@Nullable
2022-04-17 06:53:02 +00:00
private DebounceSaver debounceSaver;
2022-04-15 12:44:54 +00:00
private List<Pair<Long, LocalItem.LocalItemType>> deletedItems;
2022-04-15 12:44:54 +00:00
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation
///////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (activity == null) {
return;
}
final AppDatabase database = NewPipeDatabase.getInstance(activity);
localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable();
2022-04-15 12:44:54 +00:00
isLoadingComplete = new AtomicBoolean();
debounceSaver = new DebounceSaver(3000, this);
2022-04-15 12:44:54 +00:00
deletedItems = new ArrayList<>();
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
final Bundle savedInstanceState) {
if (!useAsFrontPage) {
2018-06-10 23:55:14 +00:00
setTitle(activity.getString(R.string.tab_bookmarks));
}
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
}
@Override
2021-10-16 19:33:45 +00:00
public void onResume() {
super.onResume();
if (activity != null) {
setTitle(activity.getString(R.string.tab_bookmarks));
}
}
2020-04-02 11:51:10 +00:00
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
2022-04-15 15:19:24 +00:00
itemListAdapter.setUseItemHandle(true);
}
@Override
protected void initListeners() {
super.initListeners();
2022-04-14 08:59:52 +00:00
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
2022-07-31 08:08:24 +00:00
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
2018-06-19 20:40:43 +00:00
final FragmentManager fragmentManager = getFM();
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.getUid(),
2018-06-19 20:40:43 +00:00
entry.name);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
NavigationHelper.openPlaylistFragment(
fragmentManager,
entry.getServiceId(),
entry.getUrl(),
entry.getName());
}
}
@Override
public void held(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistMetadataEntry) {
showLocalDialog((PlaylistMetadataEntry) selectedItem);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
}
}
2022-04-14 08:59:52 +00:00
@Override
public void drag(final LocalItem selectedItem,
final RecyclerView.ViewHolder viewHolder) {
if (itemTouchHelper != null) {
itemTouchHelper.startDrag(viewHolder);
}
}
});
}
///////////////////////////////////////////////////////////////////////////
2020-04-02 11:51:10 +00:00
// Fragment LifeCycle - Loading
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
2022-04-17 06:53:02 +00:00
if (debounceSaver != null) {
disposables.add(debounceSaver.getDebouncedSaver());
debounceSaver.setNoChangesToSave();
2022-04-17 06:53:02 +00:00
}
2022-04-15 12:44:54 +00:00
isLoadingComplete.set(false);
2024-03-29 16:59:28 +00:00
getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistsSubscriber());
}
2020-04-02 11:51:10 +00:00
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Destruction
///////////////////////////////////////////////////////////////////////////
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
2022-04-15 12:44:54 +00:00
// Save on exit
saveImmediate();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (disposables != null) {
disposables.clear();
}
if (databaseSubscription != null) {
databaseSubscription.cancel();
}
databaseSubscription = null;
2022-04-14 08:59:52 +00:00
itemTouchHelper = null;
}
@Override
public void onDestroy() {
super.onDestroy();
2022-04-17 06:53:02 +00:00
if (debounceSaver != null) {
debounceSaver.getDebouncedSaveSignal().onComplete();
2022-04-15 12:44:54 +00:00
}
if (disposables != null) {
disposables.dispose();
}
2022-04-17 06:53:02 +00:00
debounceSaver = null;
disposables = null;
localPlaylistManager = null;
remotePlaylistManager = null;
itemsListState = null;
2022-04-15 12:44:54 +00:00
isLoadingComplete = null;
deletedItems = null;
}
2020-04-02 11:51:10 +00:00
///////////////////////////////////////////////////////////////////////////
// Subscriptions Loader
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
showLoading();
2022-04-15 12:44:54 +00:00
isLoadingComplete.set(false);
if (databaseSubscription != null) {
databaseSubscription.cancel();
}
databaseSubscription = s;
databaseSubscription.request(1);
}
@Override
public void onNext(final List<PlaylistLocalItem> subscriptions) {
2022-04-17 06:53:02 +00:00
if (debounceSaver == null || !debounceSaver.getIsModified()) {
2022-04-15 12:44:54 +00:00
handleResult(subscriptions);
isLoadingComplete.set(true);
}
if (databaseSubscription != null) {
databaseSubscription.request(1);
}
}
@Override
public void onError(final Throwable exception) {
showError(new ErrorInfo(exception,
UserAction.REQUESTED_BOOKMARK, "Loading playlists"));
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull final List<PlaylistLocalItem> result) {
super.handleResult(result);
itemListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
return;
}
itemListAdapter.addItems(result);
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
hideLoading();
}
2020-04-02 11:51:10 +00:00
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected void resetFragment() {
super.resetFragment();
if (disposables != null) {
disposables.clear();
}
}
2022-04-14 08:59:52 +00:00
/*//////////////////////////////////////////////////////////////////////////
// Playlist Metadata Manipulation
//////////////////////////////////////////////////////////////////////////*/
private void changeLocalPlaylistName(final long id, final String name) {
if (localPlaylistManager == null) {
return;
}
2020-02-01 15:27:53 +00:00
if (DEBUG) {
Log.d(TAG, "Updating playlist id=[" + id + "] "
+ "with new name=[" + name + "] items");
2020-02-01 15:27:53 +00:00
}
final Disposable disposable = localPlaylistManager.renamePlaylist(id, name)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing playlist name")));
disposables.add(disposable);
}
2022-04-15 12:44:54 +00:00
private void deleteItem(final PlaylistLocalItem item) {
if (itemListAdapter == null) {
return;
}
2022-04-15 12:44:54 +00:00
itemListAdapter.removeItem(item);
if (item instanceof PlaylistMetadataEntry) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM));
} else if (item instanceof PlaylistRemoteEntity) {
deletedItems.add(new Pair<>(item.getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM));
}
if (debounceSaver != null) {
debounceSaver.setHasChangesToSave();
saveImmediate();
2022-04-15 12:44:54 +00:00
}
}
2022-04-17 06:53:02 +00:00
@Override
public void saveImmediate() {
2022-04-15 12:44:54 +00:00
if (itemListAdapter == null) {
2022-04-14 08:59:52 +00:00
return;
}
2022-04-15 12:44:54 +00:00
2022-04-14 08:59:52 +00:00
// List must be loaded and modified in order to save
2022-04-17 06:53:02 +00:00
if (isLoadingComplete == null || debounceSaver == null
|| !isLoadingComplete.get() || !debounceSaver.getIsModified()) {
2022-04-14 08:59:52 +00:00
return;
}
2022-04-15 12:44:54 +00:00
2022-04-14 08:59:52 +00:00
final List<LocalItem> items = itemListAdapter.getItemsList();
2022-04-15 12:44:54 +00:00
final List<PlaylistMetadataEntry> localItemsUpdate = new ArrayList<>();
final List<Long> localItemsDeleteUid = new ArrayList<>();
final List<PlaylistRemoteEntity> remoteItemsUpdate = new ArrayList<>();
final List<Long> remoteItemsDeleteUid = new ArrayList<>();
// Calculate display index
2022-04-14 08:59:52 +00:00
for (int i = 0; i < items.size(); i++) {
final LocalItem item = items.get(i);
2022-04-15 12:44:54 +00:00
2024-03-29 16:59:28 +00:00
if (item instanceof PlaylistMetadataEntry
&& ((PlaylistMetadataEntry) item).getDisplayIndex() != i) {
((PlaylistMetadataEntry) item).setDisplayIndex(i);
localItemsUpdate.add((PlaylistMetadataEntry) item);
} else if (item instanceof PlaylistRemoteEntity
&& ((PlaylistRemoteEntity) item).getDisplayIndex() != i) {
((PlaylistRemoteEntity) item).setDisplayIndex(i);
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
2022-04-15 12:44:54 +00:00
}
}
// Find deleted items
for (final Pair<Long, LocalItem.LocalItemType> item : deletedItems) {
if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
localItemsDeleteUid.add(item.first);
} else if (item.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
remoteItemsDeleteUid.add(item.first);
2022-04-14 08:59:52 +00:00
}
}
2022-04-15 12:44:54 +00:00
deletedItems.clear();
2022-04-15 12:44:54 +00:00
// 1. Update local playlists
// 2. Update remote playlists
2022-06-23 12:36:21 +00:00
// 3. Set NoChangesToSave
2022-04-15 12:44:54 +00:00
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
2022-06-23 12:36:21 +00:00
.mergeWith(remotePlaylistManager.updatePlaylists(
remoteItemsUpdate, remoteItemsDeleteUid))
2022-04-15 12:44:54 +00:00
.observeOn(AndroidSchedulers.mainThread())
2022-06-23 12:36:21 +00:00
.subscribe(() -> {
if (debounceSaver != null) {
debounceSaver.setNoChangesToSave();
}
},
2022-04-15 12:44:54 +00:00
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
));
2022-04-14 08:59:52 +00:00
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
// if adding grid layout, also include ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
// with an `if (shouldUseGridLayout()) ...`
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
2022-04-14 08:59:52 +00:00
ItemTouchHelper.ACTION_STATE_IDLE) {
@Override
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.abs(standardSpeed));
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
2022-04-16 04:00:02 +00:00
// Allow swap LocalBookmarkPlaylistItemHolder and RemoteBookmarkPlaylistItemHolder.
if (itemListAdapter == null
|| source.getItemViewType() != target.getItemViewType()
&& !(
(
(source instanceof LocalBookmarkPlaylistItemHolder)
|| (source instanceof RemoteBookmarkPlaylistItemHolder)
)
&& (
(target instanceof LocalBookmarkPlaylistItemHolder)
|| (target instanceof RemoteBookmarkPlaylistItemHolder)
))
) {
return false;
2022-04-14 08:59:52 +00:00
}
final int sourceIndex = source.getBindingAdapterPosition();
final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped && debounceSaver != null) {
debounceSaver.setHasChangesToSave();
2022-04-14 08:59:52 +00:00
}
return isSwapped;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
2022-04-16 04:00:02 +00:00
// Do nothing.
2022-04-14 08:59:52 +00:00
}
};
}
///////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
2022-04-15 12:44:54 +00:00
showDeleteDialog(item.getName(), item);
2022-04-14 08:59:52 +00:00
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
final String rename = getString(R.string.rename);
final String delete = getString(R.string.delete);
final String unsetThumbnail = getString(R.string.unset_playlist_thumbnail);
2022-12-09 11:01:59 +00:00
final boolean isThumbnailPermanent = localPlaylistManager
2024-03-29 15:09:13 +00:00
.getIsPlaylistThumbnailPermanent(selectedItem.getUid());
final ArrayList<String> items = new ArrayList<>();
items.add(rename);
items.add(delete);
if (isThumbnailPermanent) {
items.add(unsetThumbnail);
}
final DialogInterface.OnClickListener action = (d, index) -> {
if (items.get(index).equals(rename)) {
showRenameDialog(selectedItem);
} else if (items.get(index).equals(delete)) {
2024-03-29 15:09:13 +00:00
showDeleteDialog(selectedItem.name, selectedItem);
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
final long thumbnailStreamId = localPlaylistManager
2024-03-29 15:09:13 +00:00
.getAutomaticPlaylistThumbnailStreamId(selectedItem.getUid());
localPlaylistManager
2024-03-29 15:09:13 +00:00
.changePlaylistThumbnail(selectedItem.getUid(), thumbnailStreamId, false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
};
2023-07-12 01:54:10 +00:00
new AlertDialog.Builder(activity)
.setItems(items.toArray(new String[0]), action)
.show();
2022-12-03 08:52:04 +00:00
}
private void showRenameDialog(final PlaylistMetadataEntry selectedItem) {
final DialogEditTextBinding dialogBinding =
DialogEditTextBinding.inflate(getLayoutInflater());
2022-04-14 08:59:52 +00:00
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setText(selectedItem.name);
2023-07-12 01:54:10 +00:00
new AlertDialog.Builder(activity)
.setView(dialogBinding.getRoot())
2022-04-14 08:59:52 +00:00
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
changeLocalPlaylistName(
selectedItem.getUid(),
2022-04-14 08:59:52 +00:00
dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null)
.show();
}
2022-04-15 12:44:54 +00:00
private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
2022-04-14 08:59:52 +00:00
if (activity == null || disposables == null) {
return;
}
new AlertDialog.Builder(activity)
.setTitle(name)
.setMessage(R.string.delete_playlist_prompt)
.setCancelable(true)
2022-04-15 12:44:54 +00:00
.setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
2022-04-14 08:59:52 +00:00
.setNegativeButton(R.string.cancel, null)
.show();
}
}