Persist long press actions to settings
This commit is contained in:
parent
3d62b923c7
commit
85cb372f5f
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user