diff --git a/app/src/main/java/org/schabi/newpipe/ktx/List.kt b/app/src/main/java/org/schabi/newpipe/ktx/List.kt
deleted file mode 100644
index 0dd41bb6e..000000000
--- a/app/src/main/java/org/schabi/newpipe/ktx/List.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.schabi.newpipe.ktx
-
-fun MutableList.popFirst(filter: (A) -> Boolean): A? {
- val i = indexOfFirst(filter)
- if (i < 0) {
- return null
- }
- return removeAt(i)
-}
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt
index 506f6da0c..19cd612f8 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt
@@ -1,6 +1,7 @@
package org.schabi.newpipe.ui.components.menu
import android.content.Context
+import android.net.Uri
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
@@ -21,6 +22,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.QueuePlayNext
import androidx.compose.material.icons.filled.Share
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.core.net.toUri
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
@@ -49,6 +51,7 @@ import org.schabi.newpipe.ui.components.menu.icons.PlayFromHere
import org.schabi.newpipe.ui.components.menu.icons.PopupFromHere
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.SparseItemUtil
+import org.schabi.newpipe.util.external_communication.KoreUtils
import org.schabi.newpipe.util.external_communication.ShareUtils
data class LongPressAction(
@@ -228,13 +231,10 @@ data class LongPressAction(
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
},
+ Type.PlayWithKodi.buildAction { context ->
+ KoreUtils.playWithKore(context, item.url.toUri())
+ },
)
- /* TODO handle kodi
- + if (isKodiEnabled) listOf(
- Type.PlayWithKodi.buildAction { context ->
- KoreUtils.playWithKore(context, Uri.parse(item.url))
- },
- ) else listOf()*/
}
@JvmStatic
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt
index 9854443a1..5857f1e77 100644
--- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenu.kt
@@ -43,6 +43,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -70,10 +72,10 @@ import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamType
-import org.schabi.newpipe.ktx.popFirst
import org.schabi.newpipe.ui.components.common.ScaffoldWithToolbar
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.EnqueueNext
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.ShowChannelDetails
@@ -123,6 +125,9 @@ fun LongPressMenu(
longPressActions: List,
onDismissRequest: () -> Unit,
) {
+ val viewModel: LongPressMenuViewModel = viewModel()
+ val isHeaderEnabled by viewModel.isHeaderEnabled.collectAsState()
+ val actionArrangement by viewModel.actionArrangement.collectAsState()
var showEditor by rememberSaveable { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
@@ -140,14 +145,39 @@ fun LongPressMenu(
}
}
} else {
+ val enabledLongPressActions by remember {
+ derivedStateOf {
+ actionArrangement.mapNotNull { type ->
+ longPressActions.firstOrNull { it.type == type }
+ }
+ }
+ }
+
+ // show a clickable uploader in the header if an uploader action is available and the
+ // "show channel details" action is not enabled as a standalone action
+ val ctx = LocalContext.current
+ val onUploaderClick by remember {
+ derivedStateOf {
+ longPressActions.firstOrNull { it.type == ShowChannelDetails }
+ ?.takeIf { !actionArrangement.contains(ShowChannelDetails) }
+ ?.let { showChannelDetailsAction ->
+ {
+ showChannelDetailsAction.action(ctx)
+ onDismissRequest()
+ }
+ }
+ }
+ }
+
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismissRequest,
dragHandle = { LongPressMenuDragHandle(onEditActions = { showEditor = true }) },
) {
LongPressMenuContent(
- longPressable = longPressable,
- longPressActions = longPressActions,
+ header = longPressable.takeIf { isHeaderEnabled },
+ onUploaderClick = onUploaderClick,
+ actions = enabledLongPressActions,
onDismissRequest = onDismissRequest,
)
}
@@ -156,8 +186,9 @@ fun LongPressMenu(
@Composable
private fun LongPressMenuContent(
- longPressable: LongPressable,
- longPressActions: List,
+ header: LongPressable?,
+ onUploaderClick: (() -> Unit)?,
+ actions: List,
onDismissRequest: () -> Unit,
) {
BoxWithConstraints(
@@ -172,20 +203,10 @@ private fun LongPressMenuContent(
// width for the landscape/reduced header, measured in button widths
val headerWidthInButtonsReducedSpan = 4
val buttonsPerRow = (this.maxWidth / MinButtonWidth).toInt()
-
- // the channel icon goes in the menu header, so do not show a button for it
- val actions = longPressActions.toMutableList()
val ctx = LocalContext.current
- val onUploaderClick = actions.popFirst { it.type == ShowChannelDetails }
- ?.let { showChannelDetailsAction ->
- {
- showChannelDetailsAction.action(ctx)
- onDismissRequest()
- }
- }
Column {
- var actionIndex = -1 // -1 indicates the header
+ var actionIndex = if (header != null) -1 else 0 // -1 indicates the header
while (actionIndex < actions.size) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -224,7 +245,7 @@ private fun LongPressMenuContent(
// this branch is taken if the full-span header is going to fit on one
// line (i.e. on phones in portrait)
LongPressMenuHeader(
- item = longPressable,
+ item = header!!, // surely not null since actionIndex < 0
onUploaderClick = onUploaderClick,
modifier = Modifier
.padding(start = 6.dp, end = 6.dp, bottom = 6.dp)
@@ -241,7 +262,7 @@ private fun LongPressMenuContent(
// branch is taken, at least two buttons will be on the right side of
// the header (just one button would look off).
LongPressMenuHeader(
- item = longPressable,
+ item = header!!, // surely not null since actionIndex < 0
onUploaderClick = onUploaderClick,
modifier = Modifier
.padding(start = 8.dp, top = 11.dp, bottom = 11.dp)
@@ -667,8 +688,9 @@ private fun LongPressMenuPreview(
// longPressable is null when running the preview in an emulator for some reason...
@Suppress("USELESS_ELVIS")
LongPressMenuContent(
- longPressable = longPressable ?: LongPressablePreviews().values.first(),
- longPressActions = LongPressAction.Type.entries
+ header = longPressable ?: LongPressablePreviews().values.first(),
+ onUploaderClick = {},
+ actions = LongPressAction.Type.entries
// disable Enqueue actions just to show it off
.map { t -> t.buildAction({ t != EnqueueNext }) { } },
onDismissRequest = {},
diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuViewModel.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuViewModel.kt
new file mode 100644
index 000000000..eb9b57db1
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuViewModel.kt
@@ -0,0 +1,50 @@
+package org.schabi.newpipe.ui.components.menu
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.lifecycle.ViewModel
+import androidx.preference.PreferenceManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import org.schabi.newpipe.App
+import org.schabi.newpipe.R
+
+/**
+ * Since view models can't have access to the UI's Context, we use [App.instance] instead to fetch
+ * shared preferences. This is not the best but won't be needed anyway once we will have a Hilt
+ * injected repository that provides access to a modern alternative to shared preferences. The whole
+ * thing with the shared preference listener will not be necessary with the modern alternative.
+ */
+class LongPressMenuViewModel : ViewModel() {
+ private val _isHeaderEnabled = MutableStateFlow(
+ loadIsHeaderEnabledFromSettings(App.instance)
+ )
+ val isHeaderEnabled: StateFlow = _isHeaderEnabled.asStateFlow()
+
+ private val _actionArrangement = MutableStateFlow(
+ loadLongPressActionArrangementFromSettings(App.instance)
+ )
+ val actionArrangement: StateFlow> = _actionArrangement.asStateFlow()
+
+ private val prefs = PreferenceManager.getDefaultSharedPreferences(App.instance)
+ private val isHeaderEnabledKey =
+ App.instance.getString(R.string.long_press_menu_is_header_enabled_key)
+ private val actionArrangementKey =
+ App.instance.getString(R.string.long_press_menu_action_arrangement_key)
+ private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == isHeaderEnabledKey) {
+ _isHeaderEnabled.value = loadIsHeaderEnabledFromSettings(App.instance)
+ } else if (key == actionArrangementKey) {
+ _actionArrangement.value = loadLongPressActionArrangementFromSettings(App.instance)
+ }
+ }
+
+ init {
+ prefs.registerOnSharedPreferenceChangeListener(listener)
+ }
+
+ override fun onCleared() {
+ prefs.unregisterOnSharedPreferenceChangeListener(listener)
+ }
+}