Merge pull request #5274 from vkay94/stream-segments
Add stream segments to player controls
This commit is contained in:
commit
98ed80d305
12 changed files with 406 additions and 49 deletions
|
@ -27,7 +27,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
private boolean allowScroll = true;
|
private boolean allowScroll = true;
|
||||||
private final Rect globalRect = new Rect();
|
private final Rect globalRect = new Rect();
|
||||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||||
R.id.playQueuePanel, R.id.playbackSeekBar,
|
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2283,7 +2283,7 @@ public final class VideoDetailFragment
|
||||||
// Re-enable clicks
|
// Re-enable clicks
|
||||||
setOverlayElementsClickable(true);
|
setOverlayElementsClickable(true);
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.closeQueue();
|
player.closeItemsList();
|
||||||
}
|
}
|
||||||
setOverlayLook(appBarLayout, behavior, 0);
|
setOverlayLook(appBarLayout, behavior, 0);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.xwray.groupie.GroupAdapter
|
||||||
|
import com.xwray.groupie.GroupieViewHolder
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.
|
||||||
|
*/
|
||||||
|
class StreamSegmentAdapter(
|
||||||
|
private val listener: StreamSegmentListener
|
||||||
|
) : GroupAdapter<GroupieViewHolder>() {
|
||||||
|
|
||||||
|
var currentIndex: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the provided [StreamInfo] contains segments, `false` otherwise.
|
||||||
|
*/
|
||||||
|
fun setItems(info: StreamInfo): Boolean {
|
||||||
|
if (info.streamSegments.isNotEmpty()) {
|
||||||
|
clear()
|
||||||
|
addAll(info.streamSegments.map { StreamSegmentItem(it, listener) })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectSegment(segment: StreamSegmentItem) {
|
||||||
|
unSelectCurrentSegment()
|
||||||
|
currentIndex = max(0, getAdapterPosition(segment))
|
||||||
|
segment.isSelected = true
|
||||||
|
segment.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectSegmentAt(position: Int) {
|
||||||
|
try {
|
||||||
|
selectSegment(getGroupAtAdapterPosition(position) as StreamSegmentItem)
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
|
||||||
|
// Shouldn't happen since setItems is always called before select-methods but just in case
|
||||||
|
currentIndex = 0
|
||||||
|
Log.e("StreamSegmentAdapter", "selectSegmentAt: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unSelectCurrentSegment() {
|
||||||
|
try {
|
||||||
|
val segmentItem = getGroupAtAdapterPosition(currentIndex) as StreamSegmentItem
|
||||||
|
currentIndex = 0
|
||||||
|
segmentItem.isSelected = false
|
||||||
|
segmentItem.notifyChanged(StreamSegmentItem.PAYLOAD_SELECT)
|
||||||
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
|
// Just to make sure that getGroupAtAdapterPosition doesn't close the app
|
||||||
|
// Shouldn't happen since setItems is always called before select-methods but just in case
|
||||||
|
currentIndex = 0
|
||||||
|
Log.e("StreamSegmentAdapter", "unSelectCurrentSegment: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamSegmentListener {
|
||||||
|
fun onItemClick(item: StreamSegmentItem, seconds: Int)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.nostra13.universalimageloader.core.ImageLoader
|
||||||
|
import com.xwray.groupie.GroupieViewHolder
|
||||||
|
import com.xwray.groupie.Item
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
|
||||||
|
class StreamSegmentItem(
|
||||||
|
private val item: StreamSegment,
|
||||||
|
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||||
|
) : Item<GroupieViewHolder>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PAYLOAD_SELECT = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSelected = false
|
||||||
|
|
||||||
|
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
||||||
|
item.previewUrl?.let {
|
||||||
|
ImageLoader.getInstance().displayImage(
|
||||||
|
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
|
||||||
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
||||||
|
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
||||||
|
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||||
|
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||||
|
viewHolder.root.isSelected = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||||
|
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||||
|
viewHolder.root.isSelected = isSelected
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.bind(viewHolder, position, payloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLayout() = R.layout.item_stream_segment
|
||||||
|
}
|
|
@ -151,7 +151,7 @@ public final class MainPlayer extends Service {
|
||||||
// Android TV will handle back button in case controls will be visible
|
// Android TV will handle back button in case controls will be visible
|
||||||
// (one more additional unneeded click while the player is hidden)
|
// (one more additional unneeded click while the player is hidden)
|
||||||
player.hideControls(0, 0);
|
player.hideControls(0, 0);
|
||||||
player.closeQueue();
|
player.closeItemsList();
|
||||||
// Notification shows information about old stream but if a user selects
|
// Notification shows information about old stream but if a user selects
|
||||||
// a stream from backStack it's not actual anymore
|
// a stream from backStack it's not actual anymore
|
||||||
// So we should hide the notification at all.
|
// So we should hide the notification at all.
|
||||||
|
|
|
@ -87,9 +87,11 @@ import org.schabi.newpipe.databinding.PlayerBinding;
|
||||||
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
|
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
|
@ -245,6 +247,7 @@ public final class Player implements
|
||||||
|
|
||||||
private PlayQueue playQueue;
|
private PlayQueue playQueue;
|
||||||
private PlayQueueAdapter playQueueAdapter;
|
private PlayQueueAdapter playQueueAdapter;
|
||||||
|
private StreamSegmentAdapter segmentAdapter;
|
||||||
|
|
||||||
@Nullable private MediaSourceManager playQueueManager;
|
@Nullable private MediaSourceManager playQueueManager;
|
||||||
|
|
||||||
|
@ -301,6 +304,7 @@ public final class Player implements
|
||||||
|
|
||||||
// fullscreen player
|
// fullscreen player
|
||||||
private boolean isQueueVisible = false;
|
private boolean isQueueVisible = false;
|
||||||
|
private boolean areSegmentsVisible = false;
|
||||||
private ItemTouchHelper itemTouchHelper;
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -454,7 +458,7 @@ public final class Player implements
|
||||||
binding.channelTextView.setSelected(true);
|
binding.channelTextView.setSelected(true);
|
||||||
|
|
||||||
// Prevent hiding of bottom sheet via swipe inside queue
|
// Prevent hiding of bottom sheet via swipe inside queue
|
||||||
binding.playQueue.setNestedScrollingEnabled(false);
|
binding.itemsList.setNestedScrollingEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPlayer(final boolean playOnReady) {
|
private void initPlayer(final boolean playOnReady) {
|
||||||
|
@ -505,6 +509,7 @@ public final class Player implements
|
||||||
binding.getRoot().setOnTouchListener(listener);
|
binding.getRoot().setOnTouchListener(listener);
|
||||||
|
|
||||||
binding.queueButton.setOnClickListener(this);
|
binding.queueButton.setOnClickListener(this);
|
||||||
|
binding.segmentsButton.setOnClickListener(this);
|
||||||
binding.repeatButton.setOnClickListener(this);
|
binding.repeatButton.setOnClickListener(this);
|
||||||
binding.shuffleButton.setOnClickListener(this);
|
binding.shuffleButton.setOnClickListener(this);
|
||||||
|
|
||||||
|
@ -533,7 +538,7 @@ public final class Player implements
|
||||||
settingsContentObserver);
|
settingsContentObserver);
|
||||||
binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
|
binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.playQueuePanel, (view, windowInsets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
|
||||||
final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout();
|
final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout();
|
||||||
if (cutout != null) {
|
if (cutout != null) {
|
||||||
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
|
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
|
||||||
|
@ -665,11 +670,11 @@ public final class Player implements
|
||||||
playbackSkipSilence, playWhenReady, isMuted);
|
playbackSkipSilence, playWhenReady, isMuted);
|
||||||
},
|
},
|
||||||
() -> {
|
() -> {
|
||||||
// Completed but not found in history
|
// Completed but not found in history
|
||||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
||||||
playbackSkipSilence, playWhenReady, isMuted);
|
playbackSkipSilence, playWhenReady, isMuted);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
// Good to go...
|
// Good to go...
|
||||||
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
|
// In a case of equal PlayQueues we can re-init old one but only when it is disposed
|
||||||
|
@ -697,7 +702,7 @@ public final class Player implements
|
||||||
} else {
|
} else {
|
||||||
binding.getRoot().setVisibility(View.VISIBLE);
|
binding.getRoot().setVisibility(View.VISIBLE);
|
||||||
initVideoPlayer();
|
initVideoPlayer();
|
||||||
closeQueue();
|
closeItemsList();
|
||||||
// Android TV: without it focus will frame the whole player
|
// Android TV: without it focus will frame the whole player
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
|
|
||||||
|
@ -730,6 +735,7 @@ public final class Player implements
|
||||||
playQueueAdapter.dispose();
|
playQueueAdapter.dispose();
|
||||||
}
|
}
|
||||||
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
|
playQueueAdapter = new PlayQueueAdapter(context, playQueue);
|
||||||
|
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
|
||||||
|
|
||||||
simpleExoPlayer.setVolume(isMuted ? 0 : 1);
|
simpleExoPlayer.setVolume(isMuted ? 0 : 1);
|
||||||
notifyQueueUpdateToListeners();
|
notifyQueueUpdateToListeners();
|
||||||
|
@ -923,6 +929,7 @@ public final class Player implements
|
||||||
binding.resizeTextView.setVisibility(View.GONE);
|
binding.resizeTextView.setVisibility(View.GONE);
|
||||||
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
|
binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
|
||||||
binding.queueButton.setVisibility(View.GONE);
|
binding.queueButton.setVisibility(View.GONE);
|
||||||
|
binding.segmentsButton.setVisibility(View.GONE);
|
||||||
binding.moreOptionsButton.setVisibility(View.GONE);
|
binding.moreOptionsButton.setVisibility(View.GONE);
|
||||||
binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
|
binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
binding.primaryControls.getLayoutParams().width
|
binding.primaryControls.getLayoutParams().width
|
||||||
|
@ -939,7 +946,7 @@ public final class Player implements
|
||||||
binding.topControls.setClickable(false);
|
binding.topControls.setClickable(false);
|
||||||
binding.topControls.setFocusable(false);
|
binding.topControls.setFocusable(false);
|
||||||
binding.bottomControls.bringToFront();
|
binding.bottomControls.bringToFront();
|
||||||
closeQueue();
|
closeItemsList();
|
||||||
} else if (videoPlayerSelected()) {
|
} else if (videoPlayerSelected()) {
|
||||||
binding.fullScreenButton.setVisibility(View.GONE);
|
binding.fullScreenButton.setVisibility(View.GONE);
|
||||||
setupScreenRotationButton();
|
setupScreenRotationButton();
|
||||||
|
@ -1123,7 +1130,7 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
// Close it because when changing orientation from portrait
|
// Close it because when changing orientation from portrait
|
||||||
// (in fullscreen mode) the size of queue layout can be larger than the screen size
|
// (in fullscreen mode) the size of queue layout can be larger than the screen size
|
||||||
closeQueue();
|
closeItemsList();
|
||||||
break;
|
break;
|
||||||
case Intent.ACTION_SCREEN_ON:
|
case Intent.ACTION_SCREEN_ON:
|
||||||
// Interrupt playback only when screen turns on
|
// Interrupt playback only when screen turns on
|
||||||
|
@ -1484,6 +1491,10 @@ public final class Player implements
|
||||||
|
|
||||||
notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
|
notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
|
||||||
|
|
||||||
|
if (areSegmentsVisible) {
|
||||||
|
segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
|
||||||
|
}
|
||||||
|
|
||||||
final boolean showThumbnail = prefs.getBoolean(
|
final boolean showThumbnail = prefs.getBoolean(
|
||||||
context.getString(R.string.show_thumbnail_key), true);
|
context.getString(R.string.show_thumbnail_key), true);
|
||||||
// setMetadata only updates the metadata when any of the metadata keys are null
|
// setMetadata only updates the metadata when any of the metadata keys are null
|
||||||
|
@ -1696,10 +1707,10 @@ public final class Player implements
|
||||||
|
|
||||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||||
controlsVisibilityHandler.postDelayed(() -> {
|
controlsVisibilityHandler.postDelayed(() -> {
|
||||||
showHideShadow(false, duration);
|
showHideShadow(false, duration);
|
||||||
animateView(binding.playbackControlRoot, false, duration, 0,
|
animateView(binding.playbackControlRoot, false, duration, 0,
|
||||||
this::hideSystemUIIfNeeded);
|
this::hideSystemUIIfNeeded);
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showHideShadow(final boolean show, final long duration) {
|
private void showHideShadow(final boolean show, final long duration) {
|
||||||
|
@ -1715,6 +1726,11 @@ public final class Player implements
|
||||||
final boolean showPrev = playQueue.getIndex() != 0;
|
final boolean showPrev = playQueue.getIndex() != 0;
|
||||||
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
|
final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
|
||||||
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
|
final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
|
||||||
|
boolean showSegment = false;
|
||||||
|
if (currentMetadata != null) {
|
||||||
|
showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty()
|
||||||
|
&& !popupPlayerSelected();
|
||||||
|
}
|
||||||
|
|
||||||
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
|
binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
|
||||||
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
|
binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
|
||||||
|
@ -1722,6 +1738,8 @@ public final class Player implements
|
||||||
binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
|
binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
|
||||||
binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
|
binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
|
||||||
binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
|
binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
|
||||||
|
binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
|
||||||
|
binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSystemUIPartially() {
|
private void showSystemUIPartially() {
|
||||||
|
@ -2725,6 +2743,17 @@ public final class Player implements
|
||||||
|
|
||||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
|
|
||||||
|
if (areSegmentsVisible) {
|
||||||
|
if (segmentAdapter.setItems(info)) {
|
||||||
|
final int adapterPosition = getNearestStreamSegmentPosition(
|
||||||
|
simpleExoPlayer.getCurrentPosition());
|
||||||
|
segmentAdapter.selectSegmentAt(adapterPosition);
|
||||||
|
binding.itemsList.scrollToPosition(adapterPosition);
|
||||||
|
} else {
|
||||||
|
closeItemsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateCurrentMetadata() {
|
private void maybeUpdateCurrentMetadata() {
|
||||||
|
@ -2787,7 +2816,7 @@ public final class Player implements
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Play queue and streams
|
// Play queue, segments and streams
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
//region
|
//region
|
||||||
|
|
||||||
|
@ -2835,41 +2864,90 @@ public final class Player implements
|
||||||
|
|
||||||
hideSystemUIIfNeeded();
|
hideSystemUIIfNeeded();
|
||||||
buildQueue();
|
buildQueue();
|
||||||
//updatePlaybackButtons();//TODO verify this can be removed
|
|
||||||
|
binding.itemsListHeaderTitle.setVisibility(View.GONE);
|
||||||
|
binding.shuffleButton.setVisibility(View.VISIBLE);
|
||||||
|
binding.repeatButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
hideControls(0, 0);
|
hideControls(0, 0);
|
||||||
binding.playQueuePanel.requestFocus();
|
binding.itemsListPanel.requestFocus();
|
||||||
animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, true,
|
animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, true,
|
||||||
DEFAULT_CONTROLS_DURATION);
|
DEFAULT_CONTROLS_DURATION);
|
||||||
|
|
||||||
binding.playQueue.scrollToPosition(playQueue.getIndex());
|
binding.itemsList.scrollToPosition(playQueue.getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildQueue() {
|
private void buildQueue() {
|
||||||
binding.playQueue.setAdapter(playQueueAdapter);
|
binding.itemsList.setAdapter(playQueueAdapter);
|
||||||
binding.playQueue.setClickable(true);
|
binding.itemsList.setClickable(true);
|
||||||
binding.playQueue.setLongClickable(true);
|
binding.itemsList.setLongClickable(true);
|
||||||
|
|
||||||
binding.playQueue.clearOnScrollListeners();
|
binding.itemsList.clearOnScrollListeners();
|
||||||
binding.playQueue.addOnScrollListener(getQueueScrollListener());
|
binding.itemsList.addOnScrollListener(getQueueScrollListener());
|
||||||
|
|
||||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
itemTouchHelper.attachToRecyclerView(binding.playQueue);
|
itemTouchHelper.attachToRecyclerView(binding.itemsList);
|
||||||
|
|
||||||
playQueueAdapter.setSelectedListener(getOnSelectedListener());
|
playQueueAdapter.setSelectedListener(getOnSelectedListener());
|
||||||
|
|
||||||
binding.playQueueClose.setOnClickListener(view -> closeQueue());
|
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeQueue() {
|
private void onSegmentsClicked() {
|
||||||
if (isQueueVisible) {
|
areSegmentsVisible = true;
|
||||||
|
|
||||||
|
hideSystemUIIfNeeded();
|
||||||
|
buildSegments();
|
||||||
|
|
||||||
|
binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
|
||||||
|
binding.shuffleButton.setVisibility(View.GONE);
|
||||||
|
binding.repeatButton.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
hideControls(0, 0);
|
||||||
|
binding.itemsListPanel.requestFocus();
|
||||||
|
animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, true,
|
||||||
|
DEFAULT_CONTROLS_DURATION);
|
||||||
|
|
||||||
|
final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer
|
||||||
|
.getCurrentPosition());
|
||||||
|
segmentAdapter.selectSegmentAt(adapterPosition);
|
||||||
|
binding.itemsList.scrollToPosition(adapterPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildSegments() {
|
||||||
|
binding.itemsList.setAdapter(segmentAdapter);
|
||||||
|
binding.itemsList.setClickable(true);
|
||||||
|
binding.itemsList.setLongClickable(false);
|
||||||
|
|
||||||
|
binding.itemsList.clearOnScrollListeners();
|
||||||
|
if (itemTouchHelper != null) {
|
||||||
|
itemTouchHelper.attachToRecyclerView(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMetadata != null) {
|
||||||
|
segmentAdapter.setItems(currentMetadata.getMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.shuffleButton.setVisibility(View.GONE);
|
||||||
|
binding.repeatButton.setVisibility(View.GONE);
|
||||||
|
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeItemsList() {
|
||||||
|
if (isQueueVisible || areSegmentsVisible) {
|
||||||
isQueueVisible = false;
|
isQueueVisible = false;
|
||||||
animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, false,
|
areSegmentsVisible = false;
|
||||||
|
|
||||||
|
if (itemTouchHelper != null) {
|
||||||
|
itemTouchHelper.attachToRecyclerView(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateView(binding.itemsListPanel, SLIDE_AND_ALPHA, false,
|
||||||
DEFAULT_CONTROLS_DURATION, 0, () -> {
|
DEFAULT_CONTROLS_DURATION, 0, () -> {
|
||||||
// Even when queueLayout is GONE it receives touch events
|
// Even when queueLayout is GONE it receives touch events
|
||||||
// and ruins normal behavior of the app. This line fixes it
|
// and ruins normal behavior of the app. This line fixes it
|
||||||
binding.playQueuePanel.setTranslationY(
|
binding.itemsListPanel.setTranslationY(
|
||||||
-binding.playQueuePanel.getHeight() * 5);
|
-binding.itemsListPanel.getHeight() * 5);
|
||||||
});
|
});
|
||||||
binding.playPauseButton.requestFocus();
|
binding.playPauseButton.requestFocus();
|
||||||
}
|
}
|
||||||
|
@ -2882,12 +2960,33 @@ public final class Player implements
|
||||||
if (playQueue != null && !playQueue.isComplete()) {
|
if (playQueue != null && !playQueue.isComplete()) {
|
||||||
playQueue.fetch();
|
playQueue.fetch();
|
||||||
} else if (binding != null) {
|
} else if (binding != null) {
|
||||||
binding.playQueue.clearOnScrollListeners();
|
binding.itemsList.clearOnScrollListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
|
||||||
|
return (item, seconds) -> {
|
||||||
|
segmentAdapter.selectSegment(item);
|
||||||
|
seekTo(seconds * 1000);
|
||||||
|
triggerProgressUpdate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNearestStreamSegmentPosition(final long playbackPosition) {
|
||||||
|
int nearestPosition = 0;
|
||||||
|
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
|
||||||
|
|
||||||
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
|
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nearestPosition++;
|
||||||
|
}
|
||||||
|
return Math.max(0, nearestPosition - 1);
|
||||||
|
}
|
||||||
|
|
||||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
return new PlayQueueItemTouchCallback() {
|
return new PlayQueueItemTouchCallback() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -3313,6 +3412,9 @@ public final class Player implements
|
||||||
} else if (v.getId() == binding.queueButton.getId()) {
|
} else if (v.getId() == binding.queueButton.getId()) {
|
||||||
onQueueClicked();
|
onQueueClicked();
|
||||||
return;
|
return;
|
||||||
|
} else if (v.getId() == binding.segmentsButton.getId()) {
|
||||||
|
onSegmentsClicked();
|
||||||
|
return;
|
||||||
} else if (v.getId() == binding.repeatButton.getId()) {
|
} else if (v.getId() == binding.repeatButton.getId()) {
|
||||||
onRepeatClicked();
|
onRepeatClicked();
|
||||||
return;
|
return;
|
||||||
|
@ -3610,8 +3712,8 @@ public final class Player implements
|
||||||
binding.brightnessProgressBar.setMax(maxGestureLength);
|
binding.brightnessProgressBar.setMax(maxGestureLength);
|
||||||
|
|
||||||
setInitialGestureValues();
|
setInitialGestureValues();
|
||||||
binding.playQueuePanel.getLayoutParams().height
|
binding.itemsListPanel.getLayoutParams().height
|
||||||
= height - binding.playQueuePanel.getTop();
|
= height - binding.itemsListPanel.getTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3663,7 +3765,7 @@ public final class Player implements
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
|
binding.playbackControlRoot.setPadding(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
binding.playQueuePanel.setPadding(0, 0, 0, 0);
|
binding.itemsListPanel.setPadding(0, 0, 0, 0);
|
||||||
notifyQueueUpdateToListeners();
|
notifyQueueUpdateToListeners();
|
||||||
notifyMetadataUpdateToListeners();
|
notifyMetadataUpdateToListeners();
|
||||||
notifyPlaybackUpdateToListeners();
|
notifyPlaybackUpdateToListeners();
|
||||||
|
|
|
@ -24,7 +24,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
|
||||||
private boolean skippingInterception = false;
|
private boolean skippingInterception = false;
|
||||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||||
R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
|
R.id.detail_content_root_layout, R.id.relatedStreamsLayout,
|
||||||
R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls,
|
R.id.itemsListPanel, R.id.viewpager, R.id.bottomControls,
|
||||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M2,17h2v0.5L3,17.5v1h1v0.5L2,19v1h3v-4L2,16v1zM3,8h1L4,4L2,4v1h1v3zM2,11h1.8L2,13.1v0.9h3v-1L3.2,13L5,10.9L5,10L2,10v1zM7,5v2h14L21,5L7,5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2z" />
|
||||||
|
</vector>
|
|
@ -191,6 +191,24 @@
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/segmentsButton"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:paddingStart="3dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingEnd="3dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/ic_format_list_numbered_white_24"
|
||||||
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/moreOptionsButton"
|
android:id="@+id/moreOptionsButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -452,7 +470,7 @@
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playQueuePanel"
|
android:id="@+id/itemsListPanel"
|
||||||
android:layout_width="380dp"
|
android:layout_width="380dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
@ -461,14 +479,30 @@
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playQueueControl"
|
android:id="@+id/itemsListControl"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/itemsListHeaderTitle"
|
||||||
|
style="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignEnd="@id/itemsListClose"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="56dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/chapters"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/playQueueClose"
|
android:id="@+id/itemsListClose"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
@ -517,10 +551,10 @@
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/playQueue"
|
android:id="@+id/itemsList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/playQueueControl"
|
android:layout_below="@id/itemsListControl"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/play_queue_item" />
|
tools:listitem="@layout/play_queue_item" />
|
||||||
|
|
64
app/src/main/res/layout/item_stream_segment.xml
Normal file
64
app/src/main/res/layout/item_stream_segment.xml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selector"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/previewImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/play_queue_thumbnail_width"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/dummy_thumbnail"
|
||||||
|
app:layout_constraintDimensionRatio="16:9"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/textContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/previewImage"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/previewImage"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textSize="@dimen/video_item_search_title_text_size"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Lorem ipusum is widely used to create long sample text which is used here too" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewStartSeconds"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textViewTitle"
|
||||||
|
tools:text="04:26" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -192,6 +192,23 @@
|
||||||
app:srcCompat="@drawable/ic_list_white_24dp"
|
app:srcCompat="@drawable/ic_list_white_24dp"
|
||||||
tools:ignore="ContentDescription,RtlHardcoded" />
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/segmentsButton"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/ic_format_list_numbered_white_24"
|
||||||
|
tools:ignore="ContentDescription,RtlHardcoded" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/moreOptionsButton"
|
android:id="@+id/moreOptionsButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -450,7 +467,7 @@
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playQueuePanel"
|
android:id="@+id/itemsListPanel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/queue_background_color"
|
android:background="?attr/queue_background_color"
|
||||||
|
@ -458,14 +475,30 @@
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/playQueueControl"
|
android:id="@+id/itemsListControl"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/itemsListHeaderTitle"
|
||||||
|
style="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignEnd="@id/itemsListClose"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="56dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/chapters"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/playQueueClose"
|
android:id="@+id/itemsListClose"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
@ -514,10 +547,10 @@
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/playQueue"
|
android:id="@+id/itemsList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/playQueueControl"
|
android:layout_below="@id/itemsListControl"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/play_queue_item" />
|
tools:listitem="@layout/play_queue_item" />
|
||||||
|
|
|
@ -692,4 +692,5 @@
|
||||||
<string name="show_thumbnail_title">Show thumbnail</string>
|
<string name="show_thumbnail_title">Show thumbnail</string>
|
||||||
<string name="show_thumbnail_summary">Use thumbnail for both lock screen background and notifications</string>
|
<string name="show_thumbnail_summary">Use thumbnail for both lock screen background and notifications</string>
|
||||||
<string name="recent">Recent</string>
|
<string name="recent">Recent</string>
|
||||||
|
<string name="chapters">Chapters</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue