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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8978d23..74dd639 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,7 @@
+
-
+
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..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,9 +113,13 @@ 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)
+ viewModel.embedLyricsInFile = embedLyrics
+
// Get token upon app start
launch(Dispatchers.IO) {
try {
@@ -157,7 +163,7 @@ class MainActivity : ComponentActivity() {
}
)
- Surface( modifier = Modifier.fillMaxSize() ) {
+ Surface(modifier = Modifier.fillMaxSize()) {
if (!hasLoadedPermissions) {
LoadingScreen()
} else if (!hasPermissions) {
@@ -191,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 =
@@ -229,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 9157981..1b4d990 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,21 @@
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.TagLib
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -70,6 +77,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.
*/
@@ -124,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) {
@@ -150,6 +163,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.
@@ -223,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.
@@ -273,10 +320,12 @@ class MainViewModel : ViewModel() {
)
}
}
+
hideLyrics -> {
_cachedFilteredSongs?.value = cachedSongs!!
.filter { it.filePath.toLrcFile()?.exists() != true }
}
+
hideFolders -> {
_cachedFilteredSongs?.value = cachedSongs!!.filter {
!blacklistedFolders.contains(
@@ -287,11 +336,61 @@ class MainViewModel : ViewModel() {
)
}
}
+
else -> {
_cachedFilteredSongs?.value = emptyList()
}
}
}
+
+ 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")
+
+ val fileDescriptor = fd.dup().detachFd()
+
+ val metadata = TagLib.getMetadata(fileDescriptor, false) ?: throw IllegalStateException(
+ "Metadata is null"
+ )
+
+ fd.dup().detachFd().let {
+ TagLib.savePropertyMap(
+ it, propertyMap = metadata.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/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/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/HomeScreen.kt
index 2f11903..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
@@ -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,6 +1224,7 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: ()
}
}
}
+ //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()
@@ -1190,7 +1234,8 @@ fun BatchDownloadLyrics(songs: List, viewModel: MainViewModel, onDone: ()
sdCardFiles?.createFile(
"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()
}
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..bef5ec0 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,7 +337,9 @@ 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) {
@@ -343,7 +348,9 @@ fun SharedTransitionScope.SearchScreen(
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 +377,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()
}
@@ -388,23 +396,63 @@ 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
+
+ kotlin.runCatching {
+ viewModel.embedLyricsInFile(
+ context,
+ filePath
+ ?: throw NullPointerException("filePath is null"),
+ lrc
+ )
+ }.onFailure { exception ->
+ val errorMessage = when(exception) {
+ is NullPointerException -> {
+ context.getString(R.string.embed_non_local_song_error)
+ }
+ else -> {
+ exception.message
+ ?: context.getString(R.string.error)
+ }
+ }
+ Toast.makeText(
+ context,
+ errorMessage,
+ Toast.LENGTH_LONG
+ ).show()
+ }.onSuccess {
+ 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))
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5319ad2..57a51a4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -120,4 +120,7 @@
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 embedded to the song file
+ You tried to embed the lyrics to a non-local file. Aborting operation.
\ No newline at end of file
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")
}
}