diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 80ce027a..016dd388 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.launcher.search +import android.util.Log import androidx.activity.compose.BackHandler import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedContent @@ -76,6 +77,8 @@ fun SearchColumn( val apps by viewModel.appResults val workApps by viewModel.workAppResults val privateApps by viewModel.privateSpaceAppResults + val profiles by viewModel.profiles.collectAsState(emptyList()) + val profileStates by viewModel.profileStates.collectAsState(emptyList()) val workProfile by viewModel.workProfile.collectAsState(null) val workProfileState by viewModel.workProfileState.collectAsState(null) val privateProfile by viewModel.privateProfile.collectAsState(null) @@ -110,6 +113,7 @@ fun SearchColumn( val expandedCategory: SearchCategory? by viewModel.expandedCategory + var selectedAppProfileIndex: Int by remember(isSearchEmpty) { mutableIntStateOf(0) } var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) } var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) } var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) } @@ -171,39 +175,34 @@ fun SearchColumn( ) } - AppResults( - key = "apps", - apps = apps, - highlightedItem = bestMatch as? Application, - columns = columns, - reverse = reverse - ) - - if (privateProfile != null && isSearchEmpty) { + if (isSearchEmpty && profiles.size > 1) { AppResults( - key = "apps-priv", - apps = privateApps, - profile = privateProfile, - isProfileLocked = privateProfileState?.locked == true, - onProfileLockChange = if (hasProfilesPermission) { - { viewModel.setProfileLock(privateProfile, it) } - } else null, + apps = when(selectedAppProfileIndex) { + 1 -> privateApps + 2 -> workApps + else -> apps + }, highlightedItem = bestMatch as? Application, + profiles = profiles, + selectedProfileIndex = selectedAppProfileIndex, + onProfileSelected = { + selectedAppProfileIndex = it + }, + isProfileLocked = profileStates.getOrNull(selectedAppProfileIndex)?.locked == true, + onProfileLockChange = { p, l -> + viewModel.setProfileLock(p, l) + }, columns = columns, - reverse = reverse + reverse = reverse, + showProfileLockControls = hasProfilesPermission, ) - } - - if (workProfile != null && isSearchEmpty) { + } else { AppResults( - key = "apps-work", - apps = workApps, - profile = workProfile, - isProfileLocked = workProfileState?.locked == true, - onProfileLockChange = if (hasProfilesPermission) { - { viewModel.setProfileLock(workProfile, it) } - } else null, + apps = apps, highlightedItem = bestMatch as? Application, + onProfileSelected = { + selectedAppProfileIndex = it + }, columns = columns, reverse = reverse ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 75ff8539..7a98fb54 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -86,6 +86,11 @@ class SearchVM : ViewModel(), KoinComponent { SharingStarted.WhileSubscribed(), replay = 1 ) + val profileStates = profiles.flatMapLatest { + combine(it.map { profileManager.getProfileState(it) }) { + it.toList() + } + } val workProfile = profiles.map { it.find { it.type == Profile.Type.Work } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt index 39383b69..01761f6c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt @@ -1,20 +1,33 @@ package de.mm20.launcher2.ui.launcher.search.apps -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope 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.material.icons.rounded.WorkOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.LeadingIconTab import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import de.mm20.launcher2.icons.PrivateSpace @@ -23,13 +36,16 @@ import de.mm20.launcher2.search.Application import de.mm20.launcher2.ui.R 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.LocalGridSettings fun LazyListScope.AppResults( - key: String, - profile: Profile? = null, + onProfileSelected: (Int) -> Unit, + profiles: List = emptyList(), + selectedProfileIndex: Int = -1, + showProfileLockControls: Boolean = false, isProfileLocked: Boolean = false, - onProfileLockChange: ((Boolean) -> Unit)? = null, + onProfileLockChange: ((Profile, Boolean) -> Unit)? = null, apps: List, highlightedItem: Application? = null, columns: Int, @@ -37,47 +53,140 @@ fun LazyListScope.AppResults( ) { GridResults( - key = key, + key = "apps", items = apps, - before = if (profile != null) { + before = if (profiles.size > 1) { { - Row( - modifier = Modifier - .padding(top = 4.dp, start = 16.dp, end = 4.dp, bottom = 4.dp) - .heightIn(min = 40.dp), - verticalAlignment = Alignment.CenterVertically, + Column( + verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top, ) { - 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 + PrimaryScrollableTabRow( + selectedTabIndex = selectedProfileIndex, + containerColor = Color.Transparent, + edgePadding = 16.dp, + divider = {} + ) { + for ((i, profile) in profiles.withIndex()) { + LeadingIconTab( + selected = selectedProfileIndex == profiles.indexOf(profile), + 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) + } + ) + }, + icon = { + when (profile.type) { + Profile.Type.Personal -> Icon( + Icons.Rounded.Person, + contentDescription = null + ) + + Profile.Type.Work -> Icon( + Icons.Rounded.Work, + contentDescription = null + ) + + Profile.Type.Private -> Icon( + Icons.Rounded.PrivateSpace, + contentDescription = null + ) + } + }, + onClick = { + onProfileSelected(i) + } ) } } + HorizontalDivider() + + val profileType = profiles[selectedProfileIndex].type + if (showProfileLockControls && profileType != Profile.Type.Personal) { + if (isProfileLocked) { + Column( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth() + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.small) + .padding(vertical = 64.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_state_locked + else R.string.profile_private_profile_state_locked + ), + modifier = Modifier.padding(top = 8.dp, bottom = 32.dp), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall, + ) + Button( + modifier = Modifier, + onClick = { + onProfileLockChange?.invoke( + profiles[selectedProfileIndex], + false + ) + }, + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.Work else Icons.Rounded.LockOpen, + contentDescription = null, + modifier = Modifier + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_unlock + else R.string.profile_private_profile_action_unlock + ) + ) + } + + } + } else { + FilledTonalButton( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth(), + onClick = { + onProfileLockChange?.invoke( + profiles[selectedProfileIndex], + true + ) + }, + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, + contentDescription = null, + modifier = Modifier + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_lock + else R.string.profile_private_profile_action_lock + ) + ) + } + } + } } } } else null, diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index ddda3e83..274fee58 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -985,4 +985,10 @@ Next day Create new event Open calendar app + Work apps are paused. + Unpause + Pause work apps + Private space is locked. + Unlock + Lock private space \ No newline at end of file