9 Commits
25 changed files with 304 additions and 49 deletions
+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 = 7 versionCode = 8
versionName = "1.1.0" versionName = "1.1.1"
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")) resourceConfigurations.plus(listOf("en", "ar", "de"))
} }
signingConfigs { signingConfigs {
+1
View File
@@ -12,6 +12,7 @@
<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"
@@ -116,6 +116,7 @@ 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,
) )
@@ -2,6 +2,7 @@ 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
@@ -10,6 +11,7 @@ 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
@@ -31,11 +33,9 @@ class HomeRepository(private val context: Context) {
return pkgs.contains(packageName) return pkgs.contains(packageName)
} }
suspend fun makeSearchRequest(song: String, artist: String): Pair<String?, Boolean> { suspend fun makeSearchRequest(lContext: Context, baseUrl: String, song: String, artist: String): Pair<String?, Boolean> {
val builder = Uri.Builder() val url = Uri.parse(baseUrl)
val url = builder.scheme("https") .buildUpon()
.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,11 +47,23 @@ class HomeRepository(private val context: Context) {
).await() ).await()
if (!res.isSuccessful) { if (!res.isSuccessful) {
val errRes = json.decodeFromString(ErrorResponse.serializer(), res.body!!.string()) if (res.code >= 500) {
return Pair(lContext.resources.getString(R.string.internal_server_error), false)
}
// matches is only null if there's an error. otherwise it's 0 when there are no results if (res.body?.contentType()?.type != "text" && res.body?.contentType()?.subtype != "json") {
if (errRes.matches == null) { return Pair(lContext.resources.getString(R.string.invalid_json), false)
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)
@@ -60,10 +72,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(context.resources.getString(R.string.timeout), false) return Pair(lContext.resources.getString(R.string.timeout), false)
} }
return Pair(context.resources.getString(R.string.generic_error), false) return Pair(e.message, false)
} }
} }
} }
@@ -1,6 +1,7 @@
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
@@ -10,41 +11,60 @@ import org.jsoup.select.NodeVisitor
class LyricsRepository { class LyricsRepository {
fun fetchLyricsFromGenius(url: Uri): String { fun fetchLyricsFromGenius(url: Uri): String {
val doc = Jsoup.connect(url.toString()).userAgent(USER_AGENT).get() return try {
val divs = doc.select("div[data-lyrics-container]") val doc = Jsoup.connect(url.toString())
.userAgent(USER_AGENT)
.get()
val lyrics = StringUtil.borrowBuilder() val divs = doc.select("div[data-lyrics-container]")
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.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}"
}
}
} 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 {
@@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey 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,6 +19,8 @@ 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 keepScreenOn: Boolean = false,
val whitelistPromptSeen: Boolean = false,
val geniURLbaseURL: String = "https://api.illusionman1212.com/lyrics",
) )
class SettingsPreferencesRepository( class SettingsPreferencesRepository(
@@ -27,6 +30,8 @@ class SettingsPreferencesRepository(
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 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) }
@@ -55,11 +60,25 @@ class SettingsPreferencesRepository(
} }
} }
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[KEEP_SCREEN_ON] ?: false,
prefs[WHITELIST_PROMPT_SEEN] ?: false,
prefs[GENIURL_BASE_URL] ?: "https://api.illusionman1212.com/lyrics",
) )
} }
} }
@@ -87,6 +87,7 @@ 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
@@ -100,6 +101,7 @@ 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
@@ -115,6 +117,7 @@ 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
) } ) }
@@ -155,7 +158,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(song.title, song.artist) homeViewModel.makeRequestToGenius(context, song.title, song.artist)
} }
} }
@@ -237,6 +240,30 @@ 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(
@@ -248,7 +275,11 @@ fun HomePage(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( text = stringResource(id = R.string.no_supported_player), style = MaterialTheme.typography.headlineSmall ) Text(
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(),
@@ -305,7 +336,7 @@ fun HomePage(
) )
FilledTonalButton( FilledTonalButton(
onClick = { onClick = {
homeViewModel.makeRequestToGenius(song.title, song.artist) homeViewModel.makeRequestToGenius(context, song.title, song.artist)
} }
) { ) {
Text(stringResource(id = R.string.retry)) Text(stringResource(id = R.string.retry))
@@ -1,5 +1,6 @@
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,6 +20,7 @@ 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.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
@@ -27,6 +29,7 @@ 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
@@ -96,6 +99,7 @@ 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) PowerManagement(viewModel, uiState)
} }
@@ -185,6 +189,63 @@ private fun PowerManagement(viewModel: SettingsViewModel, uiState: SettingsState
} }
} }
@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,
@@ -1,5 +1,6 @@
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
@@ -10,9 +11,11 @@ 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
@@ -84,7 +87,7 @@ data class HomeState(
val error: String? = null, val error: String? = null,
) )
class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() { class HomeViewModel(private val homeRepository: HomeRepository, private val settingsRepository: SettingsPreferencesRepository): 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 {
@@ -101,12 +104,13 @@ class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
} }
} }
fun makeRequestToGenius(song: String, artist: String) { fun makeRequestToGenius(context: Context, 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 (res, isSuccess) = homeRepository.makeSearchRequest(song, artist) val baseUrl = settingsRepository.preferences.first().geniURLbaseURL
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(
@@ -135,7 +139,7 @@ class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
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) HomeViewModel(application.homeRepository, application.settingsPreferencesRepository)
} }
} }
} }
@@ -31,6 +31,8 @@ data class SettingsState(
val applications: Map<String, AppInfo> = emptyMap(), val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "", val whitelistSearchQuery: String = "",
val keepScreenOn: Boolean = false, 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() {
@@ -48,6 +50,21 @@ 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() { fun toggleKeepScreenOn() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.toggleKeepScreenOn() settingsPrefsRepo.toggleKeepScreenOn()
@@ -157,6 +174,8 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
state.copy( state.copy(
appTheme = it.appTheme, appTheme = it.appTheme,
keepScreenOn = it.keepScreenOn, keepScreenOn = it.keepScreenOn,
whitelistPromptSeen = it.whitelistPromptSeen,
geniURLbaseURL = it.geniURLbaseURL,
) )
} }
} }
+8
View File
@@ -5,8 +5,12 @@
<!-- General --> <!-- General -->
<string name="go_back">رجوع</string> <string name="go_back">رجوع</string>
<string name="cancel">إلغاء</string> <string name="cancel">إلغاء</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 -->
@@ -23,6 +27,8 @@
<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>
@@ -42,6 +48,8 @@
<string name="keep_device_awake_title">أبق الجهاز مستيقظا</string> <string name="keep_device_awake_title">أبق الجهاز مستيقظا</string>
<string name="keep_device_awake_desc">أبق الجهاز مستيقظا على صفحة كلمات الأغاني (يزيد من استخدام البطارية)</string> <string name="keep_device_awake_desc">أبق الجهاز مستيقظا على صفحة كلمات الأغاني (يزيد من استخدام البطارية)</string>
<string name="screen_will_stay_on">ستبقى الشاشة على قيد التشغيل</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="instrumental">هذه الأغنية موسيقية</string> <string name="instrumental">هذه الأغنية موسيقية</string>
+54
View File
@@ -0,0 +1,54 @@
<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>
+8
View File
@@ -4,8 +4,12 @@
<!-- General --> <!-- General -->
<string name="go_back">Go Back</string> <string name="go_back">Go Back</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</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 -->
@@ -22,6 +26,8 @@
<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>
@@ -41,6 +47,8 @@
<string name="keep_device_awake_title">Keep device awake</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="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="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="instrumental">This song is an instrumental</string> <string name="instrumental">This song is an instrumental</string>
+1
View File
@@ -2,4 +2,5 @@
<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>
@@ -0,0 +1,2 @@
## Veröffentlichung auf F-droid
Initiale Veröffentlichung auf F-droid
@@ -0,0 +1,2 @@
- App mit Kotlin und Jetpack Compose neu geschrieben
- Möglichkeit hinzugefügt, Spotify für Songtexte zu verwenden
@@ -0,0 +1,4 @@
- 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.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

@@ -0,0 +1 @@
Lyrics Grabbr bietet Songtexte für das aktuell abgespielte Lied anhand von Benachrichtigungen an
@@ -0,0 +1 @@
Lyrics Grabbr
@@ -0,0 +1,4 @@
- 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
@@ -0,0 +1,2 @@
The app listens to your notifications and provides lyrics for the currently playing song.
It supports any player that uses the MediaSession API.