From e234f3a46c1b1ea7257ca44bf38a545d70aa3ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 12 Aug 2024 18:59:47 +0200 Subject: [PATCH 1/6] feat: embed lyrics in file This commit adds the ability to embed lyrics directly into audio files. It includes: - Adding TagLib dependency for metadata manipulation . - Implementing `embedLyricsInFile` function in `MainViewModel` to handle embedding. - Adding a toggle in settings to enable/disable embedding. - Updating lyrics saving logic to use embedding when enabled. - Handling potential `SecurityException` during embedding on Android Q+. --- app/build.gradle.kts | 1 + .../java/pl/lambada/songsync/MainActivity.kt | 3 + .../pl/lambada/songsync/data/MainViewModel.kt | 84 ++++++++++++++++++ .../lambada/songsync/ui/screens/HomeScreen.kt | 86 +++++++++++++++---- .../songsync/ui/screens/SearchScreen.kt | 25 ++++-- gradle/libs.versions.toml | 3 + settings.gradle.kts | 2 + 7 files changed, 179 insertions(+), 25 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3df350c..67ace1a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { implementation(libs.androidx.preference) implementation(libs.ktor.core) implementation(libs.ktor.cio) + implementation(libs.taglib) implementation(libs.datastore.preferences) debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.tooling.preview) diff --git a/app/src/main/java/pl/lambada/songsync/MainActivity.kt b/app/src/main/java/pl/lambada/songsync/MainActivity.kt index 24583d8..6566cec 100644 --- a/app/src/main/java/pl/lambada/songsync/MainActivity.kt +++ b/app/src/main/java/pl/lambada/songsync/MainActivity.kt @@ -114,6 +114,9 @@ class MainActivity : ComponentActivity() { val provider = dataStore.get(stringPreferencesKey("provider"), Providers.SPOTIFY.displayName) viewModel.provider = Providers.entries.find { it.displayName == provider }!! + val embedLyrics = dataStore.get(booleanPreferencesKey("embed_lyrics"), false) + viewModel.embedLyricsInFile = embedLyrics + // Get token upon app start launch(Dispatchers.IO) { try { diff --git a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt index 9157981..2c9d0bc 100644 --- a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt +++ b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt @@ -1,14 +1,22 @@ package pl.lambada.songsync.data +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.app.RecoverableSecurityException +import android.content.ContentResolver import android.content.ContentUris import android.content.Context import android.net.Uri +import android.os.Build +import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.kyant.taglib.PropertyMap +import com.kyant.taglib.TagLib import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -70,6 +78,8 @@ class MainViewModel : ViewModel() { private var appleID = 0L // TODO: Use values from SongInfo object returned by search instead of storing them here + var embedLyricsInFile = false + /** * Refreshes the access token by sending a request to the Spotify API. */ @@ -150,6 +160,37 @@ class MainViewModel : ViewModel() { return latestVersion > currentVersion } + @SuppressLint("Range") + private fun getFileDescriptorFromPath( + context: Context, filePath: String, mode: String = "r" + ): ParcelFileDescriptor? { + val resolver: ContentResolver = context.contentResolver + val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + val selection = "${MediaStore.Files.FileColumns.DATA}=?" + val selectionArgs = arrayOf(filePath) + + resolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val fileId: Int = + cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)) + if (fileId == -1) { + return null + } else { + val fileUri: Uri = Uri.withAppendedPath(uri, fileId.toString()) + try { + return resolver.openFileDescriptor(fileUri, mode) + } catch (e: FileNotFoundException) { + Log.e("MainViewModel", "File not found: ${e.message}") + } + } + } + } + + return null + } + /** * Loads all songs from the MediaStore. * @param context The application context. @@ -292,6 +333,49 @@ class MainViewModel : ViewModel() { } } } + + fun embedLyricsInFile( + context: Context, + filePath: String, + lyrics: String, + securityExceptionHandler: (PendingIntent) -> Unit = {} + ): Boolean { + return try { + val fd = getFileDescriptorFromPath(context, filePath, mode = "w") + ?: throw IllegalStateException("File descriptor is null") + + fd.dup().detachFd().let { + TagLib.savePropertyMap( + it, propertyMap = PropertyMap().apply { + put("LYRICS", arrayOf(lyrics)) + } + ) + } + + true + } catch (securityException: SecurityException) { + handleSecurityException(securityException, securityExceptionHandler) + false + } catch (e: Exception) { + Log.e("MainViewModel", "Error embedding lyrics: ${e.message}") + false + } + } + + private fun handleSecurityException( + securityException: SecurityException, intentPassthrough: (PendingIntent) -> Unit + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val recoverableSecurityException = + securityException as? RecoverableSecurityException ?: throw RuntimeException( + securityException.message, securityException + ) + + intentPassthrough(recoverableSecurityException.userAction.actionIntent) + } else { + throw RuntimeException(securityException.message, securityException) + } + } } class NoTrackFoundException : Exception() diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt index 2f11903..46390a3 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt @@ -117,9 +117,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat.getString -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.documentfile.provider.DocumentFile import androidx.navigation.NavHostController @@ -140,10 +138,10 @@ import pl.lambada.songsync.domain.model.Song import pl.lambada.songsync.domain.model.SongInfo import pl.lambada.songsync.ui.ScreenAbout import pl.lambada.songsync.ui.ScreenSearch -import pl.lambada.songsync.util.ext.BackPressHandler import pl.lambada.songsync.ui.components.AnimatedText import pl.lambada.songsync.ui.components.SwitchItem import pl.lambada.songsync.util.dataStore +import pl.lambada.songsync.util.ext.BackPressHandler import pl.lambada.songsync.util.ext.getVersion import pl.lambada.songsync.util.ext.lowercaseWithLocale import pl.lambada.songsync.util.ext.toLrcFile @@ -166,6 +164,8 @@ fun HomeScreen( sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope, ) { + val context = LocalContext.current + var embedLyrics by remember { mutableStateOf(viewModel.embedLyricsInFile) } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() var isBatchDownload by remember { mutableStateOf(false) } Scaffold( @@ -327,6 +327,33 @@ fun HomeScreen( expanded = false } ) + DropdownMenuItem( + text = { + Text( + text = stringResource(id = R.string.embed_lyrics_in_file), + modifier = Modifier.padding(horizontal = 6.dp), + ) + }, + trailingIcon = { + Checkbox( + checked = embedLyrics, + onCheckedChange = { + embedLyrics = it + context.dataStore.set( + booleanPreferencesKey("embed_lyrics"), + it + ) + } + ) + }, + onClick = { + embedLyrics = !embedLyrics + context.dataStore.set( + booleanPreferencesKey("embed_lyrics"), + embedLyrics + ) + } + ) DropdownMenuItem( text = { Text( @@ -340,7 +367,8 @@ fun HomeScreen( } ) } - val selectedProvider = rememberSaveable { mutableStateOf(viewModel.provider) } + val selectedProvider = + rememberSaveable { mutableStateOf(viewModel.provider) } val providers = Providers.entries.toTypedArray() val context = LocalContext.current val dataStore = context.dataStore @@ -498,7 +526,11 @@ fun HomeScreenLoaded( Column { if (isBatchDownload) { BatchDownloadLyrics( - songs = if (selected.isEmpty()) displaySongs else songs.filter { selected.contains(it.filePath) }.toList(), + songs = if (selected.isEmpty()) displaySongs else songs.filter { + selected.contains( + it.filePath + ) + }.toList(), viewModel = viewModel, onDone = { onBatchDownload(false) }) } @@ -800,7 +832,10 @@ fun FiltersDialog( .padding(start = 22.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon(imageVector = Icons.Outlined.Folder, contentDescription = "Folder icon") + Icon( + imageVector = Icons.Outlined.Folder, + contentDescription = "Folder icon" + ) Spacer(modifier = Modifier.width(8.dp)) Text( text = folder.removePrefix("/storage/emulated/0/"), @@ -1145,7 +1180,10 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: () if (queryResult != null) { val lyricsResult: String try { - lyricsResult = viewModel.getSyncedLyrics(queryResult.songLink ?: "", context.getVersion())!! + lyricsResult = viewModel.getSyncedLyrics( + queryResult.songLink ?: "", + context.getVersion() + )!! } catch (e: Exception) { when (e) { is NullPointerException, is FileNotFoundException -> { @@ -1159,7 +1197,12 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: () val lrc = "[ti:${queryResult.songName}]\n" + "[ar:${queryResult.artistName}]\n" + "[by:$generatedUsingString]\n" + lyricsResult try { - file?.writeText(lrc) + if (viewModel.embedLyricsInFile) viewModel.embedLyricsInFile( + context, + song.filePath + ?: throw FileNotFoundException("File path must not be null"), + lrc + ) else file?.writeText(lrc) } catch (e: FileNotFoundException) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && !song.filePath!!.contains( "/storage/emulated/0" @@ -1181,18 +1224,23 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: () } } } - sdCardFiles?.listFiles()?.forEach { - if (it.name == file.name) { - it.delete() - return@forEach + if (viewModel.embedLyricsInFile) { + viewModel.embedLyricsInFile(context, song.filePath, lrc) + } else { + sdCardFiles?.listFiles()?.forEach { + if (it.name == file.name) { + it.delete() + return@forEach + } + } + sdCardFiles?.createFile( + "text/lrc", file.name + )?.let { + val outputStream = + context.contentResolver.openOutputStream(it.uri) + outputStream?.write(lrc.toByteArray()) + outputStream?.close() } - } - sdCardFiles?.createFile( - "text/lrc", file.name - )?.let { - val outputStream = context.contentResolver.openOutputStream(it.uri) - outputStream?.write(lrc.toByteArray()) - outputStream?.close() } } else { throw e diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt index 8337490..7a022ef 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt @@ -293,7 +293,10 @@ fun SharedTransitionScope.SearchScreen( launch(Dispatchers.IO) { try { if (lyricSuccess == LyricsStatus.NotSubmitted) { - lyricsResult = viewModel.getSyncedLyrics(result.songLink ?: "", context.getVersion()) + lyricsResult = viewModel.getSyncedLyrics( + result.songLink ?: "", + context.getVersion() + ) if (lyricsResult == null) throw NullPointerException("lyricsResult is null") else @@ -334,16 +337,24 @@ fun SharedTransitionScope.SearchScreen( val lrc = "[ti:${result.songName}]\n" + "[ar:${result.artistName}]\n" + "[by:$generatedUsingString]\n" + lyrics val file = filePath?.toLrcFile() ?: File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + ), "SongSync/${result.songName} - ${result.artistName}.lrc" ) if (!isLegacyVersion || isInternalStorage) { - file.writeText(lrc) + if (viewModel.embedLyricsInFile) viewModel.embedLyricsInFile( + context, filePath + ?: throw IllegalArgumentException("File path must not be null"), + lyrics = lrc + ) else file.writeText(lrc) } else { val sd = context.externalCacheDirs[1].absolutePath.substring( 0, - context.externalCacheDirs[1].absolutePath.indexOf("/Android/data") + context.externalCacheDirs[1].absolutePath.indexOf( + "/Android/data" + ) ) val path = filePath?.toLrcFile()?.absolutePath?.substringAfter( @@ -370,7 +381,8 @@ fun SharedTransitionScope.SearchScreen( "text/lrc", file.name )?.let { - val outputStream = context.contentResolver.openOutputStream(it.uri) + val outputStream = + context.contentResolver.openOutputStream(it.uri) outputStream?.write(lrc.toByteArray()) outputStream?.close() } @@ -389,7 +401,8 @@ fun SharedTransitionScope.SearchScreen( Text(text = stringResource(R.string.save_lrc_file)) } val clipboardManager = LocalClipboardManager.current - val copiedString = stringResource(R.string.lyrics_copied_to_clipboard) + val copiedString = + stringResource(R.string.lyrics_copied_to_clipboard) OutlinedButton( onClick = { clipboardManager.setText(AnnotatedString(lyrics)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd7869d..681d3ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ navigation-runtime-ktx = "2.8.0-beta05" preference = "1.2.1" ktor = "2.3.6" datastore = "1.1.1" +taglib = "1.0.0-alpha18" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" } @@ -41,6 +42,8 @@ androidx-preference = { group = "androidx.preference", name = "preference-ktx", ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } +taglib = { group = "com.github.Kyant0", name = "taglib", version.ref = "taglib" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7080987..880cf70 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven("https://jitpack.io") } } dependencyResolutionManagement { @@ -10,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://jitpack.io") } } From 57a22ae0e7c40120b3d20b119515d0c308d22797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 12 Aug 2024 19:07:16 +0200 Subject: [PATCH 2/6] fix: preserve existing metadata when saving lyrics --- .../main/java/pl/lambada/songsync/data/MainViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt index 2c9d0bc..46ccdbc 100644 --- a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt +++ b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt @@ -344,9 +344,15 @@ class MainViewModel : ViewModel() { val fd = getFileDescriptorFromPath(context, filePath, mode = "w") ?: throw IllegalStateException("File descriptor is null") + val fileDescriptor = fd.dup().detachFd() + + val metadata = TagLib.getMetadata(fileDescriptor, false) ?: throw IllegalStateException( + "Metadata is null" + ) + fd.dup().detachFd().let { TagLib.savePropertyMap( - it, propertyMap = PropertyMap().apply { + it, propertyMap = metadata.propertyMap.apply { put("LYRICS", arrayOf(lyrics)) } ) From 46a5e1af02e1b02efacf5441355409f6324904e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Tue, 13 Aug 2024 18:27:19 +0200 Subject: [PATCH 3/6] feat: add embed lyrics in file button Adds a button to embed lyrics in the song file. Removes the embed lyrics in file preference. --- .../lambada/songsync/ui/screens/HomeScreen.kt | 29 ++++---- .../songsync/ui/screens/SearchScreen.kt | 68 +++++++++++++------ app/src/main/res/values/strings.xml | 2 + 3 files changed, 63 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt index 46390a3..fcbfe57 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt @@ -1224,24 +1224,21 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: () } } } - if (viewModel.embedLyricsInFile) { - viewModel.embedLyricsInFile(context, song.filePath, lrc) - } else { - sdCardFiles?.listFiles()?.forEach { - if (it.name == file.name) { - it.delete() - return@forEach - } - } - sdCardFiles?.createFile( - "text/lrc", file.name - )?.let { - val outputStream = - context.contentResolver.openOutputStream(it.uri) - outputStream?.write(lrc.toByteArray()) - outputStream?.close() + //In here we won't try to embed lyrics in file because if it failed before, it will fail again + sdCardFiles?.listFiles()?.forEach { + if (it.name == file.name) { + it.delete() + return@forEach } } + sdCardFiles?.createFile( + "text/lrc", file.name + )?.let { + val outputStream = + context.contentResolver.openOutputStream(it.uri) + outputStream?.write(lrc.toByteArray()) + outputStream?.close() + } } else { throw e } diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt index 7a022ef..d5eb50a 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt @@ -343,11 +343,7 @@ fun SharedTransitionScope.SearchScreen( "SongSync/${result.songName} - ${result.artistName}.lrc" ) if (!isLegacyVersion || isInternalStorage) { - if (viewModel.embedLyricsInFile) viewModel.embedLyricsInFile( - context, filePath - ?: throw IllegalArgumentException("File path must not be null"), - lyrics = lrc - ) else file.writeText(lrc) + file.writeText(lrc) } else { val sd = context.externalCacheDirs[1].absolutePath.substring( @@ -400,26 +396,58 @@ fun SharedTransitionScope.SearchScreen( ) { Text(text = stringResource(R.string.save_lrc_file)) } - val clipboardManager = LocalClipboardManager.current - val copiedString = - stringResource(R.string.lyrics_copied_to_clipboard) - OutlinedButton( + Button( onClick = { - clipboardManager.setText(AnnotatedString(lyrics)) - Toast.makeText( - context, - copiedString, - Toast.LENGTH_SHORT - ).show() - } + val lrc = + "[ti:${result.songName}]\n" + "[ar:${result.artistName}]\n" + "[by:$generatedUsingString]\n" + lyrics + + val embeddedToFile = kotlin.runCatching { + viewModel.embedLyricsInFile( + context, + filePath ?: throw NullPointerException("filePath is null"), + lrc + ) + } + + if(embeddedToFile.isFailure) { + Toast.makeText( + context, + embeddedToFile.exceptionOrNull()?.message ?: context.getString(R.string.error), + Toast.LENGTH_LONG + ).show() + return@Button + } else { + Toast.makeText( + context, + context.getString(R.string.embedded_lyrics_in_file), + Toast.LENGTH_LONG + ).show() + } + }, ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = stringResource(R.string.copy_lyrics_to_clipboard) - ) + Text(text = stringResource(R.string.embed_lyrics_in_file)) } } + val clipboardManager = LocalClipboardManager.current + val copiedString = + stringResource(R.string.lyrics_copied_to_clipboard) + OutlinedButton( + onClick = { + clipboardManager.setText(AnnotatedString(lyrics)) + Toast.makeText( + context, + copiedString, + Toast.LENGTH_SHORT + ).show() + } + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = stringResource(R.string.copy_lyrics_to_clipboard) + ) + } + Spacer(modifier = Modifier.height(6.dp)) OutlinedCard( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5319ad2..57d7aa7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,4 +120,6 @@ Disable moving text animation in app Include translation Include translated lyrics when getting song lyrics from Netease provider + Embed lyrics to file + Lyrics has been embeded to the song file \ No newline at end of file From bf18bef87e8ec116b705618c912f90a1c65f9772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Tue, 13 Aug 2024 18:32:52 +0200 Subject: [PATCH 4/6] Fix formatting and optimize imports This commit includes minor code style improvements such as fixing formatting inconsistencies and optimizing imports. These changes enhance the readability and maintainability of the codebase. --- .../java/pl/lambada/songsync/MainActivity.kt | 17 +++++++++----- .../pl/lambada/songsync/data/MainViewModel.kt | 17 ++++++++++---- .../lyrics_providers/others/AppleAPI.kt | 5 ++++- .../lyrics_providers/others/NeteaseAPI.kt | 3 --- .../java/pl/lambada/songsync/ui/Navigator.kt | 2 +- .../songsync/ui/components/CommonTexts.kt | 22 ++++++++++++++----- .../songsync/ui/components/SwitchItem.kt | 5 +++-- .../songsync/ui/screens/AboutScreen.kt | 20 ++++++++++++----- .../songsync/ui/screens/SearchScreen.kt | 8 ++++--- 9 files changed, 68 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/pl/lambada/songsync/MainActivity.kt b/app/src/main/java/pl/lambada/songsync/MainActivity.kt index 6566cec..dbc4d72 100644 --- a/app/src/main/java/pl/lambada/songsync/MainActivity.kt +++ b/app/src/main/java/pl/lambada/songsync/MainActivity.kt @@ -63,6 +63,7 @@ import java.io.File */ class MainActivity : ComponentActivity() { val viewModel: MainViewModel by viewModels() + /** * Called when the activity is starting. * @@ -100,7 +101,8 @@ class MainActivity : ComponentActivity() { viewModel.sdCardPath = sdCardPath } - val includeTranslation = dataStore.get(booleanPreferencesKey("include_translation"), false) + val includeTranslation = + dataStore.get(booleanPreferencesKey("include_translation"), false) viewModel.includeTranslation = includeTranslation val blacklist = dataStore.get(stringPreferencesKey("blacklist"), null) @@ -111,7 +113,8 @@ class MainActivity : ComponentActivity() { val hideLyrics = dataStore.get(booleanPreferencesKey("hide_lyrics"), false) viewModel.hideLyrics = hideLyrics - val provider = dataStore.get(stringPreferencesKey("provider"), Providers.SPOTIFY.displayName) + val provider = + dataStore.get(stringPreferencesKey("provider"), Providers.SPOTIFY.displayName) viewModel.provider = Providers.entries.find { it.displayName == provider }!! val embedLyrics = dataStore.get(booleanPreferencesKey("embed_lyrics"), false) @@ -160,7 +163,7 @@ class MainActivity : ComponentActivity() { } ) - Surface( modifier = Modifier.fillMaxSize() ) { + Surface(modifier = Modifier.fillMaxSize()) { if (!hasLoadedPermissions) { LoadingScreen() } else if (!hasPermissions) { @@ -194,14 +197,15 @@ class MainActivity : ComponentActivity() { NoInternetDialog( onConfirm = { finishAndRemoveTask() }, onIgnore = { - internetConnection = true // assume connected (if spotify is down, can use other providers) + internetConnection = + true // assume connected (if spotify is down, can use other providers) } ) } + } } } } -} override fun onResume() { val notificationManager = @@ -232,7 +236,8 @@ class MainActivity : ComponentActivity() { fun RequestPermissions(onGranted: () -> Unit, context: Context, onDone: () -> Unit) { var storageManager: ActivityResultLauncher? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - storageManager = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + storageManager = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (Environment.isExternalStorageManager()) { onGranted() } diff --git a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt index 46ccdbc..1b4d990 100644 --- a/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt +++ b/app/src/main/java/pl/lambada/songsync/data/MainViewModel.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kyant.taglib.PropertyMap import com.kyant.taglib.TagLib import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -134,7 +133,11 @@ class MainViewModel : ViewModel() { when (this.provider) { Providers.SPOTIFY -> SpotifyLyricsAPI().getSyncedLyrics(songLink, version) Providers.LRCLIB -> LRCLibAPI().getSyncedLyrics(this.lrcLibID) - Providers.NETEASE -> NeteaseAPI().getSyncedLyrics(this.neteaseID, includeTranslation) + Providers.NETEASE -> NeteaseAPI().getSyncedLyrics( + this.neteaseID, + includeTranslation + ) + Providers.APPLE -> AppleAPI().getSyncedLyrics(this.appleID) } } catch (e: Exception) { @@ -264,17 +267,20 @@ class MainViewModel : ViewModel() { val data: List = when { cachedFilteredSongs.value.isNotEmpty() -> cachedFilteredSongs.value cachedSongs != null -> cachedSongs!! - else -> { return@launch } + else -> { + return@launch + } } val results = data.filter { it.title?.contains(query, ignoreCase = true) == true || - it.artist?.contains(query, ignoreCase = true) == true + it.artist?.contains(query, ignoreCase = true) == true } _searchResults.value = results } } + /** * Loads all songs' folders * @param context The application context. @@ -314,10 +320,12 @@ class MainViewModel : ViewModel() { ) } } + hideLyrics -> { _cachedFilteredSongs?.value = cachedSongs!! .filter { it.filePath.toLrcFile()?.exists() != true } } + hideFolders -> { _cachedFilteredSongs?.value = cachedSongs!!.filter { !blacklistedFolders.contains( @@ -328,6 +336,7 @@ class MainViewModel : ViewModel() { ) } } + else -> { _cachedFilteredSongs?.value = emptyList() } diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/AppleAPI.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/AppleAPI.kt index 3406077..5c6a0a7 100644 --- a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/AppleAPI.kt +++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/AppleAPI.kt @@ -43,7 +43,8 @@ class AppleAPI { songName = result.songName, artistName = result.artistName, songLink = result.url, - albumCoverLink = result.artwork.replace("{w}", "100").replace("{h}", "100").replace("{f}", "png"), + albumCoverLink = result.artwork.replace("{w}", "100").replace("{h}", "100") + .replace("{f}", "png"), appleID = result.id ) } @@ -80,11 +81,13 @@ class AppleAPI { syncedLyrics.append("<${line.endtime.toLrcTimestamp()}>\n") } } + "Line" -> { for (line in lines) { syncedLyrics.append("[${line.timestamp.toLrcTimestamp()}]${line.text[0].text}\n") } } + else -> return null } diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/NeteaseAPI.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/NeteaseAPI.kt index 84955d0..e73b7a5 100644 --- a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/NeteaseAPI.kt +++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/NeteaseAPI.kt @@ -5,8 +5,6 @@ import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.client.statement.bodyAsText -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import pl.lambada.songsync.data.EmptyQueryException import pl.lambada.songsync.data.InternalErrorException @@ -15,7 +13,6 @@ import pl.lambada.songsync.domain.model.lyrics_providers.others.NeteaseLyricsRes import pl.lambada.songsync.domain.model.lyrics_providers.others.NeteaseResponse import pl.lambada.songsync.util.networking.Ktor.client import pl.lambada.songsync.util.networking.Ktor.json -import java.net.URLEncoder class NeteaseAPI { private val baseURL = "http://music.163.com/api/" diff --git a/app/src/main/java/pl/lambada/songsync/ui/Navigator.kt b/app/src/main/java/pl/lambada/songsync/ui/Navigator.kt index 10f1f28..9b8db79 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/Navigator.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/Navigator.kt @@ -12,8 +12,8 @@ import kotlinx.serialization.Serializable import pl.lambada.songsync.data.MainViewModel import pl.lambada.songsync.domain.model.Song import pl.lambada.songsync.ui.screens.AboutScreen -import pl.lambada.songsync.ui.screens.SearchScreen import pl.lambada.songsync.ui.screens.HomeScreen +import pl.lambada.songsync.ui.screens.SearchScreen /** * Composable function for handling navigation within the app. diff --git a/app/src/main/java/pl/lambada/songsync/ui/components/CommonTexts.kt b/app/src/main/java/pl/lambada/songsync/ui/components/CommonTexts.kt index 6819841..a4f26ae 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/components/CommonTexts.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/components/CommonTexts.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay -import pl.lambada.songsync.data.MainViewModel @Composable fun AnimatedText( @@ -52,10 +51,23 @@ fun AnimatedText( modifier: Modifier = Modifier, ) { if (animate) { - MarqueeText(text = text, fontSize = fontSize, fontWeight = fontWeight, modifier = modifier, color = color) - } - else { - Text(text = text, fontSize = fontSize, fontWeight = fontWeight, modifier = modifier, color = color, maxLines = 1, overflow = TextOverflow.Ellipsis) + MarqueeText( + text = text, + fontSize = fontSize, + fontWeight = fontWeight, + modifier = modifier, + color = color + ) + } else { + Text( + text = text, + fontSize = fontSize, + fontWeight = fontWeight, + modifier = modifier, + color = color, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } } diff --git a/app/src/main/java/pl/lambada/songsync/ui/components/SwitchItem.kt b/app/src/main/java/pl/lambada/songsync/ui/components/SwitchItem.kt index 55ba5b3..80a5808 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/components/SwitchItem.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/components/SwitchItem.kt @@ -4,8 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,7 +21,8 @@ fun SwitchItem( modifier: Modifier = Modifier, innerPaddingValues: PaddingValues = PaddingValues( horizontal = 22.dp, - vertical = 16.dp), + vertical = 16.dp + ), onClick: () -> Unit, ) { Row( diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/AboutScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/AboutScreen.kt index 0cb8708..d253b7c 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/AboutScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/AboutScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -52,7 +51,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers @@ -126,7 +124,10 @@ fun AboutScreen( ) { viewModel.pureBlack.value = !selected selected = !selected - dataStore.set(key = booleanPreferencesKey("pure_black"), value = selected) + dataStore.set( + key = booleanPreferencesKey("pure_black"), + value = selected + ) } } } @@ -142,7 +143,10 @@ fun AboutScreen( ) { viewModel.disableMarquee.value = !selected selected = !selected - dataStore.set(key = booleanPreferencesKey("marquee_disable"), value = selected) + dataStore.set( + key = booleanPreferencesKey("marquee_disable"), + value = selected + ) } } } @@ -157,7 +161,10 @@ fun AboutScreen( ) { viewModel.includeTranslation = !selected selected = !selected - dataStore.set(key = booleanPreferencesKey("include_translation"), value = selected) + dataStore.set( + key = booleanPreferencesKey("include_translation"), + value = selected + ) } } } @@ -306,7 +313,8 @@ fun AboutScreen( item { AboutItem(stringResource(R.string.contributors)) { Contributor.entries.forEach { - val additionalInfo = stringResource(id = it.contributionLevel.stringResource) + val additionalInfo = + stringResource(id = it.contributionLevel.stringResource) Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt index d5eb50a..97ae9d5 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/SearchScreen.kt @@ -404,15 +404,17 @@ fun SharedTransitionScope.SearchScreen( val embeddedToFile = kotlin.runCatching { viewModel.embedLyricsInFile( context, - filePath ?: throw NullPointerException("filePath is null"), + filePath + ?: throw NullPointerException("filePath is null"), lrc ) } - if(embeddedToFile.isFailure) { + if (embeddedToFile.isFailure) { Toast.makeText( context, - embeddedToFile.exceptionOrNull()?.message ?: context.getString(R.string.error), + embeddedToFile.exceptionOrNull()?.message + ?: context.getString(R.string.error), Toast.LENGTH_LONG ).show() return@Button From abc4a29c8e4fc38998c4bb6f0e308a3af27dfe56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Tue, 13 Aug 2024 19:39:11 +0200 Subject: [PATCH 5/6] update `.idea` folder files to Java 21 --- .idea/compiler.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 24 ++++++++++++++++++++ .idea/misc.xml | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 44ca2d9..b67486e 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,30 @@