mirror of
https://github.com/AChep/keyguard-app.git
synced 2025-01-31 14:34:52 +01:00
Integrate justgetmydata.com
This commit is contained in:
parent
169dbe1d43
commit
30dce0444c
@ -1,6 +1,7 @@
|
||||
package com.artemchep.keyguard
|
||||
|
||||
const val URL_JUST_DELETE_ME = "https://justdeleteme.xyz/"
|
||||
const val URL_JUST_GET_MY_DATA = "https://justgetmydata.com/"
|
||||
const val URL_HAVE_I_BEEN_PWNED = "https://haveibeenpwned.com/"
|
||||
const val URL_2FA = "https://2fa.directory/"
|
||||
const val URL_PASSKEYS = "https://passkeys.directory/"
|
||||
|
@ -1,12 +1,15 @@
|
||||
package com.artemchep.keyguard.common.service.justgetmydata.impl
|
||||
|
||||
import arrow.core.partially1
|
||||
import com.artemchep.keyguard.common.io.attempt
|
||||
import com.artemchep.keyguard.common.io.bind
|
||||
import com.artemchep.keyguard.common.io.effectMap
|
||||
import com.artemchep.keyguard.common.io.shared
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataService
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.common.service.text.TextService
|
||||
import com.artemchep.keyguard.common.service.text.readFromResourcesAsText
|
||||
import com.artemchep.keyguard.common.service.tld.TldService
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -29,10 +32,17 @@ data class JustGetMyDataEntity(
|
||||
val emailBody: String? = null,
|
||||
)
|
||||
|
||||
fun JustGetMyDataEntity.toDomain() = kotlin.run {
|
||||
fun JustGetMyDataEntity.toDomain(
|
||||
additionalDomain: String?,
|
||||
) = kotlin.run {
|
||||
val newDomains = if (additionalDomain != null) {
|
||||
domains + additionalDomain
|
||||
} else {
|
||||
domains
|
||||
}
|
||||
JustGetMyDataServiceInfo(
|
||||
name = name,
|
||||
domains = domains,
|
||||
domains = newDomains,
|
||||
url = url,
|
||||
difficulty = difficulty,
|
||||
notes = notes,
|
||||
@ -44,13 +54,32 @@ fun JustGetMyDataEntity.toDomain() = kotlin.run {
|
||||
|
||||
class JustGetMyDataServiceImpl(
|
||||
private val textService: TextService,
|
||||
private val tldService: TldService,
|
||||
private val json: Json,
|
||||
) : JustGetMyDataService {
|
||||
private val hostRegex = "://(.*@)?([^/]+)".toRegex()
|
||||
|
||||
private val listIo = ::loadJustGetMyDataRawData
|
||||
.partially1(textService)
|
||||
.effectMap { jsonString ->
|
||||
val entities = json.decodeFromString<List<JustGetMyDataEntity>>(jsonString)
|
||||
val models = entities.map(JustGetMyDataEntity::toDomain)
|
||||
val models = entities
|
||||
.map { entity ->
|
||||
val host = entity.url
|
||||
?.let { url ->
|
||||
val result = hostRegex.find(url)
|
||||
result?.groupValues?.getOrNull(2) // get the host
|
||||
}
|
||||
val domain = host?.let {
|
||||
tldService.getDomainName(host)
|
||||
.attempt()
|
||||
.bind()
|
||||
.getOrNull()
|
||||
}
|
||||
entity.toDomain(
|
||||
additionalDomain = domain,
|
||||
)
|
||||
}
|
||||
models
|
||||
}
|
||||
.shared()
|
||||
@ -59,6 +88,7 @@ class JustGetMyDataServiceImpl(
|
||||
directDI: DirectDI,
|
||||
) : this(
|
||||
textService = directDI.instance(),
|
||||
tldService = directDI.instance(),
|
||||
json = directDI.instance(),
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
|
||||
interface GetJustGetMyDataByUrl : (String) -> IO<JustGetMyDataServiceInfo?>
|
@ -0,0 +1,54 @@
|
||||
package com.artemchep.keyguard.common.usecase.impl
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.io.effectMap
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataService
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl
|
||||
import io.ktor.http.Url
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class GetJustGetMyDataByUrlImpl(
|
||||
private val justGetMyDataService: JustGetMyDataService,
|
||||
) : GetJustGetMyDataByUrl {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
justGetMyDataService = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
url: String,
|
||||
): IO<JustGetMyDataServiceInfo?> = justGetMyDataService.get()
|
||||
.effectMap { list ->
|
||||
match(url, list)
|
||||
}
|
||||
|
||||
fun match(url: String, list: List<JustGetMyDataServiceInfo>) = kotlin.run {
|
||||
val host = parseHost(url)
|
||||
?: return@run null
|
||||
val result = list
|
||||
.firstOrNull { host in it.domains }
|
||||
result
|
||||
}
|
||||
|
||||
private fun parseHost(url: String) = if (
|
||||
url.startsWith("http://") ||
|
||||
url.startsWith("https://")
|
||||
) {
|
||||
val parsedUri = kotlin.runCatching {
|
||||
Url(url)
|
||||
}.getOrElse {
|
||||
// can not get the domain
|
||||
null
|
||||
}
|
||||
parsedUri
|
||||
?.host
|
||||
// The "www" subdomain is ignored in the database, however
|
||||
// it's only "www". Other subdomains, such as "photos",
|
||||
// should be respected.
|
||||
?.removePrefix("www.")
|
||||
} else {
|
||||
// can not get the domain
|
||||
null
|
||||
}
|
||||
}
|
@ -109,6 +109,7 @@ import com.artemchep.keyguard.common.usecase.GetFolderTreeById
|
||||
import com.artemchep.keyguard.common.usecase.GetFolders
|
||||
import com.artemchep.keyguard.common.usecase.GetGravatarUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetJustDeleteMeByUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetMarkdown
|
||||
import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||
import com.artemchep.keyguard.common.usecase.GetPasswordStrength
|
||||
@ -157,6 +158,8 @@ import com.artemchep.keyguard.feature.home.vault.util.cipherTrashAction
|
||||
import com.artemchep.keyguard.feature.home.vault.util.cipherViewPasswordHistoryAction
|
||||
import com.artemchep.keyguard.feature.home.vault.util.cipherWatchtowerAlerts
|
||||
import com.artemchep.keyguard.feature.justdeleteme.directory.JustDeleteMeServiceViewRoute
|
||||
import com.artemchep.keyguard.feature.justgetdata.directory.JustGetMyDataListRoute
|
||||
import com.artemchep.keyguard.feature.justgetdata.directory.JustGetMyDataViewRoute
|
||||
import com.artemchep.keyguard.feature.largetype.LargeTypeRoute
|
||||
import com.artemchep.keyguard.feature.loading.getErrorReadableMessage
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
@ -263,6 +266,7 @@ fun vaultViewScreenState(
|
||||
dateFormatter = instance(),
|
||||
addCipherOpenedHistory = instance(),
|
||||
getJustDeleteMeByUrl = instance(),
|
||||
getJustGetMyDataByUrl = instance(),
|
||||
windowCoroutineScope = instance(),
|
||||
placeholderFactories = allInstances(),
|
||||
linkInfoExtractors = allInstances(),
|
||||
@ -339,6 +343,7 @@ fun vaultViewScreenState(
|
||||
dateFormatter: DateFormatter,
|
||||
addCipherOpenedHistory: AddCipherOpenedHistory,
|
||||
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
|
||||
windowCoroutineScope: WindowCoroutineScope,
|
||||
placeholderFactories: List<Placeholder.Factory>,
|
||||
linkInfoExtractors: List<LinkInfoExtractor<LinkInfo, LinkInfo>>,
|
||||
@ -741,6 +746,7 @@ fun vaultViewScreenState(
|
||||
cipherIncompleteCheck = cipherIncompleteCheck,
|
||||
cipherUrlCheck = cipherUrlCheck,
|
||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
|
||||
verify = verify,
|
||||
).toList(),
|
||||
)
|
||||
@ -792,6 +798,7 @@ private fun RememberStateFlowScope.oh(
|
||||
cipherIncompleteCheck: CipherIncompleteCheck,
|
||||
cipherUrlCheck: CipherUrlCheck,
|
||||
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
|
||||
verify: ((() -> Unit) -> Unit)?,
|
||||
) = flow<VaultViewItem> {
|
||||
val cipherError = cipher.service.error
|
||||
@ -1446,6 +1453,7 @@ private fun RememberStateFlowScope.oh(
|
||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
|
||||
executeCommand = executeCommand,
|
||||
holder = holder,
|
||||
id = id,
|
||||
@ -1487,6 +1495,7 @@ private fun RememberStateFlowScope.oh(
|
||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
|
||||
executeCommand = executeCommand,
|
||||
holder = holder,
|
||||
id = id,
|
||||
@ -1875,6 +1884,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
|
||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
|
||||
executeCommand: ExecuteCommand,
|
||||
holder: Holder,
|
||||
id: String,
|
||||
@ -1938,6 +1948,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
|
||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
|
||||
executeCommand = executeCommand,
|
||||
uri = content.uri,
|
||||
info = content.info,
|
||||
@ -1968,6 +1979,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
|
||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
|
||||
executeCommand = executeCommand,
|
||||
uri = holder.uri.uri,
|
||||
info = holder.info,
|
||||
@ -2182,6 +2194,7 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
|
||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
|
||||
executeCommand: ExecuteCommand,
|
||||
uri: String,
|
||||
info: List<LinkInfo>,
|
||||
@ -2314,6 +2327,10 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
|
||||
.attempt()
|
||||
.bind()
|
||||
.getOrNull()
|
||||
val isJustGetMyData = getJustGetMyDataByUrl(url)
|
||||
.attempt()
|
||||
.bind()
|
||||
.getOrNull()
|
||||
|
||||
val isUnsecure = cipherUnsecureUrlCheck(uri)
|
||||
val dropdown = buildContextItems {
|
||||
@ -2390,6 +2407,13 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
|
||||
host = platformMarker.url.host,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
if (isJustGetMyData != null) {
|
||||
this += JustGetMyDataViewRoute.justGetMyDataActionOrNull(
|
||||
translator = this@createUriItemContextItems,
|
||||
justGetMyData = isJustGetMyData,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
}
|
||||
if (isJustDeleteMe != null) {
|
||||
this += JustDeleteMeServiceViewRoute.justDeleteMeActionOrNull(
|
||||
translator = this@createUriItemContextItems,
|
||||
|
@ -0,0 +1,37 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.Ah
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun AhDifficulty(
|
||||
modifier: Modifier = Modifier,
|
||||
model: JustGetMyDataServiceInfo,
|
||||
) {
|
||||
val difficulty = model.difficulty
|
||||
val score = when (difficulty) {
|
||||
"easy" -> 1f
|
||||
"medium" -> 0.5f
|
||||
"hard" -> 0.2f
|
||||
"limited" -> 0.0f
|
||||
"impossible" -> 0.0f
|
||||
else -> 0.0f
|
||||
}
|
||||
val text = when (difficulty) {
|
||||
"easy" -> stringResource(Res.strings.justgetmydata_difficulty_easy_label)
|
||||
"medium" -> stringResource(Res.strings.justgetmydata_difficulty_medium_label)
|
||||
"hard" -> stringResource(Res.strings.justgetmydata_difficulty_hard_label)
|
||||
"limited" -> stringResource(Res.strings.justgetmydata_difficulty_limited_availability_label)
|
||||
"impossible" -> stringResource(Res.strings.justgetmydata_difficulty_impossible_label)
|
||||
else -> ""
|
||||
}
|
||||
Ah(
|
||||
modifier = modifier,
|
||||
score = score,
|
||||
text = text,
|
||||
)
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AccountBox
|
||||
import androidx.compose.material.icons.outlined.Dataset
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.Route
|
||||
import com.artemchep.keyguard.feature.navigation.state.TranslatorScope
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
import com.artemchep.keyguard.ui.icons.iconSmall
|
||||
|
||||
object JustGetMyDataListRoute : Route {
|
||||
fun justGetMyDataActionOrNull(
|
||||
translator: TranslatorScope,
|
||||
navigate: (NavigationIntent) -> Unit,
|
||||
) = justGetMyDataAction(
|
||||
translator = translator,
|
||||
navigate = navigate,
|
||||
)
|
||||
|
||||
fun justGetMyDataAction(
|
||||
translator: TranslatorScope,
|
||||
navigate: (NavigationIntent) -> Unit,
|
||||
) = FlatItemAction(
|
||||
leading = iconSmall(Icons.Outlined.AccountBox, Icons.Outlined.Dataset),
|
||||
title = translator.translate(Res.strings.uri_action_get_my_data_account_title),
|
||||
trailing = {
|
||||
ChevronIcon()
|
||||
},
|
||||
onClick = {
|
||||
val route = JustGetMyDataListRoute
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
JustGetMyDataListScreen()
|
||||
}
|
||||
}
|
@ -0,0 +1,274 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.OpenInBrowser
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import com.artemchep.keyguard.URL_JUST_GET_MY_DATA
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.model.flatMap
|
||||
import com.artemchep.keyguard.common.model.getOrNull
|
||||
import com.artemchep.keyguard.feature.EmptySearchView
|
||||
import com.artemchep.keyguard.feature.ErrorView
|
||||
import com.artemchep.keyguard.feature.home.vault.component.SearchTextField
|
||||
import com.artemchep.keyguard.feature.justgetdata.AhDifficulty
|
||||
import com.artemchep.keyguard.feature.navigation.LocalNavigationController
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIcon
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.DefaultProgressBar
|
||||
import com.artemchep.keyguard.ui.FlatItem
|
||||
import com.artemchep.keyguard.ui.ScaffoldLazyColumn
|
||||
import com.artemchep.keyguard.ui.focus.FocusRequester2
|
||||
import com.artemchep.keyguard.ui.focus.focusRequester2
|
||||
import com.artemchep.keyguard.ui.icons.IconBox
|
||||
import com.artemchep.keyguard.ui.pulltosearch.PullToSearch
|
||||
import com.artemchep.keyguard.ui.skeleton.SkeletonItem
|
||||
import com.artemchep.keyguard.ui.toolbar.CustomToolbar
|
||||
import com.artemchep.keyguard.ui.toolbar.content.CustomToolbarContent
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
|
||||
@Composable
|
||||
fun JustGetMyDataListScreen(
|
||||
) {
|
||||
val loadableState = produceJustGetMyDataListState()
|
||||
JustGetMyDataListScreen(
|
||||
loadableState = loadableState,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun JustGetMyDataListScreen(
|
||||
loadableState: Loadable<JustGetMyDataListState>,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val filterState = run {
|
||||
val filterFlow = loadableState.getOrNull()?.filter
|
||||
remember(filterFlow) {
|
||||
filterFlow ?: MutableStateFlow(null)
|
||||
}.collectAsState()
|
||||
}
|
||||
|
||||
val listRevision =
|
||||
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.revision
|
||||
val listState = remember {
|
||||
LazyListState(
|
||||
firstVisibleItemIndex = 0,
|
||||
firstVisibleItemScrollOffset = 0,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(listRevision) {
|
||||
// TODO: How do you wait till the layout state start to represent
|
||||
// the actual data?
|
||||
val listSize =
|
||||
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.items?.size
|
||||
snapshotFlow { listState.layoutInfo.totalItemsCount }
|
||||
.withIndex()
|
||||
.filter {
|
||||
it.index > 0 || it.value == listSize
|
||||
}
|
||||
.first()
|
||||
|
||||
listState.scrollToItem(0, 0)
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester2() }
|
||||
// Auto focus the text field
|
||||
// on launch.
|
||||
LaunchedEffect(
|
||||
focusRequester,
|
||||
filterState,
|
||||
) {
|
||||
snapshotFlow { filterState.value }
|
||||
.first { it?.query?.onChange != null }
|
||||
delay(100L)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = false,
|
||||
onRefresh = {
|
||||
focusRequester.requestFocus()
|
||||
},
|
||||
)
|
||||
ScaffoldLazyColumn(
|
||||
modifier = Modifier
|
||||
.pullRefresh(pullRefreshState)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topAppBarScrollBehavior = scrollBehavior,
|
||||
topBar = {
|
||||
CustomToolbar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
) {
|
||||
Column {
|
||||
CustomToolbarContent(
|
||||
title = stringResource(Res.strings.justgetmydata_title),
|
||||
icon = {
|
||||
NavigationIcon()
|
||||
},
|
||||
actions = {
|
||||
val updatedNavigationController by rememberUpdatedState(
|
||||
LocalNavigationController.current,
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
val intent =
|
||||
NavigationIntent.NavigateToBrowser(URL_JUST_GET_MY_DATA)
|
||||
updatedNavigationController.queue(intent)
|
||||
},
|
||||
) {
|
||||
IconBox(Icons.Outlined.OpenInBrowser)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val query = filterState.value?.query
|
||||
val queryText = query?.state?.value.orEmpty()
|
||||
|
||||
val count = loadableState
|
||||
.getOrNull()
|
||||
?.content
|
||||
?.getOrNull()
|
||||
?.getOrNull()
|
||||
?.items
|
||||
?.size
|
||||
SearchTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester2(focusRequester),
|
||||
text = queryText,
|
||||
placeholder = stringResource(Res.strings.justgetmydata_search_placeholder),
|
||||
searchIcon = false,
|
||||
count = count,
|
||||
leading = {},
|
||||
trailing = {},
|
||||
onTextChange = query?.onChange,
|
||||
onGoClick = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
pullRefreshState = pullRefreshState,
|
||||
overlay = {
|
||||
val filterRevision = filterState.value?.revision
|
||||
DefaultProgressBar(
|
||||
visible = listRevision != null && filterRevision != null &&
|
||||
listRevision != filterRevision,
|
||||
)
|
||||
|
||||
PullToSearch(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding.value),
|
||||
pullRefreshState = pullRefreshState,
|
||||
)
|
||||
},
|
||||
listState = listState,
|
||||
) {
|
||||
val contentState = loadableState
|
||||
.flatMap { it.content }
|
||||
when (contentState) {
|
||||
is Loadable.Loading -> {
|
||||
for (i in 1..3) {
|
||||
item("skeleton.$i") {
|
||||
SkeletonItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Loadable.Ok -> {
|
||||
contentState.value.fold(
|
||||
ifLeft = { e ->
|
||||
item("error") {
|
||||
ErrorView(
|
||||
text = {
|
||||
Text(text = "Failed to load just-get-my-data list!")
|
||||
},
|
||||
exception = e,
|
||||
)
|
||||
}
|
||||
},
|
||||
ifRight = { content ->
|
||||
val items = content.items
|
||||
if (items.isEmpty()) {
|
||||
item("empty") {
|
||||
NoItemsPlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = { it.key },
|
||||
) { item ->
|
||||
AppItem(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement(),
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoItemsPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
EmptySearchView(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.justgetmydata_empty_label),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
modifier: Modifier,
|
||||
item: JustGetMyDataListState.Item,
|
||||
) {
|
||||
FlatItem(
|
||||
modifier = modifier,
|
||||
leading = {
|
||||
item.icon()
|
||||
},
|
||||
title = {
|
||||
Text(item.name)
|
||||
},
|
||||
trailing = {
|
||||
AhDifficulty(
|
||||
modifier = Modifier,
|
||||
model = item.data,
|
||||
)
|
||||
},
|
||||
onClick = item.onClick,
|
||||
)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import arrow.core.Either
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class JustGetMyDataListState(
|
||||
val filter: StateFlow<Filter>,
|
||||
val content: Loadable<Either<Throwable, Content>>,
|
||||
) {
|
||||
@Immutable
|
||||
data class Filter(
|
||||
val revision: Int,
|
||||
val query: TextFieldModel2,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Content(
|
||||
val revision: Int,
|
||||
val items: List<Item>,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Item(
|
||||
val key: String,
|
||||
val icon: @Composable () -> Unit,
|
||||
val name: AnnotatedString,
|
||||
val data: JustGetMyDataServiceInfo,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
)
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.core.partially1
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataService
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsAttempt
|
||||
import com.artemchep.keyguard.feature.favicon.FaviconImage
|
||||
import com.artemchep.keyguard.feature.favicon.FaviconUrl
|
||||
import com.artemchep.keyguard.feature.home.vault.search.IndexedText
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.search.search.IndexedModel
|
||||
import com.artemchep.keyguard.feature.search.search.mapSearch
|
||||
import com.artemchep.keyguard.feature.search.search.searchFilter
|
||||
import com.artemchep.keyguard.feature.search.search.searchQueryHandle
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
|
||||
private class JustDeleteMeServiceListUiException(
|
||||
msg: String,
|
||||
cause: Throwable,
|
||||
) : RuntimeException(msg, cause)
|
||||
|
||||
@Composable
|
||||
fun produceJustGetMyDataListState(
|
||||
) = with(localDI().direct) {
|
||||
produceJustGetMyDataListState(
|
||||
justGetMyDataService = instance(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun produceJustGetMyDataListState(
|
||||
justGetMyDataService: JustGetMyDataService,
|
||||
): Loadable<JustGetMyDataListState> = produceScreenState(
|
||||
key = "justgetmydata_list",
|
||||
initial = Loadable.Loading,
|
||||
args = arrayOf(),
|
||||
) {
|
||||
val queryHandle = searchQueryHandle("query")
|
||||
val queryFlow = searchFilter(queryHandle) { model, revision ->
|
||||
JustGetMyDataListState.Filter(
|
||||
revision = revision,
|
||||
query = model,
|
||||
)
|
||||
}
|
||||
|
||||
val modelComparator = Comparator { a: JustGetMyDataServiceInfo, b: JustGetMyDataServiceInfo ->
|
||||
a.name.compareTo(b.name, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun onClick(model: JustGetMyDataServiceInfo) {
|
||||
val route = JustGetMyDataViewRoute(
|
||||
args = JustGetMyDataViewRoute.Args(
|
||||
model = model,
|
||||
),
|
||||
)
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
fun List<JustGetMyDataServiceInfo>.toItems(): List<JustGetMyDataListState.Item> {
|
||||
val nameCollisions = mutableMapOf<String, Int>()
|
||||
return this
|
||||
.sortedWith(modelComparator)
|
||||
.map { serviceInfo ->
|
||||
val key = kotlin.run {
|
||||
val newNameCollisionCounter = nameCollisions
|
||||
.getOrDefault(serviceInfo.name, 0) + 1
|
||||
nameCollisions[serviceInfo.name] =
|
||||
newNameCollisionCounter
|
||||
serviceInfo.name + ":" + newNameCollisionCounter
|
||||
}
|
||||
val faviconUrl = serviceInfo.url?.let { url ->
|
||||
FaviconUrl(
|
||||
serverId = null,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
JustGetMyDataListState.Item(
|
||||
key = key,
|
||||
icon = {
|
||||
FaviconImage(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape),
|
||||
imageModel = { faviconUrl },
|
||||
)
|
||||
},
|
||||
name = AnnotatedString(serviceInfo.name),
|
||||
data = serviceInfo,
|
||||
onClick = ::onClick
|
||||
.partially1(serviceInfo),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val itemsFlow = justGetMyDataService.get()
|
||||
.asFlow()
|
||||
.map { data ->
|
||||
data
|
||||
.toItems()
|
||||
// Index for the search.
|
||||
.map { item ->
|
||||
IndexedModel(
|
||||
model = item,
|
||||
indexedText = IndexedText.invoke(item.name.text),
|
||||
)
|
||||
}
|
||||
}
|
||||
.mapSearch(
|
||||
handle = queryHandle,
|
||||
) { item, result ->
|
||||
// Replace the origin text with the one with
|
||||
// search decor applied to it.
|
||||
item.copy(name = result.highlightedText)
|
||||
}
|
||||
val contentFlow = itemsFlow
|
||||
.crashlyticsAttempt { e ->
|
||||
val msg = "Failed to get the just-get-my-data list!"
|
||||
JustDeleteMeServiceListUiException(
|
||||
msg = msg,
|
||||
cause = e,
|
||||
)
|
||||
}
|
||||
.map { result ->
|
||||
val contentOrException = result
|
||||
.map { (items, revision) ->
|
||||
JustGetMyDataListState.Content(
|
||||
revision = revision,
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
Loadable.Ok(contentOrException)
|
||||
}
|
||||
contentFlow
|
||||
.map { content ->
|
||||
val state = JustGetMyDataListState(
|
||||
filter = queryFlow,
|
||||
content = content,
|
||||
)
|
||||
Loadable.Ok(state)
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AccountBox
|
||||
import androidx.compose.material.icons.outlined.Dataset
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
import com.artemchep.keyguard.feature.navigation.DialogRoute
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.state.TranslatorScope
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.icons.iconSmall
|
||||
|
||||
data class JustGetMyDataViewRoute(
|
||||
val args: Args,
|
||||
) : DialogRoute {
|
||||
companion object {
|
||||
fun justGetMyDataActionOrNull(
|
||||
translator: TranslatorScope,
|
||||
justGetMyData: JustGetMyDataServiceInfo,
|
||||
navigate: (NavigationIntent) -> Unit,
|
||||
) = justGetMyDataAction(
|
||||
translator = translator,
|
||||
justGetMyData = justGetMyData,
|
||||
navigate = navigate,
|
||||
)
|
||||
|
||||
fun justGetMyDataAction(
|
||||
translator: TranslatorScope,
|
||||
justGetMyData: JustGetMyDataServiceInfo,
|
||||
navigate: (NavigationIntent) -> Unit,
|
||||
) = FlatItemAction(
|
||||
leading = iconSmall(Icons.Outlined.AccountBox, Icons.Outlined.Dataset),
|
||||
title = translator.translate(Res.strings.uri_action_get_my_data_account_title),
|
||||
onClick = {
|
||||
val route = JustGetMyDataViewRoute(
|
||||
args = Args(
|
||||
model = justGetMyData,
|
||||
),
|
||||
)
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
data class Args(
|
||||
val model: JustGetMyDataServiceInfo,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
JustGetMyDataViewScreen(
|
||||
args = args,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Email
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.model.getOrNull
|
||||
import com.artemchep.keyguard.feature.dialog.Dialog
|
||||
import com.artemchep.keyguard.feature.justgetdata.AhDifficulty
|
||||
import com.artemchep.keyguard.feature.navigation.LocalNavigationController
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.tfa.directory.FlatLaunchBrowserItem
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItem
|
||||
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
import com.artemchep.keyguard.ui.icons.IconBox
|
||||
import com.artemchep.keyguard.ui.icons.KeyguardTwoFa
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.markdown.MarkdownText
|
||||
import com.artemchep.keyguard.ui.poweredby.PoweredByJustGetMyData
|
||||
import com.artemchep.keyguard.ui.skeleton.SkeletonText
|
||||
import com.artemchep.keyguard.ui.theme.Dimens
|
||||
import com.artemchep.keyguard.ui.theme.combineAlpha
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun JustGetMyDataViewScreen(
|
||||
args: JustGetMyDataViewRoute.Args,
|
||||
) {
|
||||
val loadableState = produceJustGetMyDataViewState(
|
||||
args = args,
|
||||
)
|
||||
JustGetMyDataViewScreen(
|
||||
args = args,
|
||||
loadableState = loadableState,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun JustGetMyDataViewScreen(
|
||||
args: JustGetMyDataViewRoute.Args,
|
||||
loadableState: Loadable<JustGetMyDataViewState>,
|
||||
) {
|
||||
Dialog(
|
||||
icon = icon(Icons.Outlined.KeyguardTwoFa),
|
||||
title = {
|
||||
when (loadableState) {
|
||||
is Loadable.Loading -> {
|
||||
SkeletonText(
|
||||
modifier = Modifier
|
||||
.width(72.dp),
|
||||
)
|
||||
}
|
||||
|
||||
is Loadable.Ok -> {
|
||||
val title = loadableState.value.content.getOrNull()
|
||||
?.model?.name
|
||||
.orEmpty()
|
||||
Text(
|
||||
text = title,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(Res.strings.justgetmydata_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = LocalContentColor.current
|
||||
.combineAlpha(MediumEmphasisAlpha),
|
||||
)
|
||||
AhDifficulty(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp),
|
||||
model = args.model,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Column {
|
||||
val notes = args.model.notes
|
||||
if (notes != null) {
|
||||
MarkdownText(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.horizontalPadding),
|
||||
markdown = notes,
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
val navigationController by rememberUpdatedState(LocalNavigationController.current)
|
||||
|
||||
val updatedEmail by rememberUpdatedState(args.model.email)
|
||||
if (updatedEmail != null) {
|
||||
FlatItem(
|
||||
elevation = 8.dp,
|
||||
leading = {
|
||||
IconBox(Icons.Outlined.Email)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.justdeleteme_send_email_title),
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ChevronIcon()
|
||||
},
|
||||
onClick = {
|
||||
// Open the email, if it's available.
|
||||
updatedEmail?.let {
|
||||
val intent = NavigationIntent.NavigateToEmail(
|
||||
it,
|
||||
subject = args.model.emailSubject,
|
||||
body = args.model.emailBody,
|
||||
)
|
||||
navigationController.queue(intent)
|
||||
}
|
||||
},
|
||||
enabled = updatedEmail != null,
|
||||
)
|
||||
}
|
||||
|
||||
val websiteUrl = args.model.url
|
||||
if (websiteUrl != null) {
|
||||
FlatLaunchBrowserItem(
|
||||
title = stringResource(Res.strings.uri_action_launch_website_title),
|
||||
url = websiteUrl,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(8.dp),
|
||||
)
|
||||
|
||||
PoweredByJustGetMyData(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.horizontalPadding)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
contentScrollable = true,
|
||||
actions = {
|
||||
val updatedOnClose by rememberUpdatedState(loadableState.getOrNull()?.onClose)
|
||||
TextButton(
|
||||
enabled = updatedOnClose != null,
|
||||
onClick = {
|
||||
updatedOnClose?.invoke()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.strings.close))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import arrow.core.Either
|
||||
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
|
||||
|
||||
data class JustGetMyDataViewState(
|
||||
val content: Either<Throwable, Content>,
|
||||
val onClose: (() -> Unit)? = null,
|
||||
) {
|
||||
@Immutable
|
||||
data class Content(
|
||||
val model: JustGetMyDataServiceInfo,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.artemchep.keyguard.feature.justgetdata.directory
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import arrow.core.right
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.feature.navigation.state.navigatePopSelf
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.direct
|
||||
|
||||
@Composable
|
||||
fun produceJustGetMyDataViewState(
|
||||
args: JustGetMyDataViewRoute.Args,
|
||||
) = with(localDI().direct) {
|
||||
produceJustGetMyDataViewState(
|
||||
unit = Unit,
|
||||
args = args,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun produceJustGetMyDataViewState(
|
||||
unit: Unit,
|
||||
args: JustGetMyDataViewRoute.Args,
|
||||
): Loadable<JustGetMyDataViewState> = produceScreenState(
|
||||
key = "justgetmydata_view",
|
||||
initial = Loadable.Loading,
|
||||
args = arrayOf(),
|
||||
) {
|
||||
val content = JustGetMyDataViewState.Content(
|
||||
model = args.model,
|
||||
)
|
||||
val state = JustGetMyDataViewState(
|
||||
content = content
|
||||
.right(),
|
||||
onClose = {
|
||||
navigatePopSelf()
|
||||
},
|
||||
)
|
||||
flowOf(Loadable.Ok(state))
|
||||
}
|
@ -19,7 +19,6 @@ import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsMap
|
||||
import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute
|
||||
import com.artemchep.keyguard.feature.duplicates.list.DuplicatesListRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.VaultRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.folders.FoldersRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.screen.FilterParams
|
||||
@ -29,6 +28,7 @@ import com.artemchep.keyguard.feature.home.vault.screen.createFilter
|
||||
import com.artemchep.keyguard.feature.home.vault.search.filter.FilterHolder
|
||||
import com.artemchep.keyguard.feature.home.vault.search.sort.PasswordSort
|
||||
import com.artemchep.keyguard.feature.justdeleteme.directory.JustDeleteMeServiceListRoute
|
||||
import com.artemchep.keyguard.feature.justgetdata.directory.JustGetMyDataListRoute
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.state.PersistedStorage
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
@ -927,6 +927,10 @@ fun produceWatchtowerState(
|
||||
translator = this@produceScreenState,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
this += JustGetMyDataListRoute.justGetMyDataActionOrNull(
|
||||
translator = this@produceScreenState,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
this += JustDeleteMeServiceListRoute.justDeleteMeActionOrNull(
|
||||
translator = this@produceScreenState,
|
||||
navigate = ::navigate,
|
||||
|
@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.artemchep.keyguard.URL_2FA
|
||||
import com.artemchep.keyguard.URL_HAVE_I_BEEN_PWNED
|
||||
import com.artemchep.keyguard.URL_JUST_DELETE_ME
|
||||
import com.artemchep.keyguard.URL_JUST_GET_MY_DATA
|
||||
import com.artemchep.keyguard.URL_PASSKEYS
|
||||
import com.artemchep.keyguard.feature.navigation.LocalNavigationController
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
@ -38,6 +39,19 @@ fun PoweredByJustDeleteMe(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PoweredByJustGetMyData(
|
||||
modifier: Modifier = Modifier,
|
||||
fill: Boolean = false,
|
||||
) {
|
||||
PoweredByLabel(
|
||||
modifier = modifier,
|
||||
domain = "JustGetMyData",
|
||||
url = URL_JUST_GET_MY_DATA,
|
||||
fill = fill,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PoweredByHaveibeenpwned(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -265,6 +265,7 @@
|
||||
<string name="uri_action_launch_in_app_title">Launch with <xliff:g id="app" example="Keyguard">%1$s</xliff:g></string>
|
||||
<string name="uri_action_launch_in_smth_title">Launch with…</string>
|
||||
<string name="uri_action_how_to_delete_account_title">How to delete an account?</string>
|
||||
<string name="uri_action_get_my_data_account_title">How to get my data?</string>
|
||||
<string name="uri_action_autofix_unsecure_title">Auto-fix unsecure URL</string>
|
||||
<string name="uri_action_autofix_unsecure_text">Changes a protocol to secure variant if it is available</string>
|
||||
|
||||
@ -442,6 +443,24 @@
|
||||
<string name="justdeleteme_send_email_title">Send email</string>
|
||||
<string name="justdeleteme_search_placeholder">Search websites</string>
|
||||
<string name="justdeleteme_empty_label">No instructions</string>
|
||||
|
||||
<!-- Title of the 'How to delete an account?' dialog -->
|
||||
<string name="justgetmydata_title">How to get my data?</string>
|
||||
<string name="justgetmydata_subtitle">A collection of info of how to get the from your account from web services</string>
|
||||
<!-- Simple process -->
|
||||
<string name="justgetmydata_difficulty_easy_label">Easy</string>
|
||||
<!-- Some extra steps involved -->
|
||||
<string name="justgetmydata_difficulty_medium_label">Medium</string>
|
||||
<!-- Cannot be fully obtained without contacting customer services -->
|
||||
<string name="justgetmydata_difficulty_hard_label">Hard</string>
|
||||
<!-- Cannot be obtained without verifying you live in an area with privacy laws -->
|
||||
<string name="justgetmydata_difficulty_limited_availability_label">Limited availability</string>
|
||||
<!-- Cannot be obtained -->
|
||||
<string name="justgetmydata_difficulty_impossible_label">Impossible</string>
|
||||
<string name="justgetmydata_send_email_title">Send an email</string>
|
||||
<string name="justgetmydata_search_placeholder">Search websites</string>
|
||||
<string name="justgetmydata_empty_label">No instructions</string>
|
||||
|
||||
<string name="tfa_directory_title">Two-factor authentication</string>
|
||||
<string name="tfa_directory_text">List of websites with two-factor authentication support</string>
|
||||
<string name="tfa_directory_search_placeholder">Search websites</string>
|
||||
|
@ -117,6 +117,7 @@ import com.artemchep.keyguard.common.usecase.GetFont
|
||||
import com.artemchep.keyguard.common.usecase.GetFontVariants
|
||||
import com.artemchep.keyguard.common.usecase.GetGravatarUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetJustDeleteMeByUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetJustGetMyDataByUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetKeepScreenOn
|
||||
import com.artemchep.keyguard.common.usecase.GetLocaleVariants
|
||||
import com.artemchep.keyguard.common.usecase.GetMarkdown
|
||||
@ -235,6 +236,7 @@ import com.artemchep.keyguard.common.usecase.impl.GetFontImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetFontVariantsImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetGravatarUrlImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetJustDeleteMeByUrlImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetJustGetMyDataByUrlImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetKeepScreenOnImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetLocaleVariantsImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.GetMarkdownImpl
|
||||
@ -834,6 +836,11 @@ fun globalModuleJvm() = DI.Module(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<GetJustGetMyDataByUrl> {
|
||||
GetJustGetMyDataByUrlImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<ReadWordlistFromFile> {
|
||||
ReadWordlistFromFileImpl(
|
||||
directDI = this,
|
||||
|
Loading…
x
Reference in New Issue
Block a user