diff --git a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt index 76efcc3ae..16b96ee56 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/model/LibraryTabId.kt @@ -19,6 +19,7 @@ enum class LibraryTabId( LIKED("LIKED", "LIKED", R.string.library_tab_liked, SortOption.LikedSongDateLiked); companion object { + val defaultOrder: List = entries.toList() fun fromStorageKey(key: String): LibraryTabId = entries.firstOrNull { it.storageKey == key } ?: SONGS } diff --git a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt index 9efca552a..0a796a8ba 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt @@ -129,6 +129,7 @@ class UserPreferencesRepository @Inject constructor( // Transition val GLOBAL_TRANSITION_SETTINGS = stringPreferencesKey("global_transition_settings_json") val LIBRARY_TABS_ORDER = stringPreferencesKey("library_tabs_order") + val LIBRARY_HIDDEN_TABS = stringSetPreferencesKey("library_hidden_tabs") val IS_FOLDER_FILTER_ACTIVE = booleanPreferencesKey("is_folder_filter_active") val IS_FOLDERS_PLAYLIST_VIEW = booleanPreferencesKey("is_folders_playlist_view") val SHOW_TELEGRAM_CLOUD_PLAYLISTS = booleanPreferencesKey("show_telegram_cloud_playlists") @@ -873,8 +874,18 @@ suspend fun markDirectoryRulesVersionApplied(version: Int) { dataStore.edit { it[PreferencesKeys.LIBRARY_TABS_ORDER] = order } } + val libraryHiddenTabsFlow: Flow> = + pref { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] ?: emptySet() } + + suspend fun setLibraryHiddenTabs(hiddenTabs: Set) { + dataStore.edit { it[PreferencesKeys.LIBRARY_HIDDEN_TABS] = hiddenTabs } + } + suspend fun resetLibraryTabsOrder() { - dataStore.edit { it.remove(PreferencesKeys.LIBRARY_TABS_ORDER) } + dataStore.edit { + it.remove(PreferencesKeys.LIBRARY_TABS_ORDER) + it.remove(PreferencesKeys.LIBRARY_HIDDEN_TABS) + } } suspend fun migrateTabOrder() { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt index 641d2f567..91bb960fb 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/ReorderTabsSheet.kt @@ -13,13 +13,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.DragIndicator import androidx.compose.material3.AlertDialog import androidx.compose.material3.ContainedLoadingIndicator @@ -53,7 +56,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.ViewCompat import android.view.HapticFeedbackConstants import com.theveloper.pixelplay.R -import com.theveloper.pixelplay.presentation.library.LibraryTabId +import com.theveloper.pixelplay.data.model.LibraryTabId import com.theveloper.pixelplay.presentation.utils.LocalAppHapticsConfig import com.theveloper.pixelplay.presentation.utils.performAppCompatHapticFeedback import com.theveloper.pixelplay.ui.theme.GoogleSansRounded @@ -63,21 +66,25 @@ import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ReorderTabsSheet( - tabs: List, - onReorder: (List) -> Unit, + visibleTabs: List, + hiddenTabs: List, + onSave: (visible: List, hidden: Set) -> Unit, onReset: () -> Unit, onDismiss: () -> Unit ) { var showResetDialog by remember { mutableStateOf(false) } - var localTabs by remember { mutableStateOf(tabs) } + var localVisibleTabs by remember { mutableStateOf(visibleTabs) } + var localHiddenTabs by remember { mutableStateOf(hiddenTabs) } - LaunchedEffect(tabs) { - localTabs = tabs + LaunchedEffect(visibleTabs, hiddenTabs) { + localVisibleTabs = visibleTabs + localHiddenTabs = hiddenTabs } if (showResetDialog) { @@ -89,7 +96,7 @@ fun ReorderTabsSheet( TextButton( onClick = { onReset() - localTabs = tabs + // Local state will be updated by the LaunchedEffect when visibleTabs/hiddenTabs change via VM showResetDialog = false } ) { @@ -114,15 +121,30 @@ fun ReorderTabsSheet( val reorderableState = rememberReorderableLazyListState( onMove = { from, to -> - localTabs = localTabs.toMutableList().apply { - add(to.index, removeAt(from.index)) + val fromKey = from.key as? String ?: return@rememberReorderableLazyListState + val toKey = to.key as? String ?: return@rememberReorderableLazyListState + + // Only move if both items are in the visible section + if (fromKey.startsWith("v_") && toKey.startsWith("v_")) { + val fromId = fromKey.removePrefix("v_") + val toId = toKey.removePrefix("v_") + + val fromIdx = localVisibleTabs.indexOf(fromId) + val toIdx = localVisibleTabs.indexOf(toId) + + if (fromIdx != -1 && toIdx != -1) { + localVisibleTabs = localVisibleTabs.toMutableList().apply { + add(toIdx, removeAt(fromIdx)) + } + // Haptic feedback on reorder + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + } } - // Haptic feedback on reorder - performAppCompatHapticFeedback( - view, - appHapticsConfig, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING - ) }, lazyListState = listState ) @@ -152,13 +174,13 @@ fun ReorderTabsSheet( floatingActionButton = { FloatingToolBar( modifier = Modifier, - onReset = { showResetDialog = true }, // This will now trigger the dialog + onReset = { showResetDialog = true }, onDismiss = onDismiss, onClick = { scope.launch { isLoading = true - delay(700) // Simulate network/db operation - onReorder(localTabs) + delay(400) // Visual confirmation + onSave(localVisibleTabs, localHiddenTabs.toSet()) isLoading = false onDismiss() } @@ -183,11 +205,12 @@ fun ReorderTabsSheet( LazyColumn( state = listState, modifier = Modifier.fillMaxSize().padding(horizontal = 14.dp), - contentPadding = PaddingValues(bottom = 100.dp, top = 8.dp), + contentPadding = PaddingValues(bottom = 150.dp, top = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(localTabs, key = { it }) { tab -> - ReorderableItem(reorderableState, key = tab) { isDragging -> + + items(localVisibleTabs, key = { "v_$it" }) { tab -> + ReorderableItem(reorderableState, key = "v_$tab") { isDragging -> LaunchedEffect(isDragging) { if (isDragging) { performAppCompatHapticFeedback( @@ -201,12 +224,13 @@ fun ReorderTabsSheet( Surface( modifier = Modifier .fillMaxWidth() + .height(60.dp) .clip(CircleShape), shadowElevation = if (isDragging) 4.dp else 0.dp, color = MaterialTheme.colorScheme.surfaceContainerLowest ) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 18.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -216,11 +240,111 @@ fun ReorderTabsSheet( ) Spacer(modifier = Modifier.width(16.dp)) Text( - text = LibraryTabId.fromStableKey(tab) - ?.let { stringResource(it.labelRes) } - ?: tab, - style = MaterialTheme.typography.bodyLarge + text = LibraryTabId.fromStorageKey(tab) + .let { stringResource(it.titleRes) }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + + if (localVisibleTabs.size > 2) { + Surface( + onClick = { + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + localVisibleTabs = localVisibleTabs.filter { it != tab } + localHiddenTabs = localHiddenTabs + tab + }, + modifier = Modifier.size(36.dp), + shape = AbsoluteSmoothCornerShape(12.dp, 60), + color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = stringResource(R.string.reorder_tabs_cd_remove_tab), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } + } + } else { + Spacer(modifier = Modifier.width(36.dp)) + } + } + } + } + } + + if (localHiddenTabs.isNotEmpty()) { + item(key = "h_hidden") { + Text( + text = stringResource(R.string.reorder_tabs_hidden_section), + style = MaterialTheme.typography.titleMedium, + fontFamily = GoogleSansRounded, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 8.dp, top = 16.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.secondary + ) + } + + items(localHiddenTabs, key = { "h_$it" }) { tab -> + Surface( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .clip(CircleShape), + color = MaterialTheme.colorScheme.surfaceContainerLowest + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.DragIndicator, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = LibraryTabId.fromStorageKey(tab) + .let { stringResource(it.titleRes) }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) ) + Surface( + onClick = { + performAppCompatHapticFeedback( + view, + appHapticsConfig, + HapticFeedbackConstants.CLOCK_TICK, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + localHiddenTabs = localHiddenTabs.filter { it != tab } + localVisibleTabs = localVisibleTabs + tab + }, + modifier = Modifier.size(36.dp), + shape = AbsoluteSmoothCornerShape(12.dp, 60), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.reorder_tabs_cd_add_tab), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + } + } } } } @@ -266,7 +390,7 @@ fun FloatingToolBar( ) { IconButton( modifier = Modifier.align(Alignment.CenterVertically), - onClick = onReset // This now calls the lambda from the parent + onClick = onReset ) { Icon( painter = painterResource(R.drawable.outline_restart_alt_24), @@ -285,4 +409,4 @@ fun FloatingToolBar( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 2260b4880..e1c46e818 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -2094,10 +2094,13 @@ fun LibraryScreen( } if (showReorderTabsSheet) { + val hiddenTabs by playerViewModel.hiddenLibraryTabsFlow.collectAsStateWithLifecycle() ReorderTabsSheet( - tabs = tabTitles, - onReorder = { newOrder -> + visibleTabs = tabTitles, + hiddenTabs = hiddenTabs, + onSave = { newOrder, newHidden -> playerViewModel.saveLibraryTabsOrder(newOrder) + playerViewModel.saveLibraryHiddenTabs(newHidden) }, onReset = { playerViewModel.resetLibraryTabsOrder() diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index d6829b847..fa7d009bc 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -1055,19 +1055,46 @@ class PlayerViewModel @Inject constructor( initialValue = 0 // Default to Songs tab ) - val libraryTabsFlow: StateFlow> = userPreferencesRepository.libraryTabsOrderFlow - .map { orderJson -> - if (orderJson != null) { - try { - Json.decodeFromString>(orderJson) - } catch (e: Exception) { - listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED") - } - } else { - listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED") + val libraryTabsFlow: StateFlow> = combine( + userPreferencesRepository.libraryTabsOrderFlow, + userPreferencesRepository.libraryHiddenTabsFlow + ) { orderJson, hiddenTabs -> + val allTabsInOrder = if (orderJson != null) { + try { + Json.decodeFromString>(orderJson) + } catch (e: Exception) { + LibraryTabId.defaultOrder.map { it.storageKey } } + } else { + LibraryTabId.defaultOrder.map { it.storageKey } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf("SONGS", "ALBUMS", "ARTIST", "PLAYLISTS", "FOLDERS", "LIKED")) + + // Ensure all available tabs are present (e.g. new tabs from app updates) + val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey } + val mergedOrder = (allTabsInOrder + availableKeys).distinct() + + mergedOrder.filter { it !in hiddenTabs } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LibraryTabId.defaultOrder.map { it.storageKey }) + + val hiddenLibraryTabsFlow: StateFlow> = combine( + userPreferencesRepository.libraryTabsOrderFlow, + userPreferencesRepository.libraryHiddenTabsFlow + ) { orderJson, hiddenTabs -> + val allTabsInOrder = if (orderJson != null) { + try { + Json.decodeFromString>(orderJson) + } catch (e: Exception) { + LibraryTabId.defaultOrder.map { it.storageKey } + } + } else { + LibraryTabId.defaultOrder.map { it.storageKey } + } + + val availableKeys = LibraryTabId.defaultOrder.map { it.storageKey } + val mergedOrder = (allTabsInOrder + availableKeys).distinct() + + mergedOrder.filter { it in hiddenTabs } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _loadedTabs = MutableStateFlow(emptySet()) private var lastBlockedDirectories: Set? = null @@ -2803,6 +2830,12 @@ class PlayerViewModel @Inject constructor( } } + fun saveLibraryHiddenTabs(hiddenTabs: Set) { + viewModelScope.launch { + userPreferencesRepository.setLibraryHiddenTabs(hiddenTabs) + } + } + fun resetLibraryTabsOrder() { viewModelScope.launch { userPreferencesRepository.resetLibraryTabsOrder() diff --git a/app/src/main/res/values-es/strings_library.xml b/app/src/main/res/values-es/strings_library.xml index f32199d42..62906018a 100644 --- a/app/src/main/res/values-es/strings_library.xml +++ b/app/src/main/res/values-es/strings_library.xml @@ -25,7 +25,7 @@ Transferencia al reloj Ajustes Editar - Reorder pestañas + Reordenar pestañas Expandir menú @@ -255,6 +255,10 @@ ¿Restablecer el orden de las pestañas al predeterminado? Reordenando pestañas… Control de arrastre + Pestañas visibles + Pestañas eliminadas + Eliminar pestaña + Añadir pestaña Elige un artista diff --git a/app/src/main/res/values/strings_library.xml b/app/src/main/res/values/strings_library.xml index 8b7c6c2c6..21f700026 100644 --- a/app/src/main/res/values/strings_library.xml +++ b/app/src/main/res/values/strings_library.xml @@ -255,6 +255,10 @@ Reset tab order to the default? Reordering tabs… Drag handle + Visible Tabs + Removed Tabs + Remove tab + Add tab Pick an Artist