mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
remove spotify as lyrics source
In order to stay on f-droid spotify has to be removed. Doing different flavors is not worth it in my opinion.
This commit is contained in:
@@ -8,17 +8,5 @@ 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.
|
||||||
|
|||||||
+15
-16
@@ -107,31 +107,30 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.3")
|
||||||
implementation("androidx.activity:activity-compose:1.8.2")
|
implementation("androidx.activity:activity-compose:1.9.0")
|
||||||
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material3:material3:1.1.2")
|
implementation("androidx.compose.material3:material3:1.2.1")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.5.4")
|
implementation("androidx.compose.material:material-icons-extended:1.6.8")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
implementation("androidx.navigation:navigation-compose:2.7.7")
|
||||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
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.6.0")
|
||||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
implementation("org.jsoup:jsoup:1.17.2")
|
implementation("org.jsoup:jsoup:1.17.2")
|
||||||
implementation("com.adamratzman:spotify-api-kotlin-core:4.0.2")
|
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
androidTestImplementation(platform("androidx.compose:compose-bom:2024.06.00"))
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
|||||||
@@ -31,19 +31,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".SpotifyPkceLoginActivityImpl"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="lyricsgrabbr" android:host="spotify-auth" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:label="Lyrics Grabbr Notifications Listener"
|
android:label="Lyrics Grabbr Notifications Listener"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
||||||
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
|
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
|
||||||
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
|
||||||
@@ -15,24 +14,15 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
|||||||
name = PREFERENCES_NAME
|
name = PREFERENCES_NAME
|
||||||
)
|
)
|
||||||
|
|
||||||
const val SPOTIFY_REDIRECT_URI = "lyricsgrabbr://spotify-auth"
|
|
||||||
|
|
||||||
class LGApp: Application() {
|
class LGApp: Application() {
|
||||||
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
|
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
|
||||||
lateinit var homeRepository: HomeRepository
|
lateinit var homeRepository: HomeRepository
|
||||||
lateinit var lyricsRepository: LyricsRepository
|
lateinit var lyricsRepository: LyricsRepository
|
||||||
val credentialStore by lazy {
|
|
||||||
SpotifyDefaultCredentialStore(
|
|
||||||
clientId = "",
|
|
||||||
redirectUri = SPOTIFY_REDIRECT_URI,
|
|
||||||
applicationContext = applicationContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
|
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
|
||||||
homeRepository = HomeRepository(applicationContext)
|
homeRepository = HomeRepository(applicationContext)
|
||||||
lyricsRepository = LyricsRepository(applicationContext)
|
lyricsRepository = LyricsRepository()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr
|
package com.illusionman1212.lyricsgrabbr
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -22,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
@@ -60,10 +58,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = context as Activity
|
|
||||||
val application = activity.application as LGApp
|
|
||||||
|
|
||||||
val settingsViewModel: SettingsViewModel = viewModel(
|
val settingsViewModel: SettingsViewModel = viewModel(
|
||||||
factory = SettingsViewModel.Factory
|
factory = SettingsViewModel.Factory
|
||||||
)
|
)
|
||||||
@@ -83,7 +77,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
settingsViewModel.updateSpotifyAuthStatus(application.credentialStore.spotifyToken?.refreshToken != null)
|
|
||||||
appTheme.intValue = settingsViewModel.getAppTheme()
|
appTheme.intValue = settingsViewModel.getAppTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr
|
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.adamratzman.spotify.SpotifyScope
|
|
||||||
import com.adamratzman.spotify.auth.pkce.AbstractSpotifyPkceLoginActivity
|
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
|
||||||
|
|
||||||
class SpotifyPkceLoginActivityImpl: AbstractSpotifyPkceLoginActivity() {
|
|
||||||
override val clientId: String = CLIENT_ID
|
|
||||||
override val redirectUri: String = SPOTIFY_REDIRECT_URI
|
|
||||||
override val scopes: List<SpotifyScope> = listOf(SpotifyScope.UserReadCurrentlyPlaying)
|
|
||||||
|
|
||||||
override fun onFailure(exception: Exception) {
|
|
||||||
Toast.makeText(this, "Failed to authenticate", Toast.LENGTH_SHORT).show()
|
|
||||||
settingsViewModel?.updateSpotifyAuthStatus(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(api: SpotifyClientApi) {
|
|
||||||
(application as LGApp).credentialStore.setSpotifyApi(api)
|
|
||||||
Toast.makeText(this, "Successfully authenticated", Toast.LENGTH_SHORT).show()
|
|
||||||
settingsViewModel?.updateSpotifyAuthStatus(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var CLIENT_ID = ""
|
|
||||||
var settingsViewModel: SettingsViewModel? = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.data
|
package com.illusionman1212.lyricsgrabbr.data
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
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 kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.internal.StringUtil
|
import org.jsoup.internal.StringUtil
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
@@ -15,58 +8,7 @@ import org.jsoup.nodes.Node
|
|||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import org.jsoup.select.NodeVisitor
|
import org.jsoup.select.NodeVisitor
|
||||||
|
|
||||||
@Serializable
|
class LyricsRepository {
|
||||||
data class LyricLine(
|
|
||||||
val startTimeMs: ULong,
|
|
||||||
val words: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Lyrics(
|
|
||||||
val syncType: String,
|
|
||||||
val lines: List<LyricLine>,
|
|
||||||
val language: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SuccessResponse(
|
|
||||||
val lyrics: Lyrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
class LyricsRepository(private val context: Context) {
|
|
||||||
private val networkHelper = NetworkHelper(context)
|
|
||||||
private val json = Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchLyricsFromSpotify(trackId: String): Pair<String, Boolean> {
|
|
||||||
val builder = Uri.Builder()
|
|
||||||
val url = builder.scheme("https")
|
|
||||||
.authority("api.lyricstify.vercel.app")
|
|
||||||
.appendPath("v1")
|
|
||||||
.appendPath("lyrics")
|
|
||||||
.appendPath(trackId)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val res = networkHelper.client.newCall(
|
|
||||||
GET(url.toString())
|
|
||||||
).await()
|
|
||||||
|
|
||||||
val body = res.body?.string()
|
|
||||||
|
|
||||||
if (res.isSuccessful) {
|
|
||||||
return Pair(json.decodeFromString(SuccessResponse.serializer(), body!!)
|
|
||||||
.lyrics
|
|
||||||
.lines.joinToString(separator = "\n") { it.words }, true)
|
|
||||||
} else {
|
|
||||||
if (res.code == 404) {
|
|
||||||
return Pair(context.resources.getString(R.string.spotify_no_lyrics_error), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(context.resources.getString(R.string.generic_error), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchLyricsFromGenius(url: Uri): String {
|
fun fetchLyricsFromGenius(url: Uri): String {
|
||||||
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]")
|
val divs = doc.select("div[data-lyrics-container]")
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ 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.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.material3.rememberTooltipState
|
||||||
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
|
||||||
@@ -22,16 +21,19 @@ fun LGIconButton(
|
|||||||
badge: (@Composable BoxScope.() -> Unit)? = null,
|
badge: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val tooltipState = rememberTooltipState()
|
||||||
|
|
||||||
Layout(
|
Layout(
|
||||||
content = {
|
content = {
|
||||||
PlainTooltipBox(
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
Text(text = tooltip)
|
Text(text = tooltip)
|
||||||
},
|
},
|
||||||
|
state = tooltipState,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
Modifier.tooltipAnchor(),
|
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -73,4 +75,4 @@ fun LGIconButton(
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ import com.illusionman1212.lyricsgrabbr.LGApp
|
|||||||
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
|
import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter
|
||||||
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
|
||||||
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ fun HomePage(
|
|||||||
) }
|
) }
|
||||||
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
|
var shouldShowNotificationPermissionRationale by remember { mutableStateOf(
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
context as android.app.Activity,
|
context,
|
||||||
POST_NOTIFICATIONS
|
POST_NOTIFICATIONS
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -141,7 +140,6 @@ 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 rememberSaveable { mutableStateOf(false)}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (!shouldShowNotificationPermissionRationale)
|
if (!shouldShowNotificationPermissionRationale)
|
||||||
@@ -159,17 +157,9 @@ fun HomePage(
|
|||||||
|
|
||||||
LaunchedEffect(song) {
|
LaunchedEffect(song) {
|
||||||
if (song != null && song.title != uiState.notification?.title && song.artist != uiState.notification?.artist) {
|
if (song != null && song.title != uiState.notification?.title && song.artist != uiState.notification?.artist) {
|
||||||
val spotifyApi = application.credentialStore.getSpotifyClientPkceApi()
|
|
||||||
val useSpotifyApi = song.packageName == "com.spotify.music" && spotifyApi != null
|
|
||||||
getLyricsFromSpotify = useSpotifyApi
|
|
||||||
|
|
||||||
homeViewModel.setSong(song)
|
homeViewModel.setSong(song)
|
||||||
|
|
||||||
if (useSpotifyApi) {
|
homeViewModel.makeRequestToGenius(song.title, song.artist)
|
||||||
homeViewModel.makeRequestToSpotify(spotifyApi!!)
|
|
||||||
} else {
|
|
||||||
homeViewModel.makeRequestToGenius(song.title, song.artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +325,7 @@ fun HomePage(
|
|||||||
},
|
},
|
||||||
navigateToLyrics = navigateToLyrics,
|
navigateToLyrics = navigateToLyrics,
|
||||||
setLyricsTitle = { id, title, subtitle, url ->
|
setLyricsTitle = { id, title, subtitle, url ->
|
||||||
lyricsViewModel.onSongChange(id, title, subtitle, url, getLyricsFromSpotify)
|
lyricsViewModel.onSongChange(id, title, subtitle, url)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,7 @@ fun LyricsPage(
|
|||||||
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
|
|
||||||
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
|
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
|
||||||
if (uiState.useSpotifyApi) {
|
lyricsViewModel.fetchLyricsFromGenius()
|
||||||
lyricsViewModel.fetchLyricsFromSpotify()
|
|
||||||
} else {
|
|
||||||
lyricsViewModel.fetchLyricsFromGenius()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.illusionman1212.lyricsgrabbr.ui.screens
|
package com.illusionman1212.lyricsgrabbr.ui.screens
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -13,19 +12,16 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.outlined.Palette
|
import androidx.compose.material.icons.outlined.Palette
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -36,35 +32,20 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
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.LocalUriHandler
|
|
||||||
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.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.text.style.TextOverflow
|
||||||
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
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.adamratzman.spotify.SpotifyException
|
|
||||||
import com.adamratzman.spotify.auth.pkce.startSpotifyClientPkceLoginActivity
|
|
||||||
import com.illusionman1212.lyricsgrabbr.LGApp
|
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
import com.illusionman1212.lyricsgrabbr.R
|
||||||
import com.illusionman1212.lyricsgrabbr.SpotifyPkceLoginActivityImpl
|
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
|
import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton
|
||||||
import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight
|
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.mirror
|
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
|
|
||||||
|
|
||||||
private val Themes = hashMapOf(
|
private val Themes = hashMapOf(
|
||||||
Theme.LIGHT.ordinal to R.string.theme_light,
|
Theme.LIGHT.ordinal to R.string.theme_light,
|
||||||
@@ -72,56 +53,6 @@ private val Themes = hashMapOf(
|
|||||||
Theme.SYSTEM.ordinal to R.string.theme_system
|
Theme.SYSTEM.ordinal to R.string.theme_system
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: this is here as a reference because I think I will need a guard in the future
|
|
||||||
fun <T> Activity.guardValidSpotifyApi(
|
|
||||||
classBackTo: Class<out Activity>,
|
|
||||||
alreadyTriedToReauthenticate: Boolean = false,
|
|
||||||
block: suspend (api: SpotifyClientApi) -> T
|
|
||||||
): T? {
|
|
||||||
return runBlocking {
|
|
||||||
val app = (application as LGApp)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val token = app.credentialStore.spotifyToken
|
|
||||||
?: throw SpotifyException.ReAuthenticationNeededException()
|
|
||||||
val usesPkceAuth = token.refreshToken != null
|
|
||||||
val api = (if (usesPkceAuth) app.credentialStore.getSpotifyClientPkceApi()
|
|
||||||
else app.credentialStore.getSpotifyImplicitGrantApi())
|
|
||||||
?: throw SpotifyException.ReAuthenticationNeededException()
|
|
||||||
|
|
||||||
block(api)
|
|
||||||
} catch (e: SpotifyException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
val api = app.credentialStore.getSpotifyClientPkceApi()!!
|
|
||||||
if (!alreadyTriedToReauthenticate) {
|
|
||||||
try {
|
|
||||||
api.refreshToken()
|
|
||||||
app.credentialStore.spotifyToken = api.token
|
|
||||||
block(api)
|
|
||||||
} catch (e: SpotifyException.ReAuthenticationNeededException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return@runBlocking guardValidSpotifyApi(
|
|
||||||
classBackTo = classBackTo,
|
|
||||||
alreadyTriedToReauthenticate = true,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return@runBlocking guardValidSpotifyApi(
|
|
||||||
classBackTo = classBackTo,
|
|
||||||
alreadyTriedToReauthenticate = true,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//pkceClassBackTo = classBackTo
|
|
||||||
startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsPage(
|
fun SettingsPage(
|
||||||
goBack: () -> Unit,
|
goBack: () -> Unit,
|
||||||
@@ -159,7 +90,6 @@ fun SettingsPage(
|
|||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Appearance(viewModel, uiState)
|
Appearance(viewModel, uiState)
|
||||||
Lyrics(viewModel, uiState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,84 +159,6 @@ fun ThemeDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Lyrics(viewModel: SettingsViewModel, uiState: SettingsState) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = context as Activity
|
|
||||||
val application = activity.application as LGApp
|
|
||||||
|
|
||||||
var isDialogOpen by remember { mutableStateOf(false) }
|
|
||||||
var spotifyIdValue by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
SettingGroup(stringResource(id = R.string.lyrics)) {
|
|
||||||
ToggleSetting(
|
|
||||||
title = stringResource(id = R.string.use_spotify_api),
|
|
||||||
checked = uiState.isSpotifyAuthed,
|
|
||||||
icon = painterResource(id = R.drawable.spotify),
|
|
||||||
onClick = {
|
|
||||||
if (uiState.isSpotifyAuthed) {
|
|
||||||
application.credentialStore.clear()
|
|
||||||
viewModel.updateSpotifyAuthStatus(false)
|
|
||||||
} else {
|
|
||||||
isDialogOpen = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDialogOpen) {
|
|
||||||
LGAlertDialog(
|
|
||||||
onDismiss = { isDialogOpen = false },
|
|
||||||
title = stringResource(id = R.string.spotify_client_id),
|
|
||||||
buttons = {
|
|
||||||
TextButton(onClick = { isDialogOpen = false }) {
|
|
||||||
Text(stringResource(id = R.string.cancel))
|
|
||||||
}
|
|
||||||
TextButton(onClick = {
|
|
||||||
SpotifyPkceLoginActivityImpl.CLIENT_ID = spotifyIdValue
|
|
||||||
SpotifyPkceLoginActivityImpl.settingsViewModel = viewModel
|
|
||||||
activity.startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
|
|
||||||
isDialogOpen = false
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.confirm))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
val linkColor = MaterialTheme.colorScheme.surfaceTint
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val textColor = LocalContentColor.current
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
ClickableText(
|
|
||||||
spotifyDesc,
|
|
||||||
onClick = { offset ->
|
|
||||||
spotifyDesc.getStringAnnotations(tag = "href", start = offset, end = offset)
|
|
||||||
.firstOrNull()
|
|
||||||
?.let {
|
|
||||||
uriHandler.openUri(it.item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = spotifyIdValue,
|
|
||||||
onValueChange = { spotifyIdValue = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(stringResource(id = R.string.not_set))
|
|
||||||
},
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingGroup(title: String, content: @Composable () -> Unit) {
|
fun SettingGroup(title: String, content: @Composable () -> Unit) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ package com.illusionman1212.lyricsgrabbr.viewmodels
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import com.adamratzman.spotify.SpotifyClientApi
|
|
||||||
import com.illusionman1212.lyricsgrabbr.LGApp
|
import com.illusionman1212.lyricsgrabbr.LGApp
|
||||||
import com.illusionman1212.lyricsgrabbr.NotificationEvent
|
import com.illusionman1212.lyricsgrabbr.NotificationEvent
|
||||||
import com.illusionman1212.lyricsgrabbr.R
|
|
||||||
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -103,32 +100,6 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho
|
|||||||
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
|
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeRequestToSpotify(spotifyApi: SpotifyClientApi) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val track = spotifyApi.player.getCurrentlyPlaying()?.item
|
|
||||||
|
|
||||||
if (track?.id == null) {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = context.resources.getString(R.string.no_spotify_song_playing)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
isLoading = false,
|
|
||||||
error = null,
|
|
||||||
results = listOf(SearchResult(
|
|
||||||
title = _uiState.value.notification!!.title,
|
|
||||||
artist = _uiState.value.notification!!.artist,
|
|
||||||
url = track.asTrack?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
|
|
||||||
artistUrl = track.asTrack?.artists?.get(0)?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
|
|
||||||
thumbnail = track.asTrack?.album?.images?.get(0)?.url?.toUri() ?: Uri.EMPTY,
|
|
||||||
id = track.id!!,
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeRequestToGenius(song: String, artist: String) {
|
fun makeRequestToGenius(song: String, artist: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
|
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ data class LyricsState(
|
|||||||
val artist: String = "",
|
val artist: String = "",
|
||||||
val url: Uri = Uri.EMPTY,
|
val url: Uri = Uri.EMPTY,
|
||||||
val trackId: String = "",
|
val trackId: String = "",
|
||||||
val useSpotifyApi: Boolean = false,
|
|
||||||
|
|
||||||
// set by Lyrics screen
|
// set by Lyrics screen
|
||||||
val lyrics: String = "",
|
val lyrics: String = "",
|
||||||
@@ -35,24 +34,9 @@ class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel
|
|||||||
private val _uiState = MutableStateFlow(LyricsState())
|
private val _uiState = MutableStateFlow(LyricsState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri, useSpotifyApi: Boolean) {
|
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(song = title, artist = subtitle, url = url, useSpotifyApi = useSpotifyApi, trackId = trackId)
|
it.copy(song = title, artist = subtitle, url = url, trackId = trackId)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchLyricsFromSpotify() {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val (lyrics, isSuccess) = lyricsRepository.fetchLyricsFromSpotify(uiState.value.trackId)
|
|
||||||
if (isSuccess) {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
|
|
||||||
} else {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false, error = lyrics, lyrics = "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
data class SettingsState(
|
data class SettingsState(
|
||||||
val appTheme: Int = Theme.SYSTEM.ordinal,
|
val appTheme: Int = Theme.SYSTEM.ordinal,
|
||||||
val isSpotifyAuthed: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
|
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
|
||||||
@@ -35,10 +34,6 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSpotifyAuthStatus(isAuthed: Boolean) {
|
|
||||||
_uiState.value = _uiState.value.copy(isSpotifyAuthed = isAuthed)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsPrefsRepo.preferences.collectLatest {
|
settingsPrefsRepo.preferences.collectLatest {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string>
|
<string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string>
|
||||||
<string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string>
|
<string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string>
|
||||||
<string name="retry">أعد المحاولة</string>
|
<string name="retry">أعد المحاولة</string>
|
||||||
<string name="spotify_no_lyrics_error">عذرًا، لا يحتوي Spotify على كلمات لهذه الأغنية</string>
|
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="no_supported_player">لا يوجد مشغل مدعوم قيد التشغيل</string>
|
<string name="no_supported_player">لا يوجد مشغل مدعوم قيد التشغيل</string>
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
<string name="notification_post_permission_desc">يتطلب هذا التطبيق أذونات الإشعارات لبدء خدمة مقدمة بغرض الاستماع إلى إشعارات الموسيقى، ولا يمكن أن يعمل بدونها.</string>
|
<string name="notification_post_permission_desc">يتطلب هذا التطبيق أذونات الإشعارات لبدء خدمة مقدمة بغرض الاستماع إلى إشعارات الموسيقى، ولا يمكن أن يعمل بدونها.</string>
|
||||||
<string name="notification_post_permission_not_granted">لم يتم منح إذن الإشعارات، لن يعمل التطبيق بشكل صحيح</string>
|
<string name="notification_post_permission_not_granted">لم يتم منح إذن الإشعارات، لن يعمل التطبيق بشكل صحيح</string>
|
||||||
<string name="grant_permission">منح الإذن</string>
|
<string name="grant_permission">منح الإذن</string>
|
||||||
<string name="no_spotify_song_playing">No song is playing on Spotify</string>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings">الإعدادات</string>
|
<string name="settings">الإعدادات</string>
|
||||||
@@ -34,10 +32,6 @@
|
|||||||
<string name="theme_light">نهار</string>
|
<string name="theme_light">نهار</string>
|
||||||
<string name="theme_dark">ليلي</string>
|
<string name="theme_dark">ليلي</string>
|
||||||
<string name="theme_system">استخدم مظهر النظام</string>
|
<string name="theme_system">استخدم مظهر النظام</string>
|
||||||
<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<annotation weight="bold">لا يعمل هذا إلا إذا كنت تقوم بتشغيل الموسيقى من مشغل Spotify.</annotation></string>
|
|
||||||
|
|
||||||
<!-- Lyrics -->
|
<!-- Lyrics -->
|
||||||
<string name="lyrics">كلمات الأغنية</string>
|
<string name="lyrics">كلمات الأغنية</string>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<string name="timeout">Request has timed out, try again later</string>
|
<string name="timeout">Request has timed out, try again later</string>
|
||||||
<string name="generic_error">An error has occurred, try again later</string>
|
<string name="generic_error">An error has occurred, try again later</string>
|
||||||
<string name="retry">Retry</string>
|
<string name="retry">Retry</string>
|
||||||
<string name="spotify_no_lyrics_error">Sorry, Spotify doesn\'t have lyrics for this song</string>
|
|
||||||
|
|
||||||
<!-- Home -->
|
<!-- Home -->
|
||||||
<string name="no_supported_player">No supported player is running</string>
|
<string name="no_supported_player">No supported player is running</string>
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
<string name="notification_post_permission_desc">LyricsGrabbr requires notification permissions to start a foreground service for the purpose of listening to music notifications, and cannot function without it.</string>
|
<string name="notification_post_permission_desc">LyricsGrabbr requires notification permissions to start a foreground service for the purpose of listening to music notifications, and cannot function without it.</string>
|
||||||
<string name="notification_post_permission_not_granted">Notification permission is not granted, the app will not function properly</string>
|
<string name="notification_post_permission_not_granted">Notification permission is not granted, the app will not function properly</string>
|
||||||
<string name="grant_permission">Grant Permission</string>
|
<string name="grant_permission">Grant Permission</string>
|
||||||
<string name="no_spotify_song_playing">No song is playing on Spotify</string>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
@@ -33,10 +31,6 @@
|
|||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
<string name="theme_system">Use System Theme</string>
|
<string name="theme_system">Use System Theme</string>
|
||||||
<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>.\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