From f7a36a5b1cca4da8a2b991a301b3b6e7302346cb Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Sat, 17 Feb 2024 12:08:11 +0200 Subject: [PATCH] feat: Show Watchtower in the account detail --- .../feature/auth/AccountViewStateProducer.kt | 29 ++++++++ .../keyguard/feature/home/HomeScreen.kt | 4 +- .../feature/watchtower/WatchtowerRoute.kt | 13 +++- .../feature/watchtower/WatchtowerScreen.kt | 27 ++------ .../watchtower/WatchtowerStateProducer.kt | 69 +++++++++++++++---- 5 files changed, 104 insertions(+), 38 deletions(-) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt index 9c9095d2..a95f3794 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/AccountViewStateProducer.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.outlined.HideSource import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.Login import androidx.compose.material.icons.outlined.Logout +import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Send import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.material.icons.outlined.VisibilityOff @@ -83,6 +84,7 @@ import com.artemchep.keyguard.feature.navigation.state.RememberStateFlowScope import com.artemchep.keyguard.feature.navigation.state.copy import com.artemchep.keyguard.feature.navigation.state.produceScreenState import com.artemchep.keyguard.feature.send.SendRoute +import com.artemchep.keyguard.feature.watchtower.WatchtowerRoute import com.artemchep.keyguard.provider.bitwarden.ServerEnv import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.ui.FlatItemAction @@ -670,6 +672,33 @@ private fun buildItemsFlow( }, ) emit(ff3) + val watchtowerSectionItem = VaultViewItem.Section( + id = "watchtower.section", + ) + emit(watchtowerSectionItem) + val watchtowerItem = VaultViewItem.Action( + id = "watchtower", + title = scope.translate(Res.strings.watchtower_header_title), + leading = { + Icon(Icons.Outlined.Security, null) + }, + trailing = { + ChevronIcon() + }, + onClick = { + val route = WatchtowerRoute( + args = WatchtowerRoute.Args( + filter = DFilter.ById( + id = accountId.id, + what = DFilter.ById.What.ACCOUNT, + ) + ), + ) + val intent = NavigationIntent.NavigateToRoute(route) + scope.navigate(intent) + }, + ) + emit(watchtowerItem) val ff4 = VaultViewItem.Section( id = "section", text = scope.translate(Res.strings.security), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt index 22d2f25c..e0b834a4 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/HomeScreen.kt @@ -139,6 +139,8 @@ private val vaultRoute = VaultRoute() private val sendsRoute = SendRoute() +private val watchtowerRoute = WatchtowerRoute() + @Composable fun HomeScreen( defaultRoute: Route = vaultRoute, @@ -170,7 +172,7 @@ fun HomeScreen( label = TextHolder.Res(Res.strings.home_generator_label), ), Rail( - route = WatchtowerRoute, + route = watchtowerRoute, icon = Icons.Outlined.Security, iconSelected = Icons.Filled.Security, label = TextHolder.Res(Res.strings.home_watchtower_label), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerRoute.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerRoute.kt index ea9d0875..4d16e308 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerRoute.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerRoute.kt @@ -1,11 +1,20 @@ package com.artemchep.keyguard.feature.watchtower import androidx.compose.runtime.Composable +import com.artemchep.keyguard.common.model.DFilter import com.artemchep.keyguard.feature.navigation.Route -object WatchtowerRoute : Route { +data class WatchtowerRoute( + val args: Args = Args(), +) : Route { + data class Args( + val filter: DFilter? = null, + ) + @Composable override fun Content() { - WatchtowerScreen() + WatchtowerScreen( + args = args, + ) } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt index 7039a2ef..89a4f4b7 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerScreen.kt @@ -20,21 +20,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -46,7 +41,6 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.FolderOff import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Recycling import androidx.compose.material.icons.outlined.ShortText import androidx.compose.material.icons.outlined.Timer @@ -58,7 +52,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -69,7 +62,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment @@ -77,14 +69,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.semantics.Role @@ -104,21 +93,15 @@ import com.artemchep.keyguard.feature.navigation.NavigationIcon import com.artemchep.keyguard.feature.search.filter.FilterButton import com.artemchep.keyguard.feature.search.filter.FilterScreen import com.artemchep.keyguard.feature.twopane.TwoPaneScreen -import com.artemchep.keyguard.platform.leIme -import com.artemchep.keyguard.platform.leNavigationBars import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.ui.Ah import com.artemchep.keyguard.ui.DefaultEmphasisAlpha import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.ExpandedIfNotEmpty -import com.artemchep.keyguard.ui.FlatItem import com.artemchep.keyguard.ui.GridLayout -import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.OptionsButton import com.artemchep.keyguard.ui.animatedNumberText import com.artemchep.keyguard.ui.grid.preferredGridWidth -import com.artemchep.keyguard.ui.icons.ChevronIcon -import com.artemchep.keyguard.ui.icons.IconSmallBox import com.artemchep.keyguard.ui.icons.KeyguardTwoFa import com.artemchep.keyguard.ui.icons.KeyguardWebsite import com.artemchep.keyguard.ui.poweredby.PoweredBy2factorauth @@ -126,8 +109,6 @@ import com.artemchep.keyguard.ui.poweredby.PoweredByHaveibeenpwned import com.artemchep.keyguard.ui.poweredby.PoweredByPasskeys import com.artemchep.keyguard.ui.scaffoldContentWindowInsets import com.artemchep.keyguard.ui.shimmer.shimmer -import com.artemchep.keyguard.ui.skeleton.SkeletonItemPilled -import com.artemchep.keyguard.ui.skeleton.SkeletonSection import com.artemchep.keyguard.ui.skeleton.SkeletonText import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.theme.Dimens @@ -144,10 +125,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlin.math.roundToInt @Composable -fun WatchtowerScreen() { +fun WatchtowerScreen( + args: WatchtowerRoute.Args, +) { RequestAppReviewEffect() - val state = produceWatchtowerState() + val state = produceWatchtowerState( + args = args, + ) WatchtowerScreen( state = state, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt index 046c344d..53c2be66 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/watchtower/WatchtowerStateProducer.kt @@ -55,9 +55,11 @@ import org.kodein.di.instance @Composable fun produceWatchtowerState( + args: WatchtowerRoute.Args, ) = with(localDI().direct) { produceWatchtowerState( directDI = this, + args = args, getCiphers = instance(), getAccounts = instance(), getProfiles = instance(), @@ -81,6 +83,7 @@ private class WatchtowerUiException( @Composable fun produceWatchtowerState( directDI: DirectDI, + args: WatchtowerRoute.Args, getCiphers: GetCiphers, getAccounts: GetAccounts, getProfiles: GetProfiles, @@ -105,8 +108,17 @@ fun produceWatchtowerState( val ciphersRawFlow = filterHiddenProfiles( getProfiles = getProfiles, getCiphers = getCiphers, - filter = null, + filter = args.filter, ) + .map { ciphers -> + if (args.filter != null) { + val predicate = args.filter.prepare(directDI, ciphers) + ciphers + .filter { predicate(it) } + } else { + ciphers + } + } val ciphersFlow = ciphersRawFlow .map { secrets -> secrets @@ -117,6 +129,15 @@ fun produceWatchtowerState( .map { folders -> folders .filter { folder -> !folder.deleted } + .let { list -> + if (args.filter != null) { + val predicate = args.filter.prepareFolders(directDI, list) + list + .filter { predicate(it) } + } else { + list + } + } } .shareIn(screenScope, SharingStarted.WhileSubscribed(), replay = 1) @@ -184,7 +205,7 @@ fun produceWatchtowerState( ciphersFlow = ciphersFlow, ) val filteredTrashedCiphersFlow = filteredCiphers( - ciphersFlow = getCiphers() + ciphersFlow = ciphersRawFlow .map { secrets -> secrets .filter { secret -> secret.deleted } @@ -301,9 +322,10 @@ fun produceWatchtowerState( title = translate(score.formatH2()), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByPasswordStrength(score), filter, + args.filter, ), ), ), @@ -377,9 +399,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_pwned_passwords_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByPasswordPwned, filter, + args.filter, ), ), ), @@ -419,9 +442,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_unsecure_websites_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByUnsecureWebsites, filter, + args.filter, ), ), ), @@ -461,9 +485,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_duplicate_websites_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByDuplicateWebsites, filter, + args.filter, ), ), ), @@ -503,9 +528,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_inactive_2fa_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByTfaWebsites, filter, + args.filter, ), ), ), @@ -545,9 +571,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_inactive_passkey_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByPasskeyWebsites, filter, + args.filter, ), ), ), @@ -587,9 +614,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_reused_passwords_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByPasswordDuplicates, filter, + args.filter, ), ), sort = PasswordSort, @@ -630,9 +658,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_vulnerable_accounts_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByWebsitePwned, filter, + args.filter, ), ), ), @@ -738,9 +767,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_incomplete_items_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByIncomplete, filter, + args.filter, ), ), ), @@ -780,9 +810,10 @@ fun produceWatchtowerState( title = translate(Res.strings.watchtower_item_expiring_items_title), subtitle = translate(Res.strings.watchtower_header_title), filter = DFilter.And( - filters = listOf( + filters = listOfNotNull( DFilter.ByExpiring, filter, + args.filter, ), ), ), @@ -821,7 +852,12 @@ fun produceWatchtowerState( VaultRoute.watchtower( title = translate(Res.strings.watchtower_item_trashed_items_title), subtitle = translate(Res.strings.watchtower_header_title), - filter = filter, + filter = DFilter.And( + filters = listOfNotNull( + filter, + args.filter, + ), + ), trash = true, ), ) @@ -857,7 +893,12 @@ fun produceWatchtowerState( ) { val route = FoldersRoute( args = FoldersRoute.Args( - filter = filter, + filter = DFilter.And( + filters = listOfNotNull( + filter, + args.filter, + ), + ), empty = true, ), )