Migrate file search preferences

This commit is contained in:
MM20
2022-01-17 00:08:03 +01:00
parent fe2da9a60e
commit d031003845
25 changed files with 785 additions and 258 deletions
@@ -2,10 +2,9 @@ package de.mm20.launcher2.ui.base
import androidx.appcompat.app.AppCompatActivity
import de.mm20.launcher2.permissions.PermissionsManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.android.ext.android.inject
abstract class BaseActivity : AppCompatActivity(), KoinComponent {
abstract class BaseActivity : AppCompatActivity() {
private val permissionsManager: PermissionsManager by inject()
override fun onRequestPermissionsResult(
@@ -16,4 +15,9 @@ abstract class BaseActivity : AppCompatActivity(), KoinComponent {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onResume() {
super.onResume()
permissionsManager.onResume()
}
}
@@ -0,0 +1,66 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun Banner(
modifier: Modifier = Modifier,
text: String,
icon: ImageVector,
primaryAction: @Composable () -> Unit,
secondaryAction: @Composable () -> Unit = {}
) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp),
shadowElevation = 2.dp,
tonalElevation = 2.dp
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = icon,
contentDescription = null
)
Text(
text = text,
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp)
.padding(end = 16.dp),
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium)
)
}
Row(
Modifier
.align(Alignment.End)
.padding(8.dp)
) {
Box {
secondaryAction()
}
Box(modifier = Modifier.padding(start = 8.dp)) {
primaryAction()
}
}
}
}
}
@@ -1,18 +1,14 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.R
@@ -23,46 +19,21 @@ fun MissingPermissionBanner(
onClick: () -> Unit,
secondaryAction: @Composable () -> Unit = {}
) {
Surface(
Banner(
modifier = modifier,
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp),
shadowElevation = 2.dp,
tonalElevation = 2.dp
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
text = text,
icon = Icons.Rounded.Lock,
primaryAction = {
TextButton(
modifier = Modifier.padding(start = 8.dp),
onClick = onClick
) {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = Icons.Rounded.Lock,
contentDescription = null
)
Text(
text = text,
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp)
.padding(end = 16.dp),
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium)
stringResource(R.string.grant_permission),
style = MaterialTheme.typography.labelLarge
)
}
Row(
Modifier
.align(Alignment.End)
.padding(8.dp)
) {
secondaryAction()
TextButton(
modifier = Modifier.padding(start = 8.dp),
onClick = onClick) {
Text(stringResource(R.string.grant_permission), style = MaterialTheme.typography.labelLarge)
}
}
}
}
},
secondaryAction = secondaryAction
)
}
@@ -33,6 +33,7 @@ import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
@@ -97,6 +98,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/wikipedia") {
WikipediaSettingsScreen()
}
composable("settings/search/files") {
FileSearchSettingsScreen()
}
composable("settings/widgets") {
WidgetsSettingsScreen()
}
@@ -0,0 +1,216 @@
package de.mm20.launcher2.ui.settings.filesearch
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountBox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable
fun FileSearchSettingsScreen() {
val viewModel: FileSearchSettingsScreenVM = viewModel()
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(null) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onResume()
}
}
val loading by viewModel.loading.observeAsState()
PreferenceScreen(title = stringResource(R.string.preference_search_files)) {
if (loading == true) {
item {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
return@PreferenceScreen
}
item {
PreferenceCategory {
val localFiles by viewModel.localFiles.observeAsState()
val hasFilePermission by viewModel.hasFilePermission.observeAsState()
AnimatedVisibility(hasFilePermission == false) {
MissingPermissionBanner(
text = stringResource(
if (isAtLeastApiLevel(29)) R.string.missing_permission_file_search_android10 else R.string.missing_permission_file_search
), onClick = {
viewModel.requestFilePermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_localfiles),
summary = stringResource(R.string.preference_search_localfiles_summary),
value = localFiles == true && hasFilePermission == true,
onValueChanged = {
viewModel.setLocalFiles(it)
},
enabled = hasFilePermission == true
)
val nextcloud by viewModel.nextcloud.observeAsState()
val nextcloudAccount by viewModel.nextcloudAccount.observeAsState()
AnimatedVisibility(nextcloudAccount == null) {
Banner(
text = stringResource(R.string.no_account_nextcloud),
icon = Icons.Rounded.AccountBox,
primaryAction = {
TextButton(onClick = {
viewModel.login(
context as AppCompatActivity,
AccountType.Nextcloud
)
}) {
Text(
stringResource(R.string.connect_account),
style = MaterialTheme.typography.labelLarge
)
}
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_nextcloud),
summary = nextcloudAccount?.let {
stringResource(R.string.preference_search_cloud_summary, it.userName)
} ?: stringResource(R.string.preference_summary_not_logged_in),
value = nextcloud == true && nextcloudAccount != null,
onValueChanged = {
viewModel.setNextcloud(it)
},
enabled = nextcloudAccount != null
)
val owncloud by viewModel.owncloud.observeAsState()
val owncloudAccount by viewModel.owncloudAccount.observeAsState()
AnimatedVisibility(owncloudAccount == null) {
Banner(
text = stringResource(R.string.no_account_owncloud),
icon = Icons.Rounded.AccountBox,
primaryAction = {
TextButton(onClick = {
viewModel.login(
context as AppCompatActivity,
AccountType.Owncloud
)
}) {
Text(
stringResource(R.string.connect_account),
style = MaterialTheme.typography.labelLarge
)
}
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_owncloud),
summary = owncloudAccount?.let {
stringResource(R.string.preference_search_cloud_summary, it.userName)
} ?: stringResource(R.string.preference_summary_not_logged_in),
value = owncloud == true && owncloudAccount != null,
onValueChanged = {
viewModel.setOwncloud(it)
},
enabled = owncloudAccount != null
)
val onedrive by viewModel.onedrive.observeAsState()
val microsoftAccount by viewModel.microsoftAccount.observeAsState()
AnimatedVisibility(microsoftAccount == null) {
Banner(
text = stringResource(R.string.no_account_microsoft),
icon = Icons.Rounded.AccountBox,
primaryAction = {
TextButton(onClick = {
viewModel.login(
context as AppCompatActivity,
AccountType.Microsoft
)
}) {
Text(
stringResource(R.string.connect_account),
style = MaterialTheme.typography.labelLarge
)
}
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_onedrive),
summary = microsoftAccount?.let {
stringResource(R.string.preference_search_onedrive_summary, it.userName)
} ?: stringResource(R.string.preference_summary_not_logged_in),
value = onedrive == true && microsoftAccount != null,
onValueChanged = {
viewModel.setOneDrive(it)
},
enabled = microsoftAccount != null
)
val gdrive by viewModel.gdrive.observeAsState()
val googleAccount by viewModel.googleAccount.observeAsState()
AnimatedVisibility(googleAccount == null) {
Banner(
text = stringResource(R.string.no_account_google),
icon = Icons.Rounded.AccountBox,
primaryAction = {
TextButton(onClick = {
viewModel.login(
context as AppCompatActivity,
AccountType.Google
)
}) {
Text(
stringResource(R.string.connect_account),
style = MaterialTheme.typography.labelLarge
)
}
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_gdrive),
summary = googleAccount?.let {
stringResource(R.string.preference_search_gdrive_summary, it.userName)
} ?: stringResource(R.string.preference_summary_not_logged_in),
value = gdrive == true && googleAccount != null,
onValueChanged = {
viewModel.setGdrive(it)
},
enabled = googleAccount != null
)
}
}
}
}
@@ -0,0 +1,128 @@
package de.mm20.launcher2.ui.settings.filesearch
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.accounts.Account
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class FileSearchSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val accountsRepository: AccountsRepository by inject()
private val permissionsManager: PermissionsManager by inject()
val hasFilePermission =
permissionsManager.hasPermission(PermissionGroup.ExternalStorage).asLiveData()
val loading = MutableLiveData(true)
val nextcloudAccount = MutableLiveData<Account?>(null)
val owncloudAccount = MutableLiveData<Account?>(null)
val microsoftAccount = MutableLiveData<Account?>(null)
val googleAccount = MutableLiveData<Account?>(null)
fun onResume() {
viewModelScope.launch {
nextcloudAccount.value =
accountsRepository.getCurrentlySignedInAccount(AccountType.Nextcloud)
owncloudAccount.value =
accountsRepository.getCurrentlySignedInAccount(AccountType.Owncloud)
microsoftAccount.value =
accountsRepository.getCurrentlySignedInAccount(AccountType.Microsoft)
googleAccount.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Google)
loading.value = false
}
}
val localFiles = dataStore.data.map { it.fileSearch.localFiles }.asLiveData()
fun setLocalFiles(localFiles: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setLocalFiles(localFiles)
)
.build()
}
}
}
val nextcloud = dataStore.data.map { it.fileSearch.nextcloud }.asLiveData()
fun setNextcloud(nextcloud: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setNextcloud(nextcloud)
)
.build()
}
}
}
val gdrive = dataStore.data.map { it.fileSearch.gdrive }.asLiveData()
fun setGdrive(gdrive: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setGdrive(gdrive)
)
.build()
}
}
}
val onedrive = dataStore.data.map { it.fileSearch.onedrive }.asLiveData()
fun setOneDrive(onedrive: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setOnedrive(onedrive)
)
.build()
}
}
}
val owncloud = dataStore.data.map { it.fileSearch.owncloud }.asLiveData()
fun setOwncloud(owncloud: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setOwncloud(owncloud)
)
.build()
}
}
}
fun requestFilePermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.ExternalStorage)
}
fun login(context: AppCompatActivity, accountType: AccountType) {
accountsRepository.signin(context, accountType)
}
}
@@ -44,7 +44,10 @@ fun SearchSettingsScreen() {
Preference(
title = stringResource(R.string.preference_search_files),
summary = stringResource(R.string.preference_search_files_summary),
icon = Icons.Rounded.Description
icon = Icons.Rounded.Description,
onClick = {
navController?.navigate("settings/search/files")
}
)
val hasContactsPermission by viewModel.hasContactsPermission.observeAsState()