whitelist search

This commit is contained in:
2024-07-14 06:53:28 +02:00
parent 1a105f8413
commit 6c167b6f6f
6 changed files with 162 additions and 41 deletions
+1 -1
View File
@@ -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")
@@ -433,7 +433,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) {
@@ -4,18 +4,27 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.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.Clear
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -23,6 +32,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -30,15 +41,42 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage import coil.compose.AsyncImage
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.ui.theme.Typography
import com.illusionman1212.lyricsgrabbr.viewmodels.AppInfo
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) { fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(topBar = { Scaffold(topBar = {
TopAppBar( TopAppBar(
title = { title = {
Text(text = stringResource(id = R.string.whitelist)) 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 = { navigationIcon = {
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) { LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
@@ -49,46 +87,76 @@ fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
} }
) )
}) { padding -> }) { padding ->
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current
Box(Modifier.padding(padding)) { val refreshState = rememberPullToRefreshState()
LazyColumn { var isRefreshing by remember { mutableStateOf(false) }
for ((packageName, appInfo) in uiState.applications) {
item {
var isWhitelisted by remember { mutableStateOf(appInfo.isWhitelisted) }
Box( if (uiState.applications.isNotEmpty()) {
Modifier isRefreshing = false
.fillMaxWidth() }
.clickable(
role = Role.Checkbox, PullToRefreshBox(
onClick = { state = refreshState,
if (isWhitelisted) { isRefreshing = isRefreshing,
viewModel.removeFromWhitelist(packageName) onRefresh = {
} else { isRefreshing = true
viewModel.addToWhitelist(packageName) viewModel.refreshPackages(context)
} },
isWhitelisted = !isWhitelisted modifier = Modifier.padding(padding)
} ) {
)) { LazyColumn(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
Row( if (uiState.applications.any { it.value.isWhitelisted }) {
Modifier for ((packageName, appInfo) in uiState.applications.filter { it.value.isWhitelisted }) {
.fillMaxWidth() item {
.padding(horizontal = 24.dp, vertical = 5.dp), Application(packageName, appInfo, viewModel)
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,
)
}
} }
} }
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,
)
}
}
} }
@@ -28,10 +28,12 @@ data class AppInfo(
data class SettingsState( data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val applications: MutableMap<String, AppInfo> = mutableMapOf(), val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "",
) )
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): 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()
@@ -50,7 +52,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
} }
private fun getPackages(context: Context) { private fun getPackages(context: Context) {
if (uiState.value.applications.isNotEmpty()) return if (immutablePackages.isNotEmpty()) return
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val pm = context.packageManager val pm = context.packageManager
@@ -70,6 +72,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
isWhitelisted = pkg.packageName in whitelistedApps isWhitelisted = pkg.packageName in whitelistedApps
) )
} }
immutablePackages = apps
_uiState.update { _uiState.update {
SettingsState( SettingsState(
appTheme = uiState.value.appTheme, appTheme = uiState.value.appTheme,
@@ -79,16 +82,63 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
} }
} }
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) { fun addToWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_uiState.value.applications[packageName]?.let { it.isWhitelisted = true } _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) settingsPrefsRepo.addToWhitelist(packageName)
} }
} }
fun removeFromWhitelist(packageName: String) { fun removeFromWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_uiState.value.applications[packageName]?.let { it.isWhitelisted = false } _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) settingsPrefsRepo.removeFromWhitelist(packageName)
} }
} }
+2
View File
@@ -38,6 +38,8 @@
<string name="whitelist">القائمة البيضاء</string> <string name="whitelist">القائمة البيضاء</string>
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string> <string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string> <string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
<string name="search">بحث</string>
<string name="clear">مسح</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">كلمات الأغنية</string> <string name="lyrics">كلمات الأغنية</string>
+2
View File
@@ -37,6 +37,8 @@
<string name="whitelist">Whitelist</string> <string name="whitelist">Whitelist</string>
<string name="whitelist_apps">Whitelist Apps</string> <string name="whitelist_apps">Whitelist Apps</string>
<string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string> <string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string>
<string name="search">Search</string>
<string name="clear">Clear</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">Lyrics</string> <string name="lyrics">Lyrics</string>