Debounced saver & bugfix & clean code

This commit is contained in:
GGAutomaton 2022-04-15 20:44:54 +08:00
parent bfb56b4144
commit 3c48825699
11 changed files with 288 additions and 98 deletions

View file

@ -45,6 +45,10 @@ public interface PlaylistLocalItem extends LocalItem {
addItem(result, localPlaylists.get(i), itemsWithSameIndex); addItem(result, localPlaylists.get(i), itemsWithSameIndex);
i++; i++;
} }
while (j < remotePlaylists.size()) {
addItem(result, remotePlaylists.get(j), itemsWithSameIndex);
j++;
}
addItemsWithSameIndex(result, itemsWithSameIndex); addItemsWithSameIndex(result, itemsWithSameIndex);
return result; return result;

View file

@ -17,7 +17,7 @@ public class PlaylistMetadataEntry implements PlaylistLocalItem {
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
public final String thumbnailUrl; public final String thumbnailUrl;
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
public final long displayIndex; public long displayIndex;
@ColumnInfo(name = PLAYLIST_STREAM_COUNT) @ColumnInfo(name = PLAYLIST_STREAM_COUNT)
public final long streamCount; public final long streamCount;

View file

@ -2,6 +2,7 @@ package org.schabi.newpipe.database.playlist.dao;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.model.PlaylistEntity; import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
@ -36,4 +37,17 @@ public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE) @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
Flowable<Long> getCount(); Flowable<Long> getCount();
@Transaction
default long upsertPlaylist(final PlaylistEntity playlist) {
final long playlistId = playlist.getUid();
if (playlistId == -1) {
// This situation is probably impossible.
return insert(playlist);
} else {
update(playlist);
return playlistId;
}
}
} }

View file

@ -82,7 +82,19 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " FROM " + PLAYLIST_TABLE + " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID + " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + JOIN_PLAYLIST_ID + " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata(); Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
+ PLAYLIST_DISPLAY_INDEX + ", "
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
+ " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_DISPLAY_INDEX)
Flowable<List<PlaylistMetadataEntry>> getDisplayIndexOrderedPlaylistMetadata();
} }

View file

@ -2,12 +2,15 @@ package org.schabi.newpipe.database.playlist.model;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index; import androidx.room.Index;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
@Entity(tableName = PLAYLIST_TABLE, @Entity(tableName = PLAYLIST_TABLE,
indices = {@Index(value = {PLAYLIST_NAME})}) indices = {@Index(value = {PLAYLIST_NAME})})
public class PlaylistEntity { public class PlaylistEntity {
@ -36,6 +39,14 @@ public class PlaylistEntity {
this.displayIndex = displayIndex; this.displayIndex = displayIndex;
} }
@Ignore
public PlaylistEntity(final PlaylistMetadataEntry item) {
this.uid = item.uid;
this.name = item.name;
this.thumbnailUrl = item.thumbnailUrl;
this.displayIndex = item.displayIndex;
}
public long getUid() { public long getUid() {
return uid; return uid;
} }

View file

@ -54,7 +54,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
private String uploader; private String uploader;
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
private long displayIndex; private long displayIndex = -1; // Make sure the new item is on the top
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
private Long streamCount; private Long streamCount;

View file

@ -142,7 +142,6 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
} }
public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) { public boolean swapItems(final int fromAdapterPosition, final int toAdapterPosition) {
// todo: reuse this code?
final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition); final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition);
final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition); final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition);

View file

@ -6,6 +6,7 @@ import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.text.InputType; import android.text.InputType;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -30,21 +31,32 @@ import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.subjects.PublishSubject;
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> { public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
// todo: add to playlists, item handle should be invisible
// Save the list 10s after the last change occurred
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12; private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State @State
protected Parcelable itemsListState; protected Parcelable itemsListState;
@ -55,6 +67,16 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
private RemotePlaylistManager remotePlaylistManager; private RemotePlaylistManager remotePlaylistManager;
private ItemTouchHelper itemTouchHelper; private ItemTouchHelper itemTouchHelper;
private PublishSubject<Long> debouncedSaveSignal;
/* Has the playlist been fully loaded from db */
private AtomicBoolean isLoadingComplete;
/* Has the playlist been modified (e.g. items reordered or deleted) */
private AtomicBoolean isModified;
// Map from (uid, local/remote item) to the saved display index in the database.
private Map<Pair<Long, LocalItem.LocalItemType>, Long> displayIndexInDatabase;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle - Creation // Fragment LifeCycle - Creation
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -69,6 +91,12 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
localPlaylistManager = new LocalPlaylistManager(database); localPlaylistManager = new LocalPlaylistManager(database);
remotePlaylistManager = new RemotePlaylistManager(database); remotePlaylistManager = new RemotePlaylistManager(database);
disposables = new CompositeDisposable(); disposables = new CompositeDisposable();
debouncedSaveSignal = PublishSubject.create();
isLoadingComplete = new AtomicBoolean();
isModified = new AtomicBoolean();
displayIndexInDatabase = new HashMap<>();
} }
@Nullable @Nullable
@ -154,6 +182,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void startLoading(final boolean forceLoad) { public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad); super.startLoading(forceLoad);
disposables.add(getDebouncedSaver());
isLoadingComplete.set(false);
isModified.set(false);
Flowable.combineLatest(localPlaylistManager.getPlaylists(), Flowable.combineLatest(localPlaylistManager.getPlaylists(),
remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge)
.onBackpressureLatest() .onBackpressureLatest()
@ -169,6 +201,9 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState(); itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
// Save on exit
saveImmediate();
} }
@Override @Override
@ -189,14 +224,22 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (debouncedSaveSignal != null) {
debouncedSaveSignal.onComplete();
}
if (disposables != null) { if (disposables != null) {
disposables.dispose(); disposables.dispose();
} }
debouncedSaveSignal = null;
disposables = null; disposables = null;
localPlaylistManager = null; localPlaylistManager = null;
remotePlaylistManager = null; remotePlaylistManager = null;
itemsListState = null; itemsListState = null;
isLoadingComplete = null;
isModified = null;
displayIndexInDatabase = null;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -208,6 +251,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onSubscribe(final Subscription s) { public void onSubscribe(final Subscription s) {
showLoading(); showLoading();
isLoadingComplete.set(false);
if (databaseSubscription != null) { if (databaseSubscription != null) {
databaseSubscription.cancel(); databaseSubscription.cancel();
} }
@ -217,14 +262,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@Override @Override
public void onNext(final List<PlaylistLocalItem> subscriptions) { public void onNext(final List<PlaylistLocalItem> subscriptions) {
if (isModified == null || !isModified.get()) {
// If displayIndex does not match actual index, update displayIndex. checkDisplayIndexModified(subscriptions);
// This may happen when a new list is created handleResult(subscriptions);
// or on the first run after database update isLoadingComplete.set(true);
// or displayIndex is not continuous for some reason. }
checkDisplayIndexUpdate(subscriptions);
handleResult(subscriptions);
if (databaseSubscription != null) { if (databaseSubscription != null) {
databaseSubscription.request(1); databaseSubscription.request(1);
} }
@ -296,86 +338,170 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
disposables.add(disposable); disposables.add(disposable);
} }
private void changeLocalPlaylistDisplayIndex(final long id, final long displayIndex) { private void deleteItem(final PlaylistLocalItem item) {
if (itemListAdapter == null) {
return;
}
itemListAdapter.removeItem(item);
if (localPlaylistManager == null) { saveChanges();
}
private void checkDisplayIndexModified(@NonNull final List<PlaylistLocalItem> result) {
if (isModified != null && isModified.get()) {
return; return;
} }
if (DEBUG) { displayIndexInDatabase.clear();
Log.d(TAG, "Updating local playlist id=[" + id + "] "
+ "with new display_index=[" + displayIndex + "]");
}
final Disposable disposable = // If the display index does not match actual index in the list, update the display index.
localPlaylistManager.changePlaylistDisplayIndex(id, displayIndex) // This may happen when a new list is created
.observeOn(AndroidSchedulers.mainThread()) // or on the first run after database update
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError( // or displayIndex is not continuous for some reason.
new ErrorInfo(throwable, boolean isDisplayIndexModified = false;
UserAction.REQUESTED_BOOKMARK,
"Changing local playlist display_index")));
disposables.add(disposable);
}
private void changeRemotePlaylistDisplayIndex(final long id, final long displayIndex) {
if (remotePlaylistManager == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "Updating remote playlist id=[" + id + "] "
+ "with new display_index=[" + displayIndex + "]");
}
final Disposable disposable =
remotePlaylistManager.changePlaylistDisplayIndex(id, displayIndex)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Changing remote playlist display_index")));
disposables.add(disposable);
}
private void checkDisplayIndexUpdate(@NonNull final List<PlaylistLocalItem> result) {
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
final PlaylistLocalItem item = result.get(i); final PlaylistLocalItem item = result.get(i);
if (item.getDisplayIndex() != i) { if (item.getDisplayIndex() != i) {
if (item instanceof PlaylistMetadataEntry) { isDisplayIndexModified = true;
changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i);
} else if (item instanceof PlaylistRemoteEntity) {
changeRemotePlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i);
}
} }
// Updating display index in the item does not affect the value inserts into
// database, which will be recalculated during the database update. Updating
// display index in the item here is to determine whether it is recently modified.
// Save the index read from the database.
if (item instanceof PlaylistMetadataEntry) {
displayIndexInDatabase.put(new Pair<>(((PlaylistMetadataEntry) item).uid,
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM), item.getDisplayIndex());
((PlaylistMetadataEntry) item).displayIndex = i;
} else if (item instanceof PlaylistRemoteEntity) {
displayIndexInDatabase.put(new Pair<>(((PlaylistRemoteEntity) item).getUid(),
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM),
item.getDisplayIndex());
((PlaylistRemoteEntity) item).setDisplayIndex(i);
}
}
if (isDisplayIndexModified) {
saveChanges();
} }
} }
private void saveImmediate() { private void saveChanges() {
if (localPlaylistManager == null || remotePlaylistManager == null if (isModified == null || debouncedSaveSignal == null) {
|| itemListAdapter == null) {
return; return;
} }
// todo: debounce
/* isModified.set(true);
debouncedSaveSignal.onNext(System.currentTimeMillis());
}
private Disposable getDebouncedSaver() {
if (debouncedSaveSignal == null) {
return Disposable.empty();
}
return debouncedSaveSignal
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> saveImmediate(), throwable ->
showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
"Debounced saver")));
}
private void saveImmediate() {
if (itemListAdapter == null) {
return;
}
// List must be loaded and modified in order to save // List must be loaded and modified in order to save
if (isLoadingComplete == null || isModified == null if (isLoadingComplete == null || isModified == null
|| !isLoadingComplete.get() || !isModified.get()) { || !isLoadingComplete.get() || !isModified.get()) {
Log.w(TAG, "Attempting to save playlist when local playlist " Log.w(TAG, "Attempting to save playlists in bookmark when bookmark "
+ "is not loaded or not modified: playlist id=[" + playlistId + "]"); + "is not loaded or playlists not modified");
return; return;
} }
*/
// todo: is it correct?
final List<LocalItem> items = itemListAdapter.getItemsList(); final List<LocalItem> items = itemListAdapter.getItemsList();
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
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < items.size(); i++) {
final LocalItem item = items.get(i); final LocalItem item = items.get(i);
if (item instanceof PlaylistMetadataEntry) { if (item instanceof PlaylistMetadataEntry) {
changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i); ((PlaylistMetadataEntry) item).displayIndex = i;
final Long uid = ((PlaylistMetadataEntry) item).uid;
final Pair<Long, LocalItem.LocalItemType> key = new Pair<>(uid,
LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM);
final Long databaseIndex = displayIndexInDatabase.remove(key);
if (databaseIndex != null) {
if (databaseIndex != i) {
localItemsUpdate.add((PlaylistMetadataEntry) item);
}
} else {
// This should be impossible.
continue;
}
} else if (item instanceof PlaylistRemoteEntity) { } else if (item instanceof PlaylistRemoteEntity) {
changeLocalPlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i); ((PlaylistRemoteEntity) item).setDisplayIndex(i);
final Long uid = ((PlaylistRemoteEntity) item).getUid();
final Pair<Long, LocalItem.LocalItemType> key = new Pair<>(uid,
LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM);
final Long databaseIndex = displayIndexInDatabase.remove(key);
if (databaseIndex != null) {
if (databaseIndex != i) {
remoteItemsUpdate.add((PlaylistRemoteEntity) item);
}
} else {
// This should be impossible.
continue;
}
} }
} }
// Find deleted items
for (final Pair<Long, LocalItem.LocalItemType> key : displayIndexInDatabase.keySet()) {
if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
localItemsDeleteUid.add(key.first);
} else if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
remoteItemsDeleteUid.add(key.first);
}
}
displayIndexInDatabase.clear();
// 1. Update local playlists
// 2. Update remote playlists
// 3. Set isModified false
disposables.add(localPlaylistManager.updatePlaylists(localItemsUpdate, localItemsDeleteUid)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> disposables.add(remotePlaylistManager.updatePlaylists(
remoteItemsUpdate, remoteItemsDeleteUid)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
if (isModified != null) {
isModified.set(false);
}
},
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Saving playlist"))
)),
throwable -> showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
));
} }
private ItemTouchHelper.SimpleCallback getItemTouchCallback() { private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
@ -404,17 +530,26 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
@NonNull final RecyclerView.ViewHolder target) { @NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType() if (source.getItemViewType() != target.getItemViewType()
|| itemListAdapter == null) { || itemListAdapter == null) {
return false; // Allow swap LocalPlaylistItemHolder and RemotePlaylistItemHolder.
if (!(
(
(source instanceof LocalPlaylistItemHolder)
|| (source instanceof RemotePlaylistItemHolder)
)
&& (
(target instanceof LocalPlaylistItemHolder)
|| (target instanceof RemotePlaylistItemHolder)
)
)) {
return false;
}
} }
// todo: is it correct
final int sourceIndex = source.getBindingAdapterPosition(); final int sourceIndex = source.getBindingAdapterPosition();
final int targetIndex = target.getBindingAdapterPosition(); final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex); final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) { if (isSwapped) {
// todo saveChanges();
//saveChanges();
saveImmediate();
} }
return isSwapped; return isSwapped;
} }
@ -441,7 +576,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid())); showDeleteDialog(item.getName(), item);
} }
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
@ -459,15 +594,14 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
dialogBinding.dialogEditText.getText().toString())) dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.delete, (dialog, which) -> { .setNeutralButton(R.string.delete, (dialog, which) -> {
showDeleteDialog(selectedItem.name, showDeleteDialog(selectedItem.name, selectedItem);
localPlaylistManager.deletePlaylist(selectedItem.uid));
dialog.dismiss(); dialog.dismiss();
}) })
.create() .create()
.show(); .show();
} }
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) { private void showDeleteDialog(final String name, final PlaylistLocalItem item) {
if (activity == null || disposables == null) { if (activity == null || disposables == null) {
return; return;
} }
@ -476,13 +610,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
.setTitle(name) .setTitle(name)
.setMessage(R.string.delete_playlist_prompt) .setMessage(R.string.delete_playlist_prompt)
.setCancelable(true) .setCancelable(true)
.setPositiveButton(R.string.delete, (dialog, i) -> .setPositiveButton(R.string.delete, (dialog, i) -> deleteItem(item))
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
showError(new ErrorInfo(throwable,
UserAction.REQUESTED_BOOKMARK,
"Deleting playlist")))))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();
} }

View file

@ -85,7 +85,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
final View newPlaylistButton = view.findViewById(R.id.newPlaylist); final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistDisposables.add(playlistManager.getPlaylists() playlistDisposables.add(playlistManager.getDisplayIndexOrderedPlaylists()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onPlaylistsReceived)); .subscribe(this::onPlaylistsReceived));
} }

View file

@ -41,6 +41,7 @@ public class LocalPlaylistManager {
} }
final StreamEntity defaultStream = streams.get(0); final StreamEntity defaultStream = streams.get(0);
// Save to the database directly.
// Make sure the new playlist is always on the top of bookmark. // Make sure the new playlist is always on the top of bookmark.
// The index will be reassigned to non-negative number in BookmarkFragment. // The index will be reassigned to non-negative number in BookmarkFragment.
final PlaylistEntity newPlaylist = final PlaylistEntity newPlaylist =
@ -85,10 +86,31 @@ public class LocalPlaylistManager {
})).subscribeOn(Schedulers.io()); })).subscribeOn(Schedulers.io());
} }
public Completable updatePlaylists(final List<PlaylistMetadataEntry> updateItems,
final List<Long> deletedItems) {
final List<PlaylistEntity> items = new ArrayList<>(updateItems.size());
for (final PlaylistMetadataEntry item : updateItems) {
items.add(new PlaylistEntity(item));
}
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
for (final Long uid: deletedItems) {
playlistTable.deletePlaylist(uid);
}
for (final PlaylistEntity item: items) {
playlistTable.upsertPlaylist(item);
}
})).subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() { public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
} }
public Flowable<List<PlaylistMetadataEntry>> getDisplayIndexOrderedPlaylists() {
return playlistStreamTable.getDisplayIndexOrderedPlaylistMetadata()
.subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) { public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
} }
@ -107,7 +129,7 @@ public class LocalPlaylistManager {
return modifyPlaylist(playlistId, null, thumbnailUrl, -1); return modifyPlaylist(playlistId, null, thumbnailUrl, -1);
} }
public Maybe<Integer> changePlaylistDisplayIndex(final long playlistId, public Maybe<Integer> updatePlaylistDisplayIndex(final long playlistId,
final long displayIndex) { final long displayIndex) {
return modifyPlaylist(playlistId, null, null, displayIndex); return modifyPlaylist(playlistId, null, null, displayIndex);
} }

View file

@ -7,16 +7,18 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public class RemotePlaylistManager { public class RemotePlaylistManager {
private final AppDatabase database;
private final PlaylistRemoteDAO playlistRemoteTable; private final PlaylistRemoteDAO playlistRemoteTable;
public RemotePlaylistManager(final AppDatabase db) { public RemotePlaylistManager(final AppDatabase db) {
database = db;
playlistRemoteTable = db.playlistRemoteDAO(); playlistRemoteTable = db.playlistRemoteDAO();
} }
@ -34,18 +36,16 @@ public class RemotePlaylistManager {
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
public Maybe<Integer> changePlaylistDisplayIndex(final long playlistId, public Completable updatePlaylists(final List<PlaylistRemoteEntity> updateItems,
final long displayIndex) { final List<Long> deletedItems) {
return playlistRemoteTable.getPlaylist(playlistId) return Completable.fromRunnable(() -> database.runInTransaction(() -> {
.firstElement() for (final Long uid: deletedItems) {
.filter(playlistRemoteEntities -> !playlistRemoteEntities.isEmpty()) playlistRemoteTable.deletePlaylist(uid);
.map(playlistRemoteEntities -> { }
final PlaylistRemoteEntity playlist = playlistRemoteEntities.get(0); for (final PlaylistRemoteEntity item: updateItems) {
if (displayIndex != -1) { playlistRemoteTable.upsert(item);
playlist.setDisplayIndex(displayIndex); }
} })).subscribeOn(Schedulers.io());
return playlistRemoteTable.update(playlist);
}).subscribeOn(Schedulers.io());
} }
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) { public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {