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) + } +}