Skip to content

Commit f053aec

Browse files
committed
Persist long press actions to settings
1 parent 44cb2dc commit f053aec

6 files changed

Lines changed: 131 additions & 36 deletions

File tree

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressAction.kt

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,34 @@ data class LongPressAction(
5757
val enabled: (isPlayerRunning: Boolean) -> Boolean = { true },
5858
) {
5959
enum class Type(
60+
/**
61+
* A unique ID that allows saving and restoring a list of action types from settings.
62+
* MUST NOT CHANGE ACROSS APP VERSIONS!
63+
*/
64+
val id: Int,
6065
@StringRes val label: Int,
6166
val icon: ImageVector,
6267
) {
63-
Enqueue(R.string.enqueue, Icons.Default.AddToQueue),
64-
EnqueueNext(R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
65-
Background(R.string.controls_background_title, Icons.Default.Headset),
66-
Popup(R.string.controls_popup_title, Icons.Default.PictureInPicture),
67-
Play(R.string.play, Icons.Default.PlayArrow),
68-
BackgroundFromHere(R.string.background_from_here, Icons.Default.BackgroundFromHere),
69-
PopupFromHere(R.string.popup_from_here, Icons.Default.PopupFromHere),
70-
PlayFromHere(R.string.play_from_here, Icons.Default.PlayFromHere),
71-
PlayWithKodi(R.string.play_with_kodi_title, Icons.Default.Cast),
72-
Download(R.string.download, Icons.Default.Download),
73-
AddToPlaylist(R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
74-
Share(R.string.share, Icons.Default.Share),
75-
OpenInBrowser(R.string.open_in_browser, Icons.Default.OpenInBrowser),
76-
ShowChannelDetails(R.string.show_channel_details, Icons.Default.Person),
77-
MarkAsWatched(R.string.mark_as_watched, Icons.Default.Done),
78-
Delete(R.string.delete, Icons.Default.Delete),
79-
Rename(R.string.rename, Icons.Default.Edit),
80-
SetAsPlaylistThumbnail(R.string.set_as_playlist_thumbnail, Icons.Default.Image),
81-
UnsetPlaylistThumbnail(R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
82-
Unsubscribe(R.string.unsubscribe, Icons.Default.Delete),
68+
Enqueue(0, R.string.enqueue, Icons.Default.AddToQueue),
69+
EnqueueNext(1, R.string.enqueue_next_stream, Icons.Default.QueuePlayNext),
70+
Background(2, R.string.controls_background_title, Icons.Default.Headset),
71+
Popup(3, R.string.controls_popup_title, Icons.Default.PictureInPicture),
72+
Play(4, R.string.play, Icons.Default.PlayArrow),
73+
BackgroundFromHere(5, R.string.background_from_here, Icons.Default.BackgroundFromHere),
74+
PopupFromHere(6, R.string.popup_from_here, Icons.Default.PopupFromHere),
75+
PlayFromHere(7, R.string.play_from_here, Icons.Default.PlayFromHere),
76+
PlayWithKodi(8, R.string.play_with_kodi_title, Icons.Default.Cast),
77+
Download(9, R.string.download, Icons.Default.Download),
78+
AddToPlaylist(10, R.string.add_to_playlist, Icons.AutoMirrored.Default.PlaylistAdd),
79+
Share(11, R.string.share, Icons.Default.Share),
80+
OpenInBrowser(12, R.string.open_in_browser, Icons.Default.OpenInBrowser),
81+
ShowChannelDetails(13, R.string.show_channel_details, Icons.Default.Person),
82+
MarkAsWatched(14, R.string.mark_as_watched, Icons.Default.Done),
83+
Delete(15, R.string.delete, Icons.Default.Delete),
84+
Rename(16, R.string.rename, Icons.Default.Edit),
85+
SetAsPlaylistThumbnail(17, R.string.set_as_playlist_thumbnail, Icons.Default.Image),
86+
UnsetPlaylistThumbnail(18, R.string.unset_playlist_thumbnail, Icons.Default.HideImage),
87+
Unsubscribe(19, R.string.unsubscribe, Icons.Default.Delete),
8388
;
8489

8590
// TODO allow actions to return disposables
@@ -93,7 +98,7 @@ data class LongPressAction(
9398
companion object {
9499
// ShowChannelDetails is not enabled by default, since navigating to channel details can
95100
// also be done by clicking on the uploader name in the long press menu header
96-
val DefaultEnabledActions: Array<Type> = arrayOf(
101+
val DefaultEnabledActions: List<Type> = listOf(
97102
Enqueue, EnqueueNext, Background, Popup, BackgroundFromHere, Download,
98103
AddToPlaylist, Share, OpenInBrowser, MarkAsWatched, Delete,
99104
Rename, SetAsPlaylistThumbnail, UnsetPlaylistThumbnail, Unsubscribe

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditor.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import androidx.compose.ui.focus.focusTarget
5454
import androidx.compose.ui.graphics.Color
5555
import androidx.compose.ui.graphics.vector.ImageVector
5656
import androidx.compose.ui.input.key.onKeyEvent
57+
import androidx.compose.ui.platform.LocalContext
5758
import androidx.compose.ui.platform.LocalDensity
5859
import androidx.compose.ui.res.stringResource
5960
import androidx.compose.ui.text.font.FontStyle
@@ -70,8 +71,6 @@ import org.schabi.newpipe.util.letIf
7071
import org.schabi.newpipe.util.text.FixedHeightCenteredText
7172
import kotlin.math.floor
7273

73-
internal const val TAG = "LongPressMenuEditor"
74-
7574
/**
7675
* When making changes to this composable and to [LongPressMenuEditorState], make sure to test the
7776
* following use cases, and check that they still work:
@@ -89,15 +88,16 @@ internal const val TAG = "LongPressMenuEditor"
8988
*/
9089
@Composable
9190
fun LongPressMenuEditor(modifier: Modifier = Modifier) {
91+
val context = LocalContext.current
9292
val gridState = rememberLazyGridState()
9393
val coroutineScope = rememberCoroutineScope()
9494
val state = remember(gridState, coroutineScope) {
95-
LongPressMenuEditorState(gridState, coroutineScope)
95+
LongPressMenuEditorState(context, gridState, coroutineScope)
9696
}
9797

9898
DisposableEffect(Unit) {
9999
onDispose {
100-
state.onDispose()
100+
state.onDispose(context)
101101
}
102102
}
103103

app/src/main/java/org/schabi/newpipe/ui/components/menu/LongPressMenuEditorState.kt

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.schabi.newpipe.ui.components.menu
22

3+
import android.content.Context
34
import android.util.Log
45
import androidx.compose.foundation.gestures.scrollBy
56
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
@@ -24,11 +25,12 @@ import kotlinx.coroutines.Job
2425
import kotlinx.coroutines.delay
2526
import kotlinx.coroutines.isActive
2627
import kotlinx.coroutines.launch
27-
import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.Companion.DefaultEnabledActions
2828
import kotlin.math.abs
2929
import kotlin.math.max
3030
import kotlin.math.min
3131

32+
private const val TAG = "LongPressMenuEditorStat"
33+
3234
/**
3335
* This class is very tied to [LongPressMenuEditor] and interacts with the UI layer through
3436
* [gridState]. Therefore it's not a view model but rather a state holder class, see
@@ -39,33 +41,34 @@ import kotlin.math.min
3941
*/
4042
@Stable
4143
class LongPressMenuEditorState(
44+
context: Context,
4245
val gridState: LazyGridState,
4346
val coroutineScope: CoroutineScope,
4447
) {
45-
// We get the current arrangement once and do not observe on purpose
4648
val items = run {
47-
// TODO load from settings
48-
val headerEnabled = true
49-
val actionArrangement = DefaultEnabledActions
49+
// We get the current arrangement once and do not observe on purpose.
50+
val isHeaderEnabled = loadIsHeaderEnabledFromSettings(context)
51+
val actionArrangement = loadLongPressActionArrangementFromSettings(context)
52+
Log.e(TAG, "action arrangement: $actionArrangement")
5053
sequence {
5154
yield(ItemInList.EnabledCaption)
52-
if (headerEnabled) {
55+
if (isHeaderEnabled) {
5356
yield(ItemInList.HeaderBox)
5457
}
5558
yieldAll(
5659
actionArrangement
5760
.map { ItemInList.Action(it) }
58-
.ifEmpty { if (headerEnabled) listOf() else listOf(ItemInList.NoneMarker) }
61+
.ifEmpty { if (isHeaderEnabled) listOf() else listOf(ItemInList.NoneMarker) }
5962
)
6063
yield(ItemInList.HiddenCaption)
61-
if (!headerEnabled) {
64+
if (!isHeaderEnabled) {
6265
yield(ItemInList.HeaderBox)
6366
}
6467
yieldAll(
6568
LongPressAction.Type.entries
6669
.filter { !actionArrangement.contains(it) }
6770
.map { ItemInList.Action(it) }
68-
.ifEmpty { if (headerEnabled) listOf(ItemInList.NoneMarker) else listOf() }
71+
.ifEmpty { if (isHeaderEnabled) listOf(ItemInList.NoneMarker) else listOf() }
6972
)
7073
}.toList().toMutableStateList()
7174
}
@@ -365,9 +368,23 @@ class LongPressMenuEditorState(
365368
return true
366369
}
367370

368-
fun onDispose() {
371+
fun onDispose(context: Context) {
369372
completeDragGestureAndCleanUp()
370-
// TODO save to settings
373+
374+
var isHeaderEnabled = false
375+
val actionArrangement = ArrayList<LongPressAction.Type>()
376+
// All of the items before the HiddenCaption are enabled.
377+
for (item in items) {
378+
when (item) {
379+
is ItemInList.Action -> actionArrangement.add(item.type)
380+
ItemInList.HeaderBox -> isHeaderEnabled = true
381+
ItemInList.HiddenCaption -> break
382+
else -> {}
383+
}
384+
}
385+
386+
storeIsHeaderEnabledToSettings(context, isHeaderEnabled)
387+
storeLongPressActionArrangementToSettings(context, actionArrangement)
371388
}
372389
}
373390

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.schabi.newpipe.ui.components.menu
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.core.content.edit
6+
import androidx.preference.PreferenceManager
7+
import org.schabi.newpipe.R
8+
9+
private const val TAG: String = "LongPressMenuSettings"
10+
11+
fun loadIsHeaderEnabledFromSettings(context: Context): Boolean {
12+
val key = context.getString(R.string.long_press_menu_is_header_enabled_key)
13+
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, true)
14+
}
15+
16+
fun storeIsHeaderEnabledToSettings(context: Context, enabled: Boolean) {
17+
val key = context.getString(R.string.long_press_menu_is_header_enabled_key)
18+
return PreferenceManager.getDefaultSharedPreferences(context).edit {
19+
putBoolean(key, enabled)
20+
}
21+
}
22+
23+
fun loadLongPressActionArrangementFromSettings(context: Context): List<LongPressAction.Type> {
24+
val key = context.getString(R.string.long_press_menu_action_arrangement_key)
25+
val items = PreferenceManager.getDefaultSharedPreferences(context)
26+
.getString(key, null)
27+
if (items == null) {
28+
return LongPressAction.Type.DefaultEnabledActions
29+
}
30+
31+
try {
32+
val actions = items.split(',')
33+
.map { item ->
34+
LongPressAction.Type.entries.first { entry ->
35+
entry.id.toString() == item
36+
}
37+
}
38+
39+
// In case there is some bug in the stored data, make sure we don't return duplicate items,
40+
// as that would break/crash the UI and also not make any sense.
41+
val actionsDistinct = actions.distinct()
42+
if (actionsDistinct.size != actions.size) {
43+
Log.w(TAG, "Actions in settings were not distinct: $actions != $actionsDistinct")
44+
}
45+
return actionsDistinct
46+
} catch (e: NoSuchElementException) {
47+
Log.e(TAG, "Invalid action in settings", e)
48+
return LongPressAction.Type.DefaultEnabledActions
49+
}
50+
}
51+
52+
fun storeLongPressActionArrangementToSettings(context: Context, actions: List<LongPressAction.Type>) {
53+
val items = actions.joinToString(separator = ",") { it.id.toString() }
54+
val key = context.getString(R.string.long_press_menu_action_arrangement_key)
55+
PreferenceManager.getDefaultSharedPreferences(context).edit {
56+
putString(key, items)
57+
}
58+
}

app/src/main/res/values/settings_keys.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,9 @@
366366

367367
<string name="show_thumbnail_key">show_thumbnail_key</string>
368368

369+
<string name="long_press_menu_action_arrangement_key">long_press_menu_action_arrangement</string>
370+
<string name="long_press_menu_is_header_enabled_key">long_press_menu_is_header_enabled</string>
371+
369372
<!-- Values will be localized in runtime -->
370373
<string-array name="feed_update_threshold_options">
371374
<item>@string/feed_update_threshold_option_always_update</item>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.schabi.newpipe.ui.components.menu
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
class LongPressActionTest {
7+
@Test
8+
fun `LongPressAction Type ids are unique`() {
9+
val ids = LongPressAction.Type.entries.map { it.id }
10+
assertEquals(ids.size, ids.toSet().size)
11+
}
12+
}

0 commit comments

Comments
 (0)