From acde3e52d34fe2a44bd0c8478d1dda3cafee7fc2 Mon Sep 17 00:00:00 2001 From: Max Grakov Date: Sun, 1 Mar 2026 21:33:26 +0300 Subject: [PATCH] Feature/collapse playing queue fab (#358) --- .../preferences/LissenSharedPreferences.kt | 10 - .../composable/PlayingQueueComposable.kt | 263 +++++++++++------- .../advanced/AdvancedSettingsComposable.kt | 7 - .../lissen/viewmodel/PlayerViewModel.kt | 1 - .../lissen/viewmodel/SettingsViewModel.kt | 7 - app/src/main/res/values-ar/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 2 - app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-hr/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-in/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - 15 files changed, 163 insertions(+), 145 deletions(-) diff --git a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt index 6c22af29..de50121b 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt @@ -274,8 +274,6 @@ class LissenSharedPreferences val materialYouFlow = asFlow(KEY_MATERIAL_YOU_ENABLED, ::getMaterialYouColors) - val collapseOnFlingFlow = asFlow(KEY_COLLAPSE_ON_FLING, ::getCollapseOnFling) - val forceCacheFlow = asFlow(CACHE_FORCE_ENABLED, ::isForceCache) val hideCompletedFlow = asFlow(KEY_HIDE_COMPLETED, ::getHideCompleted) @@ -441,13 +439,6 @@ class LissenSharedPreferences } } - fun getCollapseOnFling(): Boolean = sharedPreferences.getBoolean(KEY_COLLAPSE_ON_FLING, true) - - fun saveCollapseOnFling(value: Boolean) = - sharedPreferences.edit { - putBoolean(KEY_COLLAPSE_ON_FLING, value) - } - fun getSoftwareCodecsEnabled(): Boolean = sharedPreferences.getBoolean(KEY_SOFTWARE_CODECS, false) fun saveSoftwareCodecsEnabled(value: Boolean) = @@ -489,7 +480,6 @@ class LissenSharedPreferences private const val KEY_PREFERRED_AUTO_DOWNLOAD_LIBRARY_TYPE = "preferred_auto_download_library_type" private const val KEY_AUTO_DOWNLOAD_DELAYED = "auto_download_delayed" private const val KEY_PREFERRED_LIBRARY_ORDERING = "preferred_library_ordering" - private const val KEY_COLLAPSE_ON_FLING = "collapse_on_fling" private const val KEY_SOFTWARE_CODECS = "software_codecs" private const val KEY_HIDE_COMPLETED = "hide_completed" diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/composable/PlayingQueueComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/composable/PlayingQueueComposable.kt index 71249dd3..936dfe4e 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/composable/PlayingQueueComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/composable/PlayingQueueComposable.kt @@ -1,10 +1,14 @@ package org.grakovne.lissen.ui.screens.player.composable import android.view.ViewConfiguration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -18,7 +22,12 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography @@ -30,9 +39,11 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -66,7 +77,6 @@ fun PlayingQueueComposable( val book by viewModel.book.observeAsState() val searchToken by viewModel.searchToken.observeAsState("") - val collapseOnFling by viewModel.collapseOnFling.collectAsState(false) val showingChapters by remember { derivedStateOf { @@ -126,6 +136,34 @@ fun PlayingQueueComposable( label = "playing_queue_font_size", ) + var showCollapseFab by remember { mutableStateOf(false) } + + LaunchedEffect(playingQueueExpanded) { + if (!playingQueueExpanded) { + showCollapseFab = false + } + } + + val fabScrollConnection = + remember(playingQueueExpanded, collapseFlingThreshold) { + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + if (!playingQueueExpanded) return Velocity.Zero + + if (available.y > collapseFlingThreshold) { + showCollapseFab = true + return Velocity.Zero + } + + if (available.y < -collapseFlingThreshold) { + showCollapseFab = false + return Velocity.Zero + } + return Velocity.Zero + } + } + } + LaunchedEffect(currentTrackIndex) { awaitFrame() scrollPlayingQueue( @@ -136,122 +174,147 @@ fun PlayingQueueComposable( playingQueueExpanded = playingQueueExpanded, ) } - Column( + + Box( modifier = modifier .fillMaxSize() - .let { - when (playingQueueExpanded) { - true -> { - it.withScrollbar( - state = listState, - color = colorScheme.onBackground.copy(alpha = scrollbarAlpha), - totalItems = showingChapters.size, - ignoreItems = emptyList(), - ) - } - - false -> { - it - } - } - }.padding(horizontal = 16.dp), + .nestedScroll(fabScrollConnection), ) { - if (playingQueueExpanded.not()) { - Text( - text = provideNowPlayingTitle(libraryViewModel.fetchPreferredLibraryType(), context), - fontSize = fontSize.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 6.dp), - ) - - Spacer(modifier = Modifier.height(12.dp)) - } - - LazyColumn( - contentPadding = - when (playingQueueExpanded) { - true -> PaddingValues(bottom = 12.dp) - false -> PaddingValues(bottom = with(density) { collapsedPlayingQueueHeight.toDp() }) - }, + Column( modifier = Modifier - .fillMaxHeight() - .scrollable( - state = rememberScrollState(), - orientation = Orientation.Vertical, - enabled = playingQueueExpanded, - ).onGloballyPositioned { - if (collapsedPlayingQueueHeight == 0) { - collapsedPlayingQueueHeight = it.size.height - } - }.onSizeChanged { intSize -> - if (intSize.height != collapsedPlayingQueueHeight) { - coroutineScope.launch { - awaitFrame() - scrollPlayingQueue( - currentTrackIndex = currentTrackIndex, - listState = listState, - playbackReady = playbackReady, - animate = false, - playingQueueExpanded = playingQueueExpanded, + .fillMaxSize() + .let { + when (playingQueueExpanded) { + true -> { + it.withScrollbar( + state = listState, + color = colorScheme.onBackground.copy(alpha = scrollbarAlpha), + totalItems = showingChapters.size, + ignoreItems = emptyList(), ) } - } - }.nestedScroll( - object : NestedScrollConnection { - override fun onPreScroll( - available: Offset, - source: NestedScrollSource, - ): Offset = if (playingQueueExpanded) Offset.Zero else available - override suspend fun onPreFling(available: Velocity): Velocity { - if (available.y < -expandFlingThreshold && !playingQueueExpanded) { - viewModel.expandPlayingQueue() - return available - } - - if (available.y > collapseFlingThreshold && playingQueueExpanded && collapseOnFling) { - viewModel.collapsePlayingQueue() - return available - } - - return Velocity.Zero + false -> { + it } - }, - ), - state = listState, + } + }.padding(horizontal = 16.dp), ) { - val maxDuration = showingChapters.maxOfOrNull { it.duration } ?: 0.0 - - itemsIndexed(showingChapters) { index, chapter -> - val isCached by cachingModelView - .provideCacheState( - bookId = book?.id ?: "", - chapterId = chapter.id, - ).observeAsState(false) - - PlaylistItemComposable( - track = chapter, - onClick = { viewModel.setChapter(chapter) }, - isSelected = chapter.id == currentTrackId?.id, - modifier = Modifier.wrapContentWidth(), - maxDuration = maxDuration, - isCached = isCached, + if (playingQueueExpanded.not()) { + Text( + text = provideNowPlayingTitle(libraryViewModel.fetchPreferredLibraryType(), context), + fontSize = fontSize.sp, + fontWeight = FontWeight.SemiBold, + color = colorScheme.primary, + modifier = Modifier.padding(horizontal = 6.dp), ) - if (index < showingChapters.size - 1) { - HorizontalDivider( - thickness = 1.dp, - modifier = - Modifier - .padding(start = 24.dp) - .padding(vertical = 8.dp), + Spacer(modifier = Modifier.height(12.dp)) + } + + LazyColumn( + contentPadding = + when (playingQueueExpanded) { + true -> PaddingValues(bottom = 12.dp) + false -> PaddingValues(bottom = with(density) { collapsedPlayingQueueHeight.toDp() }) + }, + modifier = + Modifier + .fillMaxHeight() + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Vertical, + enabled = playingQueueExpanded, + ).onGloballyPositioned { + if (collapsedPlayingQueueHeight == 0) { + collapsedPlayingQueueHeight = it.size.height + } + }.onSizeChanged { intSize -> + if (intSize.height != collapsedPlayingQueueHeight) { + coroutineScope.launch { + awaitFrame() + scrollPlayingQueue( + currentTrackIndex = currentTrackIndex, + listState = listState, + playbackReady = playbackReady, + animate = false, + playingQueueExpanded = playingQueueExpanded, + ) + } + } + }.nestedScroll( + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset = if (playingQueueExpanded) Offset.Zero else available + + override suspend fun onPreFling(available: Velocity): Velocity { + if (available.y < -expandFlingThreshold && !playingQueueExpanded) { + viewModel.expandPlayingQueue() + return available + } + + return Velocity.Zero + } + }, + ), + state = listState, + ) { + val maxDuration = showingChapters.maxOfOrNull { it.duration } ?: 0.0 + + itemsIndexed(showingChapters) { index, chapter -> + val isCached by cachingModelView + .provideCacheState( + bookId = book?.id ?: "", + chapterId = chapter.id, + ).observeAsState(false) + + PlaylistItemComposable( + track = chapter, + onClick = { viewModel.setChapter(chapter) }, + isSelected = chapter.id == currentTrackId?.id, + modifier = Modifier.wrapContentWidth(), + maxDuration = maxDuration, + isCached = isCached, ) + + if (index < showingChapters.size - 1) { + HorizontalDivider( + thickness = 1.dp, + modifier = + Modifier + .padding(start = 24.dp) + .padding(vertical = 8.dp), + ) + } } } } + + AnimatedVisibility( + visible = playingQueueExpanded && showCollapseFab, + enter = fadeIn(), + exit = fadeOut(), + modifier = + Modifier + .align(Alignment.TopCenter) + .padding(top = 8.dp), + ) { + FloatingActionButton( + onClick = { viewModel.collapsePlayingQueue() }, + containerColor = colorScheme.surfaceContainer, + contentColor = colorScheme.primary, + elevation = FloatingActionButtonDefaults.loweredElevation(), + ) { + Icon( + imageVector = Icons.Filled.ExpandMore, + contentDescription = "Collapse queue", + ) + } + } } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt index b6621380..5b9cc3fd 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt @@ -51,7 +51,6 @@ fun AdvancedSettingsComposable( val cachingModelView: CachingModelView = hiltViewModel() val viewModel: SettingsViewModel = hiltViewModel() - val collapseOnFling by viewModel.collapseOnFling.observeAsState(false) val crashReporting by viewModel.crashReporting.observeAsState(true) val bypassSsl by viewModel.bypassSsl.observeAsState(false) @@ -109,12 +108,6 @@ fun AdvancedSettingsComposable( onclick = { navController.showSeekSettings() }, ) - SettingsToggleItem( - stringResource(R.string.settings_screen_collapse_on_fling_title), - stringResource(R.string.settings_screen_collapse_on_fling_description), - collapseOnFling, - ) { viewModel.preferCollapseOnFling(it) } - AdvancedSettingsNavigationItemComposable( title = stringResource(R.string.settings_screen_custom_headers_title), description = stringResource(R.string.settings_screen_custom_header_hint), diff --git a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/PlayerViewModel.kt b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/PlayerViewModel.kt index 8d9d12a9..cc3d03f7 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/PlayerViewModel.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/PlayerViewModel.kt @@ -49,7 +49,6 @@ class PlayerViewModel val searchToken: LiveData = _searchToken val isPlaying: LiveData = mediaRepository.isPlaying - val collapseOnFling = preferences.collapseOnFlingFlow val bookmarks = mediaRepository.bookmarks diff --git a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt index 2bdd6aa2..2ba95fbc 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/SettingsViewModel.kt @@ -90,8 +90,6 @@ class SettingsViewModel private val _hideCompleted = preferences.hideCompletedFlow val hideCompleted = _hideCompleted - val collapseOnFling = MutableLiveData(preferences.getCollapseOnFling()) - private val _autoDownloadDelayed = MutableLiveData(preferences.getAutoDownloadDelayed()) val autoDownloadDelayed = _autoDownloadDelayed @@ -218,11 +216,6 @@ class SettingsViewModel preferences.saveMaterialYouColors(value) } - fun preferCollapseOnFling(value: Boolean) { - collapseOnFling.postValue(value) - preferences.saveCollapseOnFling(value) - } - fun preferSoftwareCodecsEnabled(value: Boolean) { _softwareCodecsEnabled.postValue(value) preferences.saveSoftwareCodecsEnabled(value) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 084c8b06..5268a915 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -167,8 +167,6 @@ صيغة الملف الصوتي غير مدعومه إنشاء إشارة مرجعية إشارات مرجعية - طيّ قائمة التشغيل عند السحب السريع - اسحب لطي قائمة الفصول ألوان Material You مطابقة ألوان التطبيق لخلفية الشاشة إعادة التشغيل diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5156bf15..ee2d6f24 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -25,8 +25,6 @@ Typ připojení Lokální síť Vnější síť - Sbalit frontu gestem - Sbalit seznam kapitol přejetím prstem Host je mimo provoz Chybí URL adresa hostu Chybí přihlašovací jméno diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 639aa833..88ae9a57 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -159,8 +159,6 @@ SSL Zertifikatprüfung deaktivieren Verbindung zum Server mit jedem Zertifikat Maximum - Wiedergabeliste mittels swipe einklappen - Wische um die Kapitelliste einzuklappen Material You Farben Passe die Farben der App an dein Hintergrundbild an Software-Decodierung erzwingen diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 20563996..1241d1bd 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -161,8 +161,6 @@ Visoko Definiranje preuzimanja i upravljanje sadržajem Kontrola pretraživanja, proxy zaglavlje ili zvuk - Sažmi red za reprodukciju prilikom flistanja - Prijeđi prstom da bi sažeo popis poglavlja Uskladi boje aplikacije s pozadinom Onemogućeno Forsiraj softversko dekodiranje diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b2133d7c..e00dab91 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -159,8 +159,6 @@ SSL-ellenőrzés letiltása Csatlakozás bármilyen tanúsítvánnyal rendelkező kiszolgálókhoz Maximum - Lendítés közben a lejátszási sor összecsukása - Csúsztatás a fejezetlista összecsukásához Material You színek Az alkalmazás színeinek illesztése a háttérképhez Letiltva diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 7f04e186..40310de5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -159,8 +159,6 @@ Jeda singkat sebelum memulai unduhan Nonaktifkan verifikasi SSL Hubungkan ke server dengan sertifikat apa pun - Sembunyikan antrean putar saat usap cepat - Usap untuk menyembunyikan daftar chapter Warna Material You Sesuaikan warna aplikasi dengan wallpaper Nonaktif diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index cc3fef07..801ea9c8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -159,10 +159,8 @@ Massimo Disabilita verifica SSL Connetti ai server senza alcun certificato - Scorri per comprimere l\'elenco dei capitoli Colori Material You Abbina i colori dell\'app al tuo sfondo - Comprimi coda con scorrimento rapido Disabilitato Forza decodifica software Usa codec software per la riproduzione audio diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 66509cd9..36c441f7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -162,8 +162,6 @@ Внешняя сеть Игнорировать проверку SSL Подключаться с невалидным сертификатом - Сворачивать список глав свайпом - Смахните, чтобы свернуть очередь воспроизведения Цвета Material You Подбирать цвета приложения под обои Перезапустите приложение, чтобы применить новые кодеки diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e0fdab46..49d9e5c6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -161,8 +161,6 @@ 最大 根据当前壁纸自动调整应用的颜色主题 Material You 动态配色 - 快速滑动收起播放列表 - 向下滑动即可收起章节列表 禁用 强制使用软件解码 使用软件解码器播放音频 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68f8d690..6170173a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,8 +25,6 @@ Connection type Local network External network - Collapse playback queue on fling - Swipe to collapse the chapters list Host is down Host URL is missing Username is missing