Add private space support
This commit is contained in:
@@ -120,6 +120,7 @@ dependencies {
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:compat"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:profiles"))
|
||||
implementation(project(":services:icons"))
|
||||
implementation(project(":services:music"))
|
||||
implementation(project(":services:tags"))
|
||||
|
||||
@@ -17,10 +17,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -74,8 +72,15 @@ fun SearchColumn(
|
||||
|
||||
val hideFavs by viewModel.hideFavorites
|
||||
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
|
||||
|
||||
val apps by viewModel.appResults
|
||||
val workApps by viewModel.workAppResults
|
||||
val privateApps by viewModel.privateSpaceAppResults
|
||||
val workProfile by viewModel.workProfile.collectAsState(null)
|
||||
val workProfileState by viewModel.workProfileState.collectAsState(null)
|
||||
val privateProfile by viewModel.privateProfile.collectAsState(null)
|
||||
val privateProfileState by viewModel.privateProfileState.collectAsState(null)
|
||||
|
||||
val appShortcuts by viewModel.appShortcutResults
|
||||
val contacts by viewModel.contactResults
|
||||
val files by viewModel.fileResults
|
||||
@@ -96,26 +101,13 @@ fun SearchColumn(
|
||||
val missingContactsPermission by viewModel.missingContactsPermission.collectAsState(false)
|
||||
val missingLocationPermission by viewModel.missingLocationPermission.collectAsState(false)
|
||||
val missingFilesPermission by viewModel.missingFilesPermission.collectAsState(false)
|
||||
val hasProfilesPermission by viewModel.hasProfilesPermission.collectAsState(false)
|
||||
|
||||
val pinnedTags by favoritesVM.pinnedTags.collectAsState(emptyList())
|
||||
val selectedTag by favoritesVM.selectedTag.collectAsState(null)
|
||||
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
|
||||
val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false)
|
||||
|
||||
var showWorkProfileApps by remember { mutableStateOf(false) }
|
||||
val separateWorkProfile by viewModel.separateWorkProfile.collectAsState(true)
|
||||
val visibleApps by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
!separateWorkProfile -> (apps + workApps).sorted()
|
||||
workApps.isEmpty() -> apps
|
||||
apps.isEmpty() -> workApps
|
||||
showWorkProfileApps -> workApps
|
||||
else -> apps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val expandedCategory: SearchCategory? by viewModel.expandedCategory
|
||||
|
||||
var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) }
|
||||
@@ -180,15 +172,43 @@ fun SearchColumn(
|
||||
}
|
||||
|
||||
AppResults(
|
||||
apps = visibleApps,
|
||||
showTabs = separateWorkProfile && apps.isNotEmpty() && workApps.isNotEmpty(),
|
||||
key = "apps",
|
||||
apps = apps,
|
||||
highlightedItem = bestMatch as? Application,
|
||||
selectedTab = if (showWorkProfileApps) 1 else 0,
|
||||
onSelectedTabChange = { showWorkProfileApps = it == 1 },
|
||||
columns = columns,
|
||||
reverse = reverse
|
||||
)
|
||||
|
||||
if (privateProfile != null && isSearchEmpty) {
|
||||
AppResults(
|
||||
key = "apps-priv",
|
||||
apps = privateApps,
|
||||
profile = privateProfile,
|
||||
isProfileLocked = privateProfileState?.locked == true,
|
||||
onProfileLockChange = if (hasProfilesPermission) {
|
||||
{ viewModel.setProfileLock(privateProfile, it) }
|
||||
} else null,
|
||||
highlightedItem = bestMatch as? Application,
|
||||
columns = columns,
|
||||
reverse = reverse
|
||||
)
|
||||
}
|
||||
|
||||
if (workProfile != null && isSearchEmpty) {
|
||||
AppResults(
|
||||
key = "apps-work",
|
||||
apps = workApps,
|
||||
profile = workProfile,
|
||||
isProfileLocked = workProfileState?.locked == true,
|
||||
onProfileLockChange = if (hasProfilesPermission) {
|
||||
{ viewModel.setProfileLock(workProfile, it) }
|
||||
} else null,
|
||||
highlightedItem = bestMatch as? Application,
|
||||
columns = columns,
|
||||
reverse = reverse
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSearchEmpty) {
|
||||
|
||||
ShortcutResults(
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.devicepose.DevicePoseProvider
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.SearchResultOrder
|
||||
@@ -16,6 +17,8 @@ import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||
import de.mm20.launcher2.preferences.search.SearchFilterSettings
|
||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||
import de.mm20.launcher2.preferences.ui.SearchUiSettings
|
||||
import de.mm20.launcher2.profiles.Profile
|
||||
import de.mm20.launcher2.profiles.ProfileManager
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.Article
|
||||
@@ -41,6 +44,9 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -51,6 +57,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
private val favoritesService: FavoritesService by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val profileManager: ProfileManager by inject()
|
||||
|
||||
private val fileSearchSettings: FileSearchSettings by inject()
|
||||
private val contactSearchSettings: ContactSearchSettings by inject()
|
||||
@@ -72,9 +79,37 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
val expandedCategory = mutableStateOf<SearchCategory?>(null)
|
||||
|
||||
val locationResults = mutableStateOf<List<Location>>(emptyList())
|
||||
|
||||
val profiles = profileManager.profiles.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
|
||||
val workProfile = profiles.map {
|
||||
it.find { it.type == Profile.Type.Work }
|
||||
}
|
||||
val privateProfile = profiles.map {
|
||||
it.find { it.type == Profile.Type.Private }
|
||||
}
|
||||
val workProfileState = workProfile.flatMapLatest {
|
||||
profileManager.getProfileState(it)
|
||||
}
|
||||
val privateProfileState = privateProfile.flatMapLatest {
|
||||
profileManager.getProfileState(it)
|
||||
}
|
||||
|
||||
val hasProfilesPermission = permissionsManager.hasPermission(PermissionGroup.ManageProfiles)
|
||||
|
||||
fun setProfileLock(profile: Profile?, locked: Boolean) {
|
||||
if (isAtLeastApiLevel(28) && profile != null) {
|
||||
if (locked) {
|
||||
profileManager.lockProfile(profile)
|
||||
} else {
|
||||
profileManager.unlockProfile(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val appResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val workAppResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val privateSpaceAppResults = mutableStateOf<List<Application>>(emptyList())
|
||||
|
||||
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
|
||||
val fileResults = mutableStateOf<List<File>>(emptyList())
|
||||
val contactResults = mutableStateOf<List<Contact>>(emptyList())
|
||||
|
||||
@@ -1,42 +1,86 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.apps
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Lock
|
||||
import androidx.compose.material.icons.rounded.LockOpen
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Work
|
||||
import androidx.compose.material3.FilledIconToggleButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.icons.PrivateSpace
|
||||
import de.mm20.launcher2.profiles.Profile
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.animateCorners
|
||||
import de.mm20.launcher2.ui.ktx.withCorners
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.GridResults
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
|
||||
fun LazyListScope.AppResults(
|
||||
key: String,
|
||||
profile: Profile? = null,
|
||||
isProfileLocked: Boolean = false,
|
||||
onProfileLockChange: ((Boolean) -> Unit)? = null,
|
||||
apps: List<Application>,
|
||||
showTabs: Boolean,
|
||||
selectedTab: Int,
|
||||
onSelectedTabChange: (Int) -> Unit,
|
||||
highlightedItem: Application? = null,
|
||||
columns: Int,
|
||||
reverse: Boolean,
|
||||
) {
|
||||
|
||||
GridResults(
|
||||
key = "app",
|
||||
key = key,
|
||||
items = apps,
|
||||
before = if (profile != null) {
|
||||
{
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, start = 16.dp, end = 4.dp, bottom = 4.dp)
|
||||
.heightIn(min = 40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
when (profile.type) {
|
||||
Profile.Type.Work -> Icons.Rounded.Work
|
||||
Profile.Type.Private -> Icons.Rounded.PrivateSpace
|
||||
else -> Icons.Rounded.Person
|
||||
},
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = when (profile.type) {
|
||||
Profile.Type.Personal -> stringResource(R.string.apps_profile_main)
|
||||
Profile.Type.Work -> stringResource(R.string.apps_profile_work)
|
||||
Profile.Type.Private -> stringResource(R.string.apps_profile_private)
|
||||
},
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 12.dp)
|
||||
)
|
||||
if (onProfileLockChange != null) {
|
||||
FilledIconToggleButton(checked = isProfileLocked, onCheckedChange = {
|
||||
onProfileLockChange(it)
|
||||
}) {
|
||||
Icon(
|
||||
if (isProfileLocked) Icons.Rounded.Lock else Icons.Rounded.LockOpen,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
itemContent = {
|
||||
GridItem(
|
||||
item = it,
|
||||
@@ -44,43 +88,6 @@ fun LazyListScope.AppResults(
|
||||
highlight = it.key == highlightedItem?.key
|
||||
)
|
||||
},
|
||||
before = if (showTabs) {
|
||||
{
|
||||
Column(
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top,
|
||||
) {
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(
|
||||
MaterialTheme.shapes.medium.withCorners(
|
||||
topStart = !reverse,
|
||||
topEnd = !reverse,
|
||||
bottomEnd = reverse,
|
||||
bottomStart = reverse,
|
||||
)
|
||||
),
|
||||
divider = {},
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
Tab(
|
||||
selected = selectedTab == 0,
|
||||
onClick = { onSelectedTabChange(0) },
|
||||
text = { Text(stringResource(R.string.apps_profile_main)) },
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Tab(
|
||||
selected = selectedTab == 1,
|
||||
onClick = { onSelectedTabChange(1) },
|
||||
text = { Text(stringResource(R.string.apps_profile_work)) },
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
reverse = reverse,
|
||||
columns = columns,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,10 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
|
||||
val isBottom = reverse || items.isEmpty() && after == null
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = if (reverse && isTop) 8.dp else 0.dp,
|
||||
bottom = if (!reverse && isBottom) 8.dp else 0.dp,
|
||||
)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
|
||||
MaterialTheme.shapes.medium.withCorners(
|
||||
|
||||
Reference in New Issue
Block a user