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 c695738a2..506f6da0c 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 @@ -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 = arrayOf( + val DefaultEnabledActions: List = listOf( Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download, AddToPlaylist, Share, OpenInBrowser, MarkAsWatched, Delete, Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt index b337ade5a..d705654a9 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt @@ -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) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditorState.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditorState.kt index 3eaa7a086..f33f434c0 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditorState.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditorState.kt @@ -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() + // 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) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuSettings.kt b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuSettings.kt new file mode 100644 index 000000000..be14125dc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuSettings.kt @@ -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 { + 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) { + 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) + } +} diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index f33f5eef7..cce08ad3c 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -366,6 +366,9 @@ show_thumbnail_key + long_press_menu_action_arrangement + long_press_menu_is_header_enabled + @string/feed_update_threshold_option_always_update diff --git a/app/src/test/java/org/schabi/newpipe/ui/components/menu/LongPressActionTest.kt b/app/src/test/java/org/schabi/newpipe/ui/components/menu/LongPressActionTest.kt new file mode 100644 index 000000000..26812088a --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/ui/components/menu/LongPressActionTest.kt @@ -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) + } +}