Skip to content

Commit 5ab4e3b

Browse files
committed
Add bg/popup/play shuffled actions
1 parent a4ce0a5 commit 5ab4e3b

6 files changed

Lines changed: 353 additions & 25 deletions

File tree

app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
77
import java.io.Serializable
88
import java.util.Collections
99
import java.util.concurrent.atomic.AtomicInteger
10+
import kotlinx.coroutines.reactive.awaitFirst
1011
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent
1112
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent
1213
import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent
@@ -434,6 +435,59 @@ abstract class PlayQueue internal constructor(
434435
broadcast(ReorderEvent(originalIndex, 0))
435436
}
436437

438+
/**
439+
* Repeatedly calls [fetch] until [isComplete] is `true` or some error happens, then shuffles
440+
* the whole queue without preserving [index]. [fetch] will be called at most 10 times to avoid
441+
* infinite loops, e.g. in case the playlist being fetched is infinite. This must be called only
442+
* to initialize the queue in an already shuffled state, and must not be called when the queue
443+
* is already being used e.g. by the player. The preconditions, which are also maintained as
444+
* postconditions, are thus that the queue is in a disposed / uninitialized state, and that
445+
* [index] is 0.
446+
*/
447+
suspend fun fetchAllAndShuffle() {
448+
if (eventBroadcast != null || this.index != 0) {
449+
throw UnsupportedOperationException(
450+
"Can call fetchAllAndShuffle() only on an uninitialized PlayQueue"
451+
)
452+
}
453+
454+
if (!isComplete) {
455+
init()
456+
var fetchCount = 0
457+
while (!isComplete) {
458+
if (fetchCount >= 10) {
459+
// Maybe the playlist is infinite, and anyway we don't want to overload the
460+
// servers by making too many requests. For reference, making 10 fetch requests
461+
// will mean fetching at most 1000 items on YouTube playlists, though this
462+
// changes among services.
463+
break
464+
}
465+
fetchCount += 1
466+
467+
fetch()
468+
469+
// Since `fetch()` does not return a Completable we can listen on, we have to wait
470+
// for events in `broadcastReceiver` produced by `fetch()`. This works reliably
471+
// because all `fetch()` implementations are supposed to notify all events (both
472+
// completion and errors) to `broadcastReceiver`.
473+
val event = broadcastReceiver!!
474+
.filter { !InitEvent::class.isInstance(it) }
475+
.awaitFirst()
476+
if (event !is AppendEvent || event.amount <= 0) {
477+
break // an AppendEvent with amount 0 indicates that an error occurred
478+
}
479+
}
480+
dispose()
481+
}
482+
483+
// Can't shuffle a list that's empty or only has one element
484+
if (size() <= 2) {
485+
return
486+
}
487+
backup = streams.toMutableList()
488+
streams.shuffle()
489+
}
490+
437491
/**
438492
* Unshuffles the current play queue if a backup play queue exists.
439493
*

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ import org.schabi.newpipe.player.playqueue.PlayQueue
5252
import org.schabi.newpipe.player.playqueue.PlayQueueItem
5353
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
5454
import org.schabi.newpipe.ui.components.menu.icons.BackgroundFromHere
55+
import org.schabi.newpipe.ui.components.menu.icons.BackgroundShuffled
5556
import org.schabi.newpipe.ui.components.menu.icons.PlayFromHere
57+
import org.schabi.newpipe.ui.components.menu.icons.PlayShuffled
5658
import org.schabi.newpipe.ui.components.menu.icons.PopupFromHere
59+
import org.schabi.newpipe.ui.components.menu.icons.PopupShuffled
5760
import org.schabi.newpipe.util.NavigationHelper
5861
import org.schabi.newpipe.util.external_communication.KoreUtils
5962
import org.schabi.newpipe.util.external_communication.ShareUtils
@@ -73,28 +76,31 @@ data class LongPressAction(
7376
@StringRes val label: Int,
7477
val icon: ImageVector
7578
) {
76-
Enqueue(0, R.string.enqueue, Icons.Default.AddToQueue),
77-
EnqueueNext(1, R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
78-
Background(2, R.string.controls_background_title, Icons.Default.Headset),
79-
Popup(3, R.string.controls_popup_title, Icons.Default.PictureInPicture),
80-
Play(4, R.string.play, Icons.Default.PlayArrow),
81-
BackgroundFromHere(5, R.string.background_from_here, Icons.Default.BackgroundFromHere),
82-
PopupFromHere(6, R.string.popup_from_here, Icons.Default.PopupFromHere),
83-
PlayFromHere(7, R.string.play_from_here, Icons.Default.PlayFromHere),
84-
PlayWithKodi(8, R.string.play_with_kodi_title, Icons.Default.Cast),
85-
Download(9, R.string.download, Icons.Default.Download),
86-
AddToPlaylist(10, R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
87-
Share(11, R.string.share, Icons.Default.Share),
88-
OpenInBrowser(12, R.string.open_in_browser, Icons.Default.OpenInBrowser),
89-
ShowChannelDetails(13, R.string.show_channel_details, Icons.Default.Person),
90-
MarkAsWatched(14, R.string.mark_as_watched, Icons.Default.Done),
91-
Delete(15, R.string.delete, Icons.Default.Delete),
92-
Rename(16, R.string.rename, Icons.Default.Edit),
93-
SetAsPlaylistThumbnail(17, R.string.set_as_playlist_thumbnail, Icons.Default.Image),
94-
UnsetPlaylistThumbnail(18, R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
95-
Unsubscribe(19, R.string.unsubscribe, Icons.Default.Delete),
96-
ShowDetails(20, R.string.play_queue_stream_detail, Icons.Default.Info),
97-
Remove(21, R.string.play_queue_remove, Icons.Default.Delete);
79+
ShowDetails(0, R.string.play_queue_stream_detail, Icons.Default.Info),
80+
Enqueue(1, R.string.enqueue, Icons.Default.AddToQueue),
81+
EnqueueNext(2, R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
82+
Background(3, R.string.controls_background_title, Icons.Default.Headset),
83+
Popup(4, R.string.controls_popup_title, Icons.Default.PictureInPicture),
84+
Play(5, R.string.play, Icons.Default.PlayArrow),
85+
BackgroundFromHere(6, R.string.background_from_here, Icons.Default.BackgroundFromHere),
86+
PopupFromHere(7, R.string.popup_from_here, Icons.Default.PopupFromHere),
87+
PlayFromHere(8, R.string.play_from_here, Icons.Default.PlayFromHere),
88+
BackgroundShuffled(9, R.string.background_shuffled, Icons.Default.BackgroundShuffled),
89+
PopupShuffled(10, R.string.popup_shuffled, Icons.Default.PopupShuffled),
90+
PlayShuffled(11, R.string.play_shuffled, Icons.Default.PlayShuffled),
91+
PlayWithKodi(12, R.string.play_with_kodi_title, Icons.Default.Cast),
92+
Download(13, R.string.download, Icons.Default.Download),
93+
AddToPlaylist(14, R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
94+
Share(15, R.string.share, Icons.Default.Share),
95+
OpenInBrowser(16, R.string.open_in_browser, Icons.Default.OpenInBrowser),
96+
ShowChannelDetails(17, R.string.show_channel_details, Icons.Default.Person),
97+
MarkAsWatched(18, R.string.mark_as_watched, Icons.Default.Done),
98+
Rename(19, R.string.rename, Icons.Default.Edit),
99+
SetAsPlaylistThumbnail(20, R.string.set_as_playlist_thumbnail, Icons.Default.Image),
100+
UnsetPlaylistThumbnail(21, R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
101+
Delete(22, R.string.delete, Icons.Default.Delete),
102+
Unsubscribe(23, R.string.unsubscribe, Icons.Default.Delete),
103+
Remove(24, R.string.play_queue_remove, Icons.Default.Delete);
98104

99105
fun buildAction(
100106
enabled: () -> Boolean = { true },
@@ -105,9 +111,9 @@ data class LongPressAction(
105111
// ShowChannelDetails is not enabled by default, since navigating to channel details can
106112
// also be done by clicking on the uploader name in the long press menu header
107113
val DefaultEnabledActions: List<Type> = listOf(
108-
ShowDetails, Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download,
109-
AddToPlaylist, Share, OpenInBrowser, MarkAsWatched, Delete,
110-
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe, Remove
114+
ShowDetails, Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere,
115+
BackgroundShuffled, Download, AddToPlaylist, Share, OpenInBrowser, MarkAsWatched,
116+
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Delete, Unsubscribe, Remove
111117
)
112118
}
113119
}
@@ -159,6 +165,25 @@ data class LongPressAction(
159165
)
160166
}
161167

168+
private fun buildPlayerShuffledActionList(queue: suspend (Context) -> PlayQueue): List<LongPressAction> {
169+
val shuffledQueue: suspend (Context) -> PlayQueue = { context ->
170+
val q = queue(context)
171+
q.fetchAllAndShuffle()
172+
q
173+
}
174+
return listOf(
175+
Type.BackgroundShuffled.buildAction { context ->
176+
NavigationHelper.playOnBackgroundPlayer(context, shuffledQueue(context), true)
177+
},
178+
Type.PopupShuffled.buildAction { context ->
179+
NavigationHelper.playOnPopupPlayer(context, shuffledQueue(context), true)
180+
},
181+
Type.PlayShuffled.buildAction { context ->
182+
NavigationHelper.playOnMainPlayer(context, shuffledQueue(context), false)
183+
}
184+
)
185+
}
186+
162187
private fun buildShareActionList(item: InfoItem): List<LongPressAction> {
163188
return listOf(
164189
Type.Share.buildAction { context ->
@@ -337,6 +362,7 @@ data class LongPressAction(
337362
unsetPlaylistThumbnail: Runnable?
338363
): List<LongPressAction> {
339364
return buildPlayerActionList { LocalPlaylistPlayQueue(item) } +
365+
buildPlayerShuffledActionList { LocalPlaylistPlayQueue(item) } +
340366
listOf(
341367
Type.Rename.buildAction { onRename.run() },
342368
Type.Delete.buildAction { onDelete.run() },
@@ -352,6 +378,7 @@ data class LongPressAction(
352378
onDelete: Runnable
353379
): List<LongPressAction> {
354380
return buildPlayerActionList { PlaylistPlayQueue(item.serviceId, item.url) } +
381+
buildPlayerShuffledActionList { PlaylistPlayQueue(item.serviceId, item.url) } +
355382
buildShareActionList(
356383
item.orderingName ?: "",
357384
item.orderingName ?: "",
@@ -368,6 +395,7 @@ data class LongPressAction(
368395
onUnsubscribe: Runnable?
369396
): List<LongPressAction> {
370397
return buildPlayerActionList { ChannelTabPlayQueue(item.serviceId, item.url) } +
398+
buildPlayerShuffledActionList { ChannelTabPlayQueue(item.serviceId, item.url) } +
371399
buildShareActionList(item) +
372400
listOfNotNull(
373401
Type.ShowChannelDetails.buildAction { context ->
@@ -385,6 +413,7 @@ data class LongPressAction(
385413
@JvmStatic
386414
fun fromPlaylistInfoItem(item: PlaylistInfoItem): List<LongPressAction> {
387415
return buildPlayerActionList { PlaylistPlayQueue(item.serviceId, item.url) } +
416+
buildPlayerShuffledActionList { PlaylistPlayQueue(item.serviceId, item.url) } +
388417
buildShareActionList(item)
389418
}
390419
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
@file:Suppress("UnusedReceiverParameter")
2+
3+
package org.schabi.newpipe.ui.components.menu.icons
4+
5+
import androidx.compose.foundation.layout.size
6+
import androidx.compose.material.icons.Icons
7+
import androidx.compose.material.icons.materialIcon
8+
import androidx.compose.material.icons.materialPath
9+
import androidx.compose.material3.Icon
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.vector.ImageVector
13+
import androidx.compose.ui.tooling.preview.Preview
14+
import androidx.compose.ui.unit.dp
15+
16+
/**
17+
* Obtained by combining [androidx.compose.material.icons.filled.Headset]
18+
* and the tiny arrow in [androidx.compose.material.icons.filled.ContentPasteGo].
19+
*/
20+
val Icons.Filled.BackgroundShuffled: ImageVector by lazy {
21+
materialIcon(name = "Filled.BackgroundShuffled") {
22+
materialPath {
23+
moveTo(12.0f, 1.0f)
24+
curveToRelative(-4.97f, 0.0f, -9.0f, 4.03f, -9.0f, 9.0f)
25+
verticalLineToRelative(7.0f)
26+
curveToRelative(0.0f, 1.66f, 1.34f, 3.0f, 3.0f, 3.0f)
27+
horizontalLineToRelative(3.0f)
28+
verticalLineToRelative(-8.0f)
29+
horizontalLineTo(5.0f)
30+
verticalLineToRelative(-2.0f)
31+
curveToRelative(0.0f, -3.87f, 3.13f, -7.0f, 7.0f, -7.0f)
32+
reflectiveCurveToRelative(7.0f, 3.13f, 7.0f, 7.0f)
33+
horizontalLineToRelative(2.0f)
34+
curveToRelative(0.0f, -4.97f, -4.03f, -9.0f, -9.0f, -9.0f)
35+
close()
36+
}
37+
materialPath {
38+
moveTo(13f, 12f)
39+
moveToRelative(3.145f, 2.135f)
40+
lineToRelative(-2.140f, -2.135f)
41+
lineToRelative(-1.005f, 1.005f)
42+
lineToRelative(2.135f, 2.135f)
43+
close()
44+
moveToRelative(1.505f, -2.135f)
45+
lineToRelative(1.170f, 1.170f)
46+
lineToRelative(-5.820f, 5.815f)
47+
lineToRelative(1.005f, 1.005f)
48+
lineToRelative(5.825f, -5.820f)
49+
lineToRelative(1.170f, 1.170f)
50+
lineToRelative(0.000f, -3.340f)
51+
close()
52+
moveToRelative(1.215f, 4.855f)
53+
lineToRelative(-1.005f, 1.005f)
54+
lineToRelative(0.965f, 0.965f)
55+
lineToRelative(-1.175f, 1.175f)
56+
lineToRelative(3.350f, 0.000f)
57+
lineToRelative(0.000f, -3.350f)
58+
lineToRelative(-1.170f, 1.170f)
59+
close()
60+
}
61+
/*
62+
val thickness = 0.15f
63+
materialPath {
64+
moveTo(13f, 12f)
65+
moveToRelative(3.295f - thickness, 2.585f - 3 * thickness)
66+
lineToRelative(-2.590f + 3 * thickness, -2.585f + 3 * thickness)
67+
lineToRelative(-0.705f - 2 * thickness, 0.705f + 2 * thickness)
68+
lineToRelative(2.585f - 3 * thickness, 2.585f - 3 * thickness)
69+
close()
70+
moveToRelative(1.955f - 3 * thickness, -2.585f + 3 * thickness)
71+
lineToRelative(1.020f + thickness, 1.020f + thickness)
72+
lineToRelative(-6.270f + 3 * thickness, 6.265f - 3 * thickness)
73+
lineToRelative(0.705f + 2 * thickness, 0.705f + 2 * thickness)
74+
lineToRelative(6.275f - 3 * thickness, -6.270f + 3 * thickness)
75+
lineToRelative(1.020f + thickness, 1.020f + thickness)
76+
lineToRelative(0f, -2.74f - 4 * thickness)
77+
close()
78+
moveToRelative(0.165f + 7 * thickness, 4.705f + thickness)
79+
lineToRelative(-0.705f - 2 * thickness, 0.705f + 2 * thickness)
80+
lineToRelative(1.565f - 4 * thickness, 1.565f - 4 * thickness)
81+
lineToRelative(-1.025f - thickness, 1.025f + thickness)
82+
lineToRelative(2.750f + 4 * thickness, 0f)
83+
lineToRelative(0f, -2.750f - 4 * thickness)
84+
lineToRelative(-1.020f - thickness, 1.020f + thickness)
85+
close()
86+
}
87+
*/
88+
}
89+
}
90+
91+
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
92+
@Composable
93+
private fun BackgroundShuffledPreview() {
94+
Icon(
95+
imageVector = Icons.Filled.BackgroundShuffled,
96+
contentDescription = null,
97+
modifier = Modifier.size(240.dp)
98+
)
99+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@file:Suppress("UnusedReceiverParameter")
2+
3+
package org.schabi.newpipe.ui.components.menu.icons
4+
5+
import androidx.compose.foundation.layout.size
6+
import androidx.compose.material.icons.Icons
7+
import androidx.compose.material.icons.materialIcon
8+
import androidx.compose.material.icons.materialPath
9+
import androidx.compose.material3.Icon
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.vector.ImageVector
13+
import androidx.compose.ui.tooling.preview.Preview
14+
import androidx.compose.ui.unit.dp
15+
16+
/**
17+
* Obtained by combining [androidx.compose.material.icons.filled.PlayArrow]
18+
* and the tiny arrow in [androidx.compose.material.icons.filled.ContentPasteGo].
19+
*/
20+
val Icons.Filled.PlayShuffled: ImageVector by lazy {
21+
materialIcon(name = "Filled.PlayShuffled") {
22+
materialPath {
23+
moveTo(2.5f, 2.5f)
24+
verticalLineToRelative(14.0f)
25+
lineToRelative(11.0f, -7.0f)
26+
close()
27+
}
28+
materialPath {
29+
moveTo(14f, 12f)
30+
moveToRelative(3.145f, 2.135f)
31+
lineToRelative(-2.140f, -2.135f)
32+
lineToRelative(-1.005f, 1.005f)
33+
lineToRelative(2.135f, 2.135f)
34+
close()
35+
moveToRelative(1.505f, -2.135f)
36+
lineToRelative(1.170f, 1.170f)
37+
lineToRelative(-5.820f, 5.815f)
38+
lineToRelative(1.005f, 1.005f)
39+
lineToRelative(5.825f, -5.820f)
40+
lineToRelative(1.170f, 1.170f)
41+
lineToRelative(0.000f, -3.340f)
42+
close()
43+
moveToRelative(1.215f, 4.855f)
44+
lineToRelative(-1.005f, 1.005f)
45+
lineToRelative(0.965f, 0.965f)
46+
lineToRelative(-1.175f, 1.175f)
47+
lineToRelative(3.350f, 0.000f)
48+
lineToRelative(0.000f, -3.350f)
49+
lineToRelative(-1.170f, 1.170f)
50+
close()
51+
}
52+
}
53+
}
54+
55+
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
56+
@Composable
57+
private fun PlayFromHerePreview() {
58+
Icon(
59+
imageVector = Icons.Filled.PlayShuffled,
60+
contentDescription = null,
61+
modifier = Modifier.size(240.dp)
62+
)
63+
}

0 commit comments

Comments
 (0)