feat: Show Watchtower in the account detail

This commit is contained in:
Artem Chepurnoy 2024-02-17 12:08:11 +02:00
parent 40f603c4d2
commit f7a36a5b1c
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
5 changed files with 104 additions and 38 deletions

View File

@ -15,6 +15,7 @@ import androidx.compose.material.icons.outlined.HideSource
import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.Keyboard
import androidx.compose.material.icons.outlined.Login import androidx.compose.material.icons.outlined.Login
import androidx.compose.material.icons.outlined.Logout 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.Send
import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.material.icons.outlined.VisibilityOff 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.copy
import com.artemchep.keyguard.feature.navigation.state.produceScreenState import com.artemchep.keyguard.feature.navigation.state.produceScreenState
import com.artemchep.keyguard.feature.send.SendRoute 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.provider.bitwarden.ServerEnv
import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.FlatItemAction import com.artemchep.keyguard.ui.FlatItemAction
@ -670,6 +672,33 @@ private fun buildItemsFlow(
}, },
) )
emit(ff3) 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( val ff4 = VaultViewItem.Section(
id = "section", id = "section",
text = scope.translate(Res.strings.security), text = scope.translate(Res.strings.security),

View File

@ -139,6 +139,8 @@ private val vaultRoute = VaultRoute()
private val sendsRoute = SendRoute() private val sendsRoute = SendRoute()
private val watchtowerRoute = WatchtowerRoute()
@Composable @Composable
fun HomeScreen( fun HomeScreen(
defaultRoute: Route = vaultRoute, defaultRoute: Route = vaultRoute,
@ -170,7 +172,7 @@ fun HomeScreen(
label = TextHolder.Res(Res.strings.home_generator_label), label = TextHolder.Res(Res.strings.home_generator_label),
), ),
Rail( Rail(
route = WatchtowerRoute, route = watchtowerRoute,
icon = Icons.Outlined.Security, icon = Icons.Outlined.Security,
iconSelected = Icons.Filled.Security, iconSelected = Icons.Filled.Security,
label = TextHolder.Res(Res.strings.home_watchtower_label), label = TextHolder.Res(Res.strings.home_watchtower_label),

View File

@ -1,11 +1,20 @@
package com.artemchep.keyguard.feature.watchtower package com.artemchep.keyguard.feature.watchtower
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.artemchep.keyguard.common.model.DFilter
import com.artemchep.keyguard.feature.navigation.Route 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 @Composable
override fun Content() { override fun Content() {
WatchtowerScreen() WatchtowerScreen(
args = args,
)
} }
} }

View File

@ -20,21 +20,16 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.ErrorOutline
import androidx.compose.material.icons.outlined.FolderOff import androidx.compose.material.icons.outlined.FolderOff
import androidx.compose.material.icons.outlined.Info 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.Recycling
import androidx.compose.material.icons.outlined.ShortText import androidx.compose.material.icons.outlined.ShortText
import androidx.compose.material.icons.outlined.Timer 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.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
@ -69,7 +62,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment 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.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind 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.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.Role 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.FilterButton
import com.artemchep.keyguard.feature.search.filter.FilterScreen import com.artemchep.keyguard.feature.search.filter.FilterScreen
import com.artemchep.keyguard.feature.twopane.TwoPaneScreen 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.res.Res
import com.artemchep.keyguard.ui.Ah import com.artemchep.keyguard.ui.Ah
import com.artemchep.keyguard.ui.DefaultEmphasisAlpha import com.artemchep.keyguard.ui.DefaultEmphasisAlpha
import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.DisabledEmphasisAlpha
import com.artemchep.keyguard.ui.ExpandedIfNotEmpty import com.artemchep.keyguard.ui.ExpandedIfNotEmpty
import com.artemchep.keyguard.ui.FlatItem
import com.artemchep.keyguard.ui.GridLayout import com.artemchep.keyguard.ui.GridLayout
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
import com.artemchep.keyguard.ui.OptionsButton import com.artemchep.keyguard.ui.OptionsButton
import com.artemchep.keyguard.ui.animatedNumberText import com.artemchep.keyguard.ui.animatedNumberText
import com.artemchep.keyguard.ui.grid.preferredGridWidth 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.KeyguardTwoFa
import com.artemchep.keyguard.ui.icons.KeyguardWebsite import com.artemchep.keyguard.ui.icons.KeyguardWebsite
import com.artemchep.keyguard.ui.poweredby.PoweredBy2factorauth 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.poweredby.PoweredByPasskeys
import com.artemchep.keyguard.ui.scaffoldContentWindowInsets import com.artemchep.keyguard.ui.scaffoldContentWindowInsets
import com.artemchep.keyguard.ui.shimmer.shimmer 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.skeleton.SkeletonText
import com.artemchep.keyguard.ui.surface.LocalSurfaceColor import com.artemchep.keyguard.ui.surface.LocalSurfaceColor
import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.Dimens
@ -144,10 +125,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun WatchtowerScreen() { fun WatchtowerScreen(
args: WatchtowerRoute.Args,
) {
RequestAppReviewEffect() RequestAppReviewEffect()
val state = produceWatchtowerState() val state = produceWatchtowerState(
args = args,
)
WatchtowerScreen( WatchtowerScreen(
state = state, state = state,
) )

View File

@ -55,9 +55,11 @@ import org.kodein.di.instance
@Composable @Composable
fun produceWatchtowerState( fun produceWatchtowerState(
args: WatchtowerRoute.Args,
) = with(localDI().direct) { ) = with(localDI().direct) {
produceWatchtowerState( produceWatchtowerState(
directDI = this, directDI = this,
args = args,
getCiphers = instance(), getCiphers = instance(),
getAccounts = instance(), getAccounts = instance(),
getProfiles = instance(), getProfiles = instance(),
@ -81,6 +83,7 @@ private class WatchtowerUiException(
@Composable @Composable
fun produceWatchtowerState( fun produceWatchtowerState(
directDI: DirectDI, directDI: DirectDI,
args: WatchtowerRoute.Args,
getCiphers: GetCiphers, getCiphers: GetCiphers,
getAccounts: GetAccounts, getAccounts: GetAccounts,
getProfiles: GetProfiles, getProfiles: GetProfiles,
@ -105,8 +108,17 @@ fun produceWatchtowerState(
val ciphersRawFlow = filterHiddenProfiles( val ciphersRawFlow = filterHiddenProfiles(
getProfiles = getProfiles, getProfiles = getProfiles,
getCiphers = getCiphers, 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 val ciphersFlow = ciphersRawFlow
.map { secrets -> .map { secrets ->
secrets secrets
@ -117,6 +129,15 @@ fun produceWatchtowerState(
.map { folders -> .map { folders ->
folders folders
.filter { folder -> !folder.deleted } .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) .shareIn(screenScope, SharingStarted.WhileSubscribed(), replay = 1)
@ -184,7 +205,7 @@ fun produceWatchtowerState(
ciphersFlow = ciphersFlow, ciphersFlow = ciphersFlow,
) )
val filteredTrashedCiphersFlow = filteredCiphers( val filteredTrashedCiphersFlow = filteredCiphers(
ciphersFlow = getCiphers() ciphersFlow = ciphersRawFlow
.map { secrets -> .map { secrets ->
secrets secrets
.filter { secret -> secret.deleted } .filter { secret -> secret.deleted }
@ -301,9 +322,10 @@ fun produceWatchtowerState(
title = translate(score.formatH2()), title = translate(score.formatH2()),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByPasswordStrength(score), DFilter.ByPasswordStrength(score),
filter, filter,
args.filter,
), ),
), ),
), ),
@ -377,9 +399,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_pwned_passwords_title), title = translate(Res.strings.watchtower_item_pwned_passwords_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByPasswordPwned, DFilter.ByPasswordPwned,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -419,9 +442,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_unsecure_websites_title), title = translate(Res.strings.watchtower_item_unsecure_websites_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByUnsecureWebsites, DFilter.ByUnsecureWebsites,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -461,9 +485,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_duplicate_websites_title), title = translate(Res.strings.watchtower_item_duplicate_websites_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByDuplicateWebsites, DFilter.ByDuplicateWebsites,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -503,9 +528,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_inactive_2fa_title), title = translate(Res.strings.watchtower_item_inactive_2fa_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByTfaWebsites, DFilter.ByTfaWebsites,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -545,9 +571,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_inactive_passkey_title), title = translate(Res.strings.watchtower_item_inactive_passkey_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByPasskeyWebsites, DFilter.ByPasskeyWebsites,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -587,9 +614,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_reused_passwords_title), title = translate(Res.strings.watchtower_item_reused_passwords_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByPasswordDuplicates, DFilter.ByPasswordDuplicates,
filter, filter,
args.filter,
), ),
), ),
sort = PasswordSort, sort = PasswordSort,
@ -630,9 +658,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_vulnerable_accounts_title), title = translate(Res.strings.watchtower_item_vulnerable_accounts_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByWebsitePwned, DFilter.ByWebsitePwned,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -738,9 +767,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_incomplete_items_title), title = translate(Res.strings.watchtower_item_incomplete_items_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByIncomplete, DFilter.ByIncomplete,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -780,9 +810,10 @@ fun produceWatchtowerState(
title = translate(Res.strings.watchtower_item_expiring_items_title), title = translate(Res.strings.watchtower_item_expiring_items_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = DFilter.And( filter = DFilter.And(
filters = listOf( filters = listOfNotNull(
DFilter.ByExpiring, DFilter.ByExpiring,
filter, filter,
args.filter,
), ),
), ),
), ),
@ -821,7 +852,12 @@ fun produceWatchtowerState(
VaultRoute.watchtower( VaultRoute.watchtower(
title = translate(Res.strings.watchtower_item_trashed_items_title), title = translate(Res.strings.watchtower_item_trashed_items_title),
subtitle = translate(Res.strings.watchtower_header_title), subtitle = translate(Res.strings.watchtower_header_title),
filter = filter, filter = DFilter.And(
filters = listOfNotNull(
filter,
args.filter,
),
),
trash = true, trash = true,
), ),
) )
@ -857,7 +893,12 @@ fun produceWatchtowerState(
) { ) {
val route = FoldersRoute( val route = FoldersRoute(
args = FoldersRoute.Args( args = FoldersRoute.Args(
filter = filter, filter = DFilter.And(
filters = listOfNotNull(
filter,
args.filter,
),
),
empty = true, empty = true,
), ),
) )