Integrate justgetmydata.com
This commit is contained in:
parent
169dbe1d43
commit
30dce0444c
|
@ -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/"
|
||||||
|
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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.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,
|
||||||
|
|
|
@ -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.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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue