Migrate file search preferences
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
+216
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user