4 Commits
Author SHA1 Message Date
Illusion 78a97cdc41 v1.1.0 release 2024-07-18 01:07:33 +02:00
Illusion d2b4cbb535 feat: setting to keep screen on 2024-07-16 06:50:36 +02:00
Illusion 6af9de24f4 whitelist search 2024-07-14 06:54:41 +02:00
Illusion a0baf0f476 feat: whitelist for notification listening 2024-07-14 06:54:41 +02:00
22 changed files with 542 additions and 112 deletions
-2
View File
@@ -1,4 +1,3 @@
*.iml
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea/caches
@@ -7,7 +6,6 @@
/.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 = 6 versionCode = 7
versionName = "1.0.0" versionName = "1.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -118,7 +118,7 @@ dependencies {
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.2.1") implementation("androidx.compose.material3:material3:1.3.0-beta04")
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("androidx.compose.material:material-icons-extended:1.6.8") implementation("androidx.compose.material:material-icons-extended:1.6.8")
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
+2
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name=".LGApp" android:name=".LGApp"
@@ -5,7 +5,6 @@ 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
@@ -35,6 +34,7 @@ import com.illusionman1212.lyricsgrabbr.ui.screens.HomePage
import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage
import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme
import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage
import com.illusionman1212.lyricsgrabbr.ui.screens.WhitelistPage
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -126,10 +126,22 @@ class MainActivity : AppCompatActivity() {
navController.popBackStack() navController.popBackStack()
}, },
lyricsViewModel = lyricsViewModel, lyricsViewModel = lyricsViewModel,
settingsViewModel = settingsViewModel
) )
} }
composable(Routes.Settings.route) { composable(Routes.Settings.route) {
SettingsPage( SettingsPage(
goBack = {
navController.popBackStack()
},
navigateToWhitelist = {
navController.navigate(Routes.Whitelist.route)
},
viewModel = settingsViewModel,
)
}
composable(Routes.Whitelist.route) {
WhitelistPage(
goBack = { goBack = {
navController.popBackStack() navController.popBackStack()
}, },
@@ -20,15 +20,29 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class NotificationListener: NotificationListenerService() { class NotificationListener: NotificationListenerService() {
private var notificationManager : NotificationManagerCompat? = null private var notificationManager : NotificationManagerCompat? = null
private lateinit var settingsPrefsRepo: SettingsPreferencesRepository
private val serviceScope = CoroutineScope(Dispatchers.IO)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initNotificationChannels(this) initNotificationChannels(this)
notificationManager = NotificationManagerCompat.from(applicationContext) notificationManager = NotificationManagerCompat.from(applicationContext)
settingsPrefsRepo = (application as LGApp).settingsPreferencesRepository
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -53,8 +67,12 @@ class NotificationListener: NotificationListenerService() {
} }
override fun onNotificationPosted(sbn: StatusBarNotification) { override fun onNotificationPosted(sbn: StatusBarNotification) {
serviceScope.launch {
val whitelist = settingsPrefsRepo.preferences.first().whitelist
if (sbn.packageName !in whitelist) return@launch
sendNotificationBroadcast(sbn, applicationContext) sendNotificationBroadcast(sbn, applicationContext)
} }
}
private fun getStopServiceNotificationAction(): PendingIntent { private fun getStopServiceNotificationAction(): PendingIntent {
val intent = Intent(android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val intent = Intent(android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -91,10 +109,15 @@ class NotificationListener: NotificationListenerService() {
private fun getCurrentNotifications(context: Context) { private fun getCurrentNotifications(context: Context) {
val sbns = activeNotifications val sbns = activeNotifications
serviceScope.launch {
val whitelist = settingsPrefsRepo.preferences.first().whitelist
for (sbn in sbns) { for (sbn in sbns) {
if (sbn.packageName !in whitelist) continue
sendNotificationBroadcast(sbn, context) sendNotificationBroadcast(sbn, context)
} }
} }
}
private fun getInitialNotification(): Notification { private fun getInitialNotification(): Notification {
val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) val notification: Notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
@@ -1,8 +1,9 @@
package com.illusionman1212.lyricsgrabbr package com.illusionman1212.lyricsgrabbr
sealed class Routes(val route: String) { sealed class Routes(val route: String) {
object Home : Routes("home") data object Home : Routes("home")
object Lyrics : Routes("lyrics") data object Lyrics : Routes("lyrics")
object Settings : Routes("settings") data object Settings : Routes("settings")
data object Whitelist : Routes("settings/whitelist")
} }
@@ -2,8 +2,10 @@ 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.stringSetPreferencesKey
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
enum class Theme { enum class Theme {
@@ -14,6 +16,8 @@ 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 keepScreenOn: Boolean = false,
) )
class SettingsPreferencesRepository( class SettingsPreferencesRepository(
@@ -21,6 +25,8 @@ 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 KEEP_SCREEN_ON = booleanPreferencesKey("keep_screen_on")
} }
val preferences = dataStore.data.map { mapSettingsPreferences(it) } val preferences = dataStore.data.map { mapSettingsPreferences(it) }
@@ -31,9 +37,29 @@ class SettingsPreferencesRepository(
} }
} }
suspend fun addToWhitelist(packageName: String) {
dataStore.edit { prefs ->
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) + packageName
}
}
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
}
}
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[KEEP_SCREEN_ON] ?: false,
) )
} }
} }
@@ -9,6 +9,7 @@ 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
@@ -29,11 +30,11 @@ fun LGAlertDialog(
buttons: @Composable () -> Unit, buttons: @Composable () -> Unit,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
AlertDialog( BasicAlertDialog(
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,7 +13,6 @@ 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
@@ -58,7 +57,6 @@ 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
@@ -74,6 +72,7 @@ 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
@@ -433,7 +432,6 @@ fun LongClickBottomSheet(
dragHandle = {}, dragHandle = {},
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
windowInsets = WindowInsets(top = 0.dp),
) { ) {
Column { Column {
if (artistUri != Uri.EMPTY) { if (artistUri != Uri.EMPTY) {
@@ -1,8 +1,14 @@
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
@@ -10,7 +16,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.filled.ArrowBack import androidx.compose.material.icons.automirrored.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
@@ -23,10 +29,12 @@ 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
@@ -36,12 +44,13 @@ 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
@@ -50,13 +59,27 @@ 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(
@@ -82,8 +105,7 @@ fun LyricsPage(
onClick = goBack onClick = goBack
) { ) {
Icon( Icon(
modifier = Modifier.mirror(), imageVector = Icons.AutoMirrored.Filled.ArrowBack,
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource( contentDescription = stringResource(
id = R.string.go_back id = R.string.go_back
) )
@@ -93,9 +115,19 @@ fun LyricsPage(
) )
}, },
) { padding -> ) { 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( Column(
Modifier Modifier
.padding(padding)
.fillMaxSize() .fillMaxSize()
.then( .then(
if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll( if (!uiState.isLoading && uiState.lyrics.isNotEmpty()) Modifier.verticalScroll(
@@ -117,7 +149,6 @@ fun LyricsPage(
} else if (uiState.lyrics.isEmpty()) { } else if (uiState.lyrics.isEmpty()) {
Instrumental() Instrumental()
} else { } else {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column( Column(
Modifier.padding( Modifier.padding(
@@ -147,6 +178,7 @@ fun LyricsPage(
} }
} }
} }
}
} }
@Composable @Composable
@@ -16,7 +16,9 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
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.automirrored.filled.ArrowBack
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.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
@@ -33,7 +35,6 @@ 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
@@ -61,6 +62,7 @@ private val Themes = hashMapOf(
@Composable @Composable
fun SettingsPage( fun SettingsPage(
goBack: () -> Unit, goBack: () -> Unit,
navigateToWhitelist: () -> Unit,
viewModel: SettingsViewModel, viewModel: SettingsViewModel,
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -94,12 +96,14 @@ fun SettingsPage(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Appearance(viewModel, uiState) Appearance(viewModel, uiState)
Listening(navigateToWhitelist)
PowerManagement(viewModel, uiState)
} }
} }
} }
@Composable @Composable
fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) { private 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) }
@@ -138,6 +142,49 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
} }
} }
@Composable
private fun Listening(navigateToWhitelist: () -> Unit) {
SettingGroup(title = stringResource(id = R.string.listening)) {
Box(
Modifier
.fillMaxWidth()
.clickable { navigateToWhitelist() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(imageVector = Icons.Default.Apps, contentDescription = null, modifier = Modifier.width(56.dp))
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = stringResource(id = R.string.whitelist_apps), color = MaterialTheme.colorScheme.onBackground)
Text(
text = stringResource(id = R.string.whitelist_desc),
color = ColorSecondaryLight,
fontSize = 14.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 24.dp)
)
}
}
}
}
}
@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 @Composable
fun ThemeDialog( fun ThemeDialog(
uiState: SettingsState, uiState: SettingsState,
@@ -279,7 +326,8 @@ fun DialogSetting(
@Composable @Composable
fun ToggleSetting( fun ToggleSetting(
title: String, title: String,
icon: Painter? = null, description: String? = null,
icon: ImageVector? = null,
checked: Boolean, checked: Boolean,
onClick: (Boolean) -> Unit) onClick: (Boolean) -> Unit)
{ {
@@ -294,11 +342,20 @@ 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(painter = icon, contentDescription = null, Modifier.width(56.dp)) Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp))
} else { } else {
Box(modifier = Modifier.width(56.dp)) Box(modifier = Modifier.width(56.dp))
} }
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f)) 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,
)
}
}
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = null, onCheckedChange = null,
@@ -0,0 +1,162 @@
package com.illusionman1212.lyricsgrabbr.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
import com.illusionman1212.lyricsgrabbr.ui.theme.Typography
import com.illusionman1212.lyricsgrabbr.viewmodels.AppInfo
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(topBar = {
TopAppBar(
title = {
TextField(
value = uiState.whitelistSearchQuery,
onValueChange = {
viewModel.filterPackages(it)
},
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(text = stringResource(id = R.string.search), style = Typography.titleLarge)
},
trailingIcon = {
LGIconButton(tooltip = stringResource(id = R.string.clear), onClick = { viewModel.clearWhitelistSearch() }) {
Icon(Icons.Default.Clear, contentDescription = stringResource(id = R.string.clear))
}
},
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.primary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
)
},
navigationIcon = {
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
id = R.string.go_back
))
}
}
)
}) { padding ->
val context = LocalContext.current
val refreshState = rememberPullToRefreshState()
var isRefreshing by remember { mutableStateOf(false) }
if (uiState.applications.isNotEmpty()) {
isRefreshing = false
}
PullToRefreshBox(
state = refreshState,
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.refreshPackages(context)
},
modifier = Modifier.padding(padding)
) {
LazyColumn(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
if (uiState.applications.any { it.value.isWhitelisted }) {
for ((packageName, appInfo) in uiState.applications.filter { it.value.isWhitelisted }) {
item {
Application(packageName, appInfo, viewModel)
}
}
item {
HorizontalDivider(
Modifier
.width(84.dp)
.padding(vertical = 12.dp))
}
}
for ((packageName, appInfo) in uiState.applications.filter { !it.value.isWhitelisted }) {
item {
Application(packageName, appInfo, viewModel)
}
}
}
}
}
}
@Composable
fun Application(packageName: String, app: AppInfo, viewModel: SettingsViewModel) {
Box(
Modifier
.fillMaxWidth()
.clickable(
role = Role.Checkbox,
onClick = {
if (app.isWhitelisted) {
viewModel.removeFromWhitelist(packageName)
} else {
viewModel.addToWhitelist(packageName)
}
}
)) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 5.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(modifier = Modifier.size(32.dp), model = app.icon, contentDescription = null)
Text(app.displayName, Modifier.weight(1f))
Checkbox(
checked = app.isWhitelisted,
onCheckedChange = null,
)
}
}
}
@@ -1,16 +0,0 @@
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<Annotation>(0, spannedString.length, Annotation::class.java).forEach { annotation -> spannedString.getSpans(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(
@@ -96,7 +96,9 @@ class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() {
} }
fun setSong(song: NotificationEvent) { fun setSong(song: NotificationEvent) {
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null) _uiState.update { state ->
state.copy(isLoading = true, notification = song, error = null)
}
} }
fun makeRequestToGenius(song: String, artist: String) { fun makeRequestToGenius(song: String, artist: String) {
@@ -41,12 +41,16 @@ class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel
} }
fun fetchLyricsFromGenius() { fun fetchLyricsFromGenius() {
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "") _uiState.update { state ->
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.value = _uiState.value.copy(isLoading = false, lyrics = lyrics) _uiState.update { state ->
state.copy(isLoading = false, lyrics = lyrics)
}
} }
} }
} }
@@ -1,5 +1,8 @@
package com.illusionman1212.lyricsgrabbr.viewmodels package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -14,13 +17,24 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class AppInfo(
val displayName: String,
val icon: Drawable,
var isWhitelisted: Boolean,
)
data class SettingsState( data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "",
val keepScreenOn: Boolean = false,
) )
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() { class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() {
private var immutablePackages: Map<String, AppInfo> = emptyMap()
private val _uiState = MutableStateFlow(SettingsState()) private val _uiState = MutableStateFlow(SettingsState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -34,21 +48,126 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
} }
} }
fun toggleKeepScreenOn() {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.toggleKeepScreenOn()
}
}
private suspend fun getWhitelistedApps(): Set<String> {
return settingsPrefsRepo.preferences.first().whitelist
}
private fun getPackages(context: Context) {
if (immutablePackages.isNotEmpty()) return
viewModelScope.launch(Dispatchers.IO) {
val pm = context.packageManager
val packages = pm.getInstalledApplications(PackageManager.GET_META_DATA)
.filter { it.packageName != context.packageName && pm.getLaunchIntentForPackage(it.packageName) != null }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.loadLabel(pm).toString() })
val apps: MutableMap<String, AppInfo> = mutableMapOf()
val whitelistedApps = getWhitelistedApps()
packages.forEach { pkg ->
val label = pkg.loadLabel(pm)
val icon = pkg.loadIcon(pm)
apps[pkg.packageName] = AppInfo(
displayName = label.toString(),
icon = icon,
isWhitelisted = pkg.packageName in whitelistedApps
)
}
immutablePackages = apps
_uiState.update { state ->
state.copy(
applications = apps
)
}
}
}
fun refreshPackages(context: Context) {
_uiState.update {
val apps = uiState.value.applications.toMutableMap()
apps.clear()
it.copy(whitelistSearchQuery = "", applications = apps)
}
val apps = immutablePackages.toMutableMap()
apps.clear()
immutablePackages = apps
getPackages(context)
}
fun filterPackages(query: String) {
_uiState.update { state ->
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(query.lowercase())
}.toMutableMap(),
whitelistSearchQuery = query,
)
}
}
fun clearWhitelistSearch() {
_uiState.update {
it.copy(applications = immutablePackages, whitelistSearchQuery = "")
}
}
fun addToWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { state ->
val apps = immutablePackages.toMutableMap()
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = true)
immutablePackages = apps
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
}.toMutableMap(),
)
}
settingsPrefsRepo.addToWhitelist(packageName)
}
}
fun removeFromWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { state ->
val apps = immutablePackages.toMutableMap()
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = false)
immutablePackages = apps
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
}.toMutableMap(),
)
}
settingsPrefsRepo.removeFromWhitelist(packageName)
}
}
init { init {
getPackages(context)
viewModelScope.launch { viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest { settingsPrefsRepo.preferences.collectLatest {
_uiState.value = _uiState.value.copy( _uiState.update { state ->
state.copy(
appTheme = it.appTheme, appTheme = it.appTheme,
keepScreenOn = it.keepScreenOn,
) )
} }
} }
} }
}
companion object { companion object {
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)
SettingsViewModel(application.settingsPreferencesRepository) SettingsViewModel(application.settingsPreferencesRepository, application.applicationContext)
} }
} }
} }
-9
View File
@@ -1,9 +0,0 @@
<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>
+10 -3
View File
@@ -5,7 +5,6 @@
<!-- 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="retry">أعد المحاولة</string> <string name="retry">أعد المحاولة</string>
@@ -33,10 +32,18 @@
<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="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>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">كلمات الأغنية</string>
<string name="instrumental">هذه الأغنية موسيقية</string> <string name="instrumental">هذه الأغنية موسيقية</string>
<!-- Notification --> <!-- Notification -->
+10 -3
View File
@@ -4,7 +4,6 @@
<!-- 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="retry">Retry</string> <string name="retry">Retry</string>
@@ -32,10 +31,18 @@
<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">Default</string> <string name="_default">Use System Language</string>
<string name="listening">Listening</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>
<!-- 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 -->
@@ -0,0 +1,4 @@
- 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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 294 KiB