mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
fixes
- added instructions on how to use the "Spotify for lyrics" feature in the README - added ABI splits in preparation for the f-droid relase - removed the skrape.it dependency - fixed bug where the "This song is an instrumental" screen always shows after the first time of fetching lyrics from spotify - properly position title and rotate navigation button icon on topbar for RTL languages - show errors on the lyrics screen - version 1.0.0
This commit is contained in:
@@ -23,7 +23,7 @@ class LGApp: Application() {
|
||||
lateinit var lyricsRepository: LyricsRepository
|
||||
val credentialStore by lazy {
|
||||
SpotifyDefaultCredentialStore(
|
||||
clientId = "10d81ce453e743559ea362df2e6a1fd2",
|
||||
clientId = "",
|
||||
redirectUri = SPOTIFY_REDIRECT_URI,
|
||||
applicationContext = applicationContext
|
||||
)
|
||||
|
||||
@@ -123,7 +123,6 @@ class MainActivity : ComponentActivity() {
|
||||
navController.navigate(Routes.Lyrics.route)
|
||||
},
|
||||
lyricsViewModel = lyricsViewModel,
|
||||
settingsViewModel = settingsViewModel,
|
||||
goBack = { finish() },
|
||||
lastNotification = lastNotification,
|
||||
)
|
||||
|
||||
@@ -2,14 +2,10 @@ package com.illusionman1212.lyricsgrabbr.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.illusionman1212.lyricsgrabbr.R
|
||||
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
||||
import com.illusionman1212.lyricsgrabbr.data.network.NetworkHelper
|
||||
import com.illusionman1212.lyricsgrabbr.utils.await
|
||||
import it.skrape.core.htmlDocument
|
||||
import it.skrape.selects.ElementNotFoundException
|
||||
import it.skrape.selects.html5.div
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jsoup.Jsoup
|
||||
@@ -56,8 +52,10 @@ class LyricsRepository(private val context: Context) {
|
||||
GET(url.toString())
|
||||
).await()
|
||||
|
||||
val body = res.body?.string()
|
||||
|
||||
if (res.isSuccessful) {
|
||||
return Pair(json.decodeFromString(SuccessResponse.serializer(), res.body!!.string())
|
||||
return Pair(json.decodeFromString(SuccessResponse.serializer(), body!!)
|
||||
.lyrics
|
||||
.lines.joinToString(separator = "\n") { it.words }, true)
|
||||
} else {
|
||||
@@ -70,61 +68,41 @@ class LyricsRepository(private val context: Context) {
|
||||
}
|
||||
|
||||
fun fetchLyricsFromGenius(url: Uri): String {
|
||||
// TODO: write a custom fetcher for skrape.it to use our OkHttp client
|
||||
// and get rid of jsoup. Or better yet, maybe skrape.it will be fixed in the future
|
||||
// and we can just use its built in BrowserFetcher
|
||||
val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
|
||||
val divs = doc.select("div[data-lyrics-container]")
|
||||
|
||||
try {
|
||||
val lyrics = htmlDocument(doc.toString()) {
|
||||
// set relaxed for instrumental songs
|
||||
relaxed = true
|
||||
val lyrics = StringUtil.borrowBuilder()
|
||||
|
||||
findFirst {
|
||||
div {
|
||||
withAttributeKey = "data-lyrics-container"
|
||||
findAll {
|
||||
this
|
||||
}.joinToString(separator = "\n") {
|
||||
val accum = StringUtil.borrowBuilder()
|
||||
it.element.text()
|
||||
|
||||
it.element.traverse(object : NodeVisitor {
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is TextNode) {
|
||||
accum.append(node.text().trim())
|
||||
// append space here to get proper spacing when there's a <i> tag or other inline text tags
|
||||
accum.append(" ")
|
||||
} else if (node is Element) {
|
||||
if (accum.isNotEmpty() && node.normalName() == "br") {
|
||||
accum.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun tail(node: Node, depth: Int) {
|
||||
// since we append a space after every text node, we only append a newline if
|
||||
// the last char isn't a space
|
||||
val lastCharIsWhitespace = accum.isNotEmpty() && accum[accum.length - 1] == ' '
|
||||
if (node is Element) {
|
||||
if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace) {
|
||||
accum.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
accum
|
||||
divs.forEach {
|
||||
it.traverse(object : NodeVisitor {
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is TextNode) {
|
||||
lyrics.append(node.text().trim())
|
||||
// append space here to get proper spacing when there's a <i> tag or other inline text tags
|
||||
lyrics.append(" ")
|
||||
} else if (node is Element) {
|
||||
if (lyrics.isNotEmpty() && node.normalName() == "br") {
|
||||
lyrics.append("\n")
|
||||
}
|
||||
}
|
||||
}.replace("\n\n\n", "\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return lyrics
|
||||
} catch (e: ElementNotFoundException) {
|
||||
// TODO: relaxed is broken in skrape.it, so we have to catch this exception
|
||||
return ""
|
||||
override fun tail(node: Node, depth: Int) {
|
||||
// since we append a space after every text node, we only append a newline if
|
||||
// the last char isn't a space
|
||||
val lastCharIsWhitespace = lyrics.isNotEmpty() && lyrics[lyrics.length - 1] == ' '
|
||||
if (node is Element) {
|
||||
if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace) {
|
||||
lyrics.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
lyrics.append("\n")
|
||||
}
|
||||
|
||||
return lyrics.toString().replace("\n\n\n", "\n\n")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.illusionman1212.lyricsgrabbr.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.PlainTooltipBox
|
||||
@@ -9,7 +10,9 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.ModifierInfo
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -29,9 +32,8 @@ fun LGIconButton(
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
Modifier.tooltipAnchor(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
content = content
|
||||
)
|
||||
}
|
||||
Box(Modifier.layoutId("badge")) {
|
||||
badge?.let { it() }
|
||||
|
||||
@@ -48,6 +48,7 @@ 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
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -102,7 +103,6 @@ fun HomePage(
|
||||
homeViewModel: HomeViewModel = viewModel(
|
||||
factory = HomeViewModel.Factory,
|
||||
),
|
||||
settingsViewModel: SettingsViewModel,
|
||||
lyricsViewModel: LyricsViewModel,
|
||||
lastNotification: MutableStateFlow<NotificationEvent?>
|
||||
) {
|
||||
@@ -141,7 +141,7 @@ fun HomePage(
|
||||
|
||||
val song = lastNotification.collectAsStateWithLifecycle().value
|
||||
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
var getLyricsFromSpotify by remember { mutableStateOf(false)}
|
||||
var getLyricsFromSpotify by rememberSaveable { mutableStateOf(false)}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!shouldShowNotificationPermissionRationale)
|
||||
|
||||
@@ -22,21 +22,25 @@ import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.illusionman1212.lyricsgrabbr.R
|
||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -82,6 +86,7 @@ fun LyricsPage(
|
||||
onClick = goBack
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.mirror(),
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(
|
||||
id = R.string.go_back
|
||||
@@ -107,23 +112,39 @@ fun LyricsPage(
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
if (uiState.lyrics.isEmpty()) {
|
||||
if (uiState.error != null) {
|
||||
Text(
|
||||
uiState.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 32.dp)
|
||||
)
|
||||
} else if (uiState.lyrics.isEmpty()) {
|
||||
Instrumental()
|
||||
} else {
|
||||
Column(Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp, bottom = 32.dp)) {
|
||||
SelectionContainer {
|
||||
TextField(
|
||||
value = uiState.lyrics,
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
readOnly = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Column(
|
||||
Modifier.padding(
|
||||
top = 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 32.dp
|
||||
)
|
||||
) {
|
||||
SelectionContainer {
|
||||
TextField(
|
||||
value = uiState.lyrics,
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
readOnly = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -38,15 +37,15 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -62,6 +61,7 @@ import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
|
||||
import com.illusionman1212.lyricsgrabbr.data.Theme
|
||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
||||
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
|
||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -131,25 +131,23 @@ fun SettingsPage(
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
// TODO: on RTL languages all newer(only?) android systems change the direction of the status bar
|
||||
// including all its children, so we need to figure out how to position stuff to the right
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
||||
Icon(
|
||||
modifier = Modifier.mirror(),
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(
|
||||
id = R.string.go_back
|
||||
) )
|
||||
}
|
||||
Text(text = stringResource(id = R.string.settings), style = MaterialTheme.typography.titleLarge)
|
||||
}
|
||||
Text(text = stringResource(id = R.string.settings), style = MaterialTheme.typography.titleLarge)
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
@@ -278,6 +276,7 @@ fun Lyrics(viewModel: SettingsViewModel, uiState: SettingsState) {
|
||||
val spotifyDesc = annotatedStringResource(R.string.spotify_client_id_desc) {
|
||||
when (it.key) {
|
||||
"href" -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
|
||||
"weight" -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ fun LyricsGrabbrTheme(
|
||||
}
|
||||
}
|
||||
|
||||
// BUG: color is not being set on older android version (Marshmallow and others).
|
||||
// The navigation bar color causes the nav buttons to be very hard to see
|
||||
// in light mode because the buttons are white.
|
||||
DisposableEffect(darkTheme) {
|
||||
(context as ComponentActivity).enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.illusionman1212.lyricsgrabbr.utils
|
||||
|
||||
import android.util.LayoutDirection
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.core.text.layoutDirection
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
fun Modifier.mirror(): Modifier {
|
||||
return if (Locale.getDefault().layoutDirection == LayoutDirection.RTL)
|
||||
this.scale(scaleX = -1f, scaleY = 1f)
|
||||
else
|
||||
this
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="use_spotify_api">استخدم Spotify للكلمات</string>
|
||||
<string name="spotify_client_id">Spotify Client ID</string>
|
||||
<string name="not_set">خالي</string>
|
||||
<string name="spotify_client_id_desc">يمكن لهذا التطبيق استخدام واجهة برمجة تطبيقات الويب الخاصة بـ Spotify للحصول على كلمات الأغاني. تحتاج إلى تقديم Spotify Client ID لاستخدام هذه الميزة. يمكنك الحصول على واحدة من <annotation href="https://developer.spotify.com/dashboard/applications">هنا</annotation>.\nلا يعمل هذا إلا إذا كنت تقوم بتشغيل الموسيقى من مشغل Spotify.</string>
|
||||
<string name="spotify_client_id_desc">يمكن لهذا التطبيق استخدام واجهة برمجة تطبيقات الويب الخاصة بـ Spotify للحصول على كلمات الأغاني. تحتاج إلى تقديم Spotify Client ID لاستخدام هذه الميزة. يمكنك الحصول على واحدة من <annotation href="https://developer.spotify.com/dashboard/applications">هنا</annotation>.\n<annotation weight="bold">لا يعمل هذا إلا إذا كنت تقوم بتشغيل الموسيقى من مشغل Spotify.</annotation></string>
|
||||
|
||||
<!-- Lyrics -->
|
||||
<string name="lyrics">كلمات الأغنية</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="use_spotify_api">Use Spotify for lyrics</string>
|
||||
<string name="spotify_client_id" translatable="false">Spotify Client ID</string>
|
||||
<string name="not_set">Not Set</string>
|
||||
<string name="spotify_client_id_desc">This app can use Spotify\'s Web API to get lyrics. You need to provide a Spotify Client ID to use this feature. You can get one from <annotation href="https://developer.spotify.com/dashboard/applications">here</annotation>.\nThis only works if you\'re playing music from the Spotify player.</string>
|
||||
<string name="spotify_client_id_desc">This app can use Spotify\'s Web API to get lyrics. You need to provide a Spotify Client ID to use this feature. You can get one from <annotation href="https://developer.spotify.com/dashboard/applications">here</annotation>.\n<annotation weight="bold">This only works if you\'re playing music from the Spotify player.</annotation></string>
|
||||
|
||||
<!-- Lyrics -->
|
||||
<string name="lyrics">Lyrics</string>
|
||||
|
||||
Reference in New Issue
Block a user