New hidden items UI

This commit is contained in:
MM20
2022-05-27 20:29:00 +02:00
parent bc5bdd09ff
commit a7c55b2668
23 changed files with 413 additions and 193 deletions
@@ -145,12 +145,6 @@ class LauncherActivity : BaseActivity() {
.imePadding()
)
}
val showHiddenItems by viewModel.isHiddenItemsShown.observeAsState(false)
if (showHiddenItems) {
HiddenItemsSheet(onDismiss = {
viewModel.hideHiddenItems()
})
}
}
}
}
@@ -13,7 +13,6 @@ import org.koin.core.component.inject
class LauncherActivityVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
val isHiddenItemsShown = MutableLiveData(false)
val isEditFavoritesShown = MutableLiveData(false)
private var isDarkInMode = MutableStateFlow(false)
@@ -55,14 +54,5 @@ class LauncherActivityVM : ViewModel(), KoinComponent {
isEditFavoritesShown.value = false
}
fun showHiddenItems() {
isHiddenItemsShown.value = true
}
fun hideHiddenItems() {
isHiddenItemsShown.value = false
}
val layout = dataStore.data.map { it.appearance.layout }.asLiveData()
}
@@ -1,116 +0,0 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.slideIn
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import kotlin.math.roundToInt
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class)
@Composable
fun HiddenItemsSheet(
onDismiss: () -> Unit
) {
val viewModel: HiddenItemsVM = viewModel()
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = { onDismiss() }) {
val animationState = remember {
MutableTransitionState(false).apply {
targetState = true
}
}
AnimatedVisibility(
animationState,
enter = slideIn { IntOffset(0, it.height) }
) {
val swipeState =
rememberSwipeableState(initialValue = SwipeState.Default) {
if (it == SwipeState.Dismiss) onDismiss()
return@rememberSwipeableState true
}
Surface(
modifier = Modifier
.fillMaxSize()
.swipeable(
swipeState,
mapOf(
0f to SwipeState.Default,
600.dp.toPixels() to SwipeState.Dismiss
),
orientation = Orientation.Vertical,
thresholds = { _, _ -> FractionalThreshold(0.5f) },
)
.offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Text(
stringResource(R.string.menu_hidden_items),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(24.dp)
)
val items by viewModel.hiddenItems.collectAsState(emptyList())
SearchResultGrid(
items,
modifier = Modifier
.weight(1f)
.padding(8.dp)
.verticalScroll(rememberScrollState())
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
contentAlignment = Alignment.CenterEnd
) {
TextButton(onClick = { onDismiss() }) {
Text(
stringResource(id = R.string.close),
)
}
}
}
}
}
}
}
private enum class SwipeState {
Default, Dismiss
}
@@ -1,17 +0,0 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HiddenItemsVM: ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
val hiddenItems = repository.getHiddenItems()
}
@@ -112,15 +112,6 @@ fun SearchBar(
Text(stringResource(R.string.menu_item_edit_favs))
}
)
DropdownMenuItem(
onClick = {
activityViewModel.showHiddenItems()
onDismissRequest()
},
text = {
Text(stringResource(R.string.menu_hidden_items))
}
)
DropdownMenuItem(
onClick = {
context.startActivity(
@@ -1,9 +1,20 @@
package de.mm20.launcher2.ui.launcher.search
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.launcher.search.apps.AppResults
import de.mm20.launcher2.ui.launcher.search.appshortcuts.AppShortcutResults
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults
@@ -16,6 +27,7 @@ import de.mm20.launcher2.ui.launcher.search.website.WebsiteResults
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaResults
import de.mm20.launcher2.ui.layout.BottomReversed
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchColumn(
modifier: Modifier = Modifier,
@@ -35,5 +47,50 @@ fun SearchColumn(
WikipediaResults(reverse)
WebsiteResults(reverse)
FileResults(reverse)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
val viewModel: SearchVM = viewModel()
val context = LocalContext.current
Surface(
shadowElevation = 2.dp,
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
shape = CircleShape,
onClick = { viewModel.showHiddenItems(context) }
) {
Box(
modifier = Modifier.padding(12.dp),
) {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null,
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Surface(
shadowElevation = 2.dp,
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
shape = CircleShape,
onClick = { viewModel.openSearchSettings(context) }
) {
Box(
modifier = Modifier.padding(12.dp),
) {
Icon(
imageVector = Icons.Rounded.Settings,
contentDescription = null,
)
}
}
}
}
}
@@ -1,5 +1,7 @@
package de.mm20.launcher2.ui.launcher.search
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -16,6 +18,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.WebsearchRepository
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.settings.SettingsActivity
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.widgets.WidgetRepository
@@ -231,4 +234,20 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
fun showHiddenItems(context: Context) {
context.startActivity(
Intent(context, SettingsActivity::class.java).apply {
putExtra(SettingsActivity.EXTRA_ROUTE, "settings/search/hiddenitems")
}
)
}
fun openSearchSettings(context: Context) {
context.startActivity(
Intent(context, SettingsActivity::class.java).apply {
putExtra(SettingsActivity.EXTRA_ROUTE, "settings/search")
}
)
}
}
@@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
@@ -34,6 +35,7 @@ import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
@@ -58,9 +60,8 @@ class SettingsActivity : BaseActivity() {
setContent {
val navController = rememberAnimatedNavController()
LaunchedEffect(intent) {
intent.getStringExtra("de.mm20.launcher2.settings.ROUTE")
?.let { navController.navigate(it) }
val initialRoute = remember {
intent.getStringExtra("de.mm20.launcher2.settings.ROUTE") ?: "settings"
}
val cardStyle by remember {
@@ -75,7 +76,7 @@ class SettingsActivity : BaseActivity() {
LauncherTheme {
AnimatedNavHost(
navController = navController,
startDestination = "settings",
startDestination = initialRoute,
exitTransition = { fadeOut(tween(300, 300)) },
enterTransition = { fadeIn(tween(200)) },
popEnterTransition = { fadeIn(tween(0)) },
@@ -111,6 +112,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/websearch") {
WebSearchSettingsScreen()
}
composable("settings/search/hiddenitems") {
HiddenItemsSettingsScreen()
}
composable("settings/widgets") {
WidgetsSettingsScreen()
}
@@ -176,4 +180,8 @@ class SettingsActivity : BaseActivity() {
}
}
}
companion object {
const val EXTRA_ROUTE = "de.mm20.launcher2.settings.ROUTE"
}
}
@@ -0,0 +1,205 @@
package de.mm20.launcher2.ui.settings.hiddenitems
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HiddenItemsSettingsScreen() {
val viewModel: HiddenItemsSettingsScreenVM = viewModel()
val context = LocalContext.current
val density = LocalDensity.current
val apps by viewModel.allApps.observeAsState(emptyList())
val other by viewModel.hiddenItems.observeAsState(emptyList())
PreferenceScreen(title = stringResource(R.string.preference_hidden_items)) {
items(apps, key = { it.key }) { searchable ->
val icon by remember(searchable.key) {
viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() })
}.collectAsState(null)
val isHidden by remember(searchable.key) {
viewModel.isHidden(searchable)
}.collectAsState(false)
var showPopup by remember(searchable.key) {
mutableStateOf(false)
}
Box {
HiddenItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
viewModel.setHidden(searchable, !isHidden)
},
onLongClick = {
showPopup = true
}
),
icon = icon,
label = searchable.label,
isHidden = isHidden,
)
DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false },
offset = DpOffset(16.dp, 0.dp)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_launch)) },
onClick = {
viewModel.launch(context, searchable)
showPopup = false
})
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_app_info)) },
onClick = {
viewModel.openAppInfo(context, searchable as Application)
showPopup = false
})
DropdownMenuItem(
text = {
Text(
stringResource(
if (isHidden) R.string.menu_unhide else R.string.menu_hide
)
)
},
onClick = {
viewModel.setHidden(searchable, !isHidden)
showPopup = false
})
}
}
}
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
)
}
items(other, key = { it.key }) { searchable ->
val icon by remember(searchable.key) {
viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() })
}.collectAsState(null)
val isHidden by remember(searchable.key) {
viewModel.isHidden(searchable)
}.collectAsState(false)
var showPopup by remember(searchable.key) {
mutableStateOf(false)
}
Box {
HiddenItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
viewModel.setHidden(searchable, !isHidden)
},
onLongClick = {
showPopup = true
}
),
icon = icon,
label = searchable.label,
isHidden = isHidden,
)
DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false },
offset = DpOffset(16.dp, 0.dp)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_open_file)) },
onClick = {
viewModel.launch(context, searchable)
showPopup = false
})
DropdownMenuItem(
text = {
Text(
stringResource(
if (isHidden) R.string.menu_unhide else R.string.menu_hide
)
)
},
onClick = {
viewModel.setHidden(searchable, !isHidden)
showPopup = false
})
}
}
}
}
}
@Composable
fun HiddenItem(
modifier: Modifier,
icon: LauncherIcon?,
label: String,
isHidden: Boolean,
) {
Row(
modifier = modifier
.padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
ShapedLauncherIcon(
size = 32.dp,
icon = icon,
modifier = Modifier.padding(end = 16.dp)
)
Text(
label,
modifier = Modifier.weight(1f, fill = true),
style = MaterialTheme.typography.titleMedium
)
Icon(
modifier = Modifier.alpha(if (isHidden) 0.3f else 1f),
imageVector = if (isHidden) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility,
tint = if (isHidden) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.primary,
contentDescription = null
)
}
}
@@ -0,0 +1,90 @@
package de.mm20.launcher2.ui.settings.hiddenitems
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.core.content.getSystemService
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject()
private val favoritesRepository: FavoritesRepository by inject()
private val iconRepository: IconRepository by inject()
val allApps = appRepository.getAllInstalledApps(true).map {
withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData()
val hiddenItems: LiveData<List<Searchable>> = liveData {
val hidden = withContext(Dispatchers.Default) {
favoritesRepository.getHiddenItems().first().filter { it !is Application }.sorted()
}
emit(hidden)
}
fun isHidden(searchable: Searchable): Flow<Boolean> {
return favoritesRepository.isHidden(searchable)
}
fun setHidden(searchable: Searchable, hidden: Boolean) {
if(hidden) {
favoritesRepository.hideItem(searchable)
} else {
favoritesRepository.unhideItem(searchable)
}
}
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> {
return iconRepository.getIcon(searchable, size)
}
fun launch(context: Context, searchable: Searchable) {
val bundle = Bundle()
if (isAtLeastApiLevel(31)) {
bundle.putInt("android.activity.splashScreenStyle", 1)
}
searchable.launch(context, bundle)
}
fun openAppInfo(context: Context, app: Application) {
val launcherApps = context.getSystemService<LauncherApps>()!!
if (app is LauncherApp) {
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
} else {
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${app.`package`}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
}
}
@@ -191,6 +191,13 @@ fun SearchSettingsScreen() {
onValueChanged = {
viewModel.setAutoFocus(it)
})
Preference(
title = stringResource(R.string.preference_hidden_items),
summary = stringResource(R.string.preference_hidden_items_summary),
onClick = {
navController?.navigate("settings/search/hiddenitems")
}
)
}
}
}
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_edit_favs"
android:title="@string/menu_item_edit_favs" />
<item
android:id="@+id/menu_item_hidden"
android:title="@string/menu_hidden_items" />
<item
android:id="@+id/menu_item_wallpaper"
android:title="@string/wallpaper" />
<item
android:id="@+id/menu_item_settings"
android:title="@string/settings" />
<item
android:id="@+id/menu_item_settings_old"
android:title="@string/settings" />
</menu>