Debounced saver & bugfix & clean code
This commit is contained in:
parent
bfb56b4144
commit
3c48825699
11 changed files with 288 additions and 98 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
// or on the first run after database update
|
|
||||||
// or displayIndex is not continuous for some reason.
|
|
||||||
checkDisplayIndexUpdate(subscriptions);
|
|
||||||
|
|
||||||
handleResult(subscriptions);
|
handleResult(subscriptions);
|
||||||
|
isLoadingComplete.set(true);
|
||||||
|
}
|
||||||
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 saveChanges() {
|
||||||
|
if (isModified == null || debouncedSaveSignal == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (isLoadingComplete == null || isModified == null
|
||||||
|
|| !isLoadingComplete.get() || !isModified.get()) {
|
||||||
|
Log.w(TAG, "Attempting to save playlists in bookmark when bookmark "
|
||||||
|
+ "is not loaded or playlists not modified");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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++) {
|
||||||
|
final LocalItem item = items.get(i);
|
||||||
|
|
||||||
|
if (item instanceof PlaylistMetadataEntry) {
|
||||||
|
((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) {
|
||||||
|
((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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveImmediate() {
|
// Find deleted items
|
||||||
if (localPlaylistManager == null || remotePlaylistManager == null
|
for (final Pair<Long, LocalItem.LocalItemType> key : displayIndexInDatabase.keySet()) {
|
||||||
|| itemListAdapter == null) {
|
if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_LOCAL_ITEM)) {
|
||||||
return;
|
localItemsDeleteUid.add(key.first);
|
||||||
}
|
} else if (key.second.equals(LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM)) {
|
||||||
// todo: debounce
|
remoteItemsDeleteUid.add(key.first);
|
||||||
/*
|
|
||||||
// List must be loaded and modified in order to save
|
|
||||||
if (isLoadingComplete == null || isModified == null
|
|
||||||
|| !isLoadingComplete.get() || !isModified.get()) {
|
|
||||||
Log.w(TAG, "Attempting to save playlist when local playlist "
|
|
||||||
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// todo: is it correct?
|
|
||||||
final List<LocalItem> items = itemListAdapter.getItemsList();
|
|
||||||
for (int i = 0; i < items.size(); i++) {
|
|
||||||
final LocalItem item = items.get(i);
|
|
||||||
if (item instanceof PlaylistMetadataEntry) {
|
|
||||||
changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i);
|
|
||||||
} else if (item instanceof PlaylistRemoteEntity) {
|
|
||||||
changeLocalPlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Allow swap LocalPlaylistItemHolder and RemotePlaylistItemHolder.
|
||||||
|
if (!(
|
||||||
|
(
|
||||||
|
(source instanceof LocalPlaylistItemHolder)
|
||||||
|
|| (source instanceof RemotePlaylistItemHolder)
|
||||||
|
)
|
||||||
|
&& (
|
||||||
|
(target instanceof LocalPlaylistItemHolder)
|
||||||
|
|| (target instanceof RemotePlaylistItemHolder)
|
||||||
|
)
|
||||||
|
)) {
|
||||||
return false;
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
if (displayIndex != -1) {
|
|
||||||
playlist.setDisplayIndex(displayIndex);
|
|
||||||
}
|
}
|
||||||
return playlistRemoteTable.update(playlist);
|
for (final PlaylistRemoteEntity item: updateItems) {
|
||||||
}).subscribeOn(Schedulers.io());
|
playlistRemoteTable.upsert(item);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||||
|
|
Loading…
Reference in a new issue