Skip to content

Commit bfb56b4

Browse files
committed
UI design and behavior
1 parent ba8370b commit bfb56b4

5 files changed

Lines changed: 282 additions & 53 deletions

File tree

app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import androidx.room.ColumnInfo;
44
import androidx.room.Entity;
5-
import androidx.room.Ignore;
65
import androidx.room.Index;
76
import androidx.room.PrimaryKey;
87

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

Lines changed: 157 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.schabi.newpipe.local.bookmark;
22

3+
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
4+
35
import android.os.Bundle;
46
import android.os.Parcelable;
57
import android.text.InputType;
@@ -12,6 +14,8 @@
1214
import androidx.annotation.Nullable;
1315
import androidx.appcompat.app.AlertDialog;
1416
import androidx.fragment.app.FragmentManager;
17+
import androidx.recyclerview.widget.ItemTouchHelper;
18+
import androidx.recyclerview.widget.RecyclerView;
1519

1620
import org.reactivestreams.Subscriber;
1721
import org.reactivestreams.Subscription;
@@ -41,13 +45,15 @@
4145
import io.reactivex.rxjava3.disposables.Disposable;
4246

4347
public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
48+
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
4449
@State
4550
protected Parcelable itemsListState;
4651

4752
private Subscription databaseSubscription;
4853
private CompositeDisposable disposables = new CompositeDisposable();
4954
private LocalPlaylistManager localPlaylistManager;
5055
private RemotePlaylistManager remotePlaylistManager;
56+
private ItemTouchHelper itemTouchHelper;
5157

5258
///////////////////////////////////////////////////////////////////////////
5359
// Fragment LifeCycle - Creation
@@ -98,6 +104,9 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
98104
protected void initListeners() {
99105
super.initListeners();
100106

107+
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
108+
itemTouchHelper.attachToRecyclerView(itemsList);
109+
101110
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
102111
@Override
103112
public void selected(final LocalItem selectedItem) {
@@ -126,6 +135,14 @@ public void held(final LocalItem selectedItem) {
126135
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
127136
}
128137
}
138+
139+
@Override
140+
public void drag(final LocalItem selectedItem,
141+
final RecyclerView.ViewHolder viewHolder) {
142+
if (itemTouchHelper != null) {
143+
itemTouchHelper.startDrag(viewHolder);
144+
}
145+
}
129146
});
130147
}
131148

@@ -166,6 +183,7 @@ public void onDestroyView() {
166183
}
167184

168185
databaseSubscription = null;
186+
itemTouchHelper = null;
169187
}
170188

171189
@Override
@@ -255,56 +273,9 @@ protected void resetFragment() {
255273
}
256274
}
257275

258-
///////////////////////////////////////////////////////////////////////////
259-
// Utils
260-
///////////////////////////////////////////////////////////////////////////
261-
262-
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
263-
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
264-
}
265-
266-
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
267-
final DialogEditTextBinding dialogBinding
268-
= DialogEditTextBinding.inflate(getLayoutInflater());
269-
dialogBinding.dialogEditText.setHint(R.string.name);
270-
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
271-
dialogBinding.dialogEditText.setText(selectedItem.name);
272-
273-
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
274-
builder.setView(dialogBinding.getRoot())
275-
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
276-
changeLocalPlaylistName(
277-
selectedItem.uid,
278-
dialogBinding.dialogEditText.getText().toString()))
279-
.setNegativeButton(R.string.cancel, null)
280-
.setNeutralButton(R.string.delete, (dialog, which) -> {
281-
showDeleteDialog(selectedItem.name,
282-
localPlaylistManager.deletePlaylist(selectedItem.uid));
283-
dialog.dismiss();
284-
})
285-
.create()
286-
.show();
287-
}
288-
289-
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
290-
if (activity == null || disposables == null) {
291-
return;
292-
}
293-
294-
new AlertDialog.Builder(activity)
295-
.setTitle(name)
296-
.setMessage(R.string.delete_playlist_prompt)
297-
.setCancelable(true)
298-
.setPositiveButton(R.string.delete, (dialog, i) ->
299-
disposables.add(deleteReactor
300-
.observeOn(AndroidSchedulers.mainThread())
301-
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
302-
showError(new ErrorInfo(throwable,
303-
UserAction.REQUESTED_BOOKMARK,
304-
"Deleting playlist")))))
305-
.setNegativeButton(R.string.cancel, null)
306-
.show();
307-
}
276+
/*//////////////////////////////////////////////////////////////////////////
277+
// Playlist Metadata Manipulation
278+
//////////////////////////////////////////////////////////////////////////*/
308279

309280
private void changeLocalPlaylistName(final long id, final String name) {
310281
if (localPlaylistManager == null) {
@@ -379,5 +350,141 @@ private void checkDisplayIndexUpdate(@NonNull final List<PlaylistLocalItem> resu
379350
}
380351
}
381352
}
353+
354+
private void saveImmediate() {
355+
if (localPlaylistManager == null || remotePlaylistManager == null
356+
|| itemListAdapter == null) {
357+
return;
358+
}
359+
// todo: debounce
360+
/*
361+
// List must be loaded and modified in order to save
362+
if (isLoadingComplete == null || isModified == null
363+
|| !isLoadingComplete.get() || !isModified.get()) {
364+
Log.w(TAG, "Attempting to save playlist when local playlist "
365+
+ "is not loaded or not modified: playlist id=[" + playlistId + "]");
366+
return;
367+
}
368+
*/
369+
// todo: is it correct?
370+
final List<LocalItem> items = itemListAdapter.getItemsList();
371+
for (int i = 0; i < items.size(); i++) {
372+
final LocalItem item = items.get(i);
373+
if (item instanceof PlaylistMetadataEntry) {
374+
changeLocalPlaylistDisplayIndex(((PlaylistMetadataEntry) item).uid, i);
375+
} else if (item instanceof PlaylistRemoteEntity) {
376+
changeLocalPlaylistDisplayIndex(((PlaylistRemoteEntity) item).getUid(), i);
377+
}
378+
}
379+
}
380+
381+
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
382+
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
383+
if (shouldUseGridLayout(requireContext())) {
384+
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
385+
}
386+
return new ItemTouchHelper.SimpleCallback(directions,
387+
ItemTouchHelper.ACTION_STATE_IDLE) {
388+
@Override
389+
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
390+
final int viewSize,
391+
final int viewSizeOutOfBounds,
392+
final int totalSize,
393+
final long msSinceStartScroll) {
394+
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView,
395+
viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll);
396+
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
397+
Math.abs(standardSpeed));
398+
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
399+
}
400+
401+
@Override
402+
public boolean onMove(@NonNull final RecyclerView recyclerView,
403+
@NonNull final RecyclerView.ViewHolder source,
404+
@NonNull final RecyclerView.ViewHolder target) {
405+
if (source.getItemViewType() != target.getItemViewType()
406+
|| itemListAdapter == null) {
407+
return false;
408+
}
409+
410+
// todo: is it correct
411+
final int sourceIndex = source.getBindingAdapterPosition();
412+
final int targetIndex = target.getBindingAdapterPosition();
413+
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
414+
if (isSwapped) {
415+
// todo
416+
//saveChanges();
417+
saveImmediate();
418+
}
419+
return isSwapped;
420+
}
421+
422+
@Override
423+
public boolean isLongPressDragEnabled() {
424+
return false;
425+
}
426+
427+
@Override
428+
public boolean isItemViewSwipeEnabled() {
429+
return false;
430+
}
431+
432+
@Override
433+
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
434+
final int swipeDir) {
435+
}
436+
};
437+
}
438+
439+
///////////////////////////////////////////////////////////////////////////
440+
// Utils
441+
///////////////////////////////////////////////////////////////////////////
442+
443+
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
444+
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
445+
}
446+
447+
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
448+
final DialogEditTextBinding dialogBinding
449+
= DialogEditTextBinding.inflate(getLayoutInflater());
450+
dialogBinding.dialogEditText.setHint(R.string.name);
451+
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
452+
dialogBinding.dialogEditText.setText(selectedItem.name);
453+
454+
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
455+
builder.setView(dialogBinding.getRoot())
456+
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
457+
changeLocalPlaylistName(
458+
selectedItem.uid,
459+
dialogBinding.dialogEditText.getText().toString()))
460+
.setNegativeButton(R.string.cancel, null)
461+
.setNeutralButton(R.string.delete, (dialog, which) -> {
462+
showDeleteDialog(selectedItem.name,
463+
localPlaylistManager.deletePlaylist(selectedItem.uid));
464+
dialog.dismiss();
465+
})
466+
.create()
467+
.show();
468+
}
469+
470+
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
471+
if (activity == null || disposables == null) {
472+
return;
473+
}
474+
475+
new AlertDialog.Builder(activity)
476+
.setTitle(name)
477+
.setMessage(R.string.delete_playlist_prompt)
478+
.setCancelable(true)
479+
.setPositiveButton(R.string.delete, (dialog, i) ->
480+
disposables.add(deleteReactor
481+
.observeOn(AndroidSchedulers.mainThread())
482+
.subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
483+
showError(new ErrorInfo(throwable,
484+
UserAction.REQUESTED_BOOKMARK,
485+
"Deleting playlist")))))
486+
.setNegativeButton(R.string.cancel, null)
487+
.show();
488+
}
382489
}
383490

app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.schabi.newpipe.local.holder;
22

3+
import android.view.MotionEvent;
34
import android.view.View;
45
import android.view.ViewGroup;
56

7+
import org.schabi.newpipe.R;
68
import org.schabi.newpipe.database.LocalItem;
79
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
810
import org.schabi.newpipe.local.LocalItemBuilder;
@@ -13,13 +15,16 @@
1315
import java.time.format.DateTimeFormatter;
1416

1517
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
18+
private final View itemHandleView;
19+
1620
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
17-
super(infoItemBuilder, parent);
21+
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
1822
}
1923

2024
LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
2125
final ViewGroup parent) {
2226
super(infoItemBuilder, layoutId, parent);
27+
itemHandleView = itemView.findViewById(R.id.itemHandle);
2328
}
2429

2530
@Override
@@ -38,6 +43,20 @@ public void updateFromItem(final LocalItem localItem,
3843

3944
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
4045

46+
itemHandleView.setOnTouchListener(getOnTouchListener(item));
47+
4148
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
4249
}
50+
51+
private View.OnTouchListener getOnTouchListener(final PlaylistMetadataEntry item) {
52+
return (view, motionEvent) -> {
53+
view.performClick();
54+
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
55+
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
56+
itemBuilder.getOnItemSelectedListener().drag(item,
57+
LocalPlaylistItemHolder.this);
58+
}
59+
return false;
60+
};
61+
}
4362
}

app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.schabi.newpipe.local.holder;
22

33
import android.text.TextUtils;
4+
import android.view.MotionEvent;
5+
import android.view.View;
46
import android.view.ViewGroup;
57

8+
import org.schabi.newpipe.R;
69
import org.schabi.newpipe.database.LocalItem;
710
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
811
import org.schabi.newpipe.extractor.NewPipe;
@@ -14,14 +17,17 @@
1417
import java.time.format.DateTimeFormatter;
1518

1619
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
20+
private final View itemHandleView;
21+
1722
public RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder,
1823
final ViewGroup parent) {
19-
super(infoItemBuilder, parent);
24+
this(infoItemBuilder, R.layout.list_playlist_bookmark_item, parent);
2025
}
2126

2227
RemotePlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId,
2328
final ViewGroup parent) {
2429
super(infoItemBuilder, layoutId, parent);
30+
itemHandleView = itemView.findViewById(R.id.itemHandle);
2531
}
2632

2733
@Override
@@ -46,6 +52,20 @@ public void updateFromItem(final LocalItem localItem,
4652

4753
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
4854

55+
itemHandleView.setOnTouchListener(getOnTouchListener(item));
56+
4957
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
5058
}
59+
60+
private View.OnTouchListener getOnTouchListener(final PlaylistRemoteEntity item) {
61+
return (view, motionEvent) -> {
62+
view.performClick();
63+
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null
64+
&& motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
65+
itemBuilder.getOnItemSelectedListener().drag(item,
66+
RemotePlaylistItemHolder.this);
67+
}
68+
return false;
69+
};
70+
}
5171
}

0 commit comments

Comments
 (0)