- 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:
parent
f5b5982e1c
commit
2e414cfd63
|
@ -155,18 +155,6 @@ public final class BackgroundPlayer extends Service {
|
|||
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() {
|
||||
if (basePlayerImpl != null) {
|
||||
basePlayerImpl.stopActivityBinding();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
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.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
|
@ -44,9 +52,14 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
// 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 RecyclerView itemsList;
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
|
||||
private TextView metadataTitle;
|
||||
private TextView metadataArtist;
|
||||
|
@ -157,14 +170,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
itemsList.setLayoutManager(new LinearLayoutManager(this));
|
||||
itemsList.setAdapter(player.playQueueAdapter);
|
||||
itemsList.setClickable(true);
|
||||
itemsList.setLongClickable(true);
|
||||
|
||||
player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() {
|
||||
@Override
|
||||
public void selected(PlayQueueItem item) {
|
||||
final int index = player.playQueue.indexOf(item);
|
||||
if (index != -1) player.playQueue.setIndex(index);
|
||||
}
|
||||
});
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||
|
||||
player.playQueueAdapter.setSelectedListener(getOnSelectedListener());
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
|
@ -192,6 +203,101 @@ public class BackgroundPlayerActivity extends AppCompatActivity
|
|||
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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -559,27 +559,18 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Timeline
|
||||
// ExoPlayer Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void refreshTimeline() {
|
||||
playbackManager.load();
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
||||
|
||||
final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
|
||||
|
||||
// Sanity checks
|
||||
if (currentSourceIndex < 0) return;
|
||||
final int currentSourceIndex = playQueue.getIndex();
|
||||
|
||||
// Check if already playing correct window
|
||||
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
|
||||
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
|
||||
// todo: figure out exactly why this is the case
|
||||
|
@ -591,17 +582,10 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
simpleExoPlayer.seekTo(roundedPos);
|
||||
isRecovery = false;
|
||||
}
|
||||
|
||||
if (playbackManager != null) {
|
||||
playbackManager.load();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
|
||||
|
||||
refreshTimeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -709,14 +693,12 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
public void onPositionDiscontinuity() {
|
||||
// Refresh the playback if there is a transition to the next video
|
||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex);
|
||||
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
|
||||
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
|
||||
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
|
||||
|
||||
// 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,
|
||||
// which can only offset the current track by +1.
|
||||
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
|
||||
if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -751,12 +733,16 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
|
||||
@Override
|
||||
public void sync(@Nullable final StreamInfo info) {
|
||||
if (simpleExoPlayer == null) return;
|
||||
if (info == null || simpleExoPlayer == null) return;
|
||||
if (DEBUG) Log.d(TAG, "Syncing...");
|
||||
|
||||
refreshTimeline();
|
||||
|
||||
if (info == null) return;
|
||||
// Check if on wrong window
|
||||
final int currentSourceIndex = playQueue.getIndex();
|
||||
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;
|
||||
initThumbnail(info.thumbnail_url);
|
||||
|
@ -830,6 +816,13 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
playQueue.offsetIndex(+1);
|
||||
}
|
||||
|
||||
public void onRestart() {
|
||||
if (playQueue == null) return;
|
||||
if (DEBUG) Log.d(TAG, "onRestart() called");
|
||||
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
}
|
||||
|
||||
public void seekBy(int milliSeconds) {
|
||||
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
|
||||
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
|
||||
|
|
|
@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
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.RemoveEvent;
|
||||
|
||||
|
@ -29,16 +31,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
// One-side rolling window size for default loading
|
||||
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
|
||||
// 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 PlayQueue playQueue;
|
||||
|
||||
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 SerialDisposable syncReactor;
|
||||
|
@ -53,7 +51,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
this.syncReactor = new SerialDisposable();
|
||||
|
||||
this.sources = new DynamicConcatenatingMediaSource();
|
||||
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
|
||||
|
||||
playQueue.getBroadcastReceiver()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -72,22 +69,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// 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.
|
||||
* */
|
||||
|
@ -95,12 +76,10 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
if (playQueueReactor != null) playQueueReactor.cancel();
|
||||
if (syncReactor != null) syncReactor.dispose();
|
||||
if (sources != null) sources.releaseSource();
|
||||
if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
|
||||
|
||||
playQueueReactor = null;
|
||||
syncReactor = null;
|
||||
sources = null;
|
||||
sourceToQueueIndex = null;
|
||||
playbackListener = null;
|
||||
playQueue = null;
|
||||
}
|
||||
|
@ -174,11 +153,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
populateSources();
|
||||
break;
|
||||
case SELECT:
|
||||
if (isCurrentIndexLoaded()) {
|
||||
sync();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
break;
|
||||
case REMOVE:
|
||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||
|
@ -188,8 +163,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
case REORDER:
|
||||
reset();
|
||||
break;
|
||||
case ERROR:
|
||||
case MOVE:
|
||||
final MoveEvent moveEvent = (MoveEvent) event;
|
||||
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
break;
|
||||
case ERROR:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -214,10 +192,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
|
||||
}
|
||||
|
||||
private boolean isCurrentIndexLoaded() {
|
||||
return getCurrentSourceIndex() != -1;
|
||||
}
|
||||
|
||||
private boolean tryBlock() {
|
||||
if (!isBlocked) {
|
||||
playbackListener.block();
|
||||
|
@ -228,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
}
|
||||
|
||||
private boolean tryUnblock() {
|
||||
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
|
||||
if (isPlayQueueReady() && isBlocked) {
|
||||
isBlocked = false;
|
||||
playbackListener.unblock(sources);
|
||||
return true;
|
||||
|
@ -270,7 +244,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
|
||||
private void resetSources() {
|
||||
if (this.sources != null) this.sources.releaseSource();
|
||||
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
|
||||
|
||||
this.sources = new DynamicConcatenatingMediaSource();
|
||||
}
|
||||
|
@ -294,12 +267,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
private void insert(final int queueIndex, final DeferredMediaSource source) {
|
||||
if (queueIndex < 0) return;
|
||||
|
||||
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
|
||||
if (pos < 0) {
|
||||
final int sourceIndex = -pos-1;
|
||||
sourceToQueueIndex.add(sourceIndex, queueIndex);
|
||||
sources.addMediaSource(sourceIndex, source);
|
||||
}
|
||||
sources.addMediaSource(queueIndex, source);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -310,15 +278,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
|
|||
private void remove(final int queueIndex) {
|
||||
if (queueIndex < 0) return;
|
||||
|
||||
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex);
|
||||
if (sourceIndex == -1) return;
|
||||
sources.removeMediaSource(queueIndex);
|
||||
}
|
||||
|
||||
sourceToQueueIndex.remove(sourceIndex);
|
||||
sources.removeMediaSource(sourceIndex);
|
||||
private void move(final int source, final int target) {
|
||||
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
|
||||
for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) {
|
||||
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
|
||||
}
|
||||
sources.moveMediaSource(source, target);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.reactivestreams.Subscription;
|
|||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||
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.RemoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.ReorderEvent;
|
||||
|
@ -272,6 +273,23 @@ public abstract class PlayQueue implements Serializable {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.view.ViewGroup;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||
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.RemoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.SelectEvent;
|
||||
|
@ -131,6 +132,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
notifyItemRangeRemoved(removeEvent.index(), 1);
|
||||
notifyItemChanged(removeEvent.index());
|
||||
break;
|
||||
case MOVE:
|
||||
final MoveEvent moveEvent = (MoveEvent) message;
|
||||
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
break;
|
||||
case INIT:
|
||||
case REORDER:
|
||||
default:
|
||||
notifyDataSetChanged();
|
||||
break;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package org.schabi.newpipe.playlist;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
@ -17,7 +16,9 @@ public class PlayQueueItemBuilder {
|
|||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||
|
||||
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;
|
||||
|
@ -28,7 +29,7 @@ public class PlayQueueItemBuilder {
|
|||
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.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
|
||||
|
||||
|
@ -44,10 +45,37 @@ public class PlayQueueItemBuilder {
|
|||
@Override
|
||||
public void onClick(View view) {
|
||||
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 =
|
||||
|
|
|
@ -32,7 +32,7 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
|||
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
|
||||
public final ImageView itemThumbnailView;
|
||||
public final ImageView itemThumbnailView, itemHandle;
|
||||
|
||||
public final View itemRoot;
|
||||
|
||||
|
@ -43,5 +43,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
|||
itemDurationView = v.findViewById(R.id.itemDurationView);
|
||||
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
|
||||
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
|
||||
itemHandle = v.findViewById(R.id.itemHandle);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,16 @@
|
|||
android:src="@drawable/dummy_thumbnail"
|
||||
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
|
||||
android:id="@+id/itemDurationView"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -50,13 +60,15 @@
|
|||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@id/itemThumbnailView"
|
||||
android:layout_toEndOf="@id/itemThumbnailView"
|
||||
android:layout_toLeftOf="@id/itemHandle"
|
||||
android:layout_toStartOf="@id/itemHandle"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="@dimen/video_item_search_title_text_size"
|
||||
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
|
||||
android:id="@+id/itemAdditionalDetails"
|
||||
|
@ -65,9 +77,12 @@
|
|||
android:layout_alignParentBottom="true"
|
||||
android:layout_toRightOf="@id/itemThumbnailView"
|
||||
android:layout_toEndOf="@id/itemThumbnailView"
|
||||
android:layout_toLeftOf="@id/itemHandle"
|
||||
android:layout_toStartOf="@id/itemHandle"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||
android:textColor="?attr/selector_color"
|
||||
tools:text="Uploader"/>
|
||||
|
||||
</RelativeLayout>
|
Loading…
Reference in New Issue