Persist long press actions to settings

This commit is contained in:
Stypox 2026-01-06 22:30:56 +01:00
parent 3d62b923c7
commit 85cb372f5f
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
6 changed files with 130 additions and 36 deletions

View File

@ -57,29 +57,34 @@ data class LongPressAction(
val enabled: (isPlayerRunning: Boolean) -> Boolean = { true },
) {
enum class Type(
/**
* A unique ID that allows saving and restoring a list of action types from settings.
* MUST NOT CHANGE ACROSS APP VERSIONS!
*/
val id: Int,
@StringRes val label: Int,
val icon: ImageVector,
) {
Enqueue(R.string.enqueue, Icons.Default.AddToQueue),
EnqueueNext(R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
Background(R.string.controls_background_title, Icons.Default.Headset),
Popup(R.string.controls_popup_title, Icons.Default.PictureInPicture),
Play(R.string.play, Icons.Default.PlayArrow),
BackgroundFromHere(R.string.background_from_here, Icons.Default.BackgroundFromHere),
PopupFromHere(R.string.popup_from_here, Icons.Default.PopupFromHere),
PlayFromHere(R.string.play_from_here, Icons.Default.PlayFromHere),
PlayWithKodi(R.string.play_with_kodi_title, Icons.Default.Cast),
Download(R.string.download, Icons.Default.Download),
AddToPlaylist(R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
Share(R.string.share, Icons.Default.Share),
OpenInBrowser(R.string.open_in_browser, Icons.Default.OpenInBrowser),
ShowChannelDetails(R.string.show_channel_details, Icons.Default.Person),
MarkAsWatched(R.string.mark_as_watched, Icons.Default.Done),
Delete(R.string.delete, Icons.Default.Delete),
Rename(R.string.rename, Icons.Default.Edit),
SetAsPlaylistThumbnail(R.string.set_as_playlist_thumbnail, Icons.Default.Image),
UnsetPlaylistThumbnail(R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
Unsubscribe(R.string.unsubscribe, Icons.Default.Delete),
Enqueue(0, R.string.enqueue, Icons.Default.AddToQueue),
EnqueueNext(1, R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
Background(2, R.string.controls_background_title, Icons.Default.Headset),
Popup(3, R.string.controls_popup_title, Icons.Default.PictureInPicture),
Play(4, R.string.play, Icons.Default.PlayArrow),
BackgroundFromHere(5, R.string.background_from_here, Icons.Default.BackgroundFromHere),
PopupFromHere(6, R.string.popup_from_here, Icons.Default.PopupFromHere),
PlayFromHere(7, R.string.play_from_here, Icons.Default.PlayFromHere),
PlayWithKodi(8, R.string.play_with_kodi_title, Icons.Default.Cast),
Download(9, R.string.download, Icons.Default.Download),
AddToPlaylist(10, R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
Share(11, R.string.share, Icons.Default.Share),
OpenInBrowser(12, R.string.open_in_browser, Icons.Default.OpenInBrowser),
ShowChannelDetails(13, R.string.show_channel_details, Icons.Default.Person),
MarkAsWatched(14, R.string.mark_as_watched, Icons.Default.Done),
Delete(15, R.string.delete, Icons.Default.Delete),
Rename(16, R.string.rename, Icons.Default.Edit),
SetAsPlaylistThumbnail(17, R.string.set_as_playlist_thumbnail, Icons.Default.Image),
UnsetPlaylistThumbnail(18, R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
Unsubscribe(19, R.string.unsubscribe, Icons.Default.Delete),
;
// TODO allow actions to return disposables
@ -93,7 +98,7 @@ data class LongPressAction(
companion object {
// ShowChannelDetails is not enabled by default, since navigating to channel details can
// also be done by clicking on the uploader name in the long press menu header
val DefaultEnabledActions: Array<Type> = arrayOf(
val DefaultEnabledActions: List<Type> = listOf(
Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download,
AddToPlaylist, Share, OpenInBrowser, MarkAsWatched, Delete,
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe

View File

@ -54,6 +54,7 @@ import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
@ -70,8 +71,6 @@ import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.text.FixedHeightCenteredText
import kotlin.math.floor
internal const val TAG = "LongPressMenuEditor"
/**
* When making changes to this composable and to [LongPressMenuEditorState], make sure to test the
* following use cases, and check that they still work:
@ -89,15 +88,16 @@ internal const val TAG = "LongPressMenuEditor"
*/
@Composable
fun LongPressMenuEditor(modifier: Modifier = Modifier) {
val context = LocalContext.current
val gridState = rememberLazyGridState()
val coroutineScope = rememberCoroutineScope()
val state = remember(gridState, coroutineScope) {
LongPressMenuEditorState(gridState, coroutineScope)
LongPressMenuEditorState(context, gridState, coroutineScope)
}
DisposableEffect(Unit) {
onDispose {
state.onDispose()
state.onDispose(context)
}
}

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.ui.components.menu
import android.content.Context
import android.util.Log
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
@ -24,11 +25,12 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.Companion.DefaultEnabledActions
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
private const val TAG = "LongPressMenuEditorStat"
/**
* This class is very tied to [LongPressMenuEditor] and interacts with the UI layer through
* [gridState]. Therefore it's not a view model but rather a state holder class, see
@ -39,33 +41,33 @@ import kotlin.math.min
*/
@Stable
class LongPressMenuEditorState(
context: Context,
val gridState: LazyGridState,
val coroutineScope: CoroutineScope,
) {
// We get the current arrangement once and do not observe on purpose
val items = run {
// TODO load from settings
val headerEnabled = true
val actionArrangement = DefaultEnabledActions
// We get the current arrangement once and do not observe on purpose.
val isHeaderEnabled = loadIsHeaderEnabledFromSettings(context)
val actionArrangement = loadLongPressActionArrangementFromSettings(context)
sequence {
yield(ItemInList.EnabledCaption)
if (headerEnabled) {
if (isHeaderEnabled) {
yield(ItemInList.HeaderBox)
}
yieldAll(
actionArrangement
.map { ItemInList.Action(it) }
.ifEmpty { if (headerEnabled) listOf() else listOf(ItemInList.NoneMarker) }
.ifEmpty { if (isHeaderEnabled) listOf() else listOf(ItemInList.NoneMarker) }
)
yield(ItemInList.HiddenCaption)
if (!headerEnabled) {
if (!isHeaderEnabled) {
yield(ItemInList.HeaderBox)
}
yieldAll(
LongPressAction.Type.entries
.filter { !actionArrangement.contains(it) }
.map { ItemInList.Action(it) }
.ifEmpty { if (headerEnabled) listOf(ItemInList.NoneMarker) else listOf() }
.ifEmpty { if (isHeaderEnabled) listOf(ItemInList.NoneMarker) else listOf() }
)
}.toList().toMutableStateList()
}
@ -365,9 +367,23 @@ class LongPressMenuEditorState(
return true
}
fun onDispose() {
fun onDispose(context: Context) {
completeDragGestureAndCleanUp()
// TODO save to settings
var isHeaderEnabled = false
val actionArrangement = ArrayList<LongPressAction.Type>()
// All of the items before the HiddenCaption are enabled.
for (item in items) {
when (item) {
is ItemInList.Action -> actionArrangement.add(item.type)
ItemInList.HeaderBox -> isHeaderEnabled = true
ItemInList.HiddenCaption -> break
else -> {}
}
}
storeIsHeaderEnabledToSettings(context, isHeaderEnabled)
storeLongPressActionArrangementToSettings(context, actionArrangement)
}
}

View File

@ -0,0 +1,58 @@
package org.schabi.newpipe.ui.components.menu
import android.content.Context
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
private const val TAG: String = "LongPressMenuSettings"
fun loadIsHeaderEnabledFromSettings(context: Context): Boolean {
val key = context.getString(R.string.long_press_menu_is_header_enabled_key)
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, true)
}
fun storeIsHeaderEnabledToSettings(context: Context, enabled: Boolean) {
val key = context.getString(R.string.long_press_menu_is_header_enabled_key)
return PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(key, enabled)
}
}
fun loadLongPressActionArrangementFromSettings(context: Context): List<LongPressAction.Type> {
val key = context.getString(R.string.long_press_menu_action_arrangement_key)
val items = PreferenceManager.getDefaultSharedPreferences(context)
.getString(key, null)
if (items == null) {
return LongPressAction.Type.DefaultEnabledActions
}
try {
val actions = items.split(',')
.map { item ->
LongPressAction.Type.entries.first { entry ->
entry.id.toString() == item
}
}
// In case there is some bug in the stored data, make sure we don't return duplicate items,
// as that would break/crash the UI and also not make any sense.
val actionsDistinct = actions.distinct()
if (actionsDistinct.size != actions.size) {
Log.w(TAG, "Actions in settings were not distinct: $actions != $actionsDistinct")
}
return actionsDistinct
} catch (e: NoSuchElementException) {
Log.e(TAG, "Invalid action in settings", e)
return LongPressAction.Type.DefaultEnabledActions
}
}
fun storeLongPressActionArrangementToSettings(context: Context, actions: List<LongPressAction.Type>) {
val items = actions.joinToString(separator = ",") { it.id.toString() }
val key = context.getString(R.string.long_press_menu_action_arrangement_key)
PreferenceManager.getDefaultSharedPreferences(context).edit {
putString(key, items)
}
}

View File

@ -366,6 +366,9 @@
<string name="show_thumbnail_key">show_thumbnail_key</string>
<string name="long_press_menu_action_arrangement_key">long_press_menu_action_arrangement</string>
<string name="long_press_menu_is_header_enabled_key">long_press_menu_is_header_enabled</string>
<!-- Values will be localized in runtime -->
<string-array name="feed_update_threshold_options">
<item>@string/feed_update_threshold_option_always_update</item>

View File

@ -0,0 +1,12 @@
package org.schabi.newpipe.ui.components.menu
import org.junit.Assert.assertEquals
import org.junit.Test
class LongPressActionTest {
@Test
fun `LongPressAction Type ids are unique`() {
val ids = LongPressAction.Type.entries.map { it.id }
assertEquals(ids.size, ids.toSet().size)
}
}