feat: Add a wordlist from a URL #88
This commit is contained in:
parent
e6d74bd143
commit
998c40dfc0
|
@ -9,6 +9,10 @@ data class AddWordlistRequest(
|
|||
val uri: String,
|
||||
) : Wordlist
|
||||
|
||||
data class FromUrl(
|
||||
val url: String,
|
||||
) : Wordlist
|
||||
|
||||
data class FromList(
|
||||
val list: List<String>,
|
||||
) : Wordlist
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
||||
interface ReadWordlistFromUrl : (String) -> IO<List<String>>
|
|
@ -6,16 +6,19 @@ import com.artemchep.keyguard.common.model.AddWordlistRequest
|
|||
import com.artemchep.keyguard.common.service.wordlist.repo.GeneratorWordlistRepository
|
||||
import com.artemchep.keyguard.common.usecase.AddWordlist
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromFile
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromUrl
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class AddWordlistImpl(
|
||||
private val generatorWordlistRepository: GeneratorWordlistRepository,
|
||||
private val readWordlistFromFile: ReadWordlistFromFile,
|
||||
private val readWordlistFromUrl: ReadWordlistFromUrl,
|
||||
) : AddWordlist {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
generatorWordlistRepository = directDI.instance(),
|
||||
readWordlistFromFile = directDI.instance(),
|
||||
readWordlistFromUrl = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
|
@ -29,8 +32,18 @@ class AddWordlistImpl(
|
|||
.bind()
|
||||
}
|
||||
|
||||
is AddWordlistRequest.Wordlist.FromUrl -> {
|
||||
val uri = model.wordlist.url
|
||||
readWordlistFromUrl(uri)
|
||||
.bind()
|
||||
}
|
||||
|
||||
is AddWordlistRequest.Wordlist.FromList -> model.wordlist.list
|
||||
}
|
||||
val invalidWordlist = wordlist.any { it.length > 512 }
|
||||
if (invalidWordlist) {
|
||||
throw IllegalStateException("Failed to parse the wordlist!")
|
||||
}
|
||||
generatorWordlistRepository
|
||||
.post(
|
||||
name = name,
|
||||
|
|
|
@ -5,7 +5,6 @@ import com.artemchep.keyguard.common.io.ioEffect
|
|||
import com.artemchep.keyguard.common.service.text.TextService
|
||||
import com.artemchep.keyguard.common.service.text.readFromFileAsText
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromFile
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
|
@ -20,15 +19,8 @@ class ReadWordlistFromFileImpl(
|
|||
uri: String,
|
||||
): IO<List<String>> = ioEffect {
|
||||
val content = textService.readFromFileAsText(uri)
|
||||
content
|
||||
.lineSequence()
|
||||
.filter {
|
||||
it.isNotBlank() &&
|
||||
!it.startsWith('#') &&
|
||||
!it.startsWith(';') &&
|
||||
!it.startsWith('-') &&
|
||||
!it.startsWith('/')
|
||||
}
|
||||
.toImmutableList()
|
||||
with(content) {
|
||||
ReadWordlistUtil.parseAsWordlist()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.artemchep.keyguard.common.usecase.impl
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.io.ioEffect
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromUrl
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class ReadWordlistFromUrlImpl(
|
||||
private val httpClient: HttpClient,
|
||||
) : ReadWordlistFromUrl {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
httpClient = directDI.instance("curl"),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
url: String,
|
||||
): IO<List<String>> = ioEffect {
|
||||
val request = httpClient
|
||||
.get(url)
|
||||
val content = request
|
||||
.bodyAsText()
|
||||
with(content) {
|
||||
ReadWordlistUtil.parseAsWordlist()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.artemchep.keyguard.common.usecase.impl
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
object ReadWordlistUtil {
|
||||
context(String)
|
||||
fun parseAsWordlist(): ImmutableList<String> = lineSequence()
|
||||
.filter {
|
||||
it.isNotBlank() &&
|
||||
!it.startsWith('#') &&
|
||||
!it.startsWith(';') &&
|
||||
!it.startsWith('-') &&
|
||||
!it.startsWith('/')
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -17,6 +18,7 @@ import androidx.compose.material.icons.outlined.Add
|
|||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
@ -28,8 +30,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -53,6 +57,9 @@ import com.artemchep.keyguard.res.Res
|
|||
import com.artemchep.keyguard.ui.AvatarBuilder
|
||||
import com.artemchep.keyguard.ui.DefaultFab
|
||||
import com.artemchep.keyguard.ui.DefaultSelection
|
||||
import com.artemchep.keyguard.ui.DropdownMenuItemFlat
|
||||
import com.artemchep.keyguard.ui.DropdownMinWidth
|
||||
import com.artemchep.keyguard.ui.DropdownScopeImpl
|
||||
import com.artemchep.keyguard.ui.FabState
|
||||
import com.artemchep.keyguard.ui.FlatItemLayout
|
||||
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||
|
@ -108,6 +115,9 @@ fun WordlistListScreen(
|
|||
listState.scrollToItem(0, 0)
|
||||
}
|
||||
|
||||
val primaryActionsDropdownVisibleState = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
ScaffoldLazyColumn(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
|
@ -147,8 +157,15 @@ fun WordlistListScreen(
|
|||
)
|
||||
},
|
||||
floatingActionState = run {
|
||||
val onClick =
|
||||
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.primaryAction
|
||||
val actions = loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.primaryActions.orEmpty()
|
||||
val onClick = if (actions.isNotEmpty()) {
|
||||
// lambda
|
||||
{
|
||||
primaryActionsDropdownVisibleState.value = true
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val state = FabState(
|
||||
onClick = onClick,
|
||||
model = null,
|
||||
|
@ -159,6 +176,31 @@ fun WordlistListScreen(
|
|||
DefaultFab(
|
||||
icon = {
|
||||
IconBox(main = Icons.Outlined.Add)
|
||||
|
||||
// Inject the dropdown popup to the bottom of the
|
||||
// content.
|
||||
val onDismissRequest = remember(primaryActionsDropdownVisibleState) {
|
||||
// lambda
|
||||
{
|
||||
primaryActionsDropdownVisibleState.value = false
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.widthIn(min = DropdownMinWidth),
|
||||
expanded = primaryActionsDropdownVisibleState.value,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
val actions = loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.primaryActions.orEmpty()
|
||||
val scope = DropdownScopeImpl(this, onDismissRequest = onDismissRequest)
|
||||
with(scope) {
|
||||
actions.forEachIndexed { index, action ->
|
||||
DropdownMenuItemFlat(
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
|
|
|
@ -21,7 +21,7 @@ data class WordlistListState(
|
|||
val revision: Int,
|
||||
val items: ImmutableList<Item>,
|
||||
val selection: Selection?,
|
||||
val primaryAction: (() -> Unit)?,
|
||||
val primaryActions: ImmutableList<ContextItem>,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package com.artemchep.keyguard.feature.generator.wordlist.list
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.AddLink
|
||||
import androidx.compose.material.icons.outlined.AttachFile
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.ViewList
|
||||
import androidx.compose.runtime.Composable
|
||||
import arrow.core.partially1
|
||||
import com.artemchep.keyguard.common.io.launchIn
|
||||
import com.artemchep.keyguard.common.model.AddWordlistRequest
|
||||
import com.artemchep.keyguard.common.model.DGeneratorWordlist
|
||||
import com.artemchep.keyguard.common.model.EditWordlistRequest
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.usecase.AddWordlist
|
||||
import com.artemchep.keyguard.common.usecase.EditWordlist
|
||||
|
@ -20,22 +17,18 @@ import com.artemchep.keyguard.common.usecase.RemoveWordlistById
|
|||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemStateRaw
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationResult
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationRoute
|
||||
import com.artemchep.keyguard.feature.confirmation.createConfirmationDialogIntent
|
||||
import com.artemchep.keyguard.feature.crashlytics.crashlyticsAttempt
|
||||
import com.artemchep.keyguard.feature.generator.wordlist.WordlistsRoute
|
||||
import com.artemchep.keyguard.feature.generator.wordlist.util.WordlistUtil
|
||||
import com.artemchep.keyguard.feature.generator.wordlist.view.WordlistViewRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.Selection
|
||||
import com.artemchep.keyguard.ui.buildContextItems
|
||||
import com.artemchep.keyguard.ui.icons.KeyguardWordlist
|
||||
import com.artemchep.keyguard.ui.icons.KeyguardWebsite
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
@ -250,6 +243,24 @@ fun produceWordlistListState(
|
|||
cause = e,
|
||||
)
|
||||
}
|
||||
val primaryActions = buildContextItems {
|
||||
section {
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.AttachFile),
|
||||
title = translate(Res.strings.wordlist_add_wordlist_via_file_title),
|
||||
onClick = WordlistUtil::onNewFromFile
|
||||
.partially1(this@produceScreenState)
|
||||
.partially1(addWordlist),
|
||||
)
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.KeyguardWebsite),
|
||||
title = translate(Res.strings.wordlist_add_wordlist_via_url_title),
|
||||
onClick = WordlistUtil::onNewFromUrl
|
||||
.partially1(this@produceScreenState)
|
||||
.partially1(addWordlist),
|
||||
)
|
||||
}
|
||||
}
|
||||
val contentFlow = combine(
|
||||
selectionFlow,
|
||||
itemsFlow,
|
||||
|
@ -260,9 +271,7 @@ fun produceWordlistListState(
|
|||
revision = 0,
|
||||
items = items,
|
||||
selection = selection,
|
||||
primaryAction = WordlistUtil::onNew
|
||||
.partially1(this@produceScreenState)
|
||||
.partially1(addWordlist),
|
||||
primaryActions = primaryActions,
|
||||
)
|
||||
}
|
||||
Loadable.Ok(contentOrException)
|
||||
|
|
|
@ -69,7 +69,7 @@ object WordlistUtil {
|
|||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun onNew(
|
||||
fun onNewFromFile(
|
||||
addWordlist: AddWordlist,
|
||||
) {
|
||||
val nameKey = "name"
|
||||
|
@ -126,6 +126,66 @@ object WordlistUtil {
|
|||
navigate(intent)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun onNewFromUrl(
|
||||
addWordlist: AddWordlist,
|
||||
) {
|
||||
val nameKey = "name"
|
||||
val nameItem = ConfirmationRoute.Args.Item.StringItem(
|
||||
key = nameKey,
|
||||
value = "",
|
||||
title = translate(Res.strings.generic_name),
|
||||
type = ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
||||
canBeEmpty = false,
|
||||
)
|
||||
|
||||
val urlKey = "url"
|
||||
val urlItem = ConfirmationRoute.Args.Item.StringItem(
|
||||
key = urlKey,
|
||||
value = "",
|
||||
title = translate(Res.strings.url),
|
||||
type = ConfirmationRoute.Args.Item.StringItem.Type.Command,
|
||||
canBeEmpty = false,
|
||||
)
|
||||
|
||||
val items = listOfNotNull(
|
||||
nameItem,
|
||||
urlItem,
|
||||
)
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(
|
||||
main = Icons.Outlined.KeyguardWordlist,
|
||||
secondary = Icons.Outlined.Add,
|
||||
),
|
||||
title = translate(Res.strings.wordlist_add_wordlist_title),
|
||||
items = items,
|
||||
docUrl = null,
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val name = result.data[nameKey] as? String
|
||||
?: return@registerRouteResultReceiver
|
||||
val url = result.data[urlKey] as? String
|
||||
?: return@registerRouteResultReceiver
|
||||
|
||||
val wordlist = AddWordlistRequest.Wordlist.FromUrl(
|
||||
url = url,
|
||||
)
|
||||
val request = AddWordlistRequest(
|
||||
name = name,
|
||||
wordlist = wordlist,
|
||||
)
|
||||
addWordlist(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun onDeleteByItems(
|
||||
removeWordlistById: RemoveWordlistById,
|
||||
|
|
|
@ -520,6 +520,8 @@
|
|||
<string name="wordlist_delete_many_confirmation_title">Delete the wordlists?</string>
|
||||
<string name="wordlist_edit_wordlist_title">Edit a wordlist</string>
|
||||
<string name="wordlist_add_wordlist_title">Add a wordlist</string>
|
||||
<string name="wordlist_add_wordlist_via_file_title">Load from a file</string>
|
||||
<string name="wordlist_add_wordlist_via_url_title">Load from a URL</string>
|
||||
<string name="wordlist_empty_label">No wordlists</string>
|
||||
<string name="wordlist_word_search_placeholder">Search words</string>
|
||||
|
||||
|
|
|
@ -187,6 +187,7 @@ import com.artemchep.keyguard.common.usecase.PutVaultSession
|
|||
import com.artemchep.keyguard.common.usecase.PutWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.PutWriteAccess
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromFile
|
||||
import com.artemchep.keyguard.common.usecase.ReadWordlistFromUrl
|
||||
import com.artemchep.keyguard.common.usecase.RemoveAttachment
|
||||
import com.artemchep.keyguard.common.usecase.RequestAppReview
|
||||
import com.artemchep.keyguard.common.usecase.ShowMessage
|
||||
|
@ -303,6 +304,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutVaultSessionImpl
|
|||
import com.artemchep.keyguard.common.usecase.impl.PutWebsiteIconsImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.PutWriteAccessImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.ReadWordlistFromFileImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.ReadWordlistFromUrlImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.RemoveAttachmentImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.RequestAppReviewImpl
|
||||
import com.artemchep.keyguard.common.usecase.impl.UnlockUseCaseImpl
|
||||
|
@ -846,6 +848,11 @@ fun globalModuleJvm() = DI.Module(
|
|||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<ReadWordlistFromUrl> {
|
||||
ReadWordlistFromUrlImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<PutWriteAccess> {
|
||||
PutWriteAccessImpl(
|
||||
directDI = this,
|
||||
|
|
Loading…
Reference in New Issue