From 7d9439aae3a02793acf8331ec5f160c57b29f72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 14 Nov 2024 16:34:27 +0100 Subject: [PATCH 01/10] feat: Added Quick Lyrics Search Activity Added a new activity for quick lyrics search that can be launched from an intent. The activity fetches lyrics for a given song and artist using the LyricsProviderService. The user can then send the lyrics back to the calling app. This feature is useful for quickly sharing lyrics without having to open the main app. --- .idea/misc.xml | 2 +- app/src/main/AndroidManifest.xml | 16 +++ .../quicksearch/QuickLyricsSearchActivity.kt | 108 ++++++++++++++++++ .../quicksearch/QuickLyricsSearchPage.kt | 72 ++++++++++++ .../viewmodel/QuickLyricsSearchViewModel.kt | 104 +++++++++++++++++ .../QuickLyricsSearchViewModelFactory.kt | 19 +++ .../songsync/ui/screens/home/HomeScreen.kt | 40 +++++++ .../pl/lambada/songsync/util/ResourceState.kt | 35 ++++++ .../pl/lambada/songsync/util/ScreenState.kt | 28 +++++ app/src/main/res/values/themes.xml | 9 ++ 10 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModelFactory.kt create mode 100644 app/src/main/java/pl/lambada/songsync/util/ResourceState.kt create mode 100644 app/src/main/java/pl/lambada/songsync/util/ScreenState.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..2e7eabd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2478318..4c5eb73 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,22 @@ + + + + + + + + + + + // Remove any padding from the view + v.setPadding(0, 0, 0, 0) + insets + } + + // Configure the window properties + window.run { + // Set the background to be transparent + setBackgroundDrawable(ColorDrawable(0)) + // Set the window layout to match the parent dimensions + setLayout( + WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT + ) + // Set the window type based on the Android version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // For Android O and above, use TYPE_APPLICATION_OVERLAY + setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) + } else { + // For older versions, use TYPE_SYSTEM_ALERT + setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT) + } + } + + handleShareIntent(intent) + + setContent { + val sheetState = rememberModalBottomSheetState() + val viewModelState = viewModel.state.collectAsStateWithLifecycle() + SongSyncTheme(pureBlack = userSettingsController.pureBlack) { + ModalBottomSheet( + sheetState = sheetState, + properties = ModalBottomSheetDefaults.properties, + onDismissRequest = { this.finish() } + ) { + QuickLyricsSearchPage( + state = viewModelState, + onSendLyrics = { lyrics -> + val resultIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra("lyrics", lyrics) + type = "text/plain" + } + setResult(RESULT_OK, resultIntent) + finish() + } + ) + } + } + } + } + + + private fun handleShareIntent(intent: Intent) { + when (intent.action) { + Intent.ACTION_SEND -> { + val songName = intent.getStringExtra("songName") ?: "" //TODO: Change to a exception in the VM + val artistName = intent.getStringExtra("artistName") ?: "" //TODO: Change to a exception in the VM + + viewModel.onEvent(QuickLyricsSearchViewModel.Event.Fetch(songName to artistName, this)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt new file mode 100644 index 0000000..c09991e --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt @@ -0,0 +1,72 @@ +package pl.lambada.songsync.activities.quicksearch + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import pl.lambada.songsync.activities.quicksearch.viewmodel.QuickLyricsSearchViewModel +import pl.lambada.songsync.util.ResourceState +import pl.lambada.songsync.util.ScreenState + +@Composable +fun QuickLyricsSearchPage( + state: State, + onSendLyrics: (String) -> Unit +) { + Crossfade(state.value.screenState) { pageState -> + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Search query: ${state.value.song}") + when(pageState) { + is ScreenState.Loading -> { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is ScreenState.Success -> { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Success: ${pageState.data}") + when(state.value.lyricsState) { + is ResourceState.Loading<*> -> { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is ResourceState.Success<*> -> { + state.value.lyricsState.data?.let { obtainedLyrics -> //This crunches the animation lol + Button(onClick = { onSendLyrics(obtainedLyrics) }) { + Text("Send lyrics back") + } + Text("Lyrics: $obtainedLyrics") + } + } + is ResourceState.Error<*> -> { + Text("Error: ${state.value.lyricsState.message}") + } + } + } + } + is ScreenState.Error -> { + Text("Error: ${pageState.exception.message}") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt new file mode 100644 index 0000000..062fd4b --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt @@ -0,0 +1,104 @@ +package pl.lambada.songsync.activities.quicksearch.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pl.lambada.songsync.data.UserSettingsController +import pl.lambada.songsync.data.remote.lyrics_providers.LyricsProviderService +import pl.lambada.songsync.domain.model.SongInfo +import pl.lambada.songsync.util.ResourceState +import pl.lambada.songsync.util.ScreenState +import pl.lambada.songsync.util.ext.getVersion + +class QuickLyricsSearchViewModel( + val userSettingsController: UserSettingsController, + private val lyricsProviderService: LyricsProviderService +): ViewModel() { + + private val mutableState = MutableStateFlow(QuickSearchViewState()) + val state = mutableState.asStateFlow() + + data class QuickSearchViewState( + val song: Pair? = null, // Pair of song title and artist's name + val screenState: ScreenState = ScreenState.Loading, + val lyricsState: ResourceState = ResourceState.Loading() + ) + + private fun fetchSongData(song: Pair, context: Context) { + updateScreenState(ScreenState.Loading) + viewModelScope.launch(Dispatchers.IO) { + val result = lyricsProviderService + .getSongInfo( + query = SongInfo(song.first, song.second), + offset = 0, + provider = userSettingsController.selectedProvider + ) + ?: return@launch updateScreenState(ScreenState.Error(Exception("Error fetching lyrics for the song."))) + + updateScreenState(ScreenState.Success(result)) + fetchLyrics(result.songLink, context) + } + } + + private fun fetchLyrics(songLink: String?, context: Context) { + updateLyricsState(ResourceState.Loading()) + viewModelScope.launch(Dispatchers.IO) { + val syncedLyrics = getSyncedLyrics( + link = songLink, + version = context.getVersion() + ) + + if(syncedLyrics != null) { + updateLyricsState(ResourceState.Success(syncedLyrics)) + } else { + updateLyricsState(ResourceState.Error("Error fetching lyrics for the song.")) + } + } + } + + private suspend fun getSyncedLyrics(link: String?, version: String): String? = + lyricsProviderService.getSyncedLyrics( + link, + version, + userSettingsController.selectedProvider, + userSettingsController.includeTranslation, + userSettingsController.multiPersonWordByWord, + userSettingsController.syncedMusixmatch + ) + + private fun updateScreenState(screenState: ScreenState) { + if(screenState != mutableState.value.screenState) { + mutableState.update { + it.copy(screenState = screenState) + } + } + } + + private fun updateLyricsState(lyricsState: ResourceState) { + if(lyricsState != mutableState.value.lyricsState) { + mutableState.update { + it.copy(lyricsState = lyricsState) + } + } + } + + fun onEvent(event: Event) { + when(event) { + is Event.Fetch -> { + mutableState.update { + it.copy(song = event.song) + } + fetchSongData(event.song, event.context) + } + } + } + + interface Event { + data class Fetch(val song: Pair, val context: Context): Event + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModelFactory.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModelFactory.kt new file mode 100644 index 0000000..eac6e39 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModelFactory.kt @@ -0,0 +1,19 @@ +package pl.lambada.songsync.activities.quicksearch.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import pl.lambada.songsync.data.UserSettingsController +import pl.lambada.songsync.data.remote.lyrics_providers.LyricsProviderService + +class QuickLyricsSearchViewModelFactory( + private val userSettingsController: UserSettingsController, + private val lyricsProviderService: LyricsProviderService +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(QuickLyricsSearchViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return QuickLyricsSearchViewModel(userSettingsController, lyricsProviderService) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt index 10a463b..26003de 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt @@ -1,5 +1,9 @@ package pl.lambada.songsync.ui.screens.home +import android.app.Activity +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -15,11 +19,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import pl.lambada.songsync.ui.LyricsFetchScreen @@ -170,6 +177,9 @@ fun HomeScreenLoaded( animatedVisibilityScope: AnimatedVisibilityScope, ) { val context = LocalContext.current + var lyrics by remember { + mutableStateOf("Awaiting lyrics...") + } Column { if (isBatchDownload) { @@ -184,6 +194,36 @@ fun HomeScreenLoaded( horizontalAlignment = Alignment.CenterHorizontally, contentPadding = scaffoldPadding ) { +// item { +// val launcher = rememberLauncherForActivityResult( +// contract = ActivityResultContracts.StartActivityForResult() +// ) { result -> +// if (result.resultCode == Activity.RESULT_OK) { +// val receivedLyrics = result.data?.getStringExtra("lyrics") +// if (receivedLyrics != null) { +// lyrics = receivedLyrics +// } +// } +// } +// Button( +// onClick = { +// val intent = Intent("android.intent.action.SEND").apply { +// putExtra("songName", "Faded") +// putExtra("artistName", "Alan Walker") +// type = "text/plain" +// setPackage("pl.lambada.songsync") +// } +// launcher.launch(intent) +// } +// ) { +// Text("Launch intent") +// } +// } +// +// item { +// Text(lyrics) +// } + item { Column( modifier = Modifier.padding( diff --git a/app/src/main/java/pl/lambada/songsync/util/ResourceState.kt b/app/src/main/java/pl/lambada/songsync/util/ResourceState.kt new file mode 100644 index 0000000..5fb5b46 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/util/ResourceState.kt @@ -0,0 +1,35 @@ +package pl.lambada.songsync.util + +/** + * A sealed class representing the state of a resource. + * + * @param T The type of data associated with the resource state. + * @property data The data associated with the resource state. + * @property message An optional message associated with the resource state. + */ +sealed class ResourceState(val data: T? = null, val message: String? = null) { + /** + * Represents a loading state. + * + * @param T The type of data. + * @property data The data associated with the loading state. + */ + class Loading(data: T? = null) : ResourceState(data) + + /** + * Represents a success state with optional data. + * + * @param T The type of data. + * @property data The data associated with the success state. + */ + class Success(data: T?) : ResourceState(data) + + /** + * Represents an error state with an optional message and data. + * + * @param T The type of data. + * @property message The message associated with the error state. + * @property data The data associated with the error state. + */ + class Error(message: String, data: T? = null) : ResourceState(data, message) +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/util/ScreenState.kt b/app/src/main/java/pl/lambada/songsync/util/ScreenState.kt new file mode 100644 index 0000000..eb61c30 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/util/ScreenState.kt @@ -0,0 +1,28 @@ +package pl.lambada.songsync.util + +/** + * A sealed class representing the state of a screen. + * + * @param T The type of data associated with the success state. + */ +sealed class ScreenState { + /** + * Represents a loading state. + */ + data object Loading : ScreenState() + + /** + * Represents a success state with optional data. + * + * @param T The type of data. + * @property data The data associated with the success state. + */ + data class Success(val data: T?) : ScreenState() + + /** + * Represents an error state with an exception. + * + * @property exception The exception associated with the error state. + */ + data class Error(val exception: Throwable) : ScreenState() +} \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index afa13d7..f64b09c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,4 +2,13 @@ \ No newline at end of file From 85f59d544e06b078eb5df3017aa8faf1d370c7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 14 Nov 2024 23:33:04 +0100 Subject: [PATCH 02/10] Refactor: Removed window insets handling in QuickLyricsSearchActivity Removed custom window insets handling code and system window insets disabling. This simplifies the activity's layout and removes unnecessary code for managing window insets. --- .../quicksearch/QuickLyricsSearchActivity.kt | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt index a732e74..63cb044 100644 --- a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt @@ -37,35 +37,6 @@ class QuickLyricsSearchActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() - - // Disable the default system window insets handling - WindowCompat.setDecorFitsSystemWindows(window, false) - - // Set a listener to handle window insets manually - ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets -> - // Remove any padding from the view - v.setPadding(0, 0, 0, 0) - insets - } - - // Configure the window properties - window.run { - // Set the background to be transparent - setBackgroundDrawable(ColorDrawable(0)) - // Set the window layout to match the parent dimensions - setLayout( - WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT - ) - // Set the window type based on the Android version - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // For Android O and above, use TYPE_APPLICATION_OVERLAY - setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) - } else { - // For older versions, use TYPE_SYSTEM_ALERT - setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT) - } - } - handleShareIntent(intent) setContent { From 5a6e4a891a60c863ebbf27ac31088a8d7730f4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Fri, 15 Nov 2024 02:10:35 +0100 Subject: [PATCH 03/10] Refactor: Improved UI of Quick Search Page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigned the Quick Search page UI with new components and structure, enhancing user experience. Added quick access buttons for sending lyrics and navigating to settings. Improved lyrics display with a scrollable and expandable section. Integrated album art using Coil library for improved visual appeal. Added animations for smooth transitions. Updated string resources for better localization. Signed-off-by: Gabriel Fontán --- .idea/inspectionProfiles/Project_Default.xml | 5 + .idea/misc.xml | 2 +- .idea/runConfigurations.xml | 4 + .../quicksearch/QuickLyricsSearchActivity.kt | 33 ++- .../quicksearch/QuickLyricsSearchPage.kt | 207 +++++++++++++++--- .../components/ExpandableOutlinedCard.kt | 109 +++++++++ .../components/QuickLyricsSongInfo.kt | 126 +++++++++++ .../quicksearch/components/SquaredButton.kt | 105 +++++++++ .../components/SyncedLyricsLine.kt | 59 +++++ .../viewmodel/QuickLyricsSearchViewModel.kt | 12 +- .../songsync/ui/screens/home/HomeScreen.kt | 58 ++--- .../pl/lambada/songsync/util/LyricsUtils.kt | 18 ++ app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/themes.xml | 2 +- 14 files changed, 676 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/ExpandableOutlinedCard.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/QuickLyricsSongInfo.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SquaredButton.kt create mode 100644 app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SyncedLyricsLine.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 6195b36..e0da3ee 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -3,15 +3,19 @@ diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt index 63cb044..9aac814 100644 --- a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchActivity.kt @@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheetDefaults @@ -16,6 +17,12 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.disk.DiskCache +import coil.imageLoader +import coil.memory.MemoryCache +import kotlinx.coroutines.Dispatchers import pl.lambada.songsync.activities.quicksearch.viewmodel.QuickLyricsSearchViewModel import pl.lambada.songsync.activities.quicksearch.viewmodel.QuickLyricsSearchViewModelFactory import pl.lambada.songsync.data.UserSettingsController @@ -23,7 +30,7 @@ import pl.lambada.songsync.data.remote.lyrics_providers.LyricsProviderService import pl.lambada.songsync.ui.theme.SongSyncTheme import pl.lambada.songsync.util.dataStore -class QuickLyricsSearchActivity : ComponentActivity() { +class QuickLyricsSearchActivity : AppCompatActivity() { val userSettingsController = UserSettingsController(dataStore) private val lyricsProviderService = LyricsProviderService() @@ -36,6 +43,26 @@ class QuickLyricsSearchActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + activityImageLoader = ImageLoader.Builder(this) + .memoryCache { + MemoryCache.Builder(this) + .maxSizePercent(0.35) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(this.cacheDir.resolve("image_cache")) + .maxSizeBytes(7 * 1024 * 1024) + .build() + } + .respectCacheHeaders(false) + .allowHardware(true) + .crossfade(true) + .bitmapFactoryMaxParallelism(12) + .dispatcher(Dispatchers.IO) + .build() + + enableEdgeToEdge() handleShareIntent(intent) @@ -76,4 +103,8 @@ class QuickLyricsSearchActivity : ComponentActivity() { } } } + + companion object { + lateinit var activityImageLoader: ImageLoader + } } \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt index c09991e..c5412f1 100644 --- a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/QuickLyricsSearchPage.kt @@ -1,71 +1,208 @@ package pl.lambada.songsync.activities.quicksearch +import android.util.Log import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Send +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.rounded.Subtitles import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import pl.lambada.songsync.R +import pl.lambada.songsync.activities.quicksearch.components.ButtonWithIconAndText +import pl.lambada.songsync.activities.quicksearch.components.ExpandableOutlinedCard +import pl.lambada.songsync.activities.quicksearch.components.QuickLyricsSongInfo +import pl.lambada.songsync.activities.quicksearch.components.SyncedLyricsColumn import pl.lambada.songsync.activities.quicksearch.viewmodel.QuickLyricsSearchViewModel import pl.lambada.songsync.util.ResourceState import pl.lambada.songsync.util.ScreenState +import pl.lambada.songsync.util.parseLyrics @Composable fun QuickLyricsSearchPage( state: State, onSendLyrics: (String) -> Unit ) { - Crossfade(state.value.screenState) { pageState -> - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("Search query: ${state.value.song}") - when(pageState) { - is ScreenState.Loading -> { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Crossfade(state.value.screenState) { pageState -> + Row { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth() ) { - CircularProgressIndicator() + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(R.string.showing_lyrics_for).uppercase(), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + letterSpacing = 2.sp + ) + ) + + state.value.song?.let { song -> + Text( + text = song.first, + style = MaterialTheme.typography.headlineSmall + ) + Text( + text = buildAnnotatedString { + append(stringResource(R.string.by)) + append(" ") + withStyle(MaterialTheme.typography.titleMedium.toSpanStyle()) { + append(song.second) + } + }, + ) + } + } + Row( + modifier = Modifier + .weight(1f) + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + ButtonWithIconAndText( + icon = Icons.AutoMirrored.Rounded.Send, + text = stringResource(R.string.accept), + modifier = Modifier + .weight(1f), + onClick = { onSendLyrics(state.value.lyricsState.data!!) }, + enabled = state.value.lyricsState is ResourceState.Success, + shape = RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp) + ) + ButtonWithIconAndText( + icon = Icons.Filled.Settings, + text = stringResource(R.string.settings), + modifier = Modifier + .weight(1f), + shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp) + ) + } } - } - is ScreenState.Success -> { - Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) { - Text("Success: ${pageState.data}") - when(state.value.lyricsState) { - is ResourceState.Loading<*> -> { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + + HorizontalDivider() + + when (pageState) { + is ScreenState.Loading -> { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - is ResourceState.Success<*> -> { - state.value.lyricsState.data?.let { obtainedLyrics -> //This crunches the animation lol - Button(onClick = { onSendLyrics(obtainedLyrics) }) { - Text("Send lyrics back") + } + + is ScreenState.Success -> { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + pageState.data?.let { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + imageVector = Icons.Filled.Cloud, + contentDescription = null, + ) + Text( + text = stringResource(R.string.cloud_song).uppercase(), + style = MaterialTheme.typography.bodyMedium.copy( + letterSpacing = 1.sp, + fontWeight = FontWeight.SemiBold + ) + ) + } + + QuickLyricsSongInfo( + songInfo = pageState.data, + modifier = Modifier.fillMaxWidth() + ) + } + when (state.value.lyricsState) { + is ResourceState.Loading<*> -> { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is ResourceState.Success<*> -> { + state.value.lyricsState.data?.let { _ -> //This crunches the animation lol + ExpandableOutlinedCard( + title = stringResource(R.string.song_lyrics), + subtitle = stringResource(R.string.lyrics_subtitle), + icon = Icons.Rounded.Subtitles, + isExpanded = false, + modifier = Modifier.fillMaxWidth() + ) { + SyncedLyricsColumn( + lyricsList = state.value.parsedLyrics, + modifier = Modifier.fillMaxWidth().padding(8.dp) + ) + } + } + } + + is ResourceState.Error<*> -> { + Text("Error: ${state.value.lyricsState.message}") + } } - Text("Lyrics: $obtainedLyrics") } + } - is ResourceState.Error<*> -> { - Text("Error: ${state.value.lyricsState.message}") - } + } + + is ScreenState.Error -> { + Text("Error: ${pageState.exception.message}") } } } - is ScreenState.Error -> { - Text("Error: ${pageState.exception.message}") - } } } } diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/ExpandableOutlinedCard.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/ExpandableOutlinedCard.kt new file mode 100644 index 0000000..058e2fc --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/ExpandableOutlinedCard.kt @@ -0,0 +1,109 @@ +package pl.lambada.songsync.activities.quicksearch.components + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ExpandLess +import androidx.compose.material.icons.outlined.PermDeviceInformation +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun ExpandableOutlinedCard( + modifier: Modifier = Modifier, + isExpanded: Boolean = false, + title: String, + subtitle: String, + icon: ImageVector, + content: @Composable () -> Unit, +) { + var expanded by rememberSaveable { mutableStateOf(isExpanded) } + + val animatedDegree = + animateFloatAsState(targetValue = if (expanded) 0f else -180f, label = "Button Rotation") + + OutlinedCard( + modifier = modifier.animateContentSize(), + onClick = { expanded = !expanded }, + colors = CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + ), + shape = MaterialTheme.shapes.small + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.weight(0.1f), + imageVector = icon, + contentDescription = null, + ) + Column( + modifier = Modifier.fillMaxWidth().padding(6.dp).weight(1f), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.62f), + fontWeight = FontWeight.Normal + ) + } + FilledTonalIconButton( + modifier = Modifier.size(24.dp), + onClick = { expanded = !expanded } + ) { + Icon( + Icons.Outlined.ExpandLess, + null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.rotate(animatedDegree.value) + ) + } + } + AnimatedVisibility(visible = expanded) { + content() + } + } +} + +@Composable +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun ExpandableElevatedCardPreview() { + ExpandableOutlinedCard( + title = "Title", subtitle = "Subtitle", content = { + Text(text = "Content") + }, icon = Icons.Outlined.PermDeviceInformation + ) +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/QuickLyricsSongInfo.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/QuickLyricsSongInfo.kt new file mode 100644 index 0000000..3f8f09e --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/QuickLyricsSongInfo.kt @@ -0,0 +1,126 @@ +package pl.lambada.songsync.activities.quicksearch.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import coil.compose.AsyncImage +import pl.lambada.songsync.R +import pl.lambada.songsync.activities.quicksearch.QuickLyricsSearchActivity +import pl.lambada.songsync.domain.model.SongInfo + +@Composable +fun QuickLyricsSongInfo( + modifier: Modifier = Modifier, + songInfo: SongInfo, + imageLoader: ImageLoader = QuickLyricsSearchActivity.activityImageLoader +) { + + val imageUrl: String? by remember(songInfo.albumCoverLink) { + mutableStateOf(songInfo.albumCoverLink) + } + + OutlinedCard( + modifier = modifier, + colors = CardDefaults.outlinedCardColors().copy( + containerColor = MaterialTheme.colorScheme.surface + ), + border = CardDefaults.outlinedCardBorder().copy( + width = 2.dp, + ) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + modifier = Modifier + .size(72.dp) + .clip(MaterialTheme.shapes.small), + model = imageUrl, + contentDescription = stringResource(R.string.album_cover), + imageLoader = imageLoader, + ) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = songInfo.songName ?: stringResource(R.string.unknown), + style = MaterialTheme.typography.bodyLarge.copy( + fontWeight = FontWeight.SemiBold + ) + ) + + Text( + text = songInfo.artistName ?: stringResource(R.string.unknown), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} + +@Composable +private fun TextWithIcon( + icon: ImageVector, + text: String, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null + ) + Text(text = text, style = textStyle) + } +} + +@Preview +@Composable +private fun TextWithIconPreview() { + TextWithIcon( + icon = Icons.Rounded.MusicNote, + text = "Song Name" + ) +} + +@Preview +@Composable +private fun QuickLyricsSongInfoPreview() { + QuickLyricsSongInfo( + songInfo = SongInfo( + songName = "Song Name", + artistName = "Artist Name", + albumCoverLink = "https://example.com/image.jpg" + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SquaredButton.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SquaredButton.kt new file mode 100644 index 0000000..add2ed9 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SquaredButton.kt @@ -0,0 +1,105 @@ +package pl.lambada.songsync.activities.quicksearch.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SquareButtons() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + ButtonWithIconAndText( + icon = Icons.Default.Home, + text = "Home", + modifier = Modifier.size(96.dp), + shape = RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp) + ) + ButtonWithIconAndText( + icon = Icons.Default.Settings, + text = "Settings", + modifier = Modifier.size(96.dp), + shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp) + ) + } +} + +@Composable +fun ButtonWithIconAndText( + modifier: Modifier = Modifier, + icon: ImageVector, + text: String, + enabled: Boolean = true, + backgroundColor: Color = MaterialTheme.colorScheme.secondaryContainer, + shape: CornerBasedShape = MaterialTheme.shapes.small, + onClick : () -> Unit = {} +) { + Surface( + modifier = modifier.semantics { role = Role.Button }.alpha(if (enabled) 1f else 0.4f), + onClick = onClick, + enabled = enabled, + shape = shape, + color = backgroundColor + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + modifier = Modifier + .padding(8.dp) + .defaultMinSize( + minWidth = ButtonDefaults.MinWidth, + minHeight = ButtonDefaults.MinHeight + ) + .fillMaxWidth() + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + ), + color = MaterialTheme.colorScheme.onSurface, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Preview +@Composable +fun SquareButtonsPreview() { + SquareButtons() +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SyncedLyricsLine.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SyncedLyricsLine.kt new file mode 100644 index 0000000..4ccb6e5 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SyncedLyricsLine.kt @@ -0,0 +1,59 @@ +package pl.lambada.songsync.activities.quicksearch.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SyncedLyricsLine( + time: String, + lyrics: String, + modifier: Modifier = Modifier +) { + Row(modifier = modifier) { + val formattedTime = buildAnnotatedString { + append(time.substring(0, time.length - 4)) + withStyle(style = SpanStyle(fontSize = 12.sp, color = Color.Gray.copy(alpha = 0.7f))) { + append(time.takeLast(4)) + } + } + Text( + text = formattedTime, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = lyrics, + style = MaterialTheme.typography.bodyMedium + ) + } +} + +@Composable +fun SyncedLyricsColumn( + lyricsList: List>, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { + lyricsList.forEach { (time, lyrics) -> + SyncedLyricsLine(time = time, lyrics = lyrics) + Spacer(modifier = Modifier.height(4.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt index 062fd4b..54ffa03 100644 --- a/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt +++ b/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModel.kt @@ -1,6 +1,7 @@ package pl.lambada.songsync.activities.quicksearch.viewmodel import android.content.Context +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -14,19 +15,20 @@ import pl.lambada.songsync.domain.model.SongInfo import pl.lambada.songsync.util.ResourceState import pl.lambada.songsync.util.ScreenState import pl.lambada.songsync.util.ext.getVersion +import pl.lambada.songsync.util.parseLyrics class QuickLyricsSearchViewModel( val userSettingsController: UserSettingsController, private val lyricsProviderService: LyricsProviderService ): ViewModel() { - private val mutableState = MutableStateFlow(QuickSearchViewState()) val state = mutableState.asStateFlow() data class QuickSearchViewState( val song: Pair? = null, // Pair of song title and artist's name val screenState: ScreenState = ScreenState.Loading, - val lyricsState: ResourceState = ResourceState.Loading() + val lyricsState: ResourceState = ResourceState.Loading(), + val parsedLyrics: List> = emptyList() ) private fun fetchSongData(song: Pair, context: Context) { @@ -55,6 +57,12 @@ class QuickLyricsSearchViewModel( if(syncedLyrics != null) { updateLyricsState(ResourceState.Success(syncedLyrics)) + parseLyrics(syncedLyrics).let { parsedLyrics -> + Log.d("QuickLyricsSearchVM", "Parsed lyrics: $parsedLyrics") + mutableState.update { + it.copy(parsedLyrics = parsedLyrics) + } + } } else { updateLyricsState(ResourceState.Error("Error fetching lyrics for the song.")) } diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt index 26003de..5e4156f 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt @@ -194,35 +194,35 @@ fun HomeScreenLoaded( horizontalAlignment = Alignment.CenterHorizontally, contentPadding = scaffoldPadding ) { -// item { -// val launcher = rememberLauncherForActivityResult( -// contract = ActivityResultContracts.StartActivityForResult() -// ) { result -> -// if (result.resultCode == Activity.RESULT_OK) { -// val receivedLyrics = result.data?.getStringExtra("lyrics") -// if (receivedLyrics != null) { -// lyrics = receivedLyrics -// } -// } -// } -// Button( -// onClick = { -// val intent = Intent("android.intent.action.SEND").apply { -// putExtra("songName", "Faded") -// putExtra("artistName", "Alan Walker") -// type = "text/plain" -// setPackage("pl.lambada.songsync") -// } -// launcher.launch(intent) -// } -// ) { -// Text("Launch intent") -// } -// } -// -// item { -// Text(lyrics) -// } + item { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val receivedLyrics = result.data?.getStringExtra("lyrics") + if (receivedLyrics != null) { + lyrics = receivedLyrics + } + } + } + Button( + onClick = { + val intent = Intent("android.intent.action.SEND").apply { + putExtra("songName", "Around the World") + putExtra("artistName", "Niklas Dee") + type = "text/plain" + setPackage("pl.lambada.songsync") + } + launcher.launch(intent) + } + ) { + Text("Launch intent") + } + } + + item { + Text(lyrics) + } item { Column( diff --git a/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt b/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt index 212cf16..160b0a0 100644 --- a/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt +++ b/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt @@ -346,4 +346,22 @@ fun applyOffsetToLyrics(lyrics: String, offset: Int): String { "${startChar}${applyOffset(minute, second, millisecond)}$endChar" } +} + +fun parseLyrics(lyrics: String): List> { + val timestampRegex = Regex("""[\[<](\d+):(\d+)\.(\d+)[]>]""") + val lines = lyrics.lines() + + return lines.mapNotNull { line -> + val match = timestampRegex.find(line) ?: return@mapNotNull null + val (minute, second, millisecond) = match.destructured + + val startChar = line[0] + val endChar = if (startChar == '[') ']' else '>' + + val timestamp = "${minute}:${second}.${millisecond.padStart(3, '0')}" + val text = line.substringAfter(endChar).trim() + + timestamp to text + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7dba66..8a5fafe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,4 +146,9 @@ Translation Help us translate the app to your language! Open Weblate + Showing lyrics for + by + Accept + Song lyrics + "Retrieved song lyrics that will be sent to the application " diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f64b09c..930927b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,7 @@