diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c071f10..623e1a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + = emptySet(), ) class SettingsPreferencesRepository( @@ -21,6 +23,7 @@ class SettingsPreferencesRepository( ) { companion object { private val APP_THEME = intPreferencesKey("app_theme") + private val WHITELIST = stringSetPreferencesKey("apps_whitelist") } val preferences = dataStore.data.map { mapSettingsPreferences(it) } @@ -31,9 +34,22 @@ class SettingsPreferencesRepository( } } + suspend fun addToWhitelist(packageName: String) { + dataStore.edit { prefs -> + prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) + packageName + } + } + + suspend fun removeFromWhitelist(packageName: String) { + dataStore.edit { prefs -> + prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) - packageName + } + } + private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences { return SettingsPreferences( prefs[APP_THEME] ?: Theme.SYSTEM.ordinal, + prefs[WHITELIST] ?: emptySet(), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt index c3f8cdc..817ef1a 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt @@ -16,6 +16,7 @@ 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.Apps import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Translate @@ -61,6 +62,7 @@ private val Themes = hashMapOf( @Composable fun SettingsPage( goBack: () -> Unit, + navigateToWhitelist: () -> Unit, viewModel: SettingsViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -94,6 +96,7 @@ fun SettingsPage( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Appearance(viewModel, uiState) + Listening(navigateToWhitelist) } } } @@ -138,6 +141,35 @@ 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 fun ThemeDialog( uiState: SettingsState, diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Whitelist.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Whitelist.kt new file mode 100644 index 0000000..9be68c0 --- /dev/null +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Whitelist.kt @@ -0,0 +1,94 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +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.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.viewmodels.SettingsViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) { + Scaffold(topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = R.string.whitelist)) + }, + 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 uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Box(Modifier.padding(padding)) { + LazyColumn { + for ((packageName, appInfo) in uiState.applications) { + item { + var isWhitelisted by remember { mutableStateOf(appInfo.isWhitelisted) } + + Box( + Modifier + .fillMaxWidth() + .clickable( + role = Role.Checkbox, + onClick = { + if (isWhitelisted) { + viewModel.removeFromWhitelist(packageName) + } else { + viewModel.addToWhitelist(packageName) + } + isWhitelisted = !isWhitelisted + } + )) { + 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 = appInfo.icon, contentDescription = null) + Text(appInfo.displayName, Modifier.weight(1f)) + Checkbox( + checked = isWhitelisted, + onCheckedChange = null, + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt index 8473710..7e647c1 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/SettingsViewModel.kt @@ -1,5 +1,8 @@ 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.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY @@ -14,13 +17,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +data class AppInfo( + val displayName: String, + val icon: Drawable, + var isWhitelisted: Boolean, +) + data class SettingsState( val appTheme: Int = Theme.SYSTEM.ordinal, + val applications: MutableMap = mutableMapOf(), ) -class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() { +class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() { private val _uiState = MutableStateFlow(SettingsState()) val uiState = _uiState.asStateFlow() @@ -34,7 +45,56 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi } } + private suspend fun getWhitelistedApps(): Set { + return settingsPrefsRepo.preferences.first().whitelist + } + + private fun getPackages(context: Context) { + if (uiState.value.applications.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 = 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 + ) + } + _uiState.update { + SettingsState( + appTheme = uiState.value.appTheme, + applications = apps, + ) + } + } + } + + fun addToWhitelist(packageName: String) { + viewModelScope.launch(Dispatchers.IO) { + _uiState.value.applications[packageName]?.let { it.isWhitelisted = true } + settingsPrefsRepo.addToWhitelist(packageName) + } + } + + fun removeFromWhitelist(packageName: String) { + viewModelScope.launch(Dispatchers.IO) { + _uiState.value.applications[packageName]?.let { it.isWhitelisted = false } + settingsPrefsRepo.removeFromWhitelist(packageName) + } + } + init { + getPackages(context) viewModelScope.launch { settingsPrefsRepo.preferences.collectLatest { _uiState.value = _uiState.value.copy( @@ -48,7 +108,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as LGApp) - SettingsViewModel(application.settingsPreferencesRepository) + SettingsViewModel(application.settingsPreferencesRepository, application.applicationContext) } } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 337341c..c2b4a5c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -34,6 +34,10 @@ استخدم مظهر النظام اللغة الإفتراضي + الإستماع + القائمة البيضاء + تطبيقات القائمة البيضاء + اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها كلمات الأغنية diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a47faa0..77b3406 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,10 @@ Use System Theme Language Default + Listening + Whitelist + Whitelist Apps + Choose which apps Lyrics Grabbr will listen to Lyrics