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:
@@ -8,5 +8,17 @@ App that listens to your notifications and provides lyrics for the currently pla
|
|||||||
## Supported players
|
## Supported players
|
||||||
Any player that uses the MediaSession API _should_ be supported
|
Any player that uses the MediaSession API _should_ be supported
|
||||||
|
|
||||||
|
## Using Spotify for lyrics
|
||||||
|
If you plan on using the "Use Spotify for lyrics" feature you will need to create a Spotify developer app.
|
||||||
|
You can do so [here](https://developer.spotify.com/dashboard/applications), and you should follow these steps:
|
||||||
|
- Click on "Create app" in your developer dashboard
|
||||||
|
- Name your app whatever you'd like and give it a description
|
||||||
|
- Add the following redirect URI: `lyricsgrabbr://spotify-auth`
|
||||||
|
- Select `Android` under "Which API/SDKs are you planning to use?"
|
||||||
|
- Save your newly created app
|
||||||
|
- You will be redirected to your app page where you can click on "Settings" and find your Client ID under the "Basic Information" tab
|
||||||
|
|
||||||
|
You can then use the Client ID of your app inside LyricsGrabbr to authenticate with Spotify.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
The project is licensed under the GPL3 license while the assets (icon) are licensed under the creative commons license.
|
The project is licensed under the GPL3 license while the assets (icon) are licensed under the creative commons license.
|
||||||
|
|||||||
+23
-8
@@ -1,5 +1,6 @@
|
|||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@@ -21,8 +22,8 @@ android {
|
|||||||
applicationId = "com.illusionman1212.lyricsgrabbr"
|
applicationId = "com.illusionman1212.lyricsgrabbr"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 5
|
versionCode = 6
|
||||||
versionName = "0.4.0"
|
versionName = "1.0.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -64,11 +65,26 @@ android {
|
|||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.1"
|
kotlinCompilerExtensionVersion = "1.5.1"
|
||||||
}
|
}
|
||||||
packaging {
|
|
||||||
resources {
|
androidComponents {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
val abiVersionCodes = mapOf(
|
||||||
excludes += "/META-INF/DEPENDENCIES"
|
"x86_64" to 1,
|
||||||
excludes += "mozilla/*"
|
"armeabi-v7a" to 2,
|
||||||
|
"arm64-v8a" to 3,
|
||||||
|
)
|
||||||
|
|
||||||
|
onVariants { variant ->
|
||||||
|
// Assigns a different version code for each output APK
|
||||||
|
// other than the universal APK.
|
||||||
|
variant.outputs.forEach { output ->
|
||||||
|
val name = output.filters.find { it.filterType == ABI }?.identifier
|
||||||
|
|
||||||
|
// Stores the value of abiCodes that is associated with the ABI for this variant.
|
||||||
|
val abiCode = abiVersionCodes[name] ?: 0
|
||||||
|
// Assigns the new version code to output.versionCode, which changes the version code
|
||||||
|
// for only the output APK, not for the variant itself.
|
||||||
|
output.versionCode.set((output.versionCode.get() ?: 0) * 10 + abiCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +109,6 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
implementation("io.coil-kt:coil:2.5.0")
|
implementation("io.coil-kt:coil:2.5.0")
|
||||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||||
implementation("it.skrape:skrapeit:1.2.2")
|
|
||||||
implementation("org.jsoup:jsoup:1.17.2")
|
implementation("org.jsoup:jsoup:1.17.2")
|
||||||
implementation("com.adamratzman:spotify-api-kotlin-core:4.0.2")
|
implementation("com.adamratzman:spotify-api-kotlin-core:4.0.2")
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -19,3 +19,5 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
@@ -23,7 +23,7 @@ class LGApp: Application() {
|
|||||||
lateinit var lyricsRepository: LyricsRepository
|
lateinit var lyricsRepository: LyricsRepository
|
||||||
val credentialStore by lazy {
|
val credentialStore by lazy {
|
||||||
SpotifyDefaultCredentialStore(
|
SpotifyDefaultCredentialStore(
|
||||||
clientId = "10d81ce453e743559ea362df2e6a1fd2",
|
clientId = "",
|
||||||
redirectUri = SPOTIFY_REDIRECT_URI,
|
redirectUri = SPOTIFY_REDIRECT_URI,
|
||||||
applicationContext = applicationContext
|
applicationContext = applicationContext
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
navController.navigate(Routes.Lyrics.route)
|
navController.navigate(Routes.Lyrics.route)
|
||||||
},
|
},
|
||||||
lyricsViewModel = lyricsViewModel,
|
lyricsViewModel = lyricsViewModel,
|
||||||
settingsViewModel = settingsViewModel,
|
|
||||||
goBack = { finish() },
|
goBack = { finish() },
|
||||||
lastNotification = lastNotification,
|
lastNotification = lastNotification,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ package com.illusionman1212.lyricsgrabbr.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
import com.illusionman1212.lyricsgrabbr.data.network.GET
|
||||||
import com.illusionman1212.lyricsgrabbr.data.network.NetworkHelper
|
import com.illusionman1212.lyricsgrabbr.data.network.NetworkHelper
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.await
|
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.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
@@ -56,8 +52,10 @@ class LyricsRepository(private val context: Context) {
|
|||||||
GET(url.toString())
|
GET(url.toString())
|
||||||
).await()
|
).await()
|
||||||
|
|
||||||
|
val body = res.body?.string()
|
||||||
|
|
||||||
if (res.isSuccessful) {
|
if (res.isSuccessful) {
|
||||||
return Pair(json.decodeFromString(SuccessResponse.serializer(), res.body!!.string())
|
return Pair(json.decodeFromString(SuccessResponse.serializer(), body!!)
|
||||||
.lyrics
|
.lyrics
|
||||||
.lines.joinToString(separator = "\n") { it.words }, true)
|
.lines.joinToString(separator = "\n") { it.words }, true)
|
||||||
} else {
|
} else {
|
||||||
@@ -70,34 +68,21 @@ class LyricsRepository(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fetchLyricsFromGenius(url: Uri): String {
|
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 doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
|
||||||
|
val divs = doc.select("div[data-lyrics-container]")
|
||||||
|
|
||||||
try {
|
val lyrics = StringUtil.borrowBuilder()
|
||||||
val lyrics = htmlDocument(doc.toString()) {
|
|
||||||
// set relaxed for instrumental songs
|
|
||||||
relaxed = true
|
|
||||||
|
|
||||||
findFirst {
|
divs.forEach {
|
||||||
div {
|
it.traverse(object : NodeVisitor {
|
||||||
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) {
|
override fun head(node: Node, depth: Int) {
|
||||||
if (node is TextNode) {
|
if (node is TextNode) {
|
||||||
accum.append(node.text().trim())
|
lyrics.append(node.text().trim())
|
||||||
// append space here to get proper spacing when there's a <i> tag or other inline text tags
|
// append space here to get proper spacing when there's a <i> tag or other inline text tags
|
||||||
accum.append(" ")
|
lyrics.append(" ")
|
||||||
} else if (node is Element) {
|
} else if (node is Element) {
|
||||||
if (accum.isNotEmpty() && node.normalName() == "br") {
|
if (lyrics.isNotEmpty() && node.normalName() == "br") {
|
||||||
accum.append("\n")
|
lyrics.append("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,26 +90,19 @@ class LyricsRepository(private val context: Context) {
|
|||||||
override fun tail(node: Node, depth: Int) {
|
override fun tail(node: Node, depth: Int) {
|
||||||
// since we append a space after every text node, we only append a newline if
|
// since we append a space after every text node, we only append a newline if
|
||||||
// the last char isn't a space
|
// the last char isn't a space
|
||||||
val lastCharIsWhitespace = accum.isNotEmpty() && accum[accum.length - 1] == ' '
|
val lastCharIsWhitespace = lyrics.isNotEmpty() && lyrics[lyrics.length - 1] == ' '
|
||||||
if (node is Element) {
|
if (node is Element) {
|
||||||
if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace) {
|
if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace) {
|
||||||
accum.append("\n")
|
lyrics.append("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
accum
|
lyrics.append("\n")
|
||||||
}
|
|
||||||
}
|
|
||||||
}.replace("\n\n\n", "\n\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lyrics
|
return lyrics.toString().replace("\n\n\n", "\n\n")
|
||||||
} catch (e: ElementNotFoundException) {
|
|
||||||
// TODO: relaxed is broken in skrape.it, so we have to catch this exception
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.illusionman1212.lyricsgrabbr.ui.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.PlainTooltipBox
|
import androidx.compose.material3.PlainTooltipBox
|
||||||
@@ -9,7 +10,9 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.ModifierInfo
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -29,9 +32,8 @@ fun LGIconButton(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
Modifier.tooltipAnchor(),
|
Modifier.tooltipAnchor(),
|
||||||
) {
|
content = content
|
||||||
content()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Box(Modifier.layoutId("badge")) {
|
Box(Modifier.layoutId("badge")) {
|
||||||
badge?.let { it() }
|
badge?.let { it() }
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -102,7 +103,6 @@ fun HomePage(
|
|||||||
homeViewModel: HomeViewModel = viewModel(
|
homeViewModel: HomeViewModel = viewModel(
|
||||||
factory = HomeViewModel.Factory,
|
factory = HomeViewModel.Factory,
|
||||||
),
|
),
|
||||||
settingsViewModel: SettingsViewModel,
|
|
||||||
lyricsViewModel: LyricsViewModel,
|
lyricsViewModel: LyricsViewModel,
|
||||||
lastNotification: MutableStateFlow<NotificationEvent?>
|
lastNotification: MutableStateFlow<NotificationEvent?>
|
||||||
) {
|
) {
|
||||||
@@ -141,7 +141,7 @@ fun HomePage(
|
|||||||
|
|
||||||
val song = lastNotification.collectAsStateWithLifecycle().value
|
val song = lastNotification.collectAsStateWithLifecycle().value
|
||||||
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState = homeViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
var getLyricsFromSpotify by remember { mutableStateOf(false)}
|
var getLyricsFromSpotify by rememberSaveable { mutableStateOf(false)}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!shouldShowNotificationPermissionRationale)
|
if (!shouldShowNotificationPermissionRationale)
|
||||||
|
|||||||
@@ -22,21 +22,25 @@ import androidx.compose.material3.TextField
|
|||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||||
|
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -82,6 +86,7 @@ fun LyricsPage(
|
|||||||
onClick = goBack
|
onClick = goBack
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
modifier = Modifier.mirror(),
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
id = R.string.go_back
|
id = R.string.go_back
|
||||||
@@ -107,10 +112,25 @@ fun LyricsPage(
|
|||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
} else {
|
} 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()
|
Instrumental()
|
||||||
} else {
|
} else {
|
||||||
Column(Modifier.padding(top = 12.dp, start = 12.dp, end = 12.dp, bottom = 32.dp)) {
|
|
||||||
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
|
Column(
|
||||||
|
Modifier.padding(
|
||||||
|
top = 12.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
bottom = 32.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
TextField(
|
TextField(
|
||||||
value = uiState.lyrics,
|
value = uiState.lyrics,
|
||||||
@@ -131,6 +151,7 @@ fun LyricsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
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.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.data.Theme
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
||||||
|
import com.illusionman1212.lyricsgrabbr.utils.mirror
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -131,18 +131,17 @@ fun SettingsPage(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
.padding(vertical = 8.dp, horizontal = 4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
|
||||||
Icon(
|
Icon(
|
||||||
|
modifier = Modifier.mirror(),
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
contentDescription = stringResource(
|
contentDescription = stringResource(
|
||||||
id = R.string.go_back
|
id = R.string.go_back
|
||||||
@@ -150,7 +149,6 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
Text(text = stringResource(id = R.string.settings), style = MaterialTheme.typography.titleLarge)
|
Text(text = stringResource(id = R.string.settings), style = MaterialTheme.typography.titleLarge)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
@@ -278,6 +276,7 @@ fun Lyrics(viewModel: SettingsViewModel, uiState: SettingsState) {
|
|||||||
val spotifyDesc = annotatedStringResource(R.string.spotify_client_id_desc) {
|
val spotifyDesc = annotatedStringResource(R.string.spotify_client_id_desc) {
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
"href" -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
|
"href" -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
|
||||||
|
"weight" -> SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
|
||||||
else -> null
|
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) {
|
DisposableEffect(darkTheme) {
|
||||||
(context as ComponentActivity).enableEdgeToEdge(
|
(context as ComponentActivity).enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.auto(
|
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="use_spotify_api">استخدم Spotify للكلمات</string>
|
||||||
<string name="spotify_client_id">Spotify Client ID</string>
|
<string name="spotify_client_id">Spotify Client ID</string>
|
||||||
<string name="not_set">خالي</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 -->
|
<!-- Lyrics -->
|
||||||
<string name="lyrics">كلمات الأغنية</string>
|
<string name="lyrics">كلمات الأغنية</string>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<string name="use_spotify_api">Use Spotify for lyrics</string>
|
<string name="use_spotify_api">Use Spotify for lyrics</string>
|
||||||
<string name="spotify_client_id" translatable="false">Spotify Client ID</string>
|
<string name="spotify_client_id" translatable="false">Spotify Client ID</string>
|
||||||
<string name="not_set">Not Set</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 -->
|
<!-- Lyrics -->
|
||||||
<string name="lyrics">Lyrics</string>
|
<string name="lyrics">Lyrics</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user