2 Commits
Author SHA1 Message Date
Illusion 6c167b6f6f whitelist search 2024-07-14 06:53:28 +02:00
Illusion 1a105f8413 feat: whitelist for notification listening 2024-07-12 02:36:17 +02:00
34 changed files with 153 additions and 469 deletions
+2
View File
@@ -1,3 +1,4 @@
*.iml
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea/caches
@@ -6,6 +7,7 @@
/.idea/workspace.xml /.idea/workspace.xml
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
.DS_Store
/build /build
/captures /captures
.externalNativeBuild .externalNativeBuild
+3 -3
View File
@@ -22,8 +22,8 @@ android {
applicationId = "com.illusionman1212.lyricsgrabbr" applicationId = "com.illusionman1212.lyricsgrabbr"
minSdk = 23 minSdk = 23
targetSdk = 34 targetSdk = 34
versionCode = 8 versionCode = 6
versionName = "1.1.1" versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -32,7 +32,7 @@ android {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
// https://developer.android.com/guide/topics/resources/app-languages#gradle-config // https://developer.android.com/guide/topics/resources/app-languages#gradle-config
resourceConfigurations.plus(listOf("en", "ar", "de")) resourceConfigurations.plus(listOf("en", "ar"))
} }
signingConfigs { signingConfigs {
-1
View File
@@ -12,7 +12,6 @@
<application <application
android:name=".LGApp" android:name=".LGApp"
android:allowBackup="true" android:allowBackup="true"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -116,7 +117,6 @@ class MainActivity : AppCompatActivity() {
navController.navigate(Routes.Lyrics.route) navController.navigate(Routes.Lyrics.route)
}, },
lyricsViewModel = lyricsViewModel, lyricsViewModel = lyricsViewModel,
settingsViewModel = settingsViewModel,
goBack = { finish() }, goBack = { finish() },
lastNotification = lastNotification, lastNotification = lastNotification,
) )
@@ -127,7 +127,6 @@ class MainActivity : AppCompatActivity() {
navController.popBackStack() navController.popBackStack()
}, },
lyricsViewModel = lyricsViewModel, lyricsViewModel = lyricsViewModel,
settingsViewModel = settingsViewModel
) )
} }
composable(Routes.Settings.route) { composable(Routes.Settings.route) {
@@ -2,7 +2,6 @@ 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 androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.illusionman1212.lyricsgrabbr.R import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.data.network.GET import com.illusionman1212.lyricsgrabbr.data.network.GET
@@ -11,7 +10,6 @@ import com.illusionman1212.lyricsgrabbr.utils.await
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okio.IOException import okio.IOException
import org.jsoup.SerializationException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@Serializable @Serializable
@@ -33,9 +31,11 @@ class HomeRepository(private val context: Context) {
return pkgs.contains(packageName) return pkgs.contains(packageName)
} }
suspend fun makeSearchRequest(lContext: Context, baseUrl: String, song: String, artist: String): Pair<String?, Boolean> { suspend fun makeSearchRequest(song: String, artist: String): Pair<String?, Boolean> {
val url = Uri.parse(baseUrl) val builder = Uri.Builder()
.buildUpon() val url = builder.scheme("https")
.authority("api.illusionman1212.com")
.appendPath("lyrics")
.appendPath("search") .appendPath("search")
.appendQueryParameter("q", "$song $artist") .appendQueryParameter("q", "$song $artist")
.appendQueryParameter("disableFuzzy", "true") .appendQueryParameter("disableFuzzy", "true")
@@ -47,23 +47,11 @@ class HomeRepository(private val context: Context) {
).await() ).await()
if (!res.isSuccessful) { if (!res.isSuccessful) {
if (res.code >= 500) { val errRes = json.decodeFromString(ErrorResponse.serializer(), res.body!!.string())
return Pair(lContext.resources.getString(R.string.internal_server_error), false)
}
if (res.body?.contentType()?.type != "text" && res.body?.contentType()?.subtype != "json") { // matches is only null if there's an error. otherwise it's 0 when there are no results
return Pair(lContext.resources.getString(R.string.invalid_json), false) if (errRes.matches == null) {
} return Pair(context.resources.getString(R.string.generic_error), false)
try {
val errRes = json.decodeFromString(ErrorResponse.serializer(), res.body!!.string())
// matches is only null if there's an error. otherwise it's 0 when there are no results
if (errRes.matches == null) {
return Pair(lContext.resources.getString(R.string.generic_error), false)
}
} catch (e: IllegalArgumentException) {
return Pair(lContext.resources.getString(R.string.failed_to_map_json), false)
} }
return Pair(null, false) return Pair(null, false)
@@ -72,10 +60,10 @@ class HomeRepository(private val context: Context) {
return Pair(res.body?.string(), true) return Pair(res.body?.string(), true)
} catch (e: IOException) { } catch (e: IOException) {
if (e.cause is SocketTimeoutException) { if (e.cause is SocketTimeoutException) {
return Pair(lContext.resources.getString(R.string.timeout), false) return Pair(context.resources.getString(R.string.timeout), false)
} }
return Pair(e.message, false) return Pair(context.resources.getString(R.string.generic_error), false)
} }
} }
} }
@@ -1,7 +1,6 @@
package com.illusionman1212.lyricsgrabbr.data package com.illusionman1212.lyricsgrabbr.data
import android.net.Uri import android.net.Uri
import org.jsoup.HttpStatusException
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
@@ -11,60 +10,41 @@ import org.jsoup.select.NodeVisitor
class LyricsRepository { class LyricsRepository {
fun fetchLyricsFromGenius(url: Uri): String { fun fetchLyricsFromGenius(url: Uri): String {
return try { val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get()
val doc = Jsoup.connect(url.toString()) val divs = doc.select("div[data-lyrics-container]")
.userAgent(USER_AGENT)
.get()
val divs = doc.select("div[data-lyrics-container]") val lyrics = StringUtil.borrowBuilder()
val lyrics = StringUtil.borrowBuilder()
divs.forEach { divs.forEach {
it.traverse(object : NodeVisitor { it.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) {
lyrics.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
lyrics.append(" ") lyrics.append(" ")
} else if (node is Element) { } else if (node is Element) {
if (lyrics.isNotEmpty() && node.normalName() == "br") { if (lyrics.isNotEmpty() && node.normalName() == "br") {
lyrics.append("\n") lyrics.append("\n")
}
} }
} }
}
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 = lyrics.isNotEmpty() && lyrics[lyrics.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) {
lyrics.append("\n") lyrics.append("\n")
}
} }
} }
})
lyrics.append("\n")
}
lyrics.toString().replace("\n\n\n", "\n\n")
} catch (e: HttpStatusException) {
when (e.statusCode) {
403 -> {
println("Access Forbidden: HTTP 403")
"Error 403: Cloudflare got in the way, try disabling the VPN or Proxy if enabled"
} }
else -> { })
println("HTTP Error: ${e.statusCode}")
"HTTP Error: ${e.statusCode}" lyrics.append("\n")
}
}
} catch (e: Exception) {
println("An error occurred: ${e.message}")
"An error occurred while fetching lyrics."
} }
return lyrics.toString().replace("\n\n\n", "\n\n")
} }
companion object { companion object {
@@ -2,10 +2,8 @@ package com.illusionman1212.lyricsgrabbr.data
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.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -18,9 +16,6 @@ enum class Theme {
data class SettingsPreferences( data class SettingsPreferences(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val whitelist: Set<String> = emptySet(), val whitelist: Set<String> = emptySet(),
val keepScreenOn: Boolean = false,
val whitelistPromptSeen: Boolean = false,
val geniURLbaseURL: String = "https://api.illusionman1212.com/lyrics",
) )
class SettingsPreferencesRepository( class SettingsPreferencesRepository(
@@ -29,9 +24,6 @@ class SettingsPreferencesRepository(
companion object { companion object {
private val APP_THEME = intPreferencesKey("app_theme") private val APP_THEME = intPreferencesKey("app_theme")
private val WHITELIST = stringSetPreferencesKey("apps_whitelist") private val WHITELIST = stringSetPreferencesKey("apps_whitelist")
private val KEEP_SCREEN_ON = booleanPreferencesKey("keep_screen_on")
private val WHITELIST_PROMPT_SEEN = booleanPreferencesKey("whitelist_prompt_seen")
private val GENIURL_BASE_URL = stringPreferencesKey("geniurl_base_url")
} }
val preferences = dataStore.data.map { mapSettingsPreferences(it) } val preferences = dataStore.data.map { mapSettingsPreferences(it) }
@@ -48,37 +40,16 @@ class SettingsPreferencesRepository(
} }
} }
suspend fun toggleKeepScreenOn() {
dataStore.edit { prefs ->
prefs[KEEP_SCREEN_ON] = !(prefs[KEEP_SCREEN_ON] ?: false)
}
}
suspend fun removeFromWhitelist(packageName: String) { suspend fun removeFromWhitelist(packageName: String) {
dataStore.edit { prefs -> dataStore.edit { prefs ->
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) - packageName prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) - packageName
} }
} }
suspend fun discardWhitelistPrompt() {
dataStore.edit { prefs ->
prefs[WHITELIST_PROMPT_SEEN] = true
}
}
suspend fun setGeniURLBaseURL(url: String) {
dataStore.edit { prefs ->
prefs[GENIURL_BASE_URL] = url
}
}
private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences { private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
return SettingsPreferences( return SettingsPreferences(
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal, prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
prefs[WHITELIST] ?: emptySet(), prefs[WHITELIST] ?: emptySet(),
prefs[KEEP_SCREEN_ON] ?: false,
prefs[WHITELIST_PROMPT_SEEN] ?: false,
prefs[GENIURL_BASE_URL] ?: "https://api.illusionman1212.com/lyrics",
) )
} }
} }
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -30,11 +29,11 @@ fun LGAlertDialog(
buttons: @Composable () -> Unit, buttons: @Composable () -> Unit,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
BasicAlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
properties = properties properties = properties,
) { ) {
Surface( Surface (
modifier = Modifier.wrapContentHeight().wrapContentWidth(), modifier = Modifier.wrapContentHeight().wrapContentWidth(),
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation tonalElevation = AlertDialogDefaults.TonalElevation
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -57,6 +58,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@@ -72,7 +74,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.currentStateAsState import androidx.lifecycle.compose.currentStateAsState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -87,7 +88,6 @@ import coil.compose.AsyncImage
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
@@ -101,7 +101,6 @@ fun HomePage(
factory = HomeViewModel.Factory, factory = HomeViewModel.Factory,
), ),
lyricsViewModel: LyricsViewModel, lyricsViewModel: LyricsViewModel,
settingsViewModel: SettingsViewModel,
lastNotification: MutableStateFlow<NotificationEvent?> lastNotification: MutableStateFlow<NotificationEvent?>
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -117,7 +116,6 @@ fun HomePage(
var songUri by remember { mutableStateOf(Uri.EMPTY) } var songUri by remember { mutableStateOf(Uri.EMPTY) }
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var listeningPermission = homeViewModel.getListeningPermission() var listeningPermission = homeViewModel.getListeningPermission()
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
var notificationPermission by remember { mutableStateOf( var notificationPermission by remember { mutableStateOf(
ActivityCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED ActivityCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED
) } ) }
@@ -158,7 +156,7 @@ fun HomePage(
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) {
homeViewModel.setSong(song) homeViewModel.setSong(song)
homeViewModel.makeRequestToGenius(context, song.title, song.artist) homeViewModel.makeRequestToGenius(song.title, song.artist)
} }
} }
@@ -240,30 +238,6 @@ fun HomePage(
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
} }
} else if (!settingsState.whitelistPromptSeen) {
LGAlertDialog(
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
),
onDismiss = { settingsViewModel.discardWhitelistPrompt() },
title = stringResource(id = R.string.whitelist_apps),
buttons = {
TextButton(
onClick = {
settingsViewModel.discardWhitelistPrompt()
}
) {
Text(stringResource(id = R.string.understood))
}
}
) {
Text(
text = stringResource(R.string.whitelist_prompt_desc),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
}
} }
Column( Column(
@@ -275,11 +249,7 @@ fun HomePage(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text( text = stringResource(id = R.string.no_supported_player), style = MaterialTheme.typography.headlineSmall )
text = stringResource(id = R.string.no_supported_player),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
)
if (!notificationPermission) { if (!notificationPermission) {
Text( Text(
text = stringResource(id = R.string.notification_post_permission_not_granted).uppercase(), text = stringResource(id = R.string.notification_post_permission_not_granted).uppercase(),
@@ -336,7 +306,7 @@ fun HomePage(
) )
FilledTonalButton( FilledTonalButton(
onClick = { onClick = {
homeViewModel.makeRequestToGenius(context, song.title, song.artist) homeViewModel.makeRequestToGenius(song.title, song.artist)
} }
) { ) {
Text(stringResource(id = R.string.retry)) Text(stringResource(id = R.string.retry))
@@ -1,14 +1,8 @@
package com.illusionman1212.lyricsgrabbr.ui.screens package com.illusionman1212.lyricsgrabbr.ui.screens
import android.app.Activity
import android.view.WindowManager.LayoutParams
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -16,7 +10,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -29,12 +23,10 @@ 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.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
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.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection 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
@@ -44,13 +36,12 @@ 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.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -59,27 +50,13 @@ fun LyricsPage(
lyricsViewModel: LyricsViewModel = viewModel( lyricsViewModel: LyricsViewModel = viewModel(
factory = LyricsViewModel.Factory factory = LyricsViewModel.Factory
), ),
settingsViewModel: SettingsViewModel,
) { ) {
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
val settingsState = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val context = LocalContext.current
LaunchedEffect(uiState.song, uiState.artist, uiState.url) { LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
lyricsViewModel.fetchLyricsFromGenius() lyricsViewModel.fetchLyricsFromGenius()
} }
DisposableEffect(Unit) {
val window = (context as Activity).window
if (settingsState.keepScreenOn) {
window.addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
}
onDispose {
window.clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -105,7 +82,8 @@ fun LyricsPage(
onClick = goBack onClick = goBack
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, modifier = Modifier.mirror(),
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource( contentDescription = stringResource(
id = R.string.go_back id = R.string.go_back
) )
@@ -115,63 +93,53 @@ fun LyricsPage(
) )
}, },
) { padding -> ) { padding ->
Column(Modifier.padding(padding)) { Column(
if (settingsState.keepScreenOn) { Modifier
Row( .padding(padding)
Modifier .fillMaxSize()
.background(MaterialTheme.colorScheme.secondaryContainer) .then(
.fillMaxWidth(), if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll(
horizontalArrangement = Arrangement.Center, rememberScrollState()
) { ) else Modifier
Text(stringResource(id = R.string.screen_will_stay_on).uppercase(), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSecondaryContainer) ),
} verticalArrangement = Arrangement.Center,
} horizontalAlignment = Alignment.CenterHorizontally,
Column( ) {
Modifier if (uiState.isLoading) {
.fillMaxSize() CircularProgressIndicator()
.then( } else {
if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll( if (uiState.error != null) {
rememberScrollState() Text(
) else Modifier uiState.error,
), style = MaterialTheme.typography.bodyMedium,
verticalArrangement = Arrangement.Center, modifier = Modifier.padding(bottom = 32.dp)
horizontalAlignment = Alignment.CenterHorizontally, )
) { } else if (uiState.lyrics.isEmpty()) {
if (uiState.isLoading) { Instrumental()
CircularProgressIndicator()
} else { } else {
if (uiState.error != null) {
Text( CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
uiState.error, Column(
style = MaterialTheme.typography.bodyMedium, Modifier.padding(
modifier = Modifier.padding(bottom = 32.dp) top = 12.dp,
) start = 12.dp,
} else if (uiState.lyrics.isEmpty()) { end = 12.dp,
Instrumental() bottom = 32.dp
} else { )
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { ) {
Column( SelectionContainer {
Modifier.padding( TextField(
top = 12.dp, value = uiState.lyrics,
start = 12.dp, onValueChange = {},
end = 12.dp, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
bottom = 32.dp readOnly = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
) )
) {
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,
),
)
}
} }
} }
} }
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.ui.screens package com.illusionman1212.lyricsgrabbr.ui.screens
import android.util.Patterns
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -19,8 +18,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ScreenLockPortrait
import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Translate import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -29,7 +26,6 @@ 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
@@ -38,6 +34,7 @@ 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
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
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.res.stringResource import androidx.compose.ui.res.stringResource
@@ -99,15 +96,13 @@ fun SettingsPage(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Appearance(viewModel, uiState) Appearance(viewModel, uiState)
geniURL(viewModel, uiState)
Listening(navigateToWhitelist) Listening(navigateToWhitelist)
PowerManagement(viewModel, uiState)
} }
} }
} }
@Composable @Composable
private fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) { fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
var themeOpenDialog by remember { mutableStateOf(false) } var themeOpenDialog by remember { mutableStateOf(false) }
var languageOpenDialog by remember { mutableStateOf(false) } var languageOpenDialog by remember { mutableStateOf(false) }
@@ -175,77 +170,6 @@ private fun Listening(navigateToWhitelist: () -> Unit) {
} }
} }
@Composable
private fun PowerManagement(viewModel: SettingsViewModel, uiState: SettingsState) {
SettingGroup(title = stringResource(id = R.string.power_management)) {
ToggleSetting(
title = stringResource(id = R.string.keep_device_awake_title),
description = stringResource(id = R.string.keep_device_awake_desc),
icon = Icons.Default.ScreenLockPortrait,
checked = uiState.keepScreenOn
) {
viewModel.toggleKeepScreenOn()
}
}
}
@Composable
private fun geniURL(viewModel: SettingsViewModel, uiState: SettingsState) {
var dialogOpen by remember { mutableStateOf(false) }
SettingGroup(title = "geniURL") {
DialogSetting(
title = stringResource(id = R.string.api_base_url),
value = uiState.geniURLbaseURL,
icon = Icons.Outlined.Cloud,
onClick = { dialogOpen = true }
)
}
if (dialogOpen) {
BaseURLDialog(
uiState = uiState,
viewModel = viewModel,
onDismiss = { dialogOpen = false },
)
}
}
@Composable
private fun BaseURLDialog(uiState: SettingsState, viewModel: SettingsViewModel, onDismiss: () -> Unit) {
var value by remember { mutableStateOf(uiState.geniURLbaseURL) }
var isValidUrl by remember { mutableStateOf(true) }
LGAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.base_url),
buttons = {
TextButton(
onClick = {
viewModel.setGeniURLBaseURL(value)
onDismiss()
},
enabled = isValidUrl
) {
Text(stringResource(id = R.string.confirm))
}
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.cancel))
}
},
modifier = Modifier.fillMaxWidth()
) {
TextField(
value = value,
isError = !isValidUrl,
onValueChange = { newValue ->
isValidUrl = Patterns.WEB_URL.matcher(newValue.lowercase()).matches() && newValue.startsWith("http", true)
value = newValue
},
)
}
}
@Composable @Composable
fun ThemeDialog( fun ThemeDialog(
uiState: SettingsState, uiState: SettingsState,
@@ -387,8 +311,7 @@ fun DialogSetting(
@Composable @Composable
fun ToggleSetting( fun ToggleSetting(
title: String, title: String,
description: String? = null, icon: Painter? = null,
icon: ImageVector? = null,
checked: Boolean, checked: Boolean,
onClick: (Boolean) -> Unit) onClick: (Boolean) -> Unit)
{ {
@@ -403,20 +326,11 @@ fun ToggleSetting(
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp), modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
) { ) {
if (icon != null) { if (icon != null) {
Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp)) Icon(painter = icon, contentDescription = null, Modifier.width(56.dp))
} else { } else {
Box(modifier = Modifier.width(56.dp)) Box(modifier = Modifier.width(56.dp))
} }
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
if (description != null) {
Text(
text = description,
color = ColorSecondaryLight,
fontSize = 14.sp,
)
}
}
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = null, onCheckedChange = null,
@@ -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
}
@@ -25,7 +25,7 @@ fun annotatedStringResource(
val spannedString = SpannedString(resources.getText(id)) val spannedString = SpannedString(resources.getText(id))
val resultBuilder = AnnotatedString.Builder() val resultBuilder = AnnotatedString.Builder()
resultBuilder.append(spannedString.toString()) resultBuilder.append(spannedString.toString())
spannedString.getSpans(0, spannedString.length, Annotation::class.java).forEach { annotation -> spannedString.getSpans<Annotation>(0, spannedString.length, Annotation::class.java).forEach { annotation ->
val spanStart = spannedString.getSpanStart(annotation) val spanStart = spannedString.getSpanStart(annotation)
val spanEnd = spannedString.getSpanEnd(annotation) val spanEnd = spannedString.getSpanEnd(annotation)
resultBuilder.addStringAnnotation( resultBuilder.addStringAnnotation(
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.viewmodels package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@@ -11,11 +10,9 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionman1212.lyricsgrabbr.LGApp import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.NotificationEvent import com.illusionman1212.lyricsgrabbr.NotificationEvent
import com.illusionman1212.lyricsgrabbr.data.HomeRepository import com.illusionman1212.lyricsgrabbr.data.HomeRepository
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -87,7 +84,7 @@ data class HomeState(
val error: String? = null, val error: String? = null,
) )
class HomeViewModel(private val homeRepository: HomeRepository, private val settingsRepository: SettingsPreferencesRepository): ViewModel() { class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
private val _uiState = MutableStateFlow(HomeState()) private val _uiState = MutableStateFlow(HomeState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
private val json = Json { private val json = Json {
@@ -99,18 +96,15 @@ class HomeViewModel(private val homeRepository: HomeRepository, private val sett
} }
fun setSong(song: NotificationEvent) { fun setSong(song: NotificationEvent) {
_uiState.update { state -> _uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
state.copy(isLoading = true, notification = song, error = null)
}
} }
fun makeRequestToGenius(context: Context, 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) }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val baseUrl = settingsRepository.preferences.first().geniURLbaseURL val (res, isSuccess) = homeRepository.makeSearchRequest(song, artist)
val (res, isSuccess) = homeRepository.makeSearchRequest(context, baseUrl, song, artist)
if (isSuccess) { if (isSuccess) {
val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map { val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map {
SearchResult( SearchResult(
@@ -139,7 +133,7 @@ class HomeViewModel(private val homeRepository: HomeRepository, private val sett
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as LGApp) val application = (this[APPLICATION_KEY] as LGApp)
HomeViewModel(application.homeRepository, application.settingsPreferencesRepository) HomeViewModel(application.homeRepository)
} }
} }
} }
@@ -41,16 +41,12 @@ class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel
} }
fun fetchLyricsFromGenius() { fun fetchLyricsFromGenius() {
_uiState.update { state -> _uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
state.copy(isLoading = true, lyrics = "")
}
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url) val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url)
_uiState.update { state -> _uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
state.copy(isLoading = false, lyrics = lyrics)
}
} }
} }
} }
@@ -30,9 +30,6 @@ data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val applications: Map<String, AppInfo> = emptyMap(), val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "", val whitelistSearchQuery: String = "",
val keepScreenOn: Boolean = false,
val geniURLbaseURL: String = "",
val whitelistPromptSeen: Boolean = true,
) )
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() { class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() {
@@ -50,27 +47,6 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
} }
} }
fun discardWhitelistPrompt() {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.discardWhitelistPrompt()
}
}
fun setGeniURLBaseURL(baseUrl: String) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setGeniURLBaseURL(baseUrl)
_uiState.update { state ->
state.copy(geniURLbaseURL = baseUrl)
}
}
}
fun toggleKeepScreenOn() {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.toggleKeepScreenOn()
}
}
private suspend fun getWhitelistedApps(): Set<String> { private suspend fun getWhitelistedApps(): Set<String> {
return settingsPrefsRepo.preferences.first().whitelist return settingsPrefsRepo.preferences.first().whitelist
} }
@@ -97,9 +73,10 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
) )
} }
immutablePackages = apps immutablePackages = apps
_uiState.update { state -> _uiState.update {
state.copy( SettingsState(
applications = apps appTheme = uiState.value.appTheme,
applications = apps,
) )
} }
} }
@@ -170,14 +147,9 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
getPackages(context) getPackages(context)
viewModelScope.launch { viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest { settingsPrefsRepo.preferences.collectLatest {
_uiState.update { state -> _uiState.value = _uiState.value.copy(
state.copy( appTheme = it.appTheme,
appTheme = it.appTheme, )
keepScreenOn = it.keepScreenOn,
whitelistPromptSeen = it.whitelistPromptSeen,
geniURLbaseURL = it.geniURLbaseURL,
)
}
} }
} }
} }
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM16.057,16.406c-0.188,0 -0.282,-0.094 -0.47,-0.188c-1.411,-0.847 -3.198,-1.317 -5.079,-1.317c-1.035,0 -2.163,0.188 -3.104,0.376c-0.188,0 -0.376,0.094 -0.47,0.094c-0.376,0 -0.658,-0.282 -0.658,-0.658c0,-0.376 0.188,-0.658 0.564,-0.658c1.223,-0.282 2.446,-0.47 3.763,-0.47c2.163,0 4.139,0.564 5.832,1.505c0.188,0.188 0.376,0.282 0.376,0.658C16.621,16.218 16.339,16.406 16.057,16.406zM17.091,13.773c-0.188,0 -0.376,-0.094 -0.564,-0.188c-1.599,-1.035 -3.857,-1.599 -6.302,-1.599c-1.223,0 -2.352,0.188 -3.198,0.376c-0.188,0.094 -0.282,0.094 -0.47,0.094c-0.47,0 -0.753,-0.376 -0.753,-0.753c0,-0.47 0.188,-0.658 0.658,-0.847c1.129,-0.282 2.258,-0.564 3.857,-0.564c2.634,0 5.079,0.658 7.149,1.881c0.282,0.188 0.47,0.47 0.47,0.753C17.844,13.396 17.562,13.773 17.091,13.773zM18.314,10.762c-0.188,0 -0.282,-0.094 -0.564,-0.188c-1.881,-1.035 -4.609,-1.693 -7.243,-1.693c-1.317,0 -2.728,0.188 -3.951,0.47c-0.188,0 -0.282,0.094 -0.564,0.094C5.428,9.54 5.051,9.069 5.051,8.505c0,-0.564 0.282,-0.847 0.658,-0.941C7.215,7.188 8.814,7 10.507,7c2.916,0 5.926,0.658 8.278,1.881c0.282,0.188 0.564,0.47 0.564,0.941C19.255,10.386 18.878,10.762 18.314,10.762z"/>
</vector>
+3 -12
View File
@@ -8,9 +8,6 @@
<string name="confirm">تأكيد</string> <string name="confirm">تأكيد</string>
<string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string> <string name="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</string>
<string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string> <string name="generic_error">لقد حدث خطأ، حاول مرة أخرى لاحقاً</string>
<string name="internal_server_error">تأكد من تشغيل خادم geniURL الخاص بك إذا كنت تستضيف ذاتيًا أو اتصل بمضيفك.</string>
<string name="invalid_json">الاستجابة لم تكن JSON صالحة</string>
<string name="failed_to_map_json">فشل تعيين JSON المفكك إلى النوع الداخلي</string>
<string name="retry">أعد المحاولة</string> <string name="retry">أعد المحاولة</string>
<!-- Home --> <!-- Home -->
@@ -27,8 +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="understood">مفهوم</string>
<string name="whitelist_prompt_desc">يعمل التطبيق على أساس القائمة البيضاء. وهذا يمنحك التحكم في التطبيقات التي سنستمع إليها ويقلل أيضًا من التشويش على التطبيق.\nيمكنك التحكم في القائمة البيضاء من الإعدادات</string>
<!-- Settings --> <!-- Settings -->
<string name="settings">الإعدادات</string> <string name="settings">الإعدادات</string>
@@ -38,20 +33,16 @@
<string name="theme_dark">ليلي</string> <string name="theme_dark">ليلي</string>
<string name="theme_system">استخدم مظهر النظام</string> <string name="theme_system">استخدم مظهر النظام</string>
<string name="language">اللغة</string> <string name="language">اللغة</string>
<string name="_default">استخدم لغة النظام</string> <string name="_default">الإفتراضي</string>
<string name="listening">الإستماع</string> <string name="listening">الإستماع</string>
<string name="whitelist">القائمة البيضاء</string>
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string> <string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string> <string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
<string name="search">بحث</string> <string name="search">بحث</string>
<string name="clear">مسح</string> <string name="clear">مسح</string>
<string name="power_management">إدارة إستخدام الطافة</string>
<string name="keep_device_awake_title">أبق الجهاز مستيقظا</string>
<string name="keep_device_awake_desc">أبق الجهاز مستيقظا على صفحة كلمات الأغاني (يزيد من استخدام البطارية)</string>
<string name="screen_will_stay_on">ستبقى الشاشة على قيد التشغيل</string>
<string name="api_base_url">عنوان URL الأساسي لواجهة برمجة التطبيقات (API)</string>
<string name="base_url">عنوان URL الأساسي</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">كلمات الأغنية</string>
<string name="instrumental">هذه الأغنية موسيقية</string> <string name="instrumental">هذه الأغنية موسيقية</string>
<!-- Notification --> <!-- Notification -->
-54
View File
@@ -1,54 +0,0 @@
<resources>
<string name="app_name" translatable="false">Lyrics Grabbr</string>
<!-- General -->
<string name="go_back">Zurück</string>
<string name="cancel">Abbrechen</string>
<string name="timeout">Anfrage ist abgelaufen, bitte versuche es später erneut</string>
<string name="generic_error">Ein Fehler ist aufgetreten, bitte versuche es später erneut</string>
<string name="retry">Erneut versuchen</string>
<!-- Home -->
<string name="no_supported_player">Keinen unterstützten Player gefunden</string>
<string name="open_settings">Einstellungen öffnen</string>
<string name="notification_listen_permission">Berechtigung zum Auslesen von Benachrichtigungen</string>
<string name="notification_listen_desc">Diese App benötigt die Berechtigung zum Auslesen von Benachrichtigungen, um ordnungsgemäß zu funktionieren. Bitte erteile diese Berechtigung auf dem nächsten Bildschirm.\nBITTE BEACHTEN: der einzige Weg den Dienst zu stoppen ist die Berechtigung zum Auslesen von Benachrichtigungen zu widerrufen.</string>
<string name="no_results">Keine Ergebnisse gefunden</string>
<string name="found_results">%1$d Ergebnis(se) gefunden</string>
<string name="powered_by">Verwendet <annotation href="https://github.com/sv443/geniURL">geniURL</annotation></string>
<string name="open_song_page">Seite des Songs öffnen</string>
<string name="open_artist_page">Seite des Künstlers öffnen</string>
<string name="notification_post_permission">Berechtigung für Benachrichtigungen erforderlich</string>
<string name="notification_post_permission_desc">Lyrics Grabbr benötigt die Berechtigung Benachrichtigungen zu lesen, um einen Vordergrunddienst zum Abhören zu starten und kann nicht ohne diese funktionieren.</string>
<string name="notification_post_permission_not_granted">Berechtigung zum Abhören von Benachrichtigungen nicht erteilt, daher wird die App wird nicht ordnungsgemäß funktionieren.</string>
<string name="grant_permission">Berechtigung erteilen</string>
<!-- Settings -->
<string name="settings">Einstellungen</string>
<string name="appearance">Aussehen</string>
<string name="theme">Farbschema</string>
<string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string>
<string name="theme_system">Systemstandard</string>
<string name="language">Sprache</string>
<string name="_default">Systemsprache</string>
<string name="listening">Erlaubt</string>
<string name="whitelist_apps">Erlaubte Apps</string>
<string name="whitelist_desc">Wähle aus, welchen Apps Lyrics Grabbr zuhören soll</string>
<string name="search">Suche</string>
<string name="clear">Löschen</string>
<string name="power_management">Energieeinstellungen</string>
<string name="keep_device_awake_title">Gerät wachhalten</string>
<string name="keep_device_awake_desc">Gerät auf der Songtext-Seite wachhalten (erhöht den Batterieverbrauch)</string>
<string name="screen_will_stay_on">Bildschirm bleibt an</string>
<!-- Lyrics -->
<string name="instrumental">Dies ist ein instrumentaler Song</string>
<!-- Notification -->
<string name="service_running">Dienst läuft</string>
<string name="stop_service">Dienst anhalten</string>
<string name="service_notif_desc_init">Spiele Songs mit einem unterstützten Player, um Songtexte zu erhalten</string>
<string name="service_notif_desc_playing">Für den Songtext antippen</string>
<string name="now_playing">Es läuft %1$s von %2$s</string>
</resources>
+3 -12
View File
@@ -7,9 +7,6 @@
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<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="internal_server_error">Internal server error. Make sure your geniURL server is running if self-hosting or contact your hoster.</string>
<string name="invalid_json">Response was not valid JSON</string>
<string name="failed_to_map_json">Failed to map decoded JSON to internal type.</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<!-- Home --> <!-- Home -->
@@ -26,8 +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="understood">Understood</string>
<string name="whitelist_prompt_desc">The application operates on a whitelist basis. This gives you control over which apps we\'ll listen to and also reduces noise for the application.\nYou may control the whitelist from the settings.</string>
<!-- Settings --> <!-- Settings -->
<string name="settings">Settings</string> <string name="settings">Settings</string>
@@ -37,20 +32,16 @@
<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="language">Language</string> <string name="language">Language</string>
<string name="_default">Use System Language</string> <string name="_default">Default</string>
<string name="listening">Listening</string> <string name="listening">Listening</string>
<string name="whitelist">Whitelist</string>
<string name="whitelist_apps">Whitelist Apps</string> <string name="whitelist_apps">Whitelist Apps</string>
<string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string> <string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="power_management">Power Management</string>
<string name="keep_device_awake_title">Keep device awake</string>
<string name="keep_device_awake_desc">Keep device awake on lyrics page (Increases battery usage)</string>
<string name="screen_will_stay_on">Screen Will Stay On</string>
<string name="api_base_url">API Base URL</string>
<string name="base_url">Base URL</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">Lyrics</string>
<string name="instrumental">This song is an instrumental</string> <string name="instrumental">This song is an instrumental</string>
<!-- Notification --> <!-- Notification -->
+1 -2
View File
@@ -2,5 +2,4 @@
<locale-config xmlns:android="http://schemas.android.com/apk/res/android"> <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" /> <locale android:name="en" />
<locale android:name="ar" /> <locale android:name="ar" />
<locale android:name="de" /> </locale-config>
</locale-config>
@@ -1,2 +0,0 @@
## Veröffentlichung auf F-droid
Initiale Veröffentlichung auf F-droid
@@ -1,2 +0,0 @@
- App mit Kotlin und Jetpack Compose neu geschrieben
- Möglichkeit hinzugefügt, Spotify für Songtexte zu verwenden
@@ -1,4 +0,0 @@
- Eine Whitelist von Apps ist jetzt erforderlich, damit die App Benachrichtigungen von ihnen abrufen kann
- Spotify wurde aufgrund des nicht-FOSS-SDKs vollständig entfernt
- Spracheinstellungen (Hilfe bei Übersetzungen ist sehr willkommen)
- Einstellung hinzugefügt, um das Gerät wach zu halten, während Songtexte angezeigt werden
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

@@ -1 +0,0 @@
Lyrics Grabbr bietet Songtexte für das aktuell abgespielte Lied anhand von Benachrichtigungen an
@@ -1 +0,0 @@
Lyrics Grabbr
@@ -1,4 +0,0 @@
- A whitelist of apps is now required in order for the app to pick up on notifications from them
- Completely remove Spotify due to non-FOSS SDK
- Language settings (help with translations is much appreciated)
- Added a setting to keep the device awake when reading lyrics
@@ -1,4 +0,0 @@
- Fix app crash when geniURL instance is down.
- Show a one-time notice related to whitelisting apps.
- Added a setting to allow for changing the geniURL instance.
- German translation
@@ -1,2 +0,0 @@
The app listens to your notifications and provides lyrics for the currently playing song.
It supports any player that uses the MediaSession API.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 145 KiB