diff --git a/CHANGELOG.md b/CHANGELOG.md index 7983d959..9be5fcc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.5.1 +- Fixed lyrics search issues +- Miscellaneous UI fixes + ## 6.5.0 - Updated Translations - Added new Card Style - Brat diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index f6611d30..18c92218 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -23,8 +23,8 @@ plugins { } val appName = "Rush" -val appVersionName = "6.5.0" -val appVersionCode = 6500 +val appVersionName = "6.5.1" +val appVersionCode = 6510 val gitHash = execute("git", "rev-parse", "HEAD").take(7) diff --git a/androidApp/src/main/java/com/shub39/rush/app/App.kt b/androidApp/src/main/java/com/shub39/rush/app/App.kt index f45def98..92b7afbf 100644 --- a/androidApp/src/main/java/com/shub39/rush/app/App.kt +++ b/androidApp/src/main/java/com/shub39/rush/app/App.kt @@ -74,6 +74,8 @@ fun App() { ChangelogSheet( currentLog = globalState.currentChangelog!!, onDismissRequest = { globalVM.onAction(GlobalAction.DismissChangelog) }, + showSupportButton = !globalState.isProUser, + onNavigateToPaywall = { backStack.add(Routes.PaywallPage) }, ) } diff --git a/desktopApp/src/commonMain/kotlin/com/shub39/rush/app/App.kt b/desktopApp/src/commonMain/kotlin/com/shub39/rush/app/App.kt index b9bb0587..e5034dd8 100644 --- a/desktopApp/src/commonMain/kotlin/com/shub39/rush/app/App.kt +++ b/desktopApp/src/commonMain/kotlin/com/shub39/rush/app/App.kt @@ -68,6 +68,8 @@ fun App() { ChangelogSheet( currentLog = globalState.currentChangelog!!, onDismissRequest = { globalVM.onAction(GlobalAction.DismissChangelog) }, + showSupportButton = false, + onNavigateToPaywall = {}, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 929b1913..fd415bce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.2.1" +agp = "9.3.0" minSdk = "29" targetSdk = "37" compileSdk = "37" @@ -8,15 +8,15 @@ colorpicker = "1.2.0" compose-multiplatform = "1.12.0-beta02" material3 = "1.12.0-alpha03" filekit = "0.14.2" -materialkolor = "4.1.1" +materialkolor = "5.0.0" splashscreen = "1.2.0" androidx-test-runner = "1.7.0" datastore = "1.2.1" koin = "4.2.2" -koin-plugin = "1.0.1" +koin-plugin = "1.0.2" ksoup = "0.2.6" ktor = "3.5.1" -kotlin = "2.4.0" +kotlin = "2.4.10" ksp = "2.3.9" landscapist = "2.11.0" activity-compose = "1.13.0" @@ -24,10 +24,10 @@ navigation = "1.1.1" kmpalette = "3.1.0" room3 = "3.0.0" serialization = "1.11.0" -androidx-lifecycle = "2.10.0" +androidx-lifecycle = "2.11.0" zoomable = "2.13.0" coroutines = "1.11.0" -purchases = "10.13.0" +purchases = "10.14.0" junit = "4.13.2" junit-version = "1.3.0" spotless = "8.8.0" diff --git a/shared/core/src/commonMain/kotlin/com/shub39/rush/shared/core/Util.kt b/shared/core/src/commonMain/kotlin/com/shub39/rush/shared/core/Util.kt index 7da4de21..a9a6f79d 100644 --- a/shared/core/src/commonMain/kotlin/com/shub39/rush/shared/core/Util.kt +++ b/shared/core/src/commonMain/kotlin/com/shub39/rush/shared/core/Util.kt @@ -22,27 +22,21 @@ inline fun > valueOfOrNull(name: String): T? { return enumEntries().firstOrNull { it.name == name } } +private val bracketPatterns = + listOf(Regex("""\s*\(.*?\)"""), Regex("""\s*\[.*?]"""), Regex("""\s*【.*?】""")) + private val titleCleanupPatterns = - listOf( - Regex( - """\s*\(.*?(official|video|audio|lyrics|lyric|visualizer|hd|hq|4k|remaster|remix|live|acoustic|version|edit|extended|radio|clean|explicit).*?\)""", - RegexOption.IGNORE_CASE, - ), - Regex( - """\s*\[.*?(official|video|audio|lyrics|lyric|visualizer|hd|hq|4k|remaster|remix|live|acoustic|version|edit|extended|radio|clean|explicit).*?]""", - RegexOption.IGNORE_CASE, - ), - Regex("""\s*【.*?】"""), - Regex("""\s*\|.*$"""), - Regex( - """\s*-\s*(official|video|audio|lyrics|lyric|visualizer).*$""", - RegexOption.IGNORE_CASE, - ), - Regex("""\s*\(feat\..*?\)""", RegexOption.IGNORE_CASE), - Regex("""\s*\(ft\..*?\)""", RegexOption.IGNORE_CASE), - Regex("""\s*feat\..*$""", RegexOption.IGNORE_CASE), - Regex("""\s*ft\..*$""", RegexOption.IGNORE_CASE), - ) + bracketPatterns + + listOf( + Regex("""\s*\|.*$"""), + Regex( + """\s*-\s*(official|video|audio|lyrics|lyric|visualizer).*$""", + RegexOption.IGNORE_CASE, + ), + Regex("""\s*feat\..*$""", RegexOption.IGNORE_CASE), + Regex("""\s*ft\..*$""", RegexOption.IGNORE_CASE), + ) + private val artistSeparators = listOf( " & ", @@ -60,6 +54,9 @@ private val artistSeparators = fun getMainArtist(artists: String): String { var cleaned = artists.trim() + for (pattern in bracketPatterns) { + cleaned = cleaned.replace(pattern, "") + } for (separator in artistSeparators) { if (cleaned.contains(separator, ignoreCase = true)) { cleaned = cleaned.split(separator, ignoreCase = true, limit = 2)[0] diff --git a/shared/core/src/commonTest/kotlin/TTMLParserTest.kt b/shared/core/src/commonTest/kotlin/TTMLParserTest.kt index f636ef14..28134d7e 100644 --- a/shared/core/src/commonTest/kotlin/TTMLParserTest.kt +++ b/shared/core/src/commonTest/kotlin/TTMLParserTest.kt @@ -14,8 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.shub39.rush.shared.core.util - +import com.shub39.rush.shared.core.util.TTMLParser import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse diff --git a/shared/core/src/commonTest/kotlin/UtilTest.kt b/shared/core/src/commonTest/kotlin/UtilTest.kt new file mode 100644 index 00000000..de831232 --- /dev/null +++ b/shared/core/src/commonTest/kotlin/UtilTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Shubham Gorai + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import com.shub39.rush.shared.core.getMainArtist +import com.shub39.rush.shared.core.getMainTitle +import kotlin.test.Test +import kotlin.test.assertEquals + +class UtilTest { + + @Test + fun testGetMainTitle() { + assertEquals("Song Title", getMainTitle("Song Title")) + assertEquals("Song Title", getMainTitle("Song Title (Official Video)")) + assertEquals("Song Title", getMainTitle("Song Title [Remix]")) + assertEquals("Song Title", getMainTitle("Song Title 【MV】")) + assertEquals("Song", getMainTitle("Song (Ver) [Live]")) + assertEquals("Song", getMainTitle("Song | something")) + assertEquals("Song", getMainTitle("Song - Official Video")) + assertEquals("Song", getMainTitle("Song feat. Someone")) + assertEquals("Song", getMainTitle("Song ft. Someone")) + } + + @Test + fun testGetMainArtist() { + assertEquals("Artist Name", getMainArtist("Artist Name")) + assertEquals("Artist Name", getMainArtist("Artist Name (Producer)")) + assertEquals("Artist A", getMainArtist("Artist A & Artist B")) + assertEquals("Artist A", getMainArtist("Artist A and Artist B")) + assertEquals("Artist A", getMainArtist("Artist A, Artist B")) + assertEquals("Artist A", getMainArtist("Artist A x Artist B")) + assertEquals("Artist A", getMainArtist("Artist A X Artist B")) + assertEquals("Artist A", getMainArtist("Artist A feat. Artist B")) + assertEquals("Artist A", getMainArtist("Artist A [Info] & Artist B")) + assertEquals("Artist A", getMainArtist("Artist A (Info) & Artist B")) + } +} diff --git a/shared/logic/src/commonMain/composeResources/files/changelog.json b/shared/logic/src/commonMain/composeResources/files/changelog.json index 5dd07d48..a05de333 100644 --- a/shared/logic/src/commonMain/composeResources/files/changelog.json +++ b/shared/logic/src/commonMain/composeResources/files/changelog.json @@ -1,4 +1,11 @@ [ + { + "version": "6.5.1", + "changes": [ + "Fixed lyrics search issues", + "Miscellaneous UI fixes" + ] + }, { "version": "6.5.0", "changes": [ @@ -68,11 +75,5 @@ "Added Refresh button to regenerate Messy Card", "Miscellaneous UI fixes" ] - }, - { - "version": "6.2.01", - "changes": [ - "Fixed UI bug in share cards" - ] } ] \ No newline at end of file diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/ChangelogSheet.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/ChangelogSheet.kt index 153770a1..1850a4e0 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/ChangelogSheet.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/ChangelogSheet.kt @@ -28,7 +28,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialShapes @@ -57,6 +58,8 @@ fun ChangelogSheet( modifier: Modifier = Modifier, currentLog: VersionEntry, onDismissRequest: () -> Unit, + showSupportButton: Boolean, + onNavigateToPaywall: () -> Unit, ) { RushBottomSheet(onDismissRequest = onDismissRequest, modifier = modifier, padding = 0.dp) { Column( @@ -115,8 +118,38 @@ fun ChangelogSheet( } } - Button(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) { - Text(text = stringResource(Res.string.done)) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + FilledTonalButton( + onClick = onDismissRequest, + shape = if (showSupportButton) leadingItemShape() else detachedItemShape(), + modifier = Modifier.height(ButtonDefaults.MediumContainerHeight).fillMaxWidth(), + ) { + Text( + text = stringResource(Res.string.done), + style = ButtonDefaults.textStyleFor(ButtonDefaults.MediumContainerHeight), + ) + } + + if (showSupportButton) { + FilledTonalButton( + onClick = { + onDismissRequest() + onNavigateToPaywall() + }, + shape = endItemShape(), + modifier = + Modifier.height(ButtonDefaults.MediumContainerHeight).fillMaxWidth(), + ) { + Text( + text = "Support Rush", + style = + ButtonDefaults.textStyleFor(ButtonDefaults.MediumContainerHeight), + ) + } + } } Spacer(modifier = Modifier.height(16.dp)) diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/FossPaywall.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/FossPaywall.kt index 7ba4e440..ef5eafb5 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/FossPaywall.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/component/FossPaywall.kt @@ -41,6 +41,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.shub39.rush.shared.ui.endItemShape +import com.shub39.rush.shared.ui.leadingItemShape import com.shub39.rush.shared.ui.theme.flexFontRounded import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -94,22 +96,51 @@ fun FossPaywall(modifier: Modifier = Modifier) { ) } - FilledTonalButton( - onClick = { uriHandler.openUri("https://buymeacoffee.com/shub39") }, - modifier = Modifier.height(ButtonDefaults.MediumContainerHeight), + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - Icon( - painter = painterResource(Res.drawable.buymeacoffee), - contentDescription = "Buy me a coffee", - modifier = Modifier.size(ButtonDefaults.MediumIconSize), - ) + FilledTonalButton( + onClick = { uriHandler.openUri("https://buymeacoffee.com/shub39") }, + shape = leadingItemShape(), + modifier = + Modifier.fillMaxWidth().height(ButtonDefaults.MediumContainerHeight), + ) { + Icon( + painter = painterResource(Res.drawable.buymeacoffee), + contentDescription = "Buy me a coffee", + modifier = Modifier.size(ButtonDefaults.MediumIconSize), + ) - Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) - Text( - text = stringResource(Res.string.bmc), - style = ButtonDefaults.textStyleFor(ButtonDefaults.MediumContainerHeight), - ) + Text( + text = stringResource(Res.string.bmc), + style = + ButtonDefaults.textStyleFor(ButtonDefaults.MediumContainerHeight), + ) + } + + FilledTonalButton( + onClick = { uriHandler.openUri("https://github.com/sponsors/shub39") }, + shape = endItemShape(), + modifier = + Modifier.fillMaxWidth().height(ButtonDefaults.MediumContainerHeight), + ) { + Icon( + painter = painterResource(Res.drawable.github), + contentDescription = "GitHub Sponsors", + modifier = Modifier.size(ButtonDefaults.MediumIconSize), + ) + + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + + Text( + text = "GitHub Sponsors", + style = + ButtonDefaults.textStyleFor(ButtonDefaults.MediumContainerHeight), + ) + } } } } diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/lyrics/component/PlainLyrics.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/lyrics/component/PlainLyrics.kt index 599786be..4e53df33 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/lyrics/component/PlainLyrics.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/lyrics/component/PlainLyrics.kt @@ -267,7 +267,10 @@ fun PlainLyrics( contentColor = cardContent, ), ) { - Text(text = stringResource(Res.string.source)) + Icon( + painter = painterResource(Res.drawable.genius), + contentDescription = "Genius Source", + ) } IconButton(onClick = { action(LyricsPageAction.OnToggleSearchSheet) }) { diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/setting/section/About.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/setting/section/About.kt index 649b57a3..9f4b32f9 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/setting/section/About.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/setting/section/About.kt @@ -64,6 +64,7 @@ import com.shub39.rush.shared.ui.detachedItemShape import com.shub39.rush.shared.ui.endItemShape import com.shub39.rush.shared.ui.leadingItemShape import com.shub39.rush.shared.ui.listItemColors +import com.shub39.rush.shared.ui.middleItemShape import com.shub39.rush.shared.ui.setting.component.LicenseBottomSheet import com.shub39.rush.shared.ui.theme.flexFontEmphasis import com.shub39.rush.shared.ui.theme.flexFontRounded @@ -169,6 +170,28 @@ private fun LazyListScope.engagementLinks(uriHandler: UriHandler) { uriHandler.openUri("https://buymeacoffee.com/shub39") }, ) + ListItem( + colors = listItemColors(), + leadingContent = { + Icon( + painter = painterResource(Res.drawable.github), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + trailingContent = { + Icon( + painter = painterResource(Res.drawable.open_link), + contentDescription = null, + ) + }, + headlineContent = { Text(text = "GitHub Sponsors") }, + supportingContent = { Text(text = "Support me through GitHub") }, + modifier = + Modifier.clip(middleItemShape()).clickable { + uriHandler.openUri("https://github.com/sponsors/shub39") + }, + ) ListItem( colors = listItemColors(), leadingContent = { diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/GlobalVM.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/GlobalVM.kt index 33cdc9a8..6a1a525a 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/GlobalVM.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/GlobalVM.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -99,8 +100,15 @@ class GlobalVM( private fun checkChangelog() { viewModelScope.launch { - val changeLogs = changelogManager.changelogs.first() val lastShownChangelog = otherPreferences.getLastChangelogShown().first() + val changeLogs = changelogManager.changelogs.first() + + if (lastShownChangelog.isBlank()) { + changeLogs.firstOrNull()?.version?.let { + otherPreferences.updateLastChangelogShown(it) + } + return@launch // don't show changelog on first install + } if (lastShownChangelog != changeLogs.firstOrNull()?.version) { _state.update { it.copy(currentChangelog = changeLogs.firstOrNull()) } diff --git a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/SearchSheetVM.kt b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/SearchSheetVM.kt index 112b123f..0a9fa9d9 100644 --- a/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/SearchSheetVM.kt +++ b/shared/ui/src/commonMain/kotlin/com/shub39/rush/shared/ui/viewmodels/SearchSheetVM.kt @@ -22,6 +22,7 @@ import com.shub39.rush.shared.core.Result import com.shub39.rush.shared.core.dataclasses.ExtractedColors import com.shub39.rush.shared.core.dataclasses.SearchResult import com.shub39.rush.shared.core.enums.Sources +import com.shub39.rush.shared.core.getMainArtist import com.shub39.rush.shared.core.getMainTitle import com.shub39.rush.shared.core.interfaces.SongRepository import com.shub39.rush.shared.core.listener.MediaListener @@ -58,6 +59,9 @@ class SearchSheetVM( @Provided private val repo: SongRepository, ) : ViewModel() { private var lyricsSearchStateJob: Job? = null + private var searchJob: Job? = null + private var fetchJob: Job? = null + private var observeSongInfoJob: Job? = null private val _state = stateLayer.searchSheetState private val _lastSearched = MutableStateFlow("") @@ -67,25 +71,41 @@ class SearchSheetVM( .asStateFlow() .onStart { observeSearchSheet() - observeSongInfo() + observeAutoChange() } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SearchSheetState()) - private fun observeSongInfo() { - viewModelScope.launch { - MediaListener.songInfoFlow.collect { songInfo -> - stateLayer.lyricsState.update { - it.copy( - playingSong = - it.playingSong.copy(title = songInfo.first, artist = songInfo.second) - ) - } + private fun observeAutoChange() { + stateLayer.lyricsState + .map { it.autoChange } + .distinctUntilChanged() + .onEach { state -> if (state) observeSongInfo() else observeSongInfoJob?.cancel() } + .launchIn(viewModelScope) + } - if (stateLayer.lyricsState.value.autoChange) { - searchSong("${songInfo.first} ${songInfo.second}".trim()) - } + private fun observeSongInfo() { + observeSongInfoJob?.cancel() + observeSongInfoJob = + viewModelScope.launch { + MediaListener.songInfoFlow + .distinctUntilChanged() + .onEach { songInfo -> + val mainTitle = getMainTitle(songInfo.first) + val mainArtist = getMainArtist(songInfo.second) + + stateLayer.lyricsState.update { + it.copy( + playingSong = + it.playingSong.copy(title = mainTitle, artist = mainArtist) + ) + } + + if (stateLayer.lyricsState.value.autoChange) { + searchSong("$mainTitle $mainArtist".trim()) + } + } + .launchIn(this) } - } } fun onAction(action: SearchSheetAction) { @@ -132,135 +152,148 @@ class SearchSheetVM( .launchIn(viewModelScope) } - private suspend fun searchSong( + private fun searchSong( query: String, fetch: Boolean = stateLayer.lyricsState.value.autoChange, ) { - if (query.isEmpty() || query == _lastSearched.value || _state.value.isSearching) return + searchJob?.cancel() - _state.update { it.copy(isSearching = true, error = null) } + searchJob = + viewModelScope.launch { + if (query.isEmpty() || query == _lastSearched.value) return@launch - stateLayer.lyricsState.update { it.copy(searchState = SearchState.Searching(query)) } + _state.update { it.copy(isSearching = true, error = null) } - try { - when (val result = repo.searchGenius(query)) { - is Result.Error -> { - _state.update { it.copy(error = errorStringRes(result.error)) } + stateLayer.lyricsState.update { + it.copy(searchState = SearchState.Searching(query)) } - is Result.Success -> { - _state.update { it.copy(searchResults = result.data, error = null) } - } - } - } finally { - _state.update { it.copy(isSearching = false) } - stateLayer.lyricsState.update { it.copy(searchState = SearchState.Idle) } - _lastSearched.update { query } - } + try { + when (val result = repo.searchGenius(query)) { + is Result.Error -> { + _state.update { it.copy(error = errorStringRes(result.error)) } + } - if ( - fetch && - _state.value.searchResults.isNotEmpty() && - query.contains(_state.value.searchResults.first().title.trim(), ignoreCase = true) - ) { - fetchLyrics(_state.value.searchResults.first().id) - } else { - stateLayer.lyricsState.update { - it.copy(searchState = SearchState.UserPrompt, sync = false) - } - - lyricsSearchStateJob?.cancel() - lyricsSearchStateJob = - viewModelScope.launch { - delay(5000.milliseconds) + is Result.Success -> { + _state.update { it.copy(searchResults = result.data, error = null) } + _lastSearched.update { query } + } + } + } finally { + _state.update { it.copy(isSearching = false) } + stateLayer.lyricsState.update { it.copy(searchState = SearchState.Idle) } + } + if ( + fetch && + _state.value.searchResults.isNotEmpty() && + query.contains( + _state.value.searchResults.first().title.trim(), + ignoreCase = true, + ) + ) { + fetchLyrics(_state.value.searchResults.first().id) + } else { stateLayer.lyricsState.update { - if (it.searchState == SearchState.UserPrompt) - it.copy(searchState = SearchState.Idle) - else it + it.copy(searchState = SearchState.UserPrompt, sync = false) } + + lyricsSearchStateJob?.cancel() + lyricsSearchStateJob = + viewModelScope.launch { + delay(5000.milliseconds) + + stateLayer.lyricsState.update { + if (it.searchState == SearchState.UserPrompt) + it.copy(searchState = SearchState.Idle) + else it + } + } } - } + } } - private suspend fun fetchLyrics(songId: Long) { - if (stateLayer.lyricsState.value.lyricsState is LyricsState.Fetching) return + private fun fetchLyrics(songId: Long) { + fetchJob?.cancel() - val song = - _state.value.searchResults.find { it.id == songId } - ?: _state.value.localSearchResults.find { it.id == songId } - ?: return - - stateLayer.lyricsState.update { - it.copy( - lyricsState = LyricsState.Fetching("${song.title} - ${song.artist}"), - extractedColors = ExtractedColors(), - searchState = SearchState.Idle, - sync = false, - ) - } + fetchJob = + viewModelScope.launch { + val song = + _state.value.searchResults.find { it.id == songId } + ?: _state.value.localSearchResults.find { it.id == songId } + ?: return@launch - if (songId in stateLayer.savedPageState.value.songsAsc.map { it.id }) { - val result = repo.getSong(songId).toSongUi() - - stateLayer.lyricsState.update { - it.copy( - lyricsState = LyricsState.Loaded(song = result), - source = if (result.lyrics.isNotEmpty()) Sources.LRCLIB else Sources.GENIUS, - syncedAvailable = result.syncedLyrics != null || result.ttmlLyrics != null, - sync = - (result.syncedLyrics != null || result.ttmlLyrics != null) && - (getMainTitle(it.playingSong.title) - .trim() - .equals(getMainTitle(result.title).trim(), ignoreCase = true)), - selectedLines = emptyMap(), - ) - } - - stateLayer.savedPageState.update { it.copy(currentSong = result) } - } else { - when (val result = repo.fetchSong(song)) { - is Result.Error -> { - stateLayer.lyricsState.update { - it.copy( - lyricsState = - LyricsState.LyricsError( - errorCode = errorStringRes(result.error), - debugMessage = result.message, - ) - ) - } + stateLayer.lyricsState.update { + it.copy( + lyricsState = LyricsState.Fetching("${song.title} - ${song.artist}"), + extractedColors = ExtractedColors(), + searchState = SearchState.Idle, + sync = false, + ) } - is Result.Success -> { - val retrievedSong = result.data.toSongUi() + if (songId in stateLayer.savedPageState.value.songsAsc.map { it.id }) { + val result = repo.getSong(songId).toSongUi() stateLayer.lyricsState.update { it.copy( - lyricsState = LyricsState.Loaded(song = retrievedSong), + lyricsState = LyricsState.Loaded(song = result), source = - if (retrievedSong.lyrics.isNotEmpty()) Sources.LRCLIB - else Sources.GENIUS, + if (result.lyrics.isNotEmpty()) Sources.LRCLIB else Sources.GENIUS, syncedAvailable = - retrievedSong.syncedLyrics != null || - retrievedSong.ttmlLyrics != null, - sync = - (retrievedSong.syncedLyrics != null || - retrievedSong.ttmlLyrics != null) && - (getMainTitle(it.playingSong.title) - .trim() - .equals( - getMainTitle(retrievedSong.title).trim(), - ignoreCase = true, - )), + result.syncedLyrics != null || result.ttmlLyrics != null, + sync = (result.syncedLyrics != null || result.ttmlLyrics != null), selectedLines = emptyMap(), ) } - stateLayer.savedPageState.update { it.copy(currentSong = retrievedSong) } + stateLayer.savedPageState.update { it.copy(currentSong = result) } + } else { + when (val result = repo.fetchSong(song)) { + is Result.Error -> { + stateLayer.lyricsState.update { + it.copy( + lyricsState = + LyricsState.LyricsError( + errorCode = errorStringRes(result.error), + debugMessage = result.message, + ) + ) + } + } + + is Result.Success -> { + val retrievedSong = result.data.toSongUi() + + stateLayer.lyricsState.update { + it.copy( + lyricsState = LyricsState.Loaded(song = retrievedSong), + source = + if (retrievedSong.lyrics.isNotEmpty()) Sources.LRCLIB + else Sources.GENIUS, + syncedAvailable = + retrievedSong.syncedLyrics != null || + retrievedSong.ttmlLyrics != null, + sync = + (retrievedSong.syncedLyrics != null || + retrievedSong.ttmlLyrics != null) && + (getMainTitle(it.playingSong.title) + .trim() + .equals( + getMainTitle(retrievedSong.title).trim(), + ignoreCase = true, + )), + selectedLines = emptyMap(), + ) + } + + stateLayer.savedPageState.update { + it.copy(currentSong = retrievedSong) + } + } + } } } - } } private suspend fun localSearch(query: String): List {