- Added move mechanic to background player through handles (on both thumbnail and icon).

- Added remove and open detail as long click popup dropdown on background player.
- Vastly simplified list manipulation in MediaSourceManager by delegating most control to DynamicConcatenatingMediaSource.
This commit is contained in:
John Zhen M 2017-10-08 22:43:07 -07:00 committed by John Zhen Mo
parent f5b5982e1c
commit 2e414cfd63
10 changed files with 254 additions and 108 deletions

View File

@ -155,18 +155,6 @@ public final class BackgroundPlayer extends Service {
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
} }
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
private void onClose() { private void onClose() {
if (basePlayerImpl != null) { if (basePlayerImpl != null) {
basePlayerImpl.stopActivityBinding(); basePlayerImpl.stopActivityBinding();

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Build; import android.os.Build;
@ -10,21 +11,28 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log; import android.util.Log;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder; import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -44,9 +52,14 @@ public class BackgroundPlayerActivity extends AppCompatActivity
// Views // Views
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private View rootView; private View rootView;
private RecyclerView itemsList; private RecyclerView itemsList;
private ItemTouchHelper itemTouchHelper;
private TextView metadataTitle; private TextView metadataTitle;
private TextView metadataArtist; private TextView metadataArtist;
@ -157,14 +170,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
itemsList.setLayoutManager(new LinearLayoutManager(this)); itemsList.setLayoutManager(new LinearLayoutManager(this));
itemsList.setAdapter(player.playQueueAdapter); itemsList.setAdapter(player.playQueueAdapter);
itemsList.setClickable(true); itemsList.setClickable(true);
itemsList.setLongClickable(true);
player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() { itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
@Override itemTouchHelper.attachToRecyclerView(itemsList);
public void selected(PlayQueueItem item) {
final int index = player.playQueue.indexOf(item); player.playQueueAdapter.setSelectedListener(getOnSelectedListener());
if (index != -1) player.playQueue.setIndex(index);
}
});
} }
private void buildMetadata() { private void buildMetadata() {
@ -192,6 +203,101 @@ public class BackgroundPlayerActivity extends AppCompatActivity
forwardButton.setOnClickListener(this); forwardButton.setOnClickListener(this);
} }
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, "Remove");
remove.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
final int index = player.playQueue.indexOf(item);
if (index != -1) player.playQueue.remove(index);
return true;
}
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, "Detail");
detail.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
onOpenDetail(BackgroundPlayerActivity.this, item.getUrl(), item.getTitle());
return true;
}
});
menu.show();
}
////////////////////////////////////////////////////////////////////////////
// Component Helpers
////////////////////////////////////////////////////////////////////////////
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
player.playQueue.move(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item, View view) {
final int index = player.playQueue.indexOf(item);
if (index == -1) return;
if (player.playQueue.getIndex() == index) {
player.onRestart();
} else {
player.playQueue.setIndex(index);
}
}
@Override
public void held(PlayQueueItem item, View view) {
final int index = player.playQueue.indexOf(item);
if (index != -1) buildItemPopupMenu(item, view);
}
@Override
public void onStartDrag(PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
};
}
private void onOpenDetail(Context context, String videoUrl, String videoTitle) {
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Component On-Click Listener // Component On-Click Listener
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View File

@ -559,27 +559,18 @@ public abstract class BasePlayer implements Player.EventListener,
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Timeline // ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void refreshTimeline() { @Override
playbackManager.load(); public void onTimelineChanged(Timeline timeline, Object manifest) {
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
final int currentSourceIndex = playbackManager.getCurrentSourceIndex(); final int currentSourceIndex = playQueue.getIndex();
// Sanity checks
if (currentSourceIndex < 0) return;
// Check if already playing correct window // Check if already playing correct window
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex; final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
// Check if on wrong window
if (!isCurrentWindowCorrect) {
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
}
// Check if recovering // Check if recovering
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) { if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
// todo: figure out exactly why this is the case // todo: figure out exactly why this is the case
@ -591,17 +582,10 @@ public abstract class BasePlayer implements Player.EventListener,
simpleExoPlayer.seekTo(roundedPos); simpleExoPlayer.seekTo(roundedPos);
isRecovery = false; isRecovery = false;
} }
}
/*////////////////////////////////////////////////////////////////////////// if (playbackManager != null) {
// ExoPlayer Listener playbackManager.load();
//////////////////////////////////////////////////////////////////////////*/ }
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
refreshTimeline();
} }
@Override @Override
@ -709,14 +693,12 @@ public abstract class BasePlayer implements Player.EventListener,
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Refresh the playback if there is a transition to the next video // Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex); if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
// If the user selects a new track, then the discontinuity occurs after the index is changed. // If the user selects a new track, then the discontinuity occurs after the index is changed.
// Therefore, the only source that causes a discrepancy would be autoplay, // Therefore, the only source that causes a discrepancy would be autoplay,
// which can only offset the current track by +1. // which can only offset the current track by +1.
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1); if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
} }
@Override @Override
@ -751,12 +733,16 @@ public abstract class BasePlayer implements Player.EventListener,
@Override @Override
public void sync(@Nullable final StreamInfo info) { public void sync(@Nullable final StreamInfo info) {
if (simpleExoPlayer == null) return; if (info == null || simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Syncing..."); if (DEBUG) Log.d(TAG, "Syncing...");
refreshTimeline(); // Check if on wrong window
final int currentSourceIndex = playQueue.getIndex();
if (info == null) return; if (!(simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex)) {
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
}
currentInfo = info; currentInfo = info;
initThumbnail(info.thumbnail_url); initThumbnail(info.thumbnail_url);
@ -830,6 +816,13 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue.offsetIndex(+1); playQueue.offsetIndex(+1);
} }
public void onRestart() {
if (playQueue == null) return;
if (DEBUG) Log.d(TAG, "onRestart() called");
simpleExoPlayer.seekToDefaultPosition();
}
public void seekBy(int milliSeconds) { public void seekBy(int milliSeconds) {
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))

View File

@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.DeferredMediaSource; import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
import org.schabi.newpipe.playlist.PlayQueue; import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
@ -29,16 +31,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// One-side rolling window size for default loading // One-side rolling window size for default loading
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback // Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
// todo: inject this parameter, allow user settings perhaps // todo: inject this parameter, allow user settings perhaps
private static final int WINDOW_SIZE = 2; private static final int WINDOW_SIZE = 1;
private PlaybackListener playbackListener; private PlaybackListener playbackListener;
private PlayQueue playQueue; private PlayQueue playQueue;
private DynamicConcatenatingMediaSource sources; private DynamicConcatenatingMediaSource sources;
// sourceToQueueIndex maps media source index to play queue index
// Invariant 1: this list is sorted in ascending order
// Invariant 2: this list contains no duplicates
private List<Integer> sourceToQueueIndex;
private Subscription playQueueReactor; private Subscription playQueueReactor;
private SerialDisposable syncReactor; private SerialDisposable syncReactor;
@ -53,7 +51,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
this.syncReactor = new SerialDisposable(); this.syncReactor = new SerialDisposable();
this.sources = new DynamicConcatenatingMediaSource(); this.sources = new DynamicConcatenatingMediaSource();
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
playQueue.getBroadcastReceiver() playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -72,22 +69,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Exposed Methods // Exposed Methods
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* Returns the media source index of the currently playing stream.
* */
public int getCurrentSourceIndex() {
return sourceToQueueIndex.indexOf(playQueue.getIndex());
}
/**
* Returns the play queue index of a given media source playlist index.
* */
public int getQueueIndexOf(final int sourceIndex) {
if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1;
return sourceToQueueIndex.get(sourceIndex);
}
/** /**
* Dispose the manager and releases all message buses and loaders. * Dispose the manager and releases all message buses and loaders.
* */ * */
@ -95,12 +76,10 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
if (playQueueReactor != null) playQueueReactor.cancel(); if (playQueueReactor != null) playQueueReactor.cancel();
if (syncReactor != null) syncReactor.dispose(); if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource(); if (sources != null) sources.releaseSource();
if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
playQueueReactor = null; playQueueReactor = null;
syncReactor = null; syncReactor = null;
sources = null; sources = null;
sourceToQueueIndex = null;
playbackListener = null; playbackListener = null;
playQueue = null; playQueue = null;
} }
@ -174,11 +153,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
populateSources(); populateSources();
break; break;
case SELECT: case SELECT:
if (isCurrentIndexLoaded()) { sync();
sync();
} else {
reset();
}
break; break;
case REMOVE: case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event; final RemoveEvent removeEvent = (RemoveEvent) event;
@ -188,8 +163,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
case REORDER: case REORDER:
reset(); reset();
break; break;
case ERROR:
case MOVE: case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case ERROR:
default: default:
break; break;
} }
@ -214,10 +192,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE; return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
} }
private boolean isCurrentIndexLoaded() {
return getCurrentSourceIndex() != -1;
}
private boolean tryBlock() { private boolean tryBlock() {
if (!isBlocked) { if (!isBlocked) {
playbackListener.block(); playbackListener.block();
@ -228,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
} }
private boolean tryUnblock() { private boolean tryUnblock() {
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) { if (isPlayQueueReady() && isBlocked) {
isBlocked = false; isBlocked = false;
playbackListener.unblock(sources); playbackListener.unblock(sources);
return true; return true;
@ -270,7 +244,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void resetSources() { private void resetSources() {
if (this.sources != null) this.sources.releaseSource(); if (this.sources != null) this.sources.releaseSource();
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource(); this.sources = new DynamicConcatenatingMediaSource();
} }
@ -294,12 +267,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void insert(final int queueIndex, final DeferredMediaSource source) { private void insert(final int queueIndex, final DeferredMediaSource source) {
if (queueIndex < 0) return; if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex); sources.addMediaSource(queueIndex, source);
if (pos < 0) {
final int sourceIndex = -pos-1;
sourceToQueueIndex.add(sourceIndex, queueIndex);
sources.addMediaSource(sourceIndex, source);
}
} }
/** /**
@ -310,15 +278,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void remove(final int queueIndex) { private void remove(final int queueIndex) {
if (queueIndex < 0) return; if (queueIndex < 0) return;
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex); sources.removeMediaSource(queueIndex);
if (sourceIndex == -1) return; }
sourceToQueueIndex.remove(sourceIndex); private void move(final int source, final int target) {
sources.removeMediaSource(sourceIndex); if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
// Will be slow on really large arrays, fast enough for typical use case sources.moveMediaSource(source, target);
for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) {
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
}
} }
} }

View File

@ -8,6 +8,7 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.playlist.events.AppendEvent; import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent; import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.InitEvent; import org.schabi.newpipe.playlist.events.InitEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent; import org.schabi.newpipe.playlist.events.ReorderEvent;
@ -272,6 +273,23 @@ public abstract class PlayQueue implements Serializable {
streams.remove(index); streams.remove(index);
} }
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= streams.size() || target >= streams.size()) return;
final int current = getIndex();
if (source == current) {
queueIndex.set(target);
} else if (source < current && target >= current) {
queueIndex.decrementAndGet();
} else if (source > current && target <= current) {
queueIndex.incrementAndGet();
}
streams.add(target, streams.remove(source));
broadcast(new MoveEvent(source, target));
}
/** /**
* Shuffles the current play queue. * Shuffles the current play queue.
* *

View File

@ -9,6 +9,7 @@ import android.view.ViewGroup;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.playlist.events.AppendEvent; import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent; import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage; import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent; import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.SelectEvent; import org.schabi.newpipe.playlist.events.SelectEvent;
@ -131,6 +132,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
notifyItemRangeRemoved(removeEvent.index(), 1); notifyItemRangeRemoved(removeEvent.index(), 1);
notifyItemChanged(removeEvent.index()); notifyItemChanged(removeEvent.index());
break; break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) message;
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case INIT:
case REORDER:
default: default:
notifyDataSetChanged(); notifyDataSetChanged();
break; break;

View File

@ -1,9 +1,8 @@
package org.schabi.newpipe.playlist; package org.schabi.newpipe.playlist;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoader;
@ -17,7 +16,9 @@ public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString(); private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener { public interface OnSelectedListener {
void selected(PlayQueueItem item); void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
} }
private OnSelectedListener onItemClickListener; private OnSelectedListener onItemClickListener;
@ -28,7 +29,7 @@ public class PlayQueueItemBuilder {
this.onItemClickListener = listener; this.onItemClickListener = listener;
} }
public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) { public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle()); if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader()); if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
@ -44,10 +45,37 @@ public class PlayQueueItemBuilder {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (onItemClickListener != null) { if (onItemClickListener != null) {
onItemClickListener.selected(item); onItemClickListener.selected(item, view);
} }
} }
}); });
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.held(item, view);
return true;
}
return false;
}
});
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
}
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.performClick();
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
onItemClickListener.onStartDrag(holder);
}
return false;
}
};
} }
private static final DisplayImageOptions IMAGE_OPTIONS = private static final DisplayImageOptions IMAGE_OPTIONS =

View File

@ -32,7 +32,7 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
public class PlayQueueItemHolder extends RecyclerView.ViewHolder { public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView; public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
public final ImageView itemThumbnailView; public final ImageView itemThumbnailView, itemHandle;
public final View itemRoot; public final View itemRoot;
@ -43,5 +43,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
itemDurationView = v.findViewById(R.id.itemDurationView); itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails); itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView); itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
itemHandle = v.findViewById(R.id.itemHandle);
} }
} }

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.playlist.events;
public class MoveEvent implements PlayQueueMessage {
final private int fromIndex;
final private int toIndex;
@Override
public PlayQueueEvent type() {
return PlayQueueEvent.MOVE;
}
public MoveEvent(final int oldIndex, final int newIndex) {
this.fromIndex = oldIndex;
this.toIndex = newIndex;
}
public int getFromIndex() {
return fromIndex;
}
public int getToIndex() {
return toIndex;
}
}

View File

@ -23,6 +23,16 @@
android:src="@drawable/dummy_thumbnail" android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/> tools:ignore="RtlHardcoded"/>
<ImageView
android:id="@+id/itemHandle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_alignParentRight="true"
android:scaleType="center"
android:src="?attr/filter"
tools:ignore="ContentDescription,RtlHardcoded"/>
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -50,13 +60,15 @@
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_toRightOf="@id/itemThumbnailView" android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView" android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge" android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size" android:textSize="@dimen/video_item_search_title_text_size"
android:textColor="?attr/selector_color" android:textColor="?attr/selector_color"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/> tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. "/>
<TextView <TextView
android:id="@+id/itemAdditionalDetails" android:id="@+id/itemAdditionalDetails"
@ -65,9 +77,12 @@
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/itemThumbnailView" android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView" android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:lines="1" android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size" android:textSize="@dimen/video_item_search_upload_date_text_size"
android:textColor="?attr/selector_color" android:textColor="?attr/selector_color"
tools:text="Uploader"/> tools:text="Uploader"/>
</RelativeLayout> </RelativeLayout>