Integrate justgetmydata.com

This commit is contained in:
Artem Chepurnoy 2024-02-01 17:14:33 +02:00
parent 169dbe1d43
commit 30dce0444c
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
18 changed files with 1003 additions and 4 deletions

View File

@ -1,6 +1,7 @@
package com.artemchep.keyguard package com.artemchep.keyguard
const val URL_JUST_DELETE_ME = "https://justdeleteme.xyz/" 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_HAVE_I_BEEN_PWNED = "https://haveibeenpwned.com/"
const val URL_2FA = "https://2fa.directory/" const val URL_2FA = "https://2fa.directory/"
const val URL_PASSKEYS = "https://passkeys.directory/" const val URL_PASSKEYS = "https://passkeys.directory/"

View File

@ -1,12 +1,15 @@
package com.artemchep.keyguard.common.service.justgetmydata.impl package com.artemchep.keyguard.common.service.justgetmydata.impl
import arrow.core.partially1 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.effectMap
import com.artemchep.keyguard.common.io.shared import com.artemchep.keyguard.common.io.shared
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataService import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataService
import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo import com.artemchep.keyguard.common.service.justgetmydata.JustGetMyDataServiceInfo
import com.artemchep.keyguard.common.service.text.TextService import com.artemchep.keyguard.common.service.text.TextService
import com.artemchep.keyguard.common.service.text.readFromResourcesAsText import com.artemchep.keyguard.common.service.text.readFromResourcesAsText
import com.artemchep.keyguard.common.service.tld.TldService
import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.Res
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -29,10 +32,17 @@ data class JustGetMyDataEntity(
val emailBody: String? = null, 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( JustGetMyDataServiceInfo(
name = name, name = name,
domains = domains, domains = newDomains,
url = url, url = url,
difficulty = difficulty, difficulty = difficulty,
notes = notes, notes = notes,
@ -44,13 +54,32 @@ fun JustGetMyDataEntity.toDomain() = kotlin.run {
class JustGetMyDataServiceImpl( class JustGetMyDataServiceImpl(
private val textService: TextService, private val textService: TextService,
private val tldService: TldService,
private val json: Json, private val json: Json,
) : JustGetMyDataService { ) : JustGetMyDataService {
private val hostRegex = "://(.*@)?([^/]+)".toRegex()
private val listIo = ::loadJustGetMyDataRawData private val listIo = ::loadJustGetMyDataRawData
.partially1(textService) .partially1(textService)
.effectMap { jsonString -> .effectMap { jsonString ->
val entities = json.decodeFromString<List<JustGetMyDataEntity>>(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 models
} }
.shared() .shared()
@ -59,6 +88,7 @@ class JustGetMyDataServiceImpl(
directDI: DirectDI, directDI: DirectDI,
) : this( ) : this(
textService = directDI.instance(), textService = directDI.instance(),
tldService = directDI.instance(),
json = directDI.instance(), json = directDI.instance(),
) )

View File

@ -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?>

View File

@ -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
}
}

View File

@ -109,6 +109,7 @@ import com.artemchep.keyguard.common.usecase.GetFolderTreeById
import com.artemchep.keyguard.common.usecase.GetFolders import com.artemchep.keyguard.common.usecase.GetFolders
import com.artemchep.keyguard.common.usecase.GetGravatarUrl import com.artemchep.keyguard.common.usecase.GetGravatarUrl
import com.artemchep.keyguard.common.usecase.GetJustDeleteMeByUrl 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.GetMarkdown
import com.artemchep.keyguard.common.usecase.GetOrganizations import com.artemchep.keyguard.common.usecase.GetOrganizations
import com.artemchep.keyguard.common.usecase.GetPasswordStrength 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.cipherViewPasswordHistoryAction
import com.artemchep.keyguard.feature.home.vault.util.cipherWatchtowerAlerts import com.artemchep.keyguard.feature.home.vault.util.cipherWatchtowerAlerts
import com.artemchep.keyguard.feature.justdeleteme.directory.JustDeleteMeServiceViewRoute 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.largetype.LargeTypeRoute
import com.artemchep.keyguard.feature.loading.getErrorReadableMessage import com.artemchep.keyguard.feature.loading.getErrorReadableMessage
import com.artemchep.keyguard.feature.navigation.NavigationIntent import com.artemchep.keyguard.feature.navigation.NavigationIntent
@ -263,6 +266,7 @@ fun vaultViewScreenState(
dateFormatter = instance(), dateFormatter = instance(),
addCipherOpenedHistory = instance(), addCipherOpenedHistory = instance(),
getJustDeleteMeByUrl = instance(), getJustDeleteMeByUrl = instance(),
getJustGetMyDataByUrl = instance(),
windowCoroutineScope = instance(), windowCoroutineScope = instance(),
placeholderFactories = allInstances(), placeholderFactories = allInstances(),
linkInfoExtractors = allInstances(), linkInfoExtractors = allInstances(),
@ -339,6 +343,7 @@ fun vaultViewScreenState(
dateFormatter: DateFormatter, dateFormatter: DateFormatter,
addCipherOpenedHistory: AddCipherOpenedHistory, addCipherOpenedHistory: AddCipherOpenedHistory,
getJustDeleteMeByUrl: GetJustDeleteMeByUrl, getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
placeholderFactories: List<Placeholder.Factory>, placeholderFactories: List<Placeholder.Factory>,
linkInfoExtractors: List<LinkInfoExtractor<LinkInfo, LinkInfo>>, linkInfoExtractors: List<LinkInfoExtractor<LinkInfo, LinkInfo>>,
@ -741,6 +746,7 @@ fun vaultViewScreenState(
cipherIncompleteCheck = cipherIncompleteCheck, cipherIncompleteCheck = cipherIncompleteCheck,
cipherUrlCheck = cipherUrlCheck, cipherUrlCheck = cipherUrlCheck,
getJustDeleteMeByUrl = getJustDeleteMeByUrl, getJustDeleteMeByUrl = getJustDeleteMeByUrl,
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
verify = verify, verify = verify,
).toList(), ).toList(),
) )
@ -792,6 +798,7 @@ private fun RememberStateFlowScope.oh(
cipherIncompleteCheck: CipherIncompleteCheck, cipherIncompleteCheck: CipherIncompleteCheck,
cipherUrlCheck: CipherUrlCheck, cipherUrlCheck: CipherUrlCheck,
getJustDeleteMeByUrl: GetJustDeleteMeByUrl, getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
verify: ((() -> Unit) -> Unit)?, verify: ((() -> Unit) -> Unit)?,
) = flow<VaultViewItem> { ) = flow<VaultViewItem> {
val cipherError = cipher.service.error val cipherError = cipher.service.error
@ -1446,6 +1453,7 @@ private fun RememberStateFlowScope.oh(
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck, cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl = getJustDeleteMeByUrl, getJustDeleteMeByUrl = getJustDeleteMeByUrl,
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
executeCommand = executeCommand, executeCommand = executeCommand,
holder = holder, holder = holder,
id = id, id = id,
@ -1487,6 +1495,7 @@ private fun RememberStateFlowScope.oh(
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck, cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl = getJustDeleteMeByUrl, getJustDeleteMeByUrl = getJustDeleteMeByUrl,
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
executeCommand = executeCommand, executeCommand = executeCommand,
holder = holder, holder = holder,
id = id, id = id,
@ -1875,6 +1884,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck, cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl: GetJustDeleteMeByUrl, getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
executeCommand: ExecuteCommand, executeCommand: ExecuteCommand,
holder: Holder, holder: Holder,
id: String, id: String,
@ -1938,6 +1948,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck, cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl = getJustDeleteMeByUrl, getJustDeleteMeByUrl = getJustDeleteMeByUrl,
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
executeCommand = executeCommand, executeCommand = executeCommand,
uri = content.uri, uri = content.uri,
info = content.info, info = content.info,
@ -1968,6 +1979,7 @@ private suspend fun RememberStateFlowScope.createUriItem(
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck, cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl = getJustDeleteMeByUrl, getJustDeleteMeByUrl = getJustDeleteMeByUrl,
getJustGetMyDataByUrl = getJustGetMyDataByUrl,
executeCommand = executeCommand, executeCommand = executeCommand,
uri = holder.uri.uri, uri = holder.uri.uri,
info = holder.info, info = holder.info,
@ -2182,6 +2194,7 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck, cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix, cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
getJustDeleteMeByUrl: GetJustDeleteMeByUrl, getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
getJustGetMyDataByUrl: GetJustGetMyDataByUrl,
executeCommand: ExecuteCommand, executeCommand: ExecuteCommand,
uri: String, uri: String,
info: List<LinkInfo>, info: List<LinkInfo>,
@ -2314,6 +2327,10 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
.attempt() .attempt()
.bind() .bind()
.getOrNull() .getOrNull()
val isJustGetMyData = getJustGetMyDataByUrl(url)
.attempt()
.bind()
.getOrNull()
val isUnsecure = cipherUnsecureUrlCheck(uri) val isUnsecure = cipherUnsecureUrlCheck(uri)
val dropdown = buildContextItems { val dropdown = buildContextItems {
@ -2390,6 +2407,13 @@ private suspend fun RememberStateFlowScope.createUriItemContextItems(
host = platformMarker.url.host, host = platformMarker.url.host,
navigate = ::navigate, navigate = ::navigate,
) )
if (isJustGetMyData != null) {
this += JustGetMyDataViewRoute.justGetMyDataActionOrNull(
translator = this@createUriItemContextItems,
justGetMyData = isJustGetMyData,
navigate = ::navigate,
)
}
if (isJustDeleteMe != null) { if (isJustDeleteMe != null) {
this += JustDeleteMeServiceViewRoute.justDeleteMeActionOrNull( this += JustDeleteMeServiceViewRoute.justDeleteMeActionOrNull(
translator = this@createUriItemContextItems, translator = this@createUriItemContextItems,

View File

@ -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,
)
}

View File

@ -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()
}
}

View File

@ -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,
)
}

View File

@ -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,
)
}

View File

@ -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)
}
}

View File

@ -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,
)
}
}

View File

@ -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))
}
},
)
}

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -19,7 +19,6 @@ import com.artemchep.keyguard.common.usecase.GetProfiles
import com.artemchep.keyguard.common.util.flow.persistingStateIn import com.artemchep.keyguard.common.util.flow.persistingStateIn
import com.artemchep.keyguard.feature.crashlytics.crashlyticsMap import com.artemchep.keyguard.feature.crashlytics.crashlyticsMap
import com.artemchep.keyguard.feature.duplicates.DuplicatesRoute 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.VaultRoute
import com.artemchep.keyguard.feature.home.vault.folders.FoldersRoute import com.artemchep.keyguard.feature.home.vault.folders.FoldersRoute
import com.artemchep.keyguard.feature.home.vault.screen.FilterParams 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.filter.FilterHolder
import com.artemchep.keyguard.feature.home.vault.search.sort.PasswordSort import com.artemchep.keyguard.feature.home.vault.search.sort.PasswordSort
import com.artemchep.keyguard.feature.justdeleteme.directory.JustDeleteMeServiceListRoute 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.NavigationIntent
import com.artemchep.keyguard.feature.navigation.state.PersistedStorage import com.artemchep.keyguard.feature.navigation.state.PersistedStorage
import com.artemchep.keyguard.feature.navigation.state.produceScreenState import com.artemchep.keyguard.feature.navigation.state.produceScreenState
@ -927,6 +927,10 @@ fun produceWatchtowerState(
translator = this@produceScreenState, translator = this@produceScreenState,
navigate = ::navigate, navigate = ::navigate,
) )
this += JustGetMyDataListRoute.justGetMyDataActionOrNull(
translator = this@produceScreenState,
navigate = ::navigate,
)
this += JustDeleteMeServiceListRoute.justDeleteMeActionOrNull( this += JustDeleteMeServiceListRoute.justDeleteMeActionOrNull(
translator = this@produceScreenState, translator = this@produceScreenState,
navigate = ::navigate, navigate = ::navigate,

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
import com.artemchep.keyguard.URL_2FA import com.artemchep.keyguard.URL_2FA
import com.artemchep.keyguard.URL_HAVE_I_BEEN_PWNED import com.artemchep.keyguard.URL_HAVE_I_BEEN_PWNED
import com.artemchep.keyguard.URL_JUST_DELETE_ME 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.URL_PASSKEYS
import com.artemchep.keyguard.feature.navigation.LocalNavigationController import com.artemchep.keyguard.feature.navigation.LocalNavigationController
import com.artemchep.keyguard.feature.navigation.NavigationIntent 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 @Composable
fun PoweredByHaveibeenpwned( fun PoweredByHaveibeenpwned(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -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_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_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_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_title">Auto-fix unsecure URL</string>
<string name="uri_action_autofix_unsecure_text">Changes a protocol to secure variant if it is available</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_send_email_title">Send email</string>
<string name="justdeleteme_search_placeholder">Search websites</string> <string name="justdeleteme_search_placeholder">Search websites</string>
<string name="justdeleteme_empty_label">No instructions</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_title">Two-factor authentication</string>
<string name="tfa_directory_text">List of websites with two-factor authentication support</string> <string name="tfa_directory_text">List of websites with two-factor authentication support</string>
<string name="tfa_directory_search_placeholder">Search websites</string> <string name="tfa_directory_search_placeholder">Search websites</string>

View File

@ -117,6 +117,7 @@ import com.artemchep.keyguard.common.usecase.GetFont
import com.artemchep.keyguard.common.usecase.GetFontVariants import com.artemchep.keyguard.common.usecase.GetFontVariants
import com.artemchep.keyguard.common.usecase.GetGravatarUrl import com.artemchep.keyguard.common.usecase.GetGravatarUrl
import com.artemchep.keyguard.common.usecase.GetJustDeleteMeByUrl 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.GetKeepScreenOn
import com.artemchep.keyguard.common.usecase.GetLocaleVariants import com.artemchep.keyguard.common.usecase.GetLocaleVariants
import com.artemchep.keyguard.common.usecase.GetMarkdown 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.GetFontVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetGravatarUrlImpl import com.artemchep.keyguard.common.usecase.impl.GetGravatarUrlImpl
import com.artemchep.keyguard.common.usecase.impl.GetJustDeleteMeByUrlImpl 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.GetKeepScreenOnImpl
import com.artemchep.keyguard.common.usecase.impl.GetLocaleVariantsImpl import com.artemchep.keyguard.common.usecase.impl.GetLocaleVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetMarkdownImpl import com.artemchep.keyguard.common.usecase.impl.GetMarkdownImpl
@ -834,6 +836,11 @@ fun globalModuleJvm() = DI.Module(
directDI = this, directDI = this,
) )
} }
bindSingleton<GetJustGetMyDataByUrl> {
GetJustGetMyDataByUrlImpl(
directDI = this,
)
}
bindSingleton<ReadWordlistFromFile> { bindSingleton<ReadWordlistFromFile> {
ReadWordlistFromFileImpl( ReadWordlistFromFileImpl(
directDI = this, directDI = this,