Restore tab based profile layout

Close #989
This commit is contained in:
MM20 2024-08-11 21:50:57 +02:00
parent 9bee1158d6
commit 0b01019be8
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
4 changed files with 187 additions and 68 deletions

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
@ -76,6 +77,8 @@ fun SearchColumn(
val apps by viewModel.appResults val apps by viewModel.appResults
val workApps by viewModel.workAppResults val workApps by viewModel.workAppResults
val privateApps by viewModel.privateSpaceAppResults 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 workProfile by viewModel.workProfile.collectAsState(null)
val workProfileState by viewModel.workProfileState.collectAsState(null) val workProfileState by viewModel.workProfileState.collectAsState(null)
val privateProfile by viewModel.privateProfile.collectAsState(null) val privateProfile by viewModel.privateProfile.collectAsState(null)
@ -110,6 +113,7 @@ fun SearchColumn(
val expandedCategory: SearchCategory? by viewModel.expandedCategory val expandedCategory: SearchCategory? by viewModel.expandedCategory
var selectedAppProfileIndex: Int by remember(isSearchEmpty) { mutableIntStateOf(0) }
var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) } var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) }
var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) } var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) }
var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) } var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) }
@ -171,39 +175,34 @@ fun SearchColumn(
) )
} }
AppResults( if (isSearchEmpty && profiles.size > 1) {
key = "apps",
apps = apps,
highlightedItem = bestMatch as? Application,
columns = columns,
reverse = reverse
)
if (privateProfile != null && isSearchEmpty) {
AppResults( AppResults(
key = "apps-priv", apps = when(selectedAppProfileIndex) {
apps = privateApps, 1 -> privateApps
profile = privateProfile, 2 -> workApps
isProfileLocked = privateProfileState?.locked == true, else -> apps
onProfileLockChange = if (hasProfilesPermission) { },
{ viewModel.setProfileLock(privateProfile, it) }
} else null,
highlightedItem = bestMatch as? Application, 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, columns = columns,
reverse = reverse reverse = reverse,
showProfileLockControls = hasProfilesPermission,
) )
} } else {
if (workProfile != null && isSearchEmpty) {
AppResults( AppResults(
key = "apps-work", apps = apps,
apps = workApps,
profile = workProfile,
isProfileLocked = workProfileState?.locked == true,
onProfileLockChange = if (hasProfilesPermission) {
{ viewModel.setProfileLock(workProfile, it) }
} else null,
highlightedItem = bestMatch as? Application, highlightedItem = bestMatch as? Application,
onProfileSelected = {
selectedAppProfileIndex = it
},
columns = columns, columns = columns,
reverse = reverse reverse = reverse
) )

View File

@ -86,6 +86,11 @@ class SearchVM : ViewModel(), KoinComponent {
SharingStarted.WhileSubscribed(), SharingStarted.WhileSubscribed(),
replay = 1 replay = 1
) )
val profileStates = profiles.flatMapLatest {
combine(it.map { profileManager.getProfileState(it) }) {
it.toList()
}
}
val workProfile = profiles.map { val workProfile = profiles.map {
it.find { it.type == Profile.Type.Work } it.find { it.type == Profile.Type.Work }
} }

View File

@ -1,20 +1,33 @@
package de.mm20.launcher2.ui.launcher.search.apps package de.mm20.launcher2.ui.launcher.search.apps
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.background
import androidx.compose.foundation.layout.heightIn 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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Lock import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.LockOpen import androidx.compose.material.icons.rounded.LockOpen
import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Work 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.Icon
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.icons.PrivateSpace 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.R
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem 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.launcher.search.common.grid.GridResults
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalGridSettings
fun LazyListScope.AppResults( fun LazyListScope.AppResults(
key: String, onProfileSelected: (Int) -> Unit,
profile: Profile? = null, profiles: List<Profile> = emptyList(),
selectedProfileIndex: Int = -1,
showProfileLockControls: Boolean = false,
isProfileLocked: Boolean = false, isProfileLocked: Boolean = false,
onProfileLockChange: ((Boolean) -> Unit)? = null, onProfileLockChange: ((Profile, Boolean) -> Unit)? = null,
apps: List<Application>, apps: List<Application>,
highlightedItem: Application? = null, highlightedItem: Application? = null,
columns: Int, columns: Int,
@ -37,47 +53,140 @@ fun LazyListScope.AppResults(
) { ) {
GridResults( GridResults(
key = key, key = "apps",
items = apps, items = apps,
before = if (profile != null) { before = if (profiles.size > 1) {
{ {
Row( Column(
modifier = Modifier verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top,
.padding(top = 4.dp, start = 16.dp, end = 4.dp, bottom = 4.dp)
.heightIn(min = 40.dp),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( PrimaryScrollableTabRow(
when (profile.type) { selectedTabIndex = selectedProfileIndex,
Profile.Type.Work -> Icons.Rounded.Work containerColor = Color.Transparent,
Profile.Type.Private -> Icons.Rounded.PrivateSpace edgePadding = 16.dp,
else -> Icons.Rounded.Person divider = {}
}, ) {
null, for ((i, profile) in profiles.withIndex()) {
tint = MaterialTheme.colorScheme.primary, LeadingIconTab(
) selected = selectedProfileIndex == profiles.indexOf(profile),
Text( text = {
text = when (profile.type) { Text(
Profile.Type.Personal -> stringResource(R.string.apps_profile_main) when (profile.type) {
Profile.Type.Work -> stringResource(R.string.apps_profile_work) Profile.Type.Personal -> stringResource(R.string.apps_profile_main)
Profile.Type.Private -> stringResource(R.string.apps_profile_private) 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) icon = {
.padding(horizontal = 12.dp) when (profile.type) {
) Profile.Type.Personal -> Icon(
if (onProfileLockChange != null) { Icons.Rounded.Person,
FilledIconToggleButton(checked = isProfileLocked, onCheckedChange = { contentDescription = null
onProfileLockChange(it) )
}) {
Icon( Profile.Type.Work -> Icon(
if (isProfileLocked) Icons.Rounded.Lock else Icons.Rounded.LockOpen, Icons.Rounded.Work,
null 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, } else null,

View File

@ -985,4 +985,10 @@
<string name="calendar_widget_next_day">Next day</string> <string name="calendar_widget_next_day">Next day</string>
<string name="calendar_widget_create_event">Create new event</string> <string name="calendar_widget_create_event">Create new event</string>
<string name="calendar_widget_open_calendar">Open calendar app</string> <string name="calendar_widget_open_calendar">Open calendar app</string>
<string name="profile_work_profile_state_locked">Work apps are paused.</string>
<string name="profile_work_profile_action_unlock">Unpause</string>
<string name="profile_work_profile_action_lock">Pause work apps</string>
<string name="profile_private_profile_state_locked">Private space is locked.</string>
<string name="profile_private_profile_action_unlock">Unlock</string>
<string name="profile_private_profile_action_lock">Lock private space</string>
</resources> </resources>