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
32 changed files with 127 additions and 421 deletions
+2
View File
@@ -1,3 +1,4 @@
*.iml
.gradle
/local.properties
/.idea/caches
@@ -6,6 +7,7 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
+3 -3
View File
@@ -22,8 +22,8 @@ android {
applicationId = "com.illusionman1212.lyricsgrabbr"
minSdk = 23
targetSdk = 34
versionCode = 8
versionName = "1.1.1"
versionCode = 6
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -32,7 +32,7 @@ android {
signingConfig = signingConfigs.getByName("debug")
// https://developer.android.com/guide/topics/resources/app-languages#gradle-config
resourceConfigurations.plus(listOf("en", "ar", "de"))
resourceConfigurations.plus(listOf("en", "ar"))
}
signingConfigs {
-1
View File
@@ -12,7 +12,6 @@
<application
android:name=".LGApp"
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:localeConfig="@xml/locales_config"
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
@@ -116,7 +117,6 @@ class MainActivity : AppCompatActivity() {
navController.navigate(Routes.Lyrics.route)
},
lyricsViewModel = lyricsViewModel,
settingsViewModel = settingsViewModel,
goBack = { finish() },
lastNotification = lastNotification,
)
@@ -127,7 +127,6 @@ class MainActivity : AppCompatActivity() {
navController.popBackStack()
},
lyricsViewModel = lyricsViewModel,
settingsViewModel = settingsViewModel
)
}
composable(Routes.Settings.route) {
@@ -2,7 +2,6 @@ package com.illusionman1212.lyricsgrabbr.data
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.data.network.GET
@@ -11,7 +10,6 @@ import com.illusionman1212.lyricsgrabbr.utils.await
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okio.IOException
import org.jsoup.SerializationException
import java.net.SocketTimeoutException
@Serializable
@@ -33,9 +31,11 @@ class HomeRepository(private val context: Context) {
return pkgs.contains(packageName)
}
suspend fun makeSearchRequest(lContext: Context, baseUrl: String, song: String, artist: String): Pair<String?, Boolean> {
val url = Uri.parse(baseUrl)
.buildUpon()
suspend fun makeSearchRequest(song: String, artist: String): Pair<String?, Boolean> {
val builder = Uri.Builder()
val url = builder.scheme("https")
.authority("api.illusionman1212.com")
.appendPath("lyrics")
.appendPath("search")
.appendQueryParameter("q", "$song $artist")
.appendQueryParameter("disableFuzzy", "true")
@@ -47,23 +47,11 @@ class HomeRepository(private val context: Context) {
).await()
if (!res.isSuccessful) {
if (res.code >= 500) {
return Pair(lContext.resources.getString(R.string.internal_server_error), false)
}
if (res.body?.contentType()?.type != "text" && res.body?.contentType()?.subtype != "json") {
return Pair(lContext.resources.getString(R.string.invalid_json), 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(context.resources.getString(R.string.generic_error), false)
}
return Pair(null, false)
@@ -72,10 +60,10 @@ class HomeRepository(private val context: Context) {
return Pair(res.body?.string(), true)
} catch (e: IOException) {
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)
}
}
}
@@ -2,10 +2,8 @@ package com.illusionman1212.lyricsgrabbr.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.map
@@ -18,9 +16,6 @@ enum class Theme {
data class SettingsPreferences(
val appTheme: Int = Theme.SYSTEM.ordinal,
val whitelist: Set<String> = emptySet(),
val keepScreenOn: Boolean = false,
val whitelistPromptSeen: Boolean = false,
val geniURLbaseURL: String = "https://api.illusionman1212.com/lyrics",
)
class SettingsPreferencesRepository(
@@ -29,9 +24,6 @@ class SettingsPreferencesRepository(
companion object {
private val APP_THEME = intPreferencesKey("app_theme")
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) }
@@ -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) {
dataStore.edit { prefs ->
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 {
return SettingsPreferences(
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
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.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -30,9 +29,9 @@ fun LGAlertDialog(
buttons: @Composable () -> Unit,
content: @Composable () -> Unit
) {
BasicAlertDialog(
AlertDialog(
onDismissRequest = onDismiss,
properties = properties
properties = properties,
) {
Surface (
modifier = Modifier.wrapContentHeight().wrapContentWidth(),
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
@@ -72,7 +74,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.currentStateAsState
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.utils.annotatedStringResource
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
@@ -101,7 +101,6 @@ fun HomePage(
factory = HomeViewModel.Factory,
),
lyricsViewModel: LyricsViewModel,
settingsViewModel: SettingsViewModel,
lastNotification: MutableStateFlow<NotificationEvent?>
) {
val context = LocalContext.current
@@ -117,7 +116,6 @@ fun HomePage(
var songUri by remember { mutableStateOf(Uri.EMPTY) }
var showBottomSheet by remember { mutableStateOf(false) }
var listeningPermission = homeViewModel.getListeningPermission()
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
var notificationPermission by remember { mutableStateOf(
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) {
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)
)
}
} 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(
@@ -275,11 +249,7 @@ fun HomePage(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.no_supported_player),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
)
Text( text = stringResource(id = R.string.no_supported_player), style = MaterialTheme.typography.headlineSmall )
if (!notificationPermission) {
Text(
text = stringResource(id = R.string.notification_post_permission_not_granted).uppercase(),
@@ -336,7 +306,7 @@ fun HomePage(
)
FilledTonalButton(
onClick = {
homeViewModel.makeRequestToGenius(context, song.title, song.artist)
homeViewModel.makeRequestToGenius(song.title, song.artist)
}
) {
Text(stringResource(id = R.string.retry))
@@ -1,14 +1,8 @@
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.verticalScroll
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.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -29,12 +23,10 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
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.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
import com.illusionman1212.lyricsgrabbr.utils.mirror
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -59,27 +50,13 @@ fun LyricsPage(
lyricsViewModel: LyricsViewModel = viewModel(
factory = LyricsViewModel.Factory
),
settingsViewModel: SettingsViewModel,
) {
val uiState = lyricsViewModel.uiState.collectAsStateWithLifecycle().value
val settingsState = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val context = LocalContext.current
LaunchedEffect(uiState.song, uiState.artist, uiState.url) {
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(
topBar = {
TopAppBar(
@@ -105,7 +82,8 @@ fun LyricsPage(
onClick = goBack
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
modifier = Modifier.mirror(),
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(
id = R.string.go_back
)
@@ -115,19 +93,9 @@ fun LyricsPage(
)
},
) { padding ->
Column(Modifier.padding(padding)) {
if (settingsState.keepScreenOn) {
Row(
Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(stringResource(id = R.string.screen_will_stay_on).uppercase(), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSecondaryContainer)
}
}
Column(
Modifier
.padding(padding)
.fillMaxSize()
.then(
if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll(
@@ -149,6 +117,7 @@ fun LyricsPage(
} else if (uiState.lyrics.isEmpty()) {
Instrumental()
} else {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column(
Modifier.padding(
@@ -179,7 +148,6 @@ fun LyricsPage(
}
}
}
}
@Composable
@Preview(showBackground = true)
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.ui.screens
import android.util.Patterns
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
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.filled.Apps
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.Translate
import androidx.compose.material3.Icon
@@ -29,7 +26,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -38,6 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -99,15 +96,13 @@ fun SettingsPage(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Appearance(viewModel, uiState)
geniURL(viewModel, uiState)
Listening(navigateToWhitelist)
PowerManagement(viewModel, uiState)
}
}
}
@Composable
private fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
var themeOpenDialog 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
fun ThemeDialog(
uiState: SettingsState,
@@ -387,8 +311,7 @@ fun DialogSetting(
@Composable
fun ToggleSetting(
title: String,
description: String? = null,
icon: ImageVector? = null,
icon: Painter? = null,
checked: Boolean,
onClick: (Boolean) -> Unit)
{
@@ -403,20 +326,11 @@ fun ToggleSetting(
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp))
Icon(painter = icon, contentDescription = null, Modifier.width(56.dp))
} else {
Box(modifier = Modifier.width(56.dp))
}
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
if (description != null) {
Text(
text = description,
color = ColorSecondaryLight,
fontSize = 14.sp,
)
}
}
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Switch(
checked = checked,
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 resultBuilder = AnnotatedString.Builder()
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 spanEnd = spannedString.getSpanEnd(annotation)
resultBuilder.addStringAnnotation(
@@ -1,6 +1,5 @@
package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -11,11 +10,9 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.NotificationEvent
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -87,7 +84,7 @@ data class HomeState(
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())
val uiState = _uiState.asStateFlow()
private val json = Json {
@@ -99,18 +96,15 @@ class HomeViewModel(private val homeRepository: HomeRepository, private val sett
}
fun setSong(song: NotificationEvent) {
_uiState.update { state ->
state.copy(isLoading = true, notification = song, error = null)
}
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
}
fun makeRequestToGenius(context: Context, song: String, artist: String) {
fun makeRequestToGenius(song: String, artist: String) {
viewModelScope.launch {
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
withContext(Dispatchers.IO) {
val baseUrl = settingsRepository.preferences.first().geniURLbaseURL
val (res, isSuccess) = homeRepository.makeSearchRequest(context, baseUrl, song, artist)
val (res, isSuccess) = homeRepository.makeSearchRequest(song, artist)
if (isSuccess) {
val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map {
SearchResult(
@@ -139,7 +133,7 @@ class HomeViewModel(private val homeRepository: HomeRepository, private val sett
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
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() {
_uiState.update { state ->
state.copy(isLoading = true, lyrics = "")
}
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
viewModelScope.launch {
withContext(Dispatchers.IO) {
val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url)
_uiState.update { state ->
state.copy(isLoading = false, lyrics = lyrics)
}
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
}
}
}
@@ -30,9 +30,6 @@ data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal,
val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "",
val keepScreenOn: Boolean = false,
val geniURLbaseURL: String = "",
val whitelistPromptSeen: Boolean = true,
)
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> {
return settingsPrefsRepo.preferences.first().whitelist
}
@@ -97,9 +73,10 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
)
}
immutablePackages = apps
_uiState.update { state ->
state.copy(
applications = apps
_uiState.update {
SettingsState(
appTheme = uiState.value.appTheme,
applications = apps,
)
}
}
@@ -170,17 +147,12 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
getPackages(context)
viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest {
_uiState.update { state ->
state.copy(
_uiState.value = _uiState.value.copy(
appTheme = it.appTheme,
keepScreenOn = it.keepScreenOn,
whitelistPromptSeen = it.whitelistPromptSeen,
geniURLbaseURL = it.geniURLbaseURL,
)
}
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
+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="timeout">لقد انتهت مهلة الطلب، حاول مرة أخرى لاحقًا</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>
<!-- Home -->
@@ -27,8 +24,6 @@
<string name="notification_post_permission_desc">يتطلب هذا التطبيق أذونات الإشعارات لبدء خدمة مقدمة بغرض الاستماع إلى إشعارات الموسيقى، ولا يمكن أن يعمل بدونها.</string>
<string name="notification_post_permission_not_granted">لم يتم منح إذن الإشعارات، لن يعمل التطبيق بشكل صحيح</string>
<string name="grant_permission">منح الإذن</string>
<string name="understood">مفهوم</string>
<string name="whitelist_prompt_desc">يعمل التطبيق على أساس القائمة البيضاء. وهذا يمنحك التحكم في التطبيقات التي سنستمع إليها ويقلل أيضًا من التشويش على التطبيق.\nيمكنك التحكم في القائمة البيضاء من الإعدادات</string>
<!-- Settings -->
<string name="settings">الإعدادات</string>
@@ -38,20 +33,16 @@
<string name="theme_dark">ليلي</string>
<string name="theme_system">استخدم مظهر النظام</string>
<string name="language">اللغة</string>
<string name="_default">استخدم لغة النظام</string>
<string name="_default">الإفتراضي</string>
<string name="listening">الإستماع</string>
<string name="whitelist">القائمة البيضاء</string>
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
<string name="search">بحث</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 -->
<string name="lyrics">كلمات الأغنية</string>
<string name="instrumental">هذه الأغنية موسيقية</string>
<!-- 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="timeout">Request has timed out, 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>
<!-- 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_not_granted">Notification permission is not granted, the app will not function properly</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 -->
<string name="settings">Settings</string>
@@ -37,20 +32,16 @@
<string name="theme_dark">Dark</string>
<string name="theme_system">Use System Theme</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="whitelist">Whitelist</string>
<string name="whitelist_apps">Whitelist Apps</string>
<string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string>
<string name="search">Search</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 -->
<string name="lyrics">Lyrics</string>
<string name="instrumental">This song is an instrumental</string>
<!-- Notification -->
-1
View File
@@ -2,5 +2,4 @@
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="ar" />
<locale android:name="de" />
</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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 145 KiB