feat: URL override and Placeholders
This commit is contained in:
parent
e8b4e7247c
commit
a412040d7c
|
@ -19,6 +19,7 @@ _Can be used with any Bitwarden® installation. This product is not associated w
|
||||||
- **multi-account support** with secure login and two-factor authentication support;
|
- **multi-account support** with secure login and two-factor authentication support;
|
||||||
- add items, modify, and view your vault **offline**.
|
- add items, modify, and view your vault **offline**.
|
||||||
- beautiful **Light**/**Dark theme**;
|
- beautiful **Light**/**Dark theme**;
|
||||||
|
- a support for [placeholders](wiki/PLACEHOLDERS.md) and [URL overrides](wiki/URL_OVERRIDE.md);
|
||||||
- and much more!
|
- and much more!
|
||||||
|
|
||||||
#### Looks:
|
#### Looks:
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
package com.artemchep.keyguard.copy
|
|
||||||
|
|
||||||
import com.artemchep.keyguard.common.io.IO
|
|
||||||
import com.artemchep.keyguard.common.io.io
|
|
||||||
import com.artemchep.keyguard.common.model.DSecret
|
|
||||||
import com.artemchep.keyguard.common.model.LinkInfoExecute
|
|
||||||
import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
class LinkInfoExtractorExecute(
|
|
||||||
) : LinkInfoExtractor<DSecret.Uri, LinkInfoExecute> {
|
|
||||||
override val from: KClass<DSecret.Uri> get() = DSecret.Uri::class
|
|
||||||
|
|
||||||
override val to: KClass<LinkInfoExecute> get() = LinkInfoExecute::class
|
|
||||||
|
|
||||||
override fun extractInfo(
|
|
||||||
uri: DSecret.Uri,
|
|
||||||
): IO<LinkInfoExecute> = io(LinkInfoExecute.Deny)
|
|
||||||
|
|
||||||
override fun handles(uri: DSecret.Uri): Boolean = true
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package com.artemchep.keyguard.core.session
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
|
||||||
import com.artemchep.keyguard.android.downloader.DownloadClientAndroid
|
import com.artemchep.keyguard.android.downloader.DownloadClientAndroid
|
||||||
import com.artemchep.keyguard.android.downloader.DownloadManagerImpl
|
import com.artemchep.keyguard.android.downloader.DownloadManagerImpl
|
||||||
import com.artemchep.keyguard.android.downloader.journal.DownloadRepository
|
import com.artemchep.keyguard.android.downloader.journal.DownloadRepository
|
||||||
|
@ -44,7 +43,7 @@ import com.artemchep.keyguard.copy.ClipboardServiceAndroid
|
||||||
import com.artemchep.keyguard.copy.ConnectivityServiceAndroid
|
import com.artemchep.keyguard.copy.ConnectivityServiceAndroid
|
||||||
import com.artemchep.keyguard.copy.GetBarcodeImageJvm
|
import com.artemchep.keyguard.copy.GetBarcodeImageJvm
|
||||||
import com.artemchep.keyguard.copy.LinkInfoExtractorAndroid
|
import com.artemchep.keyguard.copy.LinkInfoExtractorAndroid
|
||||||
import com.artemchep.keyguard.copy.LinkInfoExtractorExecute
|
import com.artemchep.keyguard.common.service.extract.impl.LinkInfoExtractorExecute
|
||||||
import com.artemchep.keyguard.copy.LinkInfoExtractorLaunch
|
import com.artemchep.keyguard.copy.LinkInfoExtractorLaunch
|
||||||
import com.artemchep.keyguard.copy.LogRepositoryAndroid
|
import com.artemchep.keyguard.copy.LogRepositoryAndroid
|
||||||
import com.artemchep.keyguard.copy.PermissionServiceAndroid
|
import com.artemchep.keyguard.copy.PermissionServiceAndroid
|
||||||
|
@ -59,24 +58,8 @@ import com.artemchep.keyguard.core.session.usecase.GetLocaleAndroid
|
||||||
import com.artemchep.keyguard.core.session.usecase.PutLocaleAndroid
|
import com.artemchep.keyguard.core.session.usecase.PutLocaleAndroid
|
||||||
import com.artemchep.keyguard.di.globalModuleJvm
|
import com.artemchep.keyguard.di.globalModuleJvm
|
||||||
import com.artemchep.keyguard.platform.LeContext
|
import com.artemchep.keyguard.platform.LeContext
|
||||||
import com.artemchep.keyguard.platform.util.isRelease
|
|
||||||
import db_key_value.crypto_prefs.SecurePrefKeyValueStore
|
import db_key_value.crypto_prefs.SecurePrefKeyValueStore
|
||||||
import db_key_value.shared_prefs.SharedPrefsKeyValueStore
|
import db_key_value.shared_prefs.SharedPrefsKeyValueStore
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
|
||||||
import io.ktor.client.plugins.HttpRequestRetry
|
|
||||||
import io.ktor.client.plugins.UserAgent
|
|
||||||
import io.ktor.client.plugins.cache.HttpCache
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
|
||||||
import io.ktor.client.plugins.logging.LogLevel
|
|
||||||
import io.ktor.client.plugins.logging.Logging
|
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.serialization.kotlinx.KotlinxSerializationConverter
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.bind
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.bindProvider
|
import org.kodein.di.bindProvider
|
||||||
|
@ -179,9 +162,6 @@ fun diFingerprintRepositoryModule() = DI.Module(
|
||||||
packageManager = instance(),
|
packageManager = instance(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bindSingleton<LinkInfoExtractorExecute> {
|
|
||||||
LinkInfoExtractorExecute()
|
|
||||||
}
|
|
||||||
bindSingleton<TextService> {
|
bindSingleton<TextService> {
|
||||||
TextServiceAndroid(
|
TextServiceAndroid(
|
||||||
directDI = this,
|
directDI = this,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.artemchep.keyguard.common.model
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.ui.icons.generateAccentColors
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class DGlobalUrlOverride(
|
||||||
|
val id: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val regex: Regex,
|
||||||
|
val command: String,
|
||||||
|
val createdDate: Instant,
|
||||||
|
) : Comparable<DGlobalUrlOverride> {
|
||||||
|
val accentColor = run {
|
||||||
|
val colors = generateAccentColors(name)
|
||||||
|
colors
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: DGlobalUrlOverride): Int {
|
||||||
|
return name.compareTo(other.name, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.artemchep.keyguard.common.service.execute
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
|
||||||
|
interface ExecuteCommand : (String) -> IO<Unit> {
|
||||||
|
val interpreter: String?
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.artemchep.keyguard.common.service.execute.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.bind
|
||||||
|
import com.artemchep.keyguard.common.io.ioEffect
|
||||||
|
import com.artemchep.keyguard.common.service.execute.ExecuteCommand
|
||||||
|
import com.artemchep.keyguard.platform.CurrentPlatform
|
||||||
|
import com.artemchep.keyguard.platform.Platform
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
|
||||||
|
class ExecuteCommandImpl(
|
||||||
|
) : ExecuteCommand {
|
||||||
|
private val executor: ExecuteCommand? = when (CurrentPlatform) {
|
||||||
|
is Platform.Desktop.Windows -> ExecuteCommandCmd()
|
||||||
|
|
||||||
|
is Platform.Desktop.MacOS,
|
||||||
|
is Platform.Desktop.Linux,
|
||||||
|
-> ExecuteCommandBash()
|
||||||
|
|
||||||
|
is Platform.Mobile.Android -> ExecuteCommandSh()
|
||||||
|
|
||||||
|
// Not supported.
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
override val interpreter: String? get() = executor?.interpreter
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
directDI: DirectDI,
|
||||||
|
) : this(
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invoke(command: String): IO<Unit> = ioEffect {
|
||||||
|
requireNotNull(executor) {
|
||||||
|
"Unsupported platform."
|
||||||
|
}
|
||||||
|
.invoke(command)
|
||||||
|
.bind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExecuteCommandCmd : ExecuteCommand {
|
||||||
|
override val interpreter: String get() = "cmd"
|
||||||
|
|
||||||
|
override fun invoke(command: String): IO<Unit> = ioEffect {
|
||||||
|
val arr = arrayOf(
|
||||||
|
"cmd",
|
||||||
|
"/c",
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
Runtime.getRuntime().exec(arr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExecuteCommandBash : ExecuteCommand {
|
||||||
|
override val interpreter: String get() = "bash"
|
||||||
|
|
||||||
|
override fun invoke(command: String): IO<Unit> = ioEffect {
|
||||||
|
val arr = arrayOf(
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
Runtime.getRuntime().exec(arr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExecuteCommandSh : ExecuteCommand {
|
||||||
|
override val interpreter: String get() = "sh"
|
||||||
|
|
||||||
|
override fun invoke(command: String): IO<Unit> = ioEffect {
|
||||||
|
val arr = arrayOf(
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
Runtime.getRuntime().exec(arr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.artemchep.keyguard.copy
|
package com.artemchep.keyguard.common.service.extract.impl
|
||||||
|
|
||||||
import com.artemchep.keyguard.common.io.IO
|
import com.artemchep.keyguard.common.io.IO
|
||||||
import com.artemchep.keyguard.common.io.ioEffect
|
import com.artemchep.keyguard.common.io.ioEffect
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
|
||||||
|
// See:
|
||||||
|
// https://keepass.info/help/base/placeholders.html
|
||||||
|
interface Placeholder {
|
||||||
|
operator fun get(key: String): IO<String?>?
|
||||||
|
|
||||||
|
interface Factory {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.bind
|
||||||
|
import com.artemchep.keyguard.common.util.simpleFormat2
|
||||||
|
|
||||||
|
suspend fun String.placeholderFormat(
|
||||||
|
placeholders: List<Placeholder>,
|
||||||
|
) = this
|
||||||
|
.simpleFormat2(
|
||||||
|
getter = { key ->
|
||||||
|
val p = placeholders
|
||||||
|
.firstNotNullOfOrNull { p -> p[key] }
|
||||||
|
when (p) {
|
||||||
|
// If no substitute value was found then we keep the
|
||||||
|
// placeholder intact.
|
||||||
|
null -> null
|
||||||
|
else -> p.bind().orEmpty()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.model.DSecret
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class PlaceholderScope(
|
||||||
|
val cipher: DSecret,
|
||||||
|
val now: Instant,
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.io
|
||||||
|
import com.artemchep.keyguard.common.io.map
|
||||||
|
import com.artemchep.keyguard.common.io.toIO
|
||||||
|
import com.artemchep.keyguard.common.model.DSecret
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
||||||
|
|
||||||
|
class CipherPlaceholder(
|
||||||
|
// private val getTotpCode: GetTotpCode,
|
||||||
|
private val cipher: DSecret,
|
||||||
|
) : Placeholder {
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? = when {
|
||||||
|
key.equals("uuid", ignoreCase = true) ->
|
||||||
|
cipher.service.remote?.id.let(::io)
|
||||||
|
key.equals("title", ignoreCase = true) ->
|
||||||
|
cipher.name.let(::io)
|
||||||
|
key.equals("username", ignoreCase = true) ->
|
||||||
|
cipher.login?.username.let(::io)
|
||||||
|
key.equals("password", ignoreCase = true) ->
|
||||||
|
cipher.login?.password.let(::io)
|
||||||
|
// key.equals("otp", ignoreCase = true) -> run {
|
||||||
|
// val token = cipher.login?.totp?.token
|
||||||
|
// ?: return@run null.let(::io)
|
||||||
|
// getTotpCode(token)
|
||||||
|
// .toIO()
|
||||||
|
// .map { code -> code.code }
|
||||||
|
// }
|
||||||
|
key.equals("notes", ignoreCase = true) ->
|
||||||
|
cipher.notes.let(::io)
|
||||||
|
// extras
|
||||||
|
key.equals("favorite", ignoreCase = true) ->
|
||||||
|
cipher.favorite.toString().let(::io)
|
||||||
|
// unknown
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.io
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
|
||||||
|
class CommentPlaceholder(
|
||||||
|
) : Placeholder {
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? = when {
|
||||||
|
// Comment; is removed.
|
||||||
|
key.startsWith("c:") ->
|
||||||
|
null.let(::io)
|
||||||
|
// unknown
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.ioEffect
|
||||||
|
import com.artemchep.keyguard.common.model.DSecret
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
|
||||||
|
class CustomPlaceholder(
|
||||||
|
private val cipher: DSecret,
|
||||||
|
) : Placeholder {
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? = when {
|
||||||
|
// Custom strings can be referenced using {S:Name}.
|
||||||
|
// For example, if you have a custom string named "eMail",
|
||||||
|
// you can use the placeholder {S:email}.
|
||||||
|
key.startsWith("s:", ignoreCase = true) -> ioEffect {
|
||||||
|
val name = key.substringAfter(':')
|
||||||
|
val field = cipher.fields
|
||||||
|
.firstOrNull { field ->
|
||||||
|
field.name
|
||||||
|
.equals(name, ignoreCase = true)
|
||||||
|
}
|
||||||
|
field?.value
|
||||||
|
}
|
||||||
|
// unknown
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import arrow.core.None
|
||||||
|
import arrow.core.Option
|
||||||
|
import arrow.core.some
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.io
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toJavaLocalDateTime
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class DateTimePlaceholder(
|
||||||
|
) : Placeholder {
|
||||||
|
private val now = Clock.System.now()
|
||||||
|
|
||||||
|
private val localDateTime by lazy {
|
||||||
|
val tz = TimeZone.currentSystemDefault()
|
||||||
|
now.toLocalDateTime(tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val utcDateTime by lazy {
|
||||||
|
val tz = TimeZone.UTC
|
||||||
|
now.toLocalDateTime(tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...for 2012-07-25 17:05:34 the value is 20120725170534
|
||||||
|
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
|
||||||
|
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? = when {
|
||||||
|
//
|
||||||
|
// Local Date Time
|
||||||
|
//
|
||||||
|
|
||||||
|
// Current local date/time as a simple, sortable string.
|
||||||
|
key.equals("dt_simple", ignoreCase = true) -> {
|
||||||
|
localDateTime.toJavaLocalDateTime()
|
||||||
|
.format(dateTimeFormatter)
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
// Year component of the current local date/time.
|
||||||
|
key.equals("dt_year", ignoreCase = true) -> {
|
||||||
|
localDateTime.year.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Month component of the current local date/time.
|
||||||
|
key.equals("dt_month", ignoreCase = true) -> {
|
||||||
|
localDateTime.month.value.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Day component of the current local date/time.
|
||||||
|
key.equals("dt_day", ignoreCase = true) -> {
|
||||||
|
localDateTime.dayOfMonth.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Hour component of the current local date/time.
|
||||||
|
key.equals("dt_hour", ignoreCase = true) -> {
|
||||||
|
localDateTime.hour.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Minute component of the current local date/time.
|
||||||
|
key.equals("dt_minute", ignoreCase = true) -> {
|
||||||
|
localDateTime.minute.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Second component of the current local date/time.
|
||||||
|
key.equals("dt_second", ignoreCase = true) -> {
|
||||||
|
localDateTime.second.toString().let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// UTC Date Time
|
||||||
|
//
|
||||||
|
|
||||||
|
// Current UTC date/time as a simple, sortable string.
|
||||||
|
key.equals("dt_utc_simple", ignoreCase = true) -> {
|
||||||
|
utcDateTime.toJavaLocalDateTime()
|
||||||
|
.format(dateTimeFormatter)
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
// Year component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_year", ignoreCase = true) -> {
|
||||||
|
utcDateTime.year.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Month component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_month", ignoreCase = true) -> {
|
||||||
|
utcDateTime.month.value.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Day component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_day", ignoreCase = true) -> {
|
||||||
|
utcDateTime.dayOfMonth.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Hour component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_hour", ignoreCase = true) -> {
|
||||||
|
utcDateTime.hour.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Minute component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_minute", ignoreCase = true) -> {
|
||||||
|
utcDateTime.minute.toString().let(::io)
|
||||||
|
}
|
||||||
|
// Second component of the current UTC date/time.
|
||||||
|
key.equals("dt_utc_second", ignoreCase = true) -> {
|
||||||
|
utcDateTime.second.toString().let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.io
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.util.Parser
|
||||||
|
import io.ktor.util.*
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class TextTransformPlaceholder(
|
||||||
|
) : Placeholder {
|
||||||
|
private val parser = Parser(
|
||||||
|
name = "t-conv",
|
||||||
|
count = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? {
|
||||||
|
val params = parser.parse(key)
|
||||||
|
?: return null
|
||||||
|
val command = params.params.firstOrNull()
|
||||||
|
val value = when {
|
||||||
|
// Lower-case.
|
||||||
|
command.equals("l", ignoreCase = true) ||
|
||||||
|
command.equals("lower", ignoreCase = true)
|
||||||
|
-> transformLowercase(params.value)
|
||||||
|
|
||||||
|
// Upper-case.
|
||||||
|
command.equals("u", ignoreCase = true) ||
|
||||||
|
command.equals("upper", ignoreCase = true)
|
||||||
|
-> transformUppercase(params.value)
|
||||||
|
|
||||||
|
// The Base64 encoding of the UTF-8 representation of the text.
|
||||||
|
command.equals("base64", ignoreCase = true) -> transformBase64(params.value)
|
||||||
|
|
||||||
|
// The Hex encoding of the UTF-8 representation of the text.
|
||||||
|
command.equals("hex", ignoreCase = true) -> transformHex(params.value)
|
||||||
|
|
||||||
|
// The URI-escaped representation of the text.
|
||||||
|
command.equals("uri", ignoreCase = true) -> transformUriEncode(params.value)
|
||||||
|
|
||||||
|
// The URI-unescaped representation of the text.
|
||||||
|
command.equals("uri-dec", ignoreCase = true) -> transformUriDecode(params.value)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return value.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformLowercase(
|
||||||
|
value: String,
|
||||||
|
): String = value.lowercase(Locale.ENGLISH)
|
||||||
|
|
||||||
|
private fun transformUppercase(
|
||||||
|
value: String,
|
||||||
|
): String = value.uppercase(Locale.ENGLISH)
|
||||||
|
|
||||||
|
private fun transformBase64(
|
||||||
|
value: String,
|
||||||
|
): String {
|
||||||
|
val bytes = value.toByteArray()
|
||||||
|
return java.util.Base64.getUrlEncoder()
|
||||||
|
.withoutPadding()
|
||||||
|
.encodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformHex(
|
||||||
|
value: String,
|
||||||
|
): String {
|
||||||
|
val bytes = value.toByteArray()
|
||||||
|
return hex(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformUriEncode(
|
||||||
|
value: String,
|
||||||
|
): String {
|
||||||
|
return URLEncoder.encode(value, "UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformUriDecode(
|
||||||
|
value: String,
|
||||||
|
): String {
|
||||||
|
return URLDecoder.decode(value, "UTF-8")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.io
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.Placeholder
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
class UrlPlaceholder(
|
||||||
|
private val url: String,
|
||||||
|
) : Placeholder {
|
||||||
|
private val uuu = Url(url)
|
||||||
|
|
||||||
|
override fun get(
|
||||||
|
key: String,
|
||||||
|
): IO<String?>? = when {
|
||||||
|
key.equals("url", ignoreCase = true) ||
|
||||||
|
key.equals("base", ignoreCase = true) ->
|
||||||
|
url.let(::io)
|
||||||
|
|
||||||
|
key.equals("url:rmvscm", ignoreCase = true) ||
|
||||||
|
key.equals("base:rmvscm", ignoreCase = true) -> {
|
||||||
|
// Cut out the scheme from the provided URL.
|
||||||
|
val regex = "^.*://".toRegex()
|
||||||
|
url.replace(regex, "").let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:scm", ignoreCase = true) ||
|
||||||
|
key.equals("base:scm", ignoreCase = true)-> {
|
||||||
|
uuu.protocol.name
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:host", ignoreCase = true) ||
|
||||||
|
key.equals("base:host", ignoreCase = true) -> {
|
||||||
|
uuu.host
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:port", ignoreCase = true) ||
|
||||||
|
key.equals("base:port", ignoreCase = true) -> {
|
||||||
|
uuu.port
|
||||||
|
.toString()
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:path", ignoreCase = true) ||
|
||||||
|
key.equals("base:path", ignoreCase = true) -> {
|
||||||
|
uuu.encodedPath
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:query", ignoreCase = true) ||
|
||||||
|
key.equals("base:query", ignoreCase = true) -> {
|
||||||
|
uuu.encodedQuery
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:userinfo", ignoreCase = true) ||
|
||||||
|
key.equals("base:userinfo", ignoreCase = true) -> {
|
||||||
|
"todo"
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:username", ignoreCase = true) ||
|
||||||
|
key.equals("base:username", ignoreCase = true) -> {
|
||||||
|
uuu.user
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.equals("url:password", ignoreCase = true) ||
|
||||||
|
key.equals("base:password", ignoreCase = true) -> {
|
||||||
|
uuu.password
|
||||||
|
.let(::io)
|
||||||
|
}
|
||||||
|
// unknown
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.artemchep.keyguard.common.service.placeholder.util
|
||||||
|
|
||||||
|
class Parser(
|
||||||
|
private val name: String,
|
||||||
|
private val count: Int,
|
||||||
|
) {
|
||||||
|
private val prefix = "$name:"
|
||||||
|
|
||||||
|
fun parse(key: String): ParserResult? {
|
||||||
|
val handles = key.startsWith(prefix, ignoreCase = true)
|
||||||
|
if (!handles) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val separator = key.getOrNull(prefix.length)
|
||||||
|
?: return null // a separator must be defined!
|
||||||
|
val suffix = key.substring(prefix.length + 1)
|
||||||
|
val suffixA = suffix
|
||||||
|
.split(separator)
|
||||||
|
if (suffixA.size < count) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val value = suffixA
|
||||||
|
.dropLast(count)
|
||||||
|
.joinToString(separator = "")
|
||||||
|
return ParserResult(
|
||||||
|
value = value,
|
||||||
|
params = suffixA
|
||||||
|
.takeLast(count)
|
||||||
|
.dropLast(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ParserResult(
|
||||||
|
val value: String,
|
||||||
|
val params: List<String>,
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.artemchep.keyguard.common.service.urloverride
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import com.artemchep.keyguard.provider.bitwarden.repository.BaseRepository
|
||||||
|
|
||||||
|
interface UrlOverrideRepository : BaseRepository<DGlobalUrlOverride> {
|
||||||
|
fun removeAll(): IO<Unit>
|
||||||
|
|
||||||
|
fun removeByIds(
|
||||||
|
ids: Set<String>,
|
||||||
|
): IO<Unit>
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.artemchep.keyguard.common.service.urloverride
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.effectMap
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import com.artemchep.keyguard.common.util.sqldelight.flatMapQueryToList
|
||||||
|
import com.artemchep.keyguard.core.store.DatabaseDispatcher
|
||||||
|
import com.artemchep.keyguard.core.store.DatabaseManager
|
||||||
|
import com.artemchep.keyguard.data.UrlOverrideQueries
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
class UrlOverrideRepositoryImpl(
|
||||||
|
private val databaseManager: DatabaseManager,
|
||||||
|
private val dispatcher: CoroutineDispatcher,
|
||||||
|
) : UrlOverrideRepository {
|
||||||
|
constructor(
|
||||||
|
directDI: DirectDI,
|
||||||
|
) : this(
|
||||||
|
databaseManager = directDI.instance(),
|
||||||
|
dispatcher = directDI.instance(tag = DatabaseDispatcher),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun get(): Flow<List<DGlobalUrlOverride>> =
|
||||||
|
daoEffect { dao ->
|
||||||
|
dao.get(1000)
|
||||||
|
}
|
||||||
|
.flatMapQueryToList(dispatcher)
|
||||||
|
.map { entities ->
|
||||||
|
entities
|
||||||
|
.map { entity ->
|
||||||
|
val regex = entity.regex.toRegex()
|
||||||
|
DGlobalUrlOverride(
|
||||||
|
id = entity.id.toString(),
|
||||||
|
name = entity.name,
|
||||||
|
regex = regex,
|
||||||
|
command = entity.command,
|
||||||
|
createdDate = entity.createdAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun put(model: DGlobalUrlOverride): IO<Unit> =
|
||||||
|
daoEffect { dao ->
|
||||||
|
val id = model.id
|
||||||
|
?.toLongOrNull()
|
||||||
|
val regex = model.regex.toString()
|
||||||
|
if (id != null) {
|
||||||
|
dao.update(
|
||||||
|
id = id,
|
||||||
|
name = model.name,
|
||||||
|
regex = regex,
|
||||||
|
command = model.command,
|
||||||
|
createdAt = model.createdDate,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
dao.insert(
|
||||||
|
name = model.name,
|
||||||
|
regex = regex,
|
||||||
|
command = model.command,
|
||||||
|
createdAt = model.createdDate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAll(): IO<Unit> =
|
||||||
|
daoEffect { dao ->
|
||||||
|
dao.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeByIds(ids: Set<String>): IO<Unit> =
|
||||||
|
daoEffect { dao ->
|
||||||
|
dao.transaction {
|
||||||
|
ids.forEach {
|
||||||
|
val id = it.toLongOrNull()
|
||||||
|
?: return@forEach
|
||||||
|
dao.deleteByIds(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> daoEffect(
|
||||||
|
crossinline block: suspend (UrlOverrideQueries) -> T,
|
||||||
|
): IO<T> = databaseManager
|
||||||
|
.get()
|
||||||
|
.effectMap(dispatcher) { db ->
|
||||||
|
val dao = db.urlOverrideQueries
|
||||||
|
block(dao)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.artemchep.keyguard.common.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
|
||||||
|
interface AddUrlOverride : (DGlobalUrlOverride) -> IO<Unit>
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.artemchep.keyguard.common.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.model.DSecret
|
||||||
|
|
||||||
|
interface CipherPlaceholder : (DSecret.Uri, String) -> IO<Boolean>
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.artemchep.keyguard.common.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a list of all available
|
||||||
|
* global url overrides.
|
||||||
|
*/
|
||||||
|
interface GetUrlOverrides : () -> Flow<List<DGlobalUrlOverride>>
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.artemchep.keyguard.common.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
|
||||||
|
interface RemoveUrlOverrideById : (
|
||||||
|
Set<String>,
|
||||||
|
) -> IO<Unit>
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.artemchep.keyguard.common.usecase.impl
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.model.DGeneratorEmailRelay
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import com.artemchep.keyguard.common.service.relays.repo.GeneratorEmailRelayRepository
|
||||||
|
import com.artemchep.keyguard.common.service.urloverride.UrlOverrideRepository
|
||||||
|
import com.artemchep.keyguard.common.usecase.AddEmailRelay
|
||||||
|
import com.artemchep.keyguard.common.usecase.AddUrlOverride
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
class AddUrlOverrideImpl(
|
||||||
|
private val urlOverrideRepository: UrlOverrideRepository,
|
||||||
|
) : AddUrlOverride {
|
||||||
|
constructor(directDI: DirectDI) : this(
|
||||||
|
urlOverrideRepository = directDI.instance(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invoke(model: DGlobalUrlOverride) = urlOverrideRepository
|
||||||
|
.put(model)
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.artemchep.keyguard.common.util
|
||||||
|
|
||||||
|
private const val DIVIDER_START = '{'
|
||||||
|
private const val DIVIDER_END = '}'
|
||||||
|
|
||||||
|
private sealed interface Node {
|
||||||
|
suspend fun eval(): String
|
||||||
|
|
||||||
|
data class Placeholder(
|
||||||
|
val parent: Placeholder?,
|
||||||
|
var start: Int,
|
||||||
|
val dividerStart: Char,
|
||||||
|
val dividerEnd: Char,
|
||||||
|
val getter: suspend (key: String) -> String?,
|
||||||
|
val nodes: MutableList<Node> = mutableListOf(),
|
||||||
|
) : Node {
|
||||||
|
override suspend fun eval(): String {
|
||||||
|
val key = run {
|
||||||
|
// If the number of nodes is 1, then
|
||||||
|
// we can skip joining them.
|
||||||
|
if (nodes.size == 1) {
|
||||||
|
return@run nodes
|
||||||
|
.first()
|
||||||
|
.eval()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildString {
|
||||||
|
nodes.forEach { node ->
|
||||||
|
val s = node.eval()
|
||||||
|
append(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val value = getter(key)
|
||||||
|
return value
|
||||||
|
?: "$dividerStart$key$dividerEnd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Text(
|
||||||
|
val value: String,
|
||||||
|
) : Node {
|
||||||
|
override suspend fun eval(): String = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun String.simpleFormat2(
|
||||||
|
dividerStart: Char = DIVIDER_START,
|
||||||
|
dividerEnd: Char = DIVIDER_END,
|
||||||
|
getter: suspend (key: String) -> String?,
|
||||||
|
): String {
|
||||||
|
val root = Node.Placeholder(
|
||||||
|
parent = null,
|
||||||
|
start = 0,
|
||||||
|
dividerStart = dividerStart,
|
||||||
|
dividerEnd = dividerEnd,
|
||||||
|
getter = { it },
|
||||||
|
)
|
||||||
|
var node: Node.Placeholder = root
|
||||||
|
|
||||||
|
fun appendPrefixPlainTextToSelectedNode(i: Int) {
|
||||||
|
// Copy the text before the command start
|
||||||
|
// symbol. This text is the plain text node.
|
||||||
|
if (i > node.start) {
|
||||||
|
val plainText = substring(node.start, i)
|
||||||
|
node.nodes += Node.Text(
|
||||||
|
value = plainText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (true) {
|
||||||
|
val c = getOrNull(i)
|
||||||
|
when (c) {
|
||||||
|
null -> {
|
||||||
|
appendPrefixPlainTextToSelectedNode(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerStart -> {
|
||||||
|
appendPrefixPlainTextToSelectedNode(i)
|
||||||
|
val placeholderNode = Node.Placeholder(
|
||||||
|
parent = node,
|
||||||
|
start = i + 1, // Skip the command start symbol
|
||||||
|
dividerStart = dividerStart,
|
||||||
|
dividerEnd = dividerEnd,
|
||||||
|
getter = getter,
|
||||||
|
)
|
||||||
|
node.nodes += placeholderNode
|
||||||
|
node = placeholderNode
|
||||||
|
}
|
||||||
|
|
||||||
|
dividerEnd -> {
|
||||||
|
val parent = node.parent
|
||||||
|
if (parent != null) {
|
||||||
|
appendPrefixPlainTextToSelectedNode(i)
|
||||||
|
node = parent
|
||||||
|
node.start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if (root !== node) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
return root.eval()
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ import com.artemchep.keyguard.common.service.hibp.passwords.impl.PasswordPwnageD
|
||||||
import com.artemchep.keyguard.common.service.hibp.passwords.impl.PasswordPwnageRepositoryImpl
|
import com.artemchep.keyguard.common.service.hibp.passwords.impl.PasswordPwnageRepositoryImpl
|
||||||
import com.artemchep.keyguard.common.service.relays.repo.GeneratorEmailRelayRepository
|
import com.artemchep.keyguard.common.service.relays.repo.GeneratorEmailRelayRepository
|
||||||
import com.artemchep.keyguard.common.service.relays.repo.GeneratorEmailRelayRepositoryImpl
|
import com.artemchep.keyguard.common.service.relays.repo.GeneratorEmailRelayRepositoryImpl
|
||||||
|
import com.artemchep.keyguard.common.service.urloverride.UrlOverrideRepository
|
||||||
|
import com.artemchep.keyguard.common.service.urloverride.UrlOverrideRepositoryImpl
|
||||||
import com.artemchep.keyguard.common.usecase.AddCipher
|
import com.artemchep.keyguard.common.usecase.AddCipher
|
||||||
import com.artemchep.keyguard.common.usecase.AddCipherOpenedHistory
|
import com.artemchep.keyguard.common.usecase.AddCipherOpenedHistory
|
||||||
import com.artemchep.keyguard.common.usecase.AddCipherUsedAutofillHistory
|
import com.artemchep.keyguard.common.usecase.AddCipherUsedAutofillHistory
|
||||||
|
@ -28,6 +30,7 @@ import com.artemchep.keyguard.common.usecase.AddFolder
|
||||||
import com.artemchep.keyguard.common.usecase.AddGeneratorHistory
|
import com.artemchep.keyguard.common.usecase.AddGeneratorHistory
|
||||||
import com.artemchep.keyguard.common.usecase.AddPasskeyCipher
|
import com.artemchep.keyguard.common.usecase.AddPasskeyCipher
|
||||||
import com.artemchep.keyguard.common.usecase.AddUriCipher
|
import com.artemchep.keyguard.common.usecase.AddUriCipher
|
||||||
|
import com.artemchep.keyguard.common.usecase.AddUrlOverride
|
||||||
import com.artemchep.keyguard.common.usecase.ChangeCipherNameById
|
import com.artemchep.keyguard.common.usecase.ChangeCipherNameById
|
||||||
import com.artemchep.keyguard.common.usecase.ChangeCipherPasswordById
|
import com.artemchep.keyguard.common.usecase.ChangeCipherPasswordById
|
||||||
import com.artemchep.keyguard.common.usecase.CheckPasswordLeak
|
import com.artemchep.keyguard.common.usecase.CheckPasswordLeak
|
||||||
|
@ -69,6 +72,7 @@ import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||||
import com.artemchep.keyguard.common.usecase.GetSends
|
import com.artemchep.keyguard.common.usecase.GetSends
|
||||||
import com.artemchep.keyguard.common.usecase.GetShouldRequestAppReview
|
import com.artemchep.keyguard.common.usecase.GetShouldRequestAppReview
|
||||||
|
import com.artemchep.keyguard.common.usecase.GetUrlOverrides
|
||||||
import com.artemchep.keyguard.common.usecase.MergeFolderById
|
import com.artemchep.keyguard.common.usecase.MergeFolderById
|
||||||
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
||||||
import com.artemchep.keyguard.common.usecase.PutAccountColorById
|
import com.artemchep.keyguard.common.usecase.PutAccountColorById
|
||||||
|
@ -82,6 +86,7 @@ import com.artemchep.keyguard.common.usecase.RemoveEmailRelayById
|
||||||
import com.artemchep.keyguard.common.usecase.RemoveFolderById
|
import com.artemchep.keyguard.common.usecase.RemoveFolderById
|
||||||
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory
|
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory
|
||||||
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistoryById
|
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistoryById
|
||||||
|
import com.artemchep.keyguard.common.usecase.RemoveUrlOverrideById
|
||||||
import com.artemchep.keyguard.common.usecase.RenameFolderById
|
import com.artemchep.keyguard.common.usecase.RenameFolderById
|
||||||
import com.artemchep.keyguard.common.usecase.RestoreCipherById
|
import com.artemchep.keyguard.common.usecase.RestoreCipherById
|
||||||
import com.artemchep.keyguard.common.usecase.RetryCipher
|
import com.artemchep.keyguard.common.usecase.RetryCipher
|
||||||
|
@ -95,6 +100,7 @@ import com.artemchep.keyguard.common.usecase.Watchdog
|
||||||
import com.artemchep.keyguard.common.usecase.WatchdogImpl
|
import com.artemchep.keyguard.common.usecase.WatchdogImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.AddEmailRelayImpl
|
import com.artemchep.keyguard.common.usecase.impl.AddEmailRelayImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.AddGeneratorHistoryImpl
|
import com.artemchep.keyguard.common.usecase.impl.AddGeneratorHistoryImpl
|
||||||
|
import com.artemchep.keyguard.common.usecase.impl.AddUrlOverrideImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.DownloadAttachmentImpl2
|
import com.artemchep.keyguard.common.usecase.impl.DownloadAttachmentImpl2
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetAccountStatusImpl
|
import com.artemchep.keyguard.common.usecase.impl.GetAccountStatusImpl
|
||||||
import com.artemchep.keyguard.common.usecase.impl.GetCanAddAccountImpl
|
import com.artemchep.keyguard.common.usecase.impl.GetCanAddAccountImpl
|
||||||
|
@ -160,6 +166,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.GetMetasImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.GetOrganizationsImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.GetOrganizationsImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.GetProfilesImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.GetProfilesImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.GetSendsImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.GetSendsImpl
|
||||||
|
import com.artemchep.keyguard.provider.bitwarden.usecase.GetUrlOverridesImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.MergeFolderByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.MergeFolderByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.MoveCipherToFolderByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.MoveCipherToFolderByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountColorByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountColorByIdImpl
|
||||||
|
@ -171,6 +178,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveAccountsImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveCipherByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveCipherByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveEmailRelayByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveEmailRelayByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveFolderByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveFolderByIdImpl
|
||||||
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveUrlOverrideByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RenameFolderByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RenameFolderByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RestoreCipherByIdImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RestoreCipherByIdImpl
|
||||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RetryCipherImpl
|
import com.artemchep.keyguard.provider.bitwarden.usecase.RetryCipherImpl
|
||||||
|
@ -219,6 +227,15 @@ fun DI.Builder.createSubDi2(
|
||||||
bindSingleton<RemoveEmailRelayById> {
|
bindSingleton<RemoveEmailRelayById> {
|
||||||
RemoveEmailRelayByIdImpl(this)
|
RemoveEmailRelayByIdImpl(this)
|
||||||
}
|
}
|
||||||
|
bindSingleton<AddUrlOverride> {
|
||||||
|
AddUrlOverrideImpl(this)
|
||||||
|
}
|
||||||
|
bindSingleton<GetUrlOverrides> {
|
||||||
|
GetUrlOverridesImpl(this)
|
||||||
|
}
|
||||||
|
bindSingleton<RemoveUrlOverrideById> {
|
||||||
|
RemoveUrlOverrideByIdImpl(this)
|
||||||
|
}
|
||||||
bindSingleton<GetAccounts> {
|
bindSingleton<GetAccounts> {
|
||||||
GetAccountsImpl(this)
|
GetAccountsImpl(this)
|
||||||
}
|
}
|
||||||
|
@ -419,6 +436,9 @@ fun DI.Builder.createSubDi2(
|
||||||
bindSingleton<GeneratorEmailRelayRepository> {
|
bindSingleton<GeneratorEmailRelayRepository> {
|
||||||
GeneratorEmailRelayRepositoryImpl(this)
|
GeneratorEmailRelayRepositoryImpl(this)
|
||||||
}
|
}
|
||||||
|
bindSingleton<UrlOverrideRepository> {
|
||||||
|
UrlOverrideRepositoryImpl(this)
|
||||||
|
}
|
||||||
bindSingleton<PasswordPwnageDataSourceLocal> {
|
bindSingleton<PasswordPwnageDataSourceLocal> {
|
||||||
PasswordPwnageDataSourceLocalImpl(this)
|
PasswordPwnageDataSourceLocalImpl(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.artemchep.keyguard.data.CipherUsageHistory
|
||||||
import com.artemchep.keyguard.data.Database
|
import com.artemchep.keyguard.data.Database
|
||||||
import com.artemchep.keyguard.data.GeneratorEmailRelay
|
import com.artemchep.keyguard.data.GeneratorEmailRelay
|
||||||
import com.artemchep.keyguard.data.GeneratorHistory
|
import com.artemchep.keyguard.data.GeneratorHistory
|
||||||
|
import com.artemchep.keyguard.data.UrlOverride
|
||||||
import com.artemchep.keyguard.data.bitwarden.Account
|
import com.artemchep.keyguard.data.bitwarden.Account
|
||||||
import com.artemchep.keyguard.data.bitwarden.Cipher
|
import com.artemchep.keyguard.data.bitwarden.Cipher
|
||||||
import com.artemchep.keyguard.data.bitwarden.Collection
|
import com.artemchep.keyguard.data.bitwarden.Collection
|
||||||
|
@ -106,6 +107,7 @@ class DatabaseManagerImpl(
|
||||||
cipherUsageHistoryAdapter = CipherUsageHistory.Adapter(InstantToLongAdapter),
|
cipherUsageHistoryAdapter = CipherUsageHistory.Adapter(InstantToLongAdapter),
|
||||||
generatorHistoryAdapter = GeneratorHistory.Adapter(InstantToLongAdapter),
|
generatorHistoryAdapter = GeneratorHistory.Adapter(InstantToLongAdapter),
|
||||||
generatorEmailRelayAdapter = GeneratorEmailRelay.Adapter(InstantToLongAdapter),
|
generatorEmailRelayAdapter = GeneratorEmailRelay.Adapter(InstantToLongAdapter),
|
||||||
|
urlOverrideAdapter = UrlOverride.Adapter(InstantToLongAdapter),
|
||||||
cipherAdapter = Cipher.Adapter(bitwardenCipherToStringAdapter),
|
cipherAdapter = Cipher.Adapter(bitwardenCipherToStringAdapter),
|
||||||
sendAdapter = Send.Adapter(bitwardenSendToStringAdapter),
|
sendAdapter = Send.Adapter(bitwardenSendToStringAdapter),
|
||||||
collectionAdapter = Collection.Adapter(bitwardenCollectionToStringAdapter),
|
collectionAdapter = Collection.Adapter(bitwardenCollectionToStringAdapter),
|
||||||
|
|
|
@ -34,6 +34,7 @@ class ConfirmationRoute(
|
||||||
override val value: String = "",
|
override val value: String = "",
|
||||||
val title: String,
|
val title: String,
|
||||||
val hint: String? = null,
|
val hint: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
val type: Type = Type.Text,
|
val type: Type = Type.Text,
|
||||||
/**
|
/**
|
||||||
* `true` if the empty value is a valid
|
* `true` if the empty value is a valid
|
||||||
|
@ -44,6 +45,8 @@ class ConfirmationRoute(
|
||||||
enum class Type {
|
enum class Type {
|
||||||
Text,
|
Text,
|
||||||
Token,
|
Token,
|
||||||
|
Regex,
|
||||||
|
Command,
|
||||||
Password,
|
Password,
|
||||||
Username,
|
Username,
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,8 +222,11 @@ private fun ConfirmationStringItem(
|
||||||
isVisible = !item.sensitive,
|
isVisible = !item.sensitive,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
FlatTextField(
|
FlatTextField(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = Dimens.horizontalPadding),
|
.padding(horizontal = Dimens.horizontalPadding),
|
||||||
label = item.title,
|
label = item.title,
|
||||||
value = item.state,
|
value = item.state,
|
||||||
|
@ -294,6 +297,20 @@ private fun ConfirmationStringItem(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (item.description != null) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = Dimens.horizontalPadding,
|
||||||
|
vertical = 8.dp,
|
||||||
|
),
|
||||||
|
text = item.description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = LocalContentColor.current
|
||||||
|
.combineAlpha(MediumEmphasisAlpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
|
|
@ -29,6 +29,7 @@ data class ConfirmationState(
|
||||||
override val value: String,
|
override val value: String,
|
||||||
val state: TextFieldModel2,
|
val state: TextFieldModel2,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val monospace: Boolean,
|
val monospace: Boolean,
|
||||||
val password: Boolean,
|
val password: Boolean,
|
||||||
|
|
|
@ -78,7 +78,9 @@ fun confirmationState(
|
||||||
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Token
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Token
|
||||||
val monospace =
|
val monospace =
|
||||||
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Password ||
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Password ||
|
||||||
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Token
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Token ||
|
||||||
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Regex ||
|
||||||
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Command
|
||||||
val password =
|
val password =
|
||||||
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Password
|
item.type == ConfirmationRoute.Args.Item.StringItem.Type.Password
|
||||||
val generator = when (item.type) {
|
val generator = when (item.type) {
|
||||||
|
@ -86,6 +88,8 @@ fun confirmationState(
|
||||||
ConfirmationRoute.Args.Item.StringItem.Type.Password -> ConfirmationState.Item.StringItem.Generator.Password
|
ConfirmationRoute.Args.Item.StringItem.Type.Password -> ConfirmationState.Item.StringItem.Generator.Password
|
||||||
ConfirmationRoute.Args.Item.StringItem.Type.Token,
|
ConfirmationRoute.Args.Item.StringItem.Type.Token,
|
||||||
ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
||||||
|
ConfirmationRoute.Args.Item.StringItem.Type.Regex,
|
||||||
|
ConfirmationRoute.Args.Item.StringItem.Type.Command,
|
||||||
-> null
|
-> null
|
||||||
}
|
}
|
||||||
requireNotNull(state)
|
requireNotNull(state)
|
||||||
|
@ -99,6 +103,7 @@ fun confirmationState(
|
||||||
ConfirmationState.Item.StringItem(
|
ConfirmationState.Item.StringItem(
|
||||||
key = item.key,
|
key = item.key,
|
||||||
title = item.title,
|
title = item.title,
|
||||||
|
description = item.description,
|
||||||
sensitive = sensitive,
|
sensitive = sensitive,
|
||||||
monospace = monospace,
|
monospace = monospace,
|
||||||
password = password,
|
password = password,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.artemchep.keyguard.common.model.Loadable
|
import com.artemchep.keyguard.common.model.Loadable
|
||||||
import com.artemchep.keyguard.common.model.flatMap
|
import com.artemchep.keyguard.common.model.flatMap
|
||||||
|
import com.artemchep.keyguard.common.model.fold
|
||||||
import com.artemchep.keyguard.common.model.getOrNull
|
import com.artemchep.keyguard.common.model.getOrNull
|
||||||
import com.artemchep.keyguard.feature.EmptyView
|
import com.artemchep.keyguard.feature.EmptyView
|
||||||
import com.artemchep.keyguard.feature.ErrorView
|
import com.artemchep.keyguard.feature.ErrorView
|
||||||
|
@ -47,10 +48,12 @@ import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
||||||
import com.artemchep.keyguard.ui.FabState
|
import com.artemchep.keyguard.ui.FabState
|
||||||
import com.artemchep.keyguard.ui.FlatDropdown
|
import com.artemchep.keyguard.ui.FlatDropdown
|
||||||
import com.artemchep.keyguard.ui.FlatItemTextContent
|
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||||
|
import com.artemchep.keyguard.ui.OptionsButton
|
||||||
import com.artemchep.keyguard.ui.ScaffoldLazyColumn
|
import com.artemchep.keyguard.ui.ScaffoldLazyColumn
|
||||||
import com.artemchep.keyguard.ui.icons.IconBox
|
import com.artemchep.keyguard.ui.icons.IconBox
|
||||||
import com.artemchep.keyguard.ui.skeleton.SkeletonItem
|
import com.artemchep.keyguard.ui.skeleton.SkeletonItem
|
||||||
import com.artemchep.keyguard.ui.toolbar.CustomToolbar
|
import com.artemchep.keyguard.ui.toolbar.CustomToolbar
|
||||||
|
import com.artemchep.keyguard.ui.toolbar.LargeToolbar
|
||||||
import com.artemchep.keyguard.ui.toolbar.content.CustomToolbarContent
|
import com.artemchep.keyguard.ui.toolbar.content.CustomToolbarContent
|
||||||
import com.artemchep.keyguard.ui.util.DividerColor
|
import com.artemchep.keyguard.ui.util.DividerColor
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
@ -73,7 +76,7 @@ fun EmailRelayListScreen(
|
||||||
fun EmailRelayListScreen(
|
fun EmailRelayListScreen(
|
||||||
loadableState: Loadable<EmailRelayListState>,
|
loadableState: Loadable<EmailRelayListState>,
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
val listRevision =
|
val listRevision =
|
||||||
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.revision
|
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.revision
|
||||||
|
@ -115,18 +118,15 @@ fun EmailRelayListScreen(
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topAppBarScrollBehavior = scrollBehavior,
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
topBar = {
|
topBar = {
|
||||||
CustomToolbar(
|
LargeToolbar(
|
||||||
scrollBehavior = scrollBehavior,
|
title = {
|
||||||
) {
|
Text(stringResource(Res.strings.emailrelay_list_header_title))
|
||||||
Column {
|
},
|
||||||
CustomToolbarContent(
|
navigationIcon = {
|
||||||
title = stringResource(Res.strings.emailrelay_list_header_title),
|
|
||||||
icon = {
|
|
||||||
NavigationIcon()
|
NavigationIcon()
|
||||||
},
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
val selectionOrNull =
|
val selectionOrNull =
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.artemchep.keyguard.feature.generator.emailrelay
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.CopyAll
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material.icons.outlined.Email
|
import androidx.compose.material.icons.outlined.Email
|
||||||
|
@ -154,6 +155,16 @@ fun produceEmailRelayListState(
|
||||||
|
|
||||||
fun onNew(model: EmailRelay) = onEdit(model, null)
|
fun onNew(model: EmailRelay) = onEdit(model, null)
|
||||||
|
|
||||||
|
fun onDuplicate(entity: DGeneratorEmailRelay) {
|
||||||
|
val createdAt = Clock.System.now()
|
||||||
|
val model = entity.copy(
|
||||||
|
id = null,
|
||||||
|
createdDate = createdAt,
|
||||||
|
)
|
||||||
|
addEmailRelay(model)
|
||||||
|
.launchIn(appScope)
|
||||||
|
}
|
||||||
|
|
||||||
fun onDelete(
|
fun onDelete(
|
||||||
emailRelayIds: Set<String>,
|
emailRelayIds: Set<String>,
|
||||||
) {
|
) {
|
||||||
|
@ -258,6 +269,12 @@ fun produceEmailRelayListState(
|
||||||
.partially1(it),
|
.partially1(it),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.CopyAll,
|
||||||
|
title = translate(Res.strings.duplicate),
|
||||||
|
onClick = ::onDuplicate
|
||||||
|
.partially1(it),
|
||||||
|
)
|
||||||
this += FlatItemAction(
|
this += FlatItemAction(
|
||||||
icon = Icons.Outlined.Delete,
|
icon = Icons.Outlined.Delete,
|
||||||
title = translate(Res.strings.delete),
|
title = translate(Res.strings.delete),
|
||||||
|
|
|
@ -78,6 +78,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingSubscriptio
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingThemeUseAmoledDarkProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingThemeUseAmoledDarkProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingTwoPanelLayoutLandscapeProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingTwoPanelLayoutLandscapeProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingTwoPanelLayoutPortraitProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingTwoPanelLayoutPortraitProvider
|
||||||
|
import com.artemchep.keyguard.feature.home.settings.component.settingUrlOverrideProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingUseExternalBrowserProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingUseExternalBrowserProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingVaultLockAfterRebootProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingVaultLockAfterRebootProvider
|
||||||
import com.artemchep.keyguard.feature.home.settings.component.settingVaultLockAfterScreenOffProvider
|
import com.artemchep.keyguard.feature.home.settings.component.settingVaultLockAfterScreenOffProvider
|
||||||
|
@ -147,6 +148,7 @@ object Setting {
|
||||||
const val LAUNCH_YUBIKEY = "launch_yubikey"
|
const val LAUNCH_YUBIKEY = "launch_yubikey"
|
||||||
const val DATA_SAFETY = "data_safety"
|
const val DATA_SAFETY = "data_safety"
|
||||||
const val FEATURES_OVERVIEW = "features_overview"
|
const val FEATURES_OVERVIEW = "features_overview"
|
||||||
|
const val URL_OVERRIDE = "url_override"
|
||||||
const val RATE_APP = "rate_app"
|
const val RATE_APP = "rate_app"
|
||||||
const val CONCEAL = "conceal"
|
const val CONCEAL = "conceal"
|
||||||
const val MARKDOWN = "markdown"
|
const val MARKDOWN = "markdown"
|
||||||
|
@ -221,6 +223,7 @@ val hub = mapOf<String, (DirectDI) -> SettingComponent>(
|
||||||
Setting.LAUNCH_APP_PICKER to ::settingLaunchAppPicker,
|
Setting.LAUNCH_APP_PICKER to ::settingLaunchAppPicker,
|
||||||
Setting.DATA_SAFETY to ::settingDataSafetyProvider,
|
Setting.DATA_SAFETY to ::settingDataSafetyProvider,
|
||||||
Setting.FEATURES_OVERVIEW to ::settingFeaturesOverviewProvider,
|
Setting.FEATURES_OVERVIEW to ::settingFeaturesOverviewProvider,
|
||||||
|
Setting.URL_OVERRIDE to ::settingUrlOverrideProvider,
|
||||||
Setting.RATE_APP to ::settingRateAppProvider,
|
Setting.RATE_APP to ::settingRateAppProvider,
|
||||||
Setting.DIVIDER to ::settingSectionProvider,
|
Setting.DIVIDER to ::settingSectionProvider,
|
||||||
Setting.CONCEAL to ::settingConcealFieldsProvider,
|
Setting.CONCEAL to ::settingConcealFieldsProvider,
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.artemchep.keyguard.feature.home.settings.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.Link
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import com.artemchep.keyguard.feature.navigation.LocalNavigationController
|
||||||
|
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||||
|
import com.artemchep.keyguard.feature.urloverride.UrlOverrideListRoute
|
||||||
|
import com.artemchep.keyguard.res.Res
|
||||||
|
import com.artemchep.keyguard.ui.FlatItem
|
||||||
|
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||||
|
import com.artemchep.keyguard.ui.icons.icon
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
|
||||||
|
fun settingUrlOverrideProvider(
|
||||||
|
directDI: DirectDI,
|
||||||
|
) = settingUrlOverrideProvider()
|
||||||
|
|
||||||
|
fun settingUrlOverrideProvider(): SettingComponent = kotlin.run {
|
||||||
|
val item = SettingIi(
|
||||||
|
search = SettingIi.Search(
|
||||||
|
group = "about",
|
||||||
|
tokens = listOf(
|
||||||
|
"url",
|
||||||
|
"override",
|
||||||
|
"placeholder",
|
||||||
|
"scheme",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val navigationController by rememberUpdatedState(LocalNavigationController.current)
|
||||||
|
SettingUrlOverride(
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToRoute(
|
||||||
|
route = UrlOverrideListRoute,
|
||||||
|
)
|
||||||
|
navigationController.queue(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
flowOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingUrlOverride(
|
||||||
|
onClick: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
FlatItem(
|
||||||
|
leading = icon<RowScope>(Icons.Outlined.Link),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.strings.pref_item_url_override_title),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,6 +18,12 @@ fun OtherSettingsScreen() {
|
||||||
SettingPaneItem.Item(Setting.FEATURES_OVERVIEW),
|
SettingPaneItem.Item(Setting.FEATURES_OVERVIEW),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SettingPaneItem.Group(
|
||||||
|
key = "other",
|
||||||
|
list = listOf(
|
||||||
|
SettingPaneItem.Item(Setting.URL_OVERRIDE),
|
||||||
|
),
|
||||||
|
),
|
||||||
SettingPaneItem.Group(
|
SettingPaneItem.Group(
|
||||||
key = "security",
|
key = "security",
|
||||||
list = listOf(
|
list = listOf(
|
||||||
|
|
|
@ -2,30 +2,51 @@ package com.artemchep.keyguard.feature.home.vault.component
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Terminal
|
||||||
import androidx.compose.material.icons.outlined.Warning
|
import androidx.compose.material.icons.outlined.Warning
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem
|
import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem
|
||||||
|
import com.artemchep.keyguard.ui.ContextItem
|
||||||
import com.artemchep.keyguard.ui.DisabledEmphasisAlpha
|
import com.artemchep.keyguard.ui.DisabledEmphasisAlpha
|
||||||
|
import com.artemchep.keyguard.ui.DropdownMenuItemFlat
|
||||||
|
import com.artemchep.keyguard.ui.DropdownMinWidth
|
||||||
|
import com.artemchep.keyguard.ui.DropdownScopeImpl
|
||||||
import com.artemchep.keyguard.ui.ExpandedIfNotEmpty
|
import com.artemchep.keyguard.ui.ExpandedIfNotEmpty
|
||||||
import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
||||||
import com.artemchep.keyguard.ui.FlatDropdown
|
import com.artemchep.keyguard.ui.FlatDropdown
|
||||||
import com.artemchep.keyguard.ui.FlatItemTextContent
|
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||||
|
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
|
||||||
|
import com.artemchep.keyguard.ui.icons.IconSmallBox
|
||||||
import com.artemchep.keyguard.ui.theme.combineAlpha
|
import com.artemchep.keyguard.ui.theme.combineAlpha
|
||||||
import com.artemchep.keyguard.ui.theme.warning
|
import com.artemchep.keyguard.ui.theme.warning
|
||||||
import com.artemchep.keyguard.ui.theme.warningContainer
|
import com.artemchep.keyguard.ui.theme.warningContainer
|
||||||
|
@ -44,11 +65,40 @@ fun VaultViewUriItem(
|
||||||
content = {
|
content = {
|
||||||
FlatItemTextContent(
|
FlatItemTextContent(
|
||||||
title = {
|
title = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
text = item.title,
|
text = item.title,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 5,
|
maxLines = 5,
|
||||||
)
|
)
|
||||||
|
ExpandedIfNotEmptyForRow(
|
||||||
|
item.matchTypeTitle,
|
||||||
|
) { matchTypeTitle ->
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 128.dp)
|
||||||
|
.border(
|
||||||
|
Dp.Hairline,
|
||||||
|
DividerColor,
|
||||||
|
MaterialTheme.shapes.small,
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
horizontal = 8.dp,
|
||||||
|
vertical = 4.dp,
|
||||||
|
),
|
||||||
|
text = matchTypeTitle,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
text = if (item.text != null) {
|
text = if (item.text != null) {
|
||||||
// composable
|
// composable
|
||||||
|
@ -101,34 +151,113 @@ fun VaultViewUriItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
trailing = {
|
var selectedDropdown by remember {
|
||||||
ExpandedIfNotEmptyForRow(
|
mutableStateOf<List<ContextItem>>(emptyList())
|
||||||
item.matchTypeTitle,
|
}
|
||||||
) { matchTypeTitle ->
|
if (item.overrides.isNotEmpty()) FlowRow(
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(max = 128.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(
|
.fillMaxWidth(),
|
||||||
top = 8.dp,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
bottom = 8.dp,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item.overrides.forEach { override ->
|
||||||
|
val updatedDropdownState = rememberUpdatedState(override.dropdown)
|
||||||
|
UrlOverrideItem(
|
||||||
|
title = override.title,
|
||||||
|
text = override.text,
|
||||||
|
onClick = {
|
||||||
|
selectedDropdown = updatedDropdownState.value
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.border(
|
}
|
||||||
Dp.Hairline,
|
}
|
||||||
DividerColor,
|
|
||||||
MaterialTheme.shapes.small,
|
// Inject the dropdown popup to the bottom of the
|
||||||
)
|
// content.
|
||||||
.padding(
|
val onDismissRequest = {
|
||||||
horizontal = 8.dp,
|
selectedDropdown = emptyList()
|
||||||
vertical = 4.dp,
|
}
|
||||||
),
|
DropdownMenu(
|
||||||
text = matchTypeTitle,
|
expanded = selectedDropdown.isNotEmpty(),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
onDismissRequest = onDismissRequest,
|
||||||
textAlign = TextAlign.End,
|
modifier = Modifier
|
||||||
maxLines = 2,
|
.widthIn(min = DropdownMinWidth),
|
||||||
|
) {
|
||||||
|
val scope = DropdownScopeImpl(this, onDismissRequest = onDismissRequest)
|
||||||
|
selectedDropdown.forEach { action ->
|
||||||
|
scope.DropdownMenuItemFlat(
|
||||||
|
action = action,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dropdown = item.dropdown,
|
dropdown = item.dropdown,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UrlOverrideItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
start = 8.dp,
|
||||||
|
end = 8.dp,
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 8.dp,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp),
|
||||||
|
) {
|
||||||
|
IconSmallBox(
|
||||||
|
main = Icons.Outlined.Terminal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 128.dp)
|
||||||
|
.alignByBaseline(),
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 128.dp)
|
||||||
|
.alignByBaseline(),
|
||||||
|
text = text,
|
||||||
|
color = LocalContentColor.current
|
||||||
|
.combineAlpha(MediumEmphasisAlpha),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -214,8 +214,19 @@ sealed interface VaultViewItem {
|
||||||
* to the item.
|
* to the item.
|
||||||
*/
|
*/
|
||||||
val dropdown: List<ContextItem> = emptyList(),
|
val dropdown: List<ContextItem> = emptyList(),
|
||||||
|
val overrides: List<Override> = emptyList(),
|
||||||
) : VaultViewItem {
|
) : VaultViewItem {
|
||||||
companion object
|
companion object;
|
||||||
|
|
||||||
|
data class Override(
|
||||||
|
val title: String,
|
||||||
|
val text: String,
|
||||||
|
/**
|
||||||
|
* List of the callable actions appended
|
||||||
|
* to the item.
|
||||||
|
*/
|
||||||
|
val dropdown: List<ContextItem> = emptyList(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Totp(
|
data class Totp(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.artemchep.keyguard.feature.home.vault.screen
|
package com.artemchep.keyguard.feature.home.vault.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
@ -20,7 +19,6 @@ import androidx.compose.material.icons.outlined.PhoneIphone
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material.icons.outlined.Terminal
|
import androidx.compose.material.icons.outlined.Terminal
|
||||||
import androidx.compose.material.icons.outlined.Textsms
|
import androidx.compose.material.icons.outlined.Textsms
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -47,6 +45,7 @@ import com.artemchep.keyguard.common.model.DAccount
|
||||||
import com.artemchep.keyguard.common.model.DCollection
|
import com.artemchep.keyguard.common.model.DCollection
|
||||||
import com.artemchep.keyguard.common.model.DFilter
|
import com.artemchep.keyguard.common.model.DFilter
|
||||||
import com.artemchep.keyguard.common.model.DFolderTree
|
import com.artemchep.keyguard.common.model.DFolderTree
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
import com.artemchep.keyguard.common.model.DOrganization
|
import com.artemchep.keyguard.common.model.DOrganization
|
||||||
import com.artemchep.keyguard.common.model.DSecret
|
import com.artemchep.keyguard.common.model.DSecret
|
||||||
import com.artemchep.keyguard.common.model.DownloadAttachmentRequest
|
import com.artemchep.keyguard.common.model.DownloadAttachmentRequest
|
||||||
|
@ -65,9 +64,17 @@ import com.artemchep.keyguard.common.model.formatH
|
||||||
import com.artemchep.keyguard.common.model.titleH
|
import com.artemchep.keyguard.common.model.titleH
|
||||||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||||
import com.artemchep.keyguard.common.service.download.DownloadManager
|
import com.artemchep.keyguard.common.service.download.DownloadManager
|
||||||
|
import com.artemchep.keyguard.common.service.execute.ExecuteCommand
|
||||||
import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor
|
import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor
|
||||||
import com.artemchep.keyguard.common.service.extract.LinkInfoRegistry
|
import com.artemchep.keyguard.common.service.extract.LinkInfoRegistry
|
||||||
import com.artemchep.keyguard.common.service.passkey.PassKeyService
|
import com.artemchep.keyguard.common.service.passkey.PassKeyService
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.CipherPlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.CommentPlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.CustomPlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.DateTimePlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.TextTransformPlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.impl.UrlPlaceholder
|
||||||
|
import com.artemchep.keyguard.common.service.placeholder.placeholderFormat
|
||||||
import com.artemchep.keyguard.common.service.twofa.TwoFaService
|
import com.artemchep.keyguard.common.service.twofa.TwoFaService
|
||||||
import com.artemchep.keyguard.common.usecase.AddCipherOpenedHistory
|
import com.artemchep.keyguard.common.usecase.AddCipherOpenedHistory
|
||||||
import com.artemchep.keyguard.common.usecase.ChangeCipherNameById
|
import com.artemchep.keyguard.common.usecase.ChangeCipherNameById
|
||||||
|
@ -98,6 +105,7 @@ 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
|
||||||
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
||||||
|
import com.artemchep.keyguard.common.usecase.GetUrlOverrides
|
||||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||||
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
||||||
import com.artemchep.keyguard.common.usecase.PasskeyTargetCheck
|
import com.artemchep.keyguard.common.usecase.PasskeyTargetCheck
|
||||||
|
@ -209,6 +217,7 @@ fun vaultViewScreenState(
|
||||||
getWebsiteIcons = instance(),
|
getWebsiteIcons = instance(),
|
||||||
getTotpCode = instance(),
|
getTotpCode = instance(),
|
||||||
getPasswordStrength = instance(),
|
getPasswordStrength = instance(),
|
||||||
|
getUrlOverrides = instance(),
|
||||||
passkeyTargetCheck = instance(),
|
passkeyTargetCheck = instance(),
|
||||||
cipherUnsecureUrlCheck = instance(),
|
cipherUnsecureUrlCheck = instance(),
|
||||||
cipherUnsecureUrlAutoFix = instance(),
|
cipherUnsecureUrlAutoFix = instance(),
|
||||||
|
@ -219,6 +228,7 @@ fun vaultViewScreenState(
|
||||||
changeCipherPasswordById = instance(),
|
changeCipherPasswordById = instance(),
|
||||||
checkPasswordLeak = instance(),
|
checkPasswordLeak = instance(),
|
||||||
retryCipher = instance(),
|
retryCipher = instance(),
|
||||||
|
executeCommand = instance(),
|
||||||
copyCipherById = instance(),
|
copyCipherById = instance(),
|
||||||
restoreCipherById = instance(),
|
restoreCipherById = instance(),
|
||||||
trashCipherById = instance(),
|
trashCipherById = instance(),
|
||||||
|
@ -250,7 +260,14 @@ fun vaultViewScreenState(
|
||||||
private class Holder(
|
private class Holder(
|
||||||
val uri: DSecret.Uri,
|
val uri: DSecret.Uri,
|
||||||
val info: List<LinkInfo>,
|
val info: List<LinkInfo>,
|
||||||
|
val overrides: List<Override> = emptyList(),
|
||||||
|
) {
|
||||||
|
data class Override(
|
||||||
|
val override: DGlobalUrlOverride,
|
||||||
|
val uri: String,
|
||||||
|
val info: List<LinkInfo>,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun vaultViewScreenState(
|
fun vaultViewScreenState(
|
||||||
|
@ -270,6 +287,7 @@ fun vaultViewScreenState(
|
||||||
getWebsiteIcons: GetWebsiteIcons,
|
getWebsiteIcons: GetWebsiteIcons,
|
||||||
getTotpCode: GetTotpCode,
|
getTotpCode: GetTotpCode,
|
||||||
getPasswordStrength: GetPasswordStrength,
|
getPasswordStrength: GetPasswordStrength,
|
||||||
|
getUrlOverrides: GetUrlOverrides,
|
||||||
passkeyTargetCheck: PasskeyTargetCheck,
|
passkeyTargetCheck: PasskeyTargetCheck,
|
||||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||||
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||||
|
@ -280,6 +298,7 @@ fun vaultViewScreenState(
|
||||||
changeCipherPasswordById: ChangeCipherPasswordById,
|
changeCipherPasswordById: ChangeCipherPasswordById,
|
||||||
checkPasswordLeak: CheckPasswordLeak,
|
checkPasswordLeak: CheckPasswordLeak,
|
||||||
retryCipher: RetryCipher,
|
retryCipher: RetryCipher,
|
||||||
|
executeCommand: ExecuteCommand,
|
||||||
copyCipherById: CopyCipherById,
|
copyCipherById: CopyCipherById,
|
||||||
restoreCipherById: RestoreCipherById,
|
restoreCipherById: RestoreCipherById,
|
||||||
trashCipherById: TrashCipherById,
|
trashCipherById: TrashCipherById,
|
||||||
|
@ -408,6 +427,7 @@ fun vaultViewScreenState(
|
||||||
getAppIcons(),
|
getAppIcons(),
|
||||||
getWebsiteIcons(),
|
getWebsiteIcons(),
|
||||||
getCanWrite(),
|
getCanWrite(),
|
||||||
|
getUrlOverrides(),
|
||||||
) { array ->
|
) { array ->
|
||||||
val accountOrNull = array[0] as DAccount?
|
val accountOrNull = array[0] as DAccount?
|
||||||
val secretOrNull = array[1] as DSecret?
|
val secretOrNull = array[1] as DSecret?
|
||||||
|
@ -419,6 +439,7 @@ fun vaultViewScreenState(
|
||||||
val appIcons = array[7] as Boolean
|
val appIcons = array[7] as Boolean
|
||||||
val websiteIcons = array[8] as Boolean
|
val websiteIcons = array[8] as Boolean
|
||||||
val canAddSecret = array[9] as Boolean
|
val canAddSecret = array[9] as Boolean
|
||||||
|
val urlOverrides = array[10] as List<DGlobalUrlOverride>
|
||||||
|
|
||||||
val content = when {
|
val content = when {
|
||||||
accountOrNull == null || secretOrNull == null -> VaultViewState.Content.NotFound
|
accountOrNull == null || secretOrNull == null -> VaultViewState.Content.NotFound
|
||||||
|
@ -447,16 +468,70 @@ fun vaultViewScreenState(
|
||||||
val canEdit = canAddSecret && secretOrNull.canEdit() && !hasCanNotWriteCiphers
|
val canEdit = canAddSecret && secretOrNull.canEdit() && !hasCanNotWriteCiphers
|
||||||
val canDelete = canAddSecret && secretOrNull.canDelete() && !hasCanNotWriteCiphers
|
val canDelete = canAddSecret && secretOrNull.canDelete() && !hasCanNotWriteCiphers
|
||||||
|
|
||||||
|
val placeholders = listOf(
|
||||||
|
CipherPlaceholder(secretOrNull),
|
||||||
|
CommentPlaceholder(),
|
||||||
|
CustomPlaceholder(secretOrNull),
|
||||||
|
DateTimePlaceholder(),
|
||||||
|
TextTransformPlaceholder(),
|
||||||
|
)
|
||||||
val extractors = LinkInfoRegistry(linkInfoExtractors)
|
val extractors = LinkInfoRegistry(linkInfoExtractors)
|
||||||
val cipherUris = secretOrNull
|
val cipherUris = secretOrNull
|
||||||
.uris
|
.uris
|
||||||
.map { uri ->
|
.map { uri ->
|
||||||
|
when (uri.match) {
|
||||||
|
// Regular expressions may use the {} control
|
||||||
|
// symbols already. Since we do not want to break
|
||||||
|
// existing data we ignore the placeholders and overrides.
|
||||||
|
DSecret.Uri.MatchType.RegularExpression -> {
|
||||||
val extra = extractors.process(uri)
|
val extra = extractors.process(uri)
|
||||||
Holder(
|
Holder(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
info = extra,
|
info = extra,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
|
val newUriString = uri.uri.placeholderFormat(placeholders)
|
||||||
|
val newUri = uri.copy(uri = newUriString)
|
||||||
|
|
||||||
|
// Process URL overrides
|
||||||
|
val urlOverridePlaceholders by lazy {
|
||||||
|
placeholders + listOf(
|
||||||
|
UrlPlaceholder(newUriString),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val urlOverrideList = urlOverrides
|
||||||
|
.filter { override ->
|
||||||
|
override.regex
|
||||||
|
.matches(newUriString)
|
||||||
|
}
|
||||||
|
.map { override ->
|
||||||
|
val command = override.command
|
||||||
|
.placeholderFormat(
|
||||||
|
placeholders = urlOverridePlaceholders,
|
||||||
|
)
|
||||||
|
val extra = extractors.process(
|
||||||
|
DSecret.Uri(
|
||||||
|
uri = command,
|
||||||
|
match = DSecret.Uri.MatchType.Exact,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Holder.Override(
|
||||||
|
override = override,
|
||||||
|
uri = command,
|
||||||
|
info = extra,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extra = extractors.process(newUri)
|
||||||
|
Holder(
|
||||||
|
uri = newUri,
|
||||||
|
info = extra,
|
||||||
|
overrides = urlOverrideList,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val icon = secretOrNull.toVaultItemIcon(
|
val icon = secretOrNull.toVaultItemIcon(
|
||||||
appIcons = appIcons,
|
appIcons = appIcons,
|
||||||
websiteIcons = websiteIcons,
|
websiteIcons = websiteIcons,
|
||||||
|
@ -605,6 +680,7 @@ fun vaultViewScreenState(
|
||||||
cipherFieldSwitchToggle = cipherFieldSwitchToggle,
|
cipherFieldSwitchToggle = cipherFieldSwitchToggle,
|
||||||
checkPasswordLeak = checkPasswordLeak,
|
checkPasswordLeak = checkPasswordLeak,
|
||||||
retryCipher = retryCipher,
|
retryCipher = retryCipher,
|
||||||
|
executeCommand = executeCommand,
|
||||||
markdown = markdown,
|
markdown = markdown,
|
||||||
concealFields = concealFields || secretOrNull.reprompt,
|
concealFields = concealFields || secretOrNull.reprompt,
|
||||||
websiteIcons = websiteIcons,
|
websiteIcons = websiteIcons,
|
||||||
|
@ -655,6 +731,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
cipherFieldSwitchToggle: CipherFieldSwitchToggle,
|
cipherFieldSwitchToggle: CipherFieldSwitchToggle,
|
||||||
checkPasswordLeak: CheckPasswordLeak,
|
checkPasswordLeak: CheckPasswordLeak,
|
||||||
retryCipher: RetryCipher,
|
retryCipher: RetryCipher,
|
||||||
|
executeCommand: ExecuteCommand,
|
||||||
markdown: Boolean,
|
markdown: Boolean,
|
||||||
concealFields: Boolean,
|
concealFields: Boolean,
|
||||||
websiteIcons: Boolean,
|
websiteIcons: Boolean,
|
||||||
|
@ -1295,7 +1372,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
linkedApps
|
linkedApps
|
||||||
.mapIndexed { index, holder ->
|
.mapIndexed { index, holder ->
|
||||||
val id = "link.app.$index"
|
val id = "link.app.$index"
|
||||||
val item = aaaa(
|
val item = createUriItem(
|
||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
disabledContentColor = disabledContentColor,
|
disabledContentColor = disabledContentColor,
|
||||||
|
@ -1303,6 +1380,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||||
|
executeCommand = executeCommand,
|
||||||
holder = holder,
|
holder = holder,
|
||||||
id = id,
|
id = id,
|
||||||
accountId = account.accountId(),
|
accountId = account.accountId(),
|
||||||
|
@ -1335,7 +1413,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
linkedWebsites
|
linkedWebsites
|
||||||
.mapIndexed { index, holder ->
|
.mapIndexed { index, holder ->
|
||||||
val id = "link.website.$index"
|
val id = "link.website.$index"
|
||||||
val item = aaaa(
|
val item = createUriItem(
|
||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
disabledContentColor = disabledContentColor,
|
disabledContentColor = disabledContentColor,
|
||||||
|
@ -1343,6 +1421,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||||
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||||
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||||
|
executeCommand = executeCommand,
|
||||||
holder = holder,
|
holder = holder,
|
||||||
id = id,
|
id = id,
|
||||||
accountId = account.accountId(),
|
accountId = account.accountId(),
|
||||||
|
@ -1741,7 +1820,7 @@ private fun RememberStateFlowScope.oh(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun RememberStateFlowScope.aaaa(
|
private suspend fun RememberStateFlowScope.createUriItem(
|
||||||
canEdit: Boolean,
|
canEdit: Boolean,
|
||||||
contentColor: Color,
|
contentColor: Color,
|
||||||
disabledContentColor: Color,
|
disabledContentColor: Color,
|
||||||
|
@ -1749,12 +1828,35 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||||
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||||
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||||
|
executeCommand: ExecuteCommand,
|
||||||
holder: Holder,
|
holder: Holder,
|
||||||
id: String,
|
id: String,
|
||||||
accountId: String,
|
accountId: String,
|
||||||
cipherId: String,
|
cipherId: String,
|
||||||
copy: CopyText,
|
copy: CopyText,
|
||||||
): VaultViewItem.Uri {
|
): VaultViewItem.Uri {
|
||||||
|
val overrides = holder
|
||||||
|
.overrides
|
||||||
|
.map {
|
||||||
|
val command = it.uri
|
||||||
|
val dropdown = createUriItemContextItems(
|
||||||
|
canEdit = false,
|
||||||
|
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||||
|
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||||
|
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||||
|
executeCommand = executeCommand,
|
||||||
|
uri = command,
|
||||||
|
info = it.info,
|
||||||
|
cipherId = cipherId,
|
||||||
|
copy = copy,
|
||||||
|
)
|
||||||
|
VaultViewItem.Uri.Override(
|
||||||
|
title = it.override.name,
|
||||||
|
text = command,
|
||||||
|
dropdown = dropdown,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val uri = holder.uri
|
val uri = holder.uri
|
||||||
|
|
||||||
val matchTypeTitle = holder.uri.match
|
val matchTypeTitle = holder.uri.match
|
||||||
|
@ -1764,6 +1866,18 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
translate(it)
|
translate(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dropdown = createUriItemContextItems(
|
||||||
|
canEdit = canEdit,
|
||||||
|
cipherUnsecureUrlCheck = cipherUnsecureUrlCheck,
|
||||||
|
cipherUnsecureUrlAutoFix = cipherUnsecureUrlAutoFix,
|
||||||
|
getJustDeleteMeByUrl = getJustDeleteMeByUrl,
|
||||||
|
executeCommand = executeCommand,
|
||||||
|
uri = holder.uri.uri,
|
||||||
|
info = holder.info,
|
||||||
|
cipherId = cipherId,
|
||||||
|
copy = copy,
|
||||||
|
)
|
||||||
|
|
||||||
val platformMarker = holder.info
|
val platformMarker = holder.info
|
||||||
.firstOrNull { it is LinkInfoPlatform } as LinkInfoPlatform?
|
.firstOrNull { it is LinkInfoPlatform } as LinkInfoPlatform?
|
||||||
when (platformMarker) {
|
when (platformMarker) {
|
||||||
|
@ -1772,55 +1886,6 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
.firstOrNull { it is LinkInfoAndroid } as LinkInfoAndroid?
|
.firstOrNull { it is LinkInfoAndroid } as LinkInfoAndroid?
|
||||||
when (androidMarker) {
|
when (androidMarker) {
|
||||||
is LinkInfoAndroid.Installed -> {
|
is LinkInfoAndroid.Installed -> {
|
||||||
val dropdown = buildContextItems {
|
|
||||||
section {
|
|
||||||
this += copy.FlatItemAction(
|
|
||||||
title = translate(Res.strings.copy_package_name),
|
|
||||||
value = platformMarker.packageName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += FlatItemAction(
|
|
||||||
leading = {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
painter = androidMarker.icon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = translate(Res.strings.uri_action_launch_app_title),
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent =
|
|
||||||
NavigationIntent.NavigateToApp(platformMarker.packageName)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Launch,
|
|
||||||
title = translate(Res.strings.uri_action_launch_play_store_title),
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent =
|
|
||||||
NavigationIntent.NavigateToBrowser(platformMarker.playStoreUrl)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += createShareAction(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = platformMarker.playStoreUrl,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VaultViewItem.Uri(
|
return VaultViewItem.Uri(
|
||||||
id = id,
|
id = id,
|
||||||
icon = {
|
icon = {
|
||||||
|
@ -1839,35 +1904,6 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val dropdown = buildContextItems {
|
|
||||||
section {
|
|
||||||
this += copy.FlatItemAction(
|
|
||||||
title = translate(Res.strings.copy_package_name),
|
|
||||||
value = platformMarker.packageName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Launch,
|
|
||||||
title = translate(Res.strings.uri_action_launch_play_store_title),
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent =
|
|
||||||
NavigationIntent.NavigateToBrowser(platformMarker.playStoreUrl)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += createShareAction(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = platformMarker.playStoreUrl,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VaultViewItem.Uri(
|
return VaultViewItem.Uri(
|
||||||
id = id,
|
id = id,
|
||||||
icon = {
|
icon = {
|
||||||
|
@ -1885,14 +1921,6 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LinkInfoPlatform.IOS -> {
|
is LinkInfoPlatform.IOS -> {
|
||||||
val dropdown = buildContextItems {
|
|
||||||
section {
|
|
||||||
this += copy.FlatItemAction(
|
|
||||||
title = translate(Res.strings.copy_package_name),
|
|
||||||
value = platformMarker.packageName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VaultViewItem.Uri(
|
return VaultViewItem.Uri(
|
||||||
id = id,
|
id = id,
|
||||||
icon = {
|
icon = {
|
||||||
|
@ -1909,99 +1937,14 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
|
|
||||||
is LinkInfoPlatform.Web -> {
|
is LinkInfoPlatform.Web -> {
|
||||||
val url = platformMarker.url.toString()
|
val url = platformMarker.url.toString()
|
||||||
val isJustDeleteMe = getJustDeleteMeByUrl(url)
|
|
||||||
.attempt()
|
|
||||||
.bind()
|
|
||||||
.getOrNull()
|
|
||||||
|
|
||||||
val isUnsecure = cipherUnsecureUrlCheck(holder.uri.uri)
|
val isUnsecure = cipherUnsecureUrlCheck(holder.uri.uri)
|
||||||
val dropdown = buildContextItems {
|
|
||||||
section {
|
|
||||||
this += copy.FlatItemAction(
|
|
||||||
title = translate(Res.strings.copy_url),
|
|
||||||
value = url,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Launch,
|
|
||||||
title = translate(Res.strings.uri_action_launch_browser_title),
|
|
||||||
text = url,
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent = NavigationIntent.NavigateToBrowser(url)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
url.removeSuffix("/") !=
|
|
||||||
platformMarker.frontPageUrl.toString().removeSuffix("/")
|
|
||||||
) {
|
|
||||||
val launchUrl = platformMarker.frontPageUrl.toString()
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Launch,
|
|
||||||
title = translate(Res.strings.uri_action_launch_browser_main_page_title),
|
|
||||||
text = launchUrl,
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent = NavigationIntent.NavigateToBrowser(launchUrl)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUnsecure) {
|
|
||||||
section {
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.AutoAwesome,
|
|
||||||
title = "Auto-fix unsecure URL",
|
|
||||||
text = "Changes a protocol to secure variant if it is available",
|
|
||||||
onClick = if (canEdit) {
|
|
||||||
// lambda
|
|
||||||
{
|
|
||||||
val ff = mapOf(
|
|
||||||
cipherId to setOf(holder.uri.uri),
|
|
||||||
)
|
|
||||||
cipherUnsecureUrlAutoFix(ff)
|
|
||||||
.launchIn(appScope)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += createShareAction(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = uri.uri,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += WebsiteLeakRoute.checkBreachesWebsiteActionOrNull(
|
|
||||||
translator = this@aaaa,
|
|
||||||
host = platformMarker.url.host,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
if (isJustDeleteMe != null) {
|
|
||||||
this += JustDeleteMeServiceViewRoute.justDeleteMeActionOrNull(
|
|
||||||
translator = this@aaaa,
|
|
||||||
justDeleteMe = isJustDeleteMe,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val faviconUrl = FaviconUrl(
|
val faviconUrl = FaviconUrl(
|
||||||
serverId = accountId,
|
serverId = accountId,
|
||||||
url = url,
|
url = url,
|
||||||
).takeIf { websiteIcons }
|
).takeIf { websiteIcons }
|
||||||
val warningTitle = "Unsecure".takeIf { isUnsecure }
|
val warningTitle = translate(Res.strings.uri_unsecure)
|
||||||
|
.takeIf { isUnsecure }
|
||||||
return VaultViewItem.Uri(
|
return VaultViewItem.Uri(
|
||||||
id = id,
|
id = id,
|
||||||
icon = {
|
icon = {
|
||||||
|
@ -2037,6 +1980,7 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
warningTitle = warningTitle,
|
warningTitle = warningTitle,
|
||||||
matchTypeTitle = matchTypeTitle,
|
matchTypeTitle = matchTypeTitle,
|
||||||
dropdown = dropdown,
|
dropdown = dropdown,
|
||||||
|
overrides = overrides,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2045,93 +1989,6 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
.firstNotNullOfOrNull { it as? LinkInfoLaunch.Allow }
|
.firstNotNullOfOrNull { it as? LinkInfoLaunch.Allow }
|
||||||
val canExecute = holder.info
|
val canExecute = holder.info
|
||||||
.firstNotNullOfOrNull { it as? LinkInfoExecute.Allow }
|
.firstNotNullOfOrNull { it as? LinkInfoExecute.Allow }
|
||||||
val dropdown = buildContextItems {
|
|
||||||
section {
|
|
||||||
this += copy.FlatItemAction(
|
|
||||||
title = translate(Res.strings.copy),
|
|
||||||
value = uri.uri,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
if (canExecute != null) {
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Terminal,
|
|
||||||
title = "Execute",
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent = NavigationIntent.NavigateToBrowser(uri.uri)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (canLuanch != null) {
|
|
||||||
if (canLuanch.apps.size > 1) {
|
|
||||||
this += FlatItemAction(
|
|
||||||
icon = Icons.Outlined.Launch,
|
|
||||||
title = translate(Res.strings.uri_action_launch_in_smth_title),
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent = NavigationIntent.NavigateToBrowser(uri.uri)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val icon = canLuanch.apps.first().icon
|
|
||||||
this += FlatItemAction(
|
|
||||||
leading = {
|
|
||||||
if (icon != null) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
painter = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(Icons.Outlined.Launch, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = translate(
|
|
||||||
Res.strings.uri_action_launch_in_app_title,
|
|
||||||
canLuanch.apps.first().label,
|
|
||||||
),
|
|
||||||
trailing = {
|
|
||||||
ChevronIcon()
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
val intent = NavigationIntent.NavigateToBrowser(uri.uri)
|
|
||||||
navigate(intent)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += LargeTypeRoute.showInLargeTypeActionOrNull(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = uri.uri,
|
|
||||||
colorize = true,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
this += LargeTypeRoute.showInLargeTypeActionAndLockOrNull(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = uri.uri,
|
|
||||||
colorize = true,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
this += createShareAction(
|
|
||||||
translator = this@aaaa,
|
|
||||||
text = uri.uri,
|
|
||||||
navigate = ::navigate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VaultViewItem.Uri(
|
return VaultViewItem.Uri(
|
||||||
id = id,
|
id = id,
|
||||||
icon = {
|
icon = {
|
||||||
|
@ -2218,6 +2075,310 @@ private suspend fun RememberStateFlowScope.aaaa(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun RememberStateFlowScope.createUriItemContextItems(
|
||||||
|
canEdit: Boolean,
|
||||||
|
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||||
|
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||||
|
getJustDeleteMeByUrl: GetJustDeleteMeByUrl,
|
||||||
|
executeCommand: ExecuteCommand,
|
||||||
|
uri: String,
|
||||||
|
info: List<LinkInfo>,
|
||||||
|
cipherId: String,
|
||||||
|
copy: CopyText,
|
||||||
|
): List<ContextItem> {
|
||||||
|
val platformMarker = info
|
||||||
|
.firstOrNull { it is LinkInfoPlatform } as LinkInfoPlatform?
|
||||||
|
when (platformMarker) {
|
||||||
|
is LinkInfoPlatform.Android -> {
|
||||||
|
val androidMarker = info
|
||||||
|
.firstOrNull { it is LinkInfoAndroid } as LinkInfoAndroid?
|
||||||
|
when (androidMarker) {
|
||||||
|
is LinkInfoAndroid.Installed -> {
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += copy.FlatItemAction(
|
||||||
|
title = translate(Res.strings.copy_package_name),
|
||||||
|
value = platformMarker.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += FlatItemAction(
|
||||||
|
leading = {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
painter = androidMarker.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = translate(Res.strings.uri_action_launch_app_title),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent =
|
||||||
|
NavigationIntent.NavigateToApp(platformMarker.packageName)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Launch,
|
||||||
|
title = translate(Res.strings.uri_action_launch_play_store_title),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent =
|
||||||
|
NavigationIntent.NavigateToBrowser(platformMarker.playStoreUrl)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += createShareAction(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = platformMarker.playStoreUrl,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += copy.FlatItemAction(
|
||||||
|
title = translate(Res.strings.copy_package_name),
|
||||||
|
value = platformMarker.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Launch,
|
||||||
|
title = translate(Res.strings.uri_action_launch_play_store_title),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent =
|
||||||
|
NavigationIntent.NavigateToBrowser(platformMarker.playStoreUrl)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += createShareAction(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = platformMarker.playStoreUrl,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is LinkInfoPlatform.IOS -> {
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += copy.FlatItemAction(
|
||||||
|
title = translate(Res.strings.copy_package_name),
|
||||||
|
value = platformMarker.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
is LinkInfoPlatform.Web -> {
|
||||||
|
val url = platformMarker.url.toString()
|
||||||
|
|
||||||
|
val isJustDeleteMe = getJustDeleteMeByUrl(url)
|
||||||
|
.attempt()
|
||||||
|
.bind()
|
||||||
|
.getOrNull()
|
||||||
|
|
||||||
|
val isUnsecure = cipherUnsecureUrlCheck(uri)
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += copy.FlatItemAction(
|
||||||
|
title = translate(Res.strings.copy_url),
|
||||||
|
value = url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Launch,
|
||||||
|
title = translate(Res.strings.uri_action_launch_browser_title),
|
||||||
|
text = url,
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToBrowser(url)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
url.removeSuffix("/") !=
|
||||||
|
platformMarker.frontPageUrl.toString().removeSuffix("/")
|
||||||
|
) {
|
||||||
|
val launchUrl = platformMarker.frontPageUrl.toString()
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Launch,
|
||||||
|
title = translate(Res.strings.uri_action_launch_browser_main_page_title),
|
||||||
|
text = launchUrl,
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToBrowser(launchUrl)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isUnsecure && canEdit) {
|
||||||
|
section {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.AutoAwesome,
|
||||||
|
title = translate(Res.strings.uri_action_autofix_unsecure_title),
|
||||||
|
text = translate(Res.strings.uri_action_autofix_unsecure_text),
|
||||||
|
onClick = {
|
||||||
|
val ff = mapOf(
|
||||||
|
cipherId to setOf(uri),
|
||||||
|
)
|
||||||
|
cipherUnsecureUrlAutoFix(ff)
|
||||||
|
.launchIn(appScope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += createShareAction(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = uri,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += WebsiteLeakRoute.checkBreachesWebsiteActionOrNull(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
host = platformMarker.url.host,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
if (isJustDeleteMe != null) {
|
||||||
|
this += JustDeleteMeServiceViewRoute.justDeleteMeActionOrNull(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
justDeleteMe = isJustDeleteMe,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val canLuanch = info
|
||||||
|
.firstNotNullOfOrNull { it as? LinkInfoLaunch.Allow }
|
||||||
|
val canExecute = info
|
||||||
|
.firstNotNullOfOrNull { it as? LinkInfoExecute.Allow }
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += copy.FlatItemAction(
|
||||||
|
title = translate(Res.strings.copy),
|
||||||
|
value = uri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
if (canExecute != null) {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Terminal,
|
||||||
|
title = translate(Res.strings.execute_command),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
executeCommand(canExecute.command)
|
||||||
|
.launchIn(appScope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (canLuanch != null) {
|
||||||
|
if (canLuanch.apps.size > 1) {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Launch,
|
||||||
|
title = translate(Res.strings.uri_action_launch_in_smth_title),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToBrowser(uri)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val icon = canLuanch.apps.first().icon
|
||||||
|
this += FlatItemAction(
|
||||||
|
leading = {
|
||||||
|
if (icon != null) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Outlined.Launch, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = translate(
|
||||||
|
Res.strings.uri_action_launch_in_app_title,
|
||||||
|
canLuanch.apps.first().label,
|
||||||
|
),
|
||||||
|
trailing = {
|
||||||
|
ChevronIcon()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToBrowser(uri)
|
||||||
|
navigate(intent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += LargeTypeRoute.showInLargeTypeActionOrNull(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = uri,
|
||||||
|
colorize = true,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
this += LargeTypeRoute.showInLargeTypeActionAndLockOrNull(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = uri,
|
||||||
|
colorize = true,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
this += createShareAction(
|
||||||
|
translator = this@createUriItemContextItems,
|
||||||
|
text = uri,
|
||||||
|
navigate = ::navigate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dropdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun RememberStateFlowScope.create(
|
fun RememberStateFlowScope.create(
|
||||||
copy: CopyText,
|
copy: CopyText,
|
||||||
id: String,
|
id: String,
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.artemchep.keyguard.feature.urloverride
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.artemchep.keyguard.feature.navigation.Route
|
||||||
|
|
||||||
|
object UrlOverrideListRoute : Route {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
UrlOverrideListScreen()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package com.artemchep.keyguard.feature.urloverride
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material.icons.outlined.Terminal
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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.EmptyView
|
||||||
|
import com.artemchep.keyguard.feature.ErrorView
|
||||||
|
import com.artemchep.keyguard.feature.home.vault.component.rememberSecretAccentColor
|
||||||
|
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.AvatarBuilder
|
||||||
|
import com.artemchep.keyguard.ui.DefaultFab
|
||||||
|
import com.artemchep.keyguard.ui.DefaultSelection
|
||||||
|
import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
||||||
|
import com.artemchep.keyguard.ui.FabState
|
||||||
|
import com.artemchep.keyguard.ui.FlatDropdown
|
||||||
|
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||||
|
import com.artemchep.keyguard.ui.ScaffoldLazyColumn
|
||||||
|
import com.artemchep.keyguard.ui.icons.IconBox
|
||||||
|
import com.artemchep.keyguard.ui.skeleton.SkeletonItem
|
||||||
|
import com.artemchep.keyguard.ui.toolbar.LargeToolbar
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.withIndex
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UrlOverrideListScreen() {
|
||||||
|
val loadableState = produceUrlOverrideListState(
|
||||||
|
)
|
||||||
|
EmailRelayListScreen(
|
||||||
|
loadableState = loadableState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun EmailRelayListScreen(
|
||||||
|
loadableState: Loadable<UrlOverrideListState>,
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldLazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
|
topBar = {
|
||||||
|
LargeToolbar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(Res.strings.urloverride_list_header_title))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
NavigationIcon()
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
val navigationController by rememberUpdatedState(LocalNavigationController.current)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
val intent = NavigationIntent.NavigateToBrowser(
|
||||||
|
url = "https://github.com/AChep/keyguard-app/blob/master/wiki/URL_OVERRIDE.md",
|
||||||
|
)
|
||||||
|
navigationController.queue(intent)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.HelpOutline,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
val selectionOrNull =
|
||||||
|
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.selection
|
||||||
|
DefaultSelection(
|
||||||
|
state = selectionOrNull,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionState = run {
|
||||||
|
val onClick =
|
||||||
|
loadableState.getOrNull()?.content?.getOrNull()?.getOrNull()?.primaryAction
|
||||||
|
val state = FabState(
|
||||||
|
onClick = onClick,
|
||||||
|
model = null,
|
||||||
|
)
|
||||||
|
rememberUpdatedState(newValue = state)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
DefaultFab(
|
||||||
|
icon = {
|
||||||
|
IconBox(main = Icons.Outlined.Add)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.strings.add),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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 URL override list!")
|
||||||
|
},
|
||||||
|
exception = e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ifRight = { content ->
|
||||||
|
val items = content.items
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
item("empty") {
|
||||||
|
NoItemsPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = { it.key },
|
||||||
|
) { item ->
|
||||||
|
UrlOverrideItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItemPlacement(),
|
||||||
|
item = item,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoItemsPlaceholder(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
EmptyView(
|
||||||
|
modifier = modifier,
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.strings.urloverride_empty_label),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UrlOverrideItem(
|
||||||
|
modifier: Modifier,
|
||||||
|
item: UrlOverrideListState.Item,
|
||||||
|
) {
|
||||||
|
val selectableState by item.selectableState.collectAsState()
|
||||||
|
val backgroundColor = when {
|
||||||
|
selectableState.selected -> MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else -> Color.Unspecified
|
||||||
|
}
|
||||||
|
FlatDropdown(
|
||||||
|
modifier = modifier,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
leading = {
|
||||||
|
val accent = rememberSecretAccentColor(
|
||||||
|
accentLight = item.accentLight,
|
||||||
|
accentDark = item.accentDark,
|
||||||
|
)
|
||||||
|
AvatarBuilder(
|
||||||
|
icon = item.icon,
|
||||||
|
accent = accent,
|
||||||
|
active = true,
|
||||||
|
badge = {
|
||||||
|
// Do nothing.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
FlatItemTextContent(
|
||||||
|
title = {
|
||||||
|
Text(item.title)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(4.dp),
|
||||||
|
)
|
||||||
|
val codeModifier = Modifier
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(14.dp),
|
||||||
|
imageVector = Icons.Outlined.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LocalTextStyle.current.color,
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = codeModifier,
|
||||||
|
text = item.regex,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(4.dp),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(14.dp),
|
||||||
|
imageVector = Icons.Outlined.Terminal,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LocalTextStyle.current.color,
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = codeModifier,
|
||||||
|
text = item.command,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 6,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailing = {
|
||||||
|
ExpandedIfNotEmptyForRow(
|
||||||
|
selectableState.selected.takeIf { selectableState.selecting },
|
||||||
|
) { selected ->
|
||||||
|
Checkbox(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
checked = selected,
|
||||||
|
onCheckedChange = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropdown = item.dropdown,
|
||||||
|
onClick = selectableState.onClick,
|
||||||
|
onLongClick = selectableState.onLongClick,
|
||||||
|
enabled = true,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.artemchep.keyguard.feature.urloverride
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.artemchep.keyguard.common.model.Loadable
|
||||||
|
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||||
|
import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
||||||
|
import com.artemchep.keyguard.ui.ContextItem
|
||||||
|
import com.artemchep.keyguard.ui.Selection
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class UrlOverrideListState(
|
||||||
|
val content: Loadable<Either<Throwable, Content>>,
|
||||||
|
) {
|
||||||
|
@Immutable
|
||||||
|
data class Content(
|
||||||
|
val revision: Int,
|
||||||
|
val items: ImmutableList<Item>,
|
||||||
|
val selection: Selection?,
|
||||||
|
val primaryAction: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class Item(
|
||||||
|
val key: String,
|
||||||
|
val title: String,
|
||||||
|
val regex: AnnotatedString,
|
||||||
|
val command: AnnotatedString,
|
||||||
|
val icon: VaultItemIcon,
|
||||||
|
val accentLight: Color,
|
||||||
|
val accentDark: Color,
|
||||||
|
val dropdown: ImmutableList<ContextItem>,
|
||||||
|
val selectableState: StateFlow<SelectableItemState>,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,366 @@
|
||||||
|
package com.artemchep.keyguard.feature.urloverride
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.CopyAll
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.Link
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import arrow.core.partially1
|
||||||
|
import com.artemchep.keyguard.common.io.launchIn
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import com.artemchep.keyguard.common.model.Loadable
|
||||||
|
import com.artemchep.keyguard.common.service.execute.ExecuteCommand
|
||||||
|
import com.artemchep.keyguard.common.usecase.AddUrlOverride
|
||||||
|
import com.artemchep.keyguard.common.usecase.GetUrlOverrides
|
||||||
|
import com.artemchep.keyguard.common.usecase.RemoveUrlOverrideById
|
||||||
|
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.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.platform.CurrentPlatform
|
||||||
|
import com.artemchep.keyguard.platform.Platform
|
||||||
|
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.icon
|
||||||
|
import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import org.kodein.di.compose.localDI
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
private class UrlOverrideListUiException(
|
||||||
|
msg: String,
|
||||||
|
cause: Throwable,
|
||||||
|
) : RuntimeException(msg, cause)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun produceUrlOverrideListState(
|
||||||
|
) = with(localDI().direct) {
|
||||||
|
produceUrlOverrideListState(
|
||||||
|
addUrlOverride = instance(),
|
||||||
|
removeUrlOverrideById = instance(),
|
||||||
|
getUrlOverrides = instance(),
|
||||||
|
executeCommand = instance(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun produceUrlOverrideListState(
|
||||||
|
addUrlOverride: AddUrlOverride,
|
||||||
|
removeUrlOverrideById: RemoveUrlOverrideById,
|
||||||
|
getUrlOverrides: GetUrlOverrides,
|
||||||
|
executeCommand: ExecuteCommand,
|
||||||
|
): Loadable<UrlOverrideListState> = produceScreenState(
|
||||||
|
key = "urloverride_list",
|
||||||
|
initial = Loadable.Loading,
|
||||||
|
args = arrayOf(),
|
||||||
|
) {
|
||||||
|
val selectionHandle = selectionHandle("selection")
|
||||||
|
|
||||||
|
fun onEdit(entity: DGlobalUrlOverride?) {
|
||||||
|
val nameKey = "name"
|
||||||
|
val nameItem = ConfirmationRoute.Args.Item.StringItem(
|
||||||
|
key = nameKey,
|
||||||
|
value = entity?.name.orEmpty(),
|
||||||
|
title = translate(Res.strings.generic_name),
|
||||||
|
type = ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
||||||
|
canBeEmpty = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val regexKey = "regex"
|
||||||
|
val regexItem = ConfirmationRoute.Args.Item.StringItem(
|
||||||
|
key = regexKey,
|
||||||
|
value = entity?.regex?.toString().orEmpty(),
|
||||||
|
title = translate(Res.strings.regex),
|
||||||
|
// A hint explains how would a user write a regex that
|
||||||
|
// matches both HTTPS and HTTP schemes.
|
||||||
|
hint = "^https?://.*",
|
||||||
|
description = translate(Res.strings.urloverride_regex_note),
|
||||||
|
type = ConfirmationRoute.Args.Item.StringItem.Type.Regex,
|
||||||
|
canBeEmpty = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val commandKey = "command"
|
||||||
|
val commandItem = ConfirmationRoute.Args.Item.StringItem(
|
||||||
|
key = commandKey,
|
||||||
|
value = entity?.command.orEmpty(),
|
||||||
|
title = translate(Res.strings.command),
|
||||||
|
// A hint explains how would a user write a command that
|
||||||
|
// converts all links to use the HTTPS scheme.
|
||||||
|
hint = "https://{url:rmvscm}",
|
||||||
|
type = ConfirmationRoute.Args.Item.StringItem.Type.Command,
|
||||||
|
canBeEmpty = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val items2 = listOf(
|
||||||
|
nameItem,
|
||||||
|
regexItem,
|
||||||
|
commandItem,
|
||||||
|
)
|
||||||
|
val route = registerRouteResultReceiver(
|
||||||
|
route = ConfirmationRoute(
|
||||||
|
args = ConfirmationRoute.Args(
|
||||||
|
icon = icon(
|
||||||
|
main = Icons.Outlined.Link,
|
||||||
|
secondary = if (entity != null) {
|
||||||
|
Icons.Outlined.Edit
|
||||||
|
} else {
|
||||||
|
Icons.Outlined.Add
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title = translate(Res.strings.urloverride_header_title),
|
||||||
|
items = items2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) { result ->
|
||||||
|
if (result is ConfirmationResult.Confirm) {
|
||||||
|
val name = result.data[nameKey] as? String
|
||||||
|
?: return@registerRouteResultReceiver
|
||||||
|
val regex = result.data[regexKey] as? String
|
||||||
|
?: return@registerRouteResultReceiver
|
||||||
|
val placeholder = result.data[commandKey] as? String
|
||||||
|
?: return@registerRouteResultReceiver
|
||||||
|
val createdAt = Clock.System.now()
|
||||||
|
val model = DGlobalUrlOverride(
|
||||||
|
id = entity?.id,
|
||||||
|
name = name,
|
||||||
|
regex = regex.toRegex(),
|
||||||
|
command = placeholder,
|
||||||
|
createdDate = createdAt,
|
||||||
|
)
|
||||||
|
addUrlOverride(model)
|
||||||
|
.launchIn(appScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intent = NavigationIntent.NavigateToRoute(route)
|
||||||
|
navigate(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNew() = onEdit(null)
|
||||||
|
|
||||||
|
fun onDuplicate(entity: DGlobalUrlOverride) {
|
||||||
|
val createdAt = Clock.System.now()
|
||||||
|
val model = entity.copy(
|
||||||
|
id = null,
|
||||||
|
createdDate = createdAt,
|
||||||
|
)
|
||||||
|
addUrlOverride(model)
|
||||||
|
.launchIn(appScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDelete(
|
||||||
|
emailRelayIds: Set<String>,
|
||||||
|
) {
|
||||||
|
val title = if (emailRelayIds.size > 1) {
|
||||||
|
translate(Res.strings.urloverride_delete_many_confirmation_title)
|
||||||
|
} else {
|
||||||
|
translate(Res.strings.urloverride_delete_one_confirmation_title)
|
||||||
|
}
|
||||||
|
val intent = createConfirmationDialogIntent(
|
||||||
|
icon = icon(Icons.Outlined.Delete),
|
||||||
|
title = title,
|
||||||
|
) {
|
||||||
|
removeUrlOverrideById(emailRelayIds)
|
||||||
|
.launchIn(appScope)
|
||||||
|
}
|
||||||
|
navigate(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
val itemsRawFlow = getUrlOverrides()
|
||||||
|
// Automatically de-select items
|
||||||
|
// that do not exist.
|
||||||
|
combine(
|
||||||
|
itemsRawFlow,
|
||||||
|
selectionHandle.idsFlow,
|
||||||
|
) { items, selectedItemIds ->
|
||||||
|
val newSelectedItemIds = selectedItemIds
|
||||||
|
.asSequence()
|
||||||
|
.filter { id ->
|
||||||
|
items.any { it.id == id }
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
newSelectedItemIds.takeIf { it.size < selectedItemIds.size }
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
.onEach { ids -> selectionHandle.setSelection(ids) }
|
||||||
|
.launchIn(screenScope)
|
||||||
|
val selectionFlow = combine(
|
||||||
|
itemsRawFlow,
|
||||||
|
selectionHandle.idsFlow,
|
||||||
|
) { items, selectedItemIds ->
|
||||||
|
val selectedItems = items
|
||||||
|
.filter { it.id in selectedItemIds }
|
||||||
|
items to selectedItems
|
||||||
|
}
|
||||||
|
.map { (allItems, selectedItems) ->
|
||||||
|
if (selectedItems.isEmpty()) {
|
||||||
|
return@map null
|
||||||
|
}
|
||||||
|
|
||||||
|
val actions = mutableListOf<FlatItemAction>()
|
||||||
|
actions += FlatItemAction(
|
||||||
|
leading = icon(Icons.Outlined.Delete),
|
||||||
|
title = translate(Res.strings.delete),
|
||||||
|
onClick = {
|
||||||
|
val ids = selectedItems.mapNotNull { it.id }.toSet()
|
||||||
|
onDelete(ids)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Selection(
|
||||||
|
count = selectedItems.size,
|
||||||
|
actions = actions.toPersistentList(),
|
||||||
|
onSelectAll = if (selectedItems.size < allItems.size) {
|
||||||
|
val allIds = allItems
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { it.id }
|
||||||
|
.toSet()
|
||||||
|
selectionHandle::setSelection
|
||||||
|
.partially1(allIds)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClear = selectionHandle::clearSelection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val itemsFlow = itemsRawFlow
|
||||||
|
.map { list ->
|
||||||
|
list
|
||||||
|
.map {
|
||||||
|
val dropdown = buildContextItems {
|
||||||
|
section {
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Edit,
|
||||||
|
title = translate(Res.strings.edit),
|
||||||
|
onClick = ::onEdit
|
||||||
|
.partially1(it),
|
||||||
|
)
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.CopyAll,
|
||||||
|
title = translate(Res.strings.duplicate),
|
||||||
|
onClick = ::onDuplicate
|
||||||
|
.partially1(it),
|
||||||
|
)
|
||||||
|
this += FlatItemAction(
|
||||||
|
icon = Icons.Outlined.Delete,
|
||||||
|
title = translate(Res.strings.delete),
|
||||||
|
onClick = ::onDelete
|
||||||
|
.partially1(setOfNotNull(it.id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val icon = VaultItemIcon.TextIcon(
|
||||||
|
run {
|
||||||
|
val words = it.name.split(" ")
|
||||||
|
if (words.size <= 1) {
|
||||||
|
return@run words.firstOrNull()?.take(2).orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
words
|
||||||
|
.take(2)
|
||||||
|
.joinToString("") { it.take(1) }
|
||||||
|
}.uppercase(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val selectableFlow = selectionHandle
|
||||||
|
.idsFlow
|
||||||
|
.map { selectedIds ->
|
||||||
|
SelectableItemStateRaw(
|
||||||
|
selecting = selectedIds.isNotEmpty(),
|
||||||
|
selected = it.id in selectedIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map { raw ->
|
||||||
|
val onClick = if (raw.selecting) {
|
||||||
|
// lambda
|
||||||
|
selectionHandle::toggleSelection.partially1(it.id.orEmpty())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val onLongClick = if (raw.selecting) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
// lambda
|
||||||
|
selectionHandle::toggleSelection.partially1(it.id.orEmpty())
|
||||||
|
}
|
||||||
|
SelectableItemState(
|
||||||
|
selecting = raw.selecting,
|
||||||
|
selected = raw.selected,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val selectableStateFlow =
|
||||||
|
if (list.size >= 100) {
|
||||||
|
val sharing = SharingStarted.WhileSubscribed(1000L)
|
||||||
|
selectableFlow.persistingStateIn(this, sharing)
|
||||||
|
} else {
|
||||||
|
selectableFlow.stateIn(this)
|
||||||
|
}
|
||||||
|
val regex = it.regex.toString().let(::AnnotatedString)
|
||||||
|
val command = it.command.let(::AnnotatedString)
|
||||||
|
UrlOverrideListState.Item(
|
||||||
|
key = it.id.orEmpty(),
|
||||||
|
title = it.name,
|
||||||
|
regex = regex,
|
||||||
|
command = command,
|
||||||
|
icon = icon,
|
||||||
|
accentLight = it.accentColor.light,
|
||||||
|
accentDark = it.accentColor.dark,
|
||||||
|
dropdown = dropdown,
|
||||||
|
selectableState = selectableStateFlow,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toPersistentList()
|
||||||
|
}
|
||||||
|
.crashlyticsAttempt { e ->
|
||||||
|
val msg = "Failed to get the URL override list!"
|
||||||
|
UrlOverrideListUiException(
|
||||||
|
msg = msg,
|
||||||
|
cause = e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val contentFlow = combine(
|
||||||
|
selectionFlow,
|
||||||
|
itemsFlow,
|
||||||
|
) { selection, itemsResult ->
|
||||||
|
val contentOrException = itemsResult
|
||||||
|
.map { items ->
|
||||||
|
UrlOverrideListState.Content(
|
||||||
|
revision = 0,
|
||||||
|
items = items,
|
||||||
|
selection = selection,
|
||||||
|
primaryAction = ::onNew,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loadable.Ok(contentOrException)
|
||||||
|
}
|
||||||
|
contentFlow
|
||||||
|
.map { content ->
|
||||||
|
val state = UrlOverrideListState(
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
Loadable.Ok(state)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.model.DGlobalUrlOverride
|
||||||
|
import com.artemchep.keyguard.common.service.urloverride.UrlOverrideRepository
|
||||||
|
import com.artemchep.keyguard.common.usecase.GetUrlOverrides
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Artem Chepurnyi
|
||||||
|
*/
|
||||||
|
class GetUrlOverridesImpl(
|
||||||
|
private val urlOverrideRepository: UrlOverrideRepository,
|
||||||
|
private val dispatcher: CoroutineContext = Dispatchers.Default,
|
||||||
|
) : GetUrlOverrides {
|
||||||
|
constructor(directDI: DirectDI) : this(
|
||||||
|
urlOverrideRepository = directDI.instance(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invoke(): Flow<List<DGlobalUrlOverride>> = urlOverrideRepository
|
||||||
|
.get()
|
||||||
|
.map { list ->
|
||||||
|
list
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
.flowOn(dispatcher)
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||||
|
|
||||||
|
import com.artemchep.keyguard.common.io.IO
|
||||||
|
import com.artemchep.keyguard.common.io.map
|
||||||
|
import com.artemchep.keyguard.common.service.urloverride.UrlOverrideRepository
|
||||||
|
import com.artemchep.keyguard.common.usecase.RemoveUrlOverrideById
|
||||||
|
import org.kodein.di.DirectDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Artem Chepurnyi
|
||||||
|
*/
|
||||||
|
class RemoveUrlOverrideByIdImpl(
|
||||||
|
private val urlOverrideRepository: UrlOverrideRepository,
|
||||||
|
) : RemoveUrlOverrideById {
|
||||||
|
constructor(directDI: DirectDI) : this(
|
||||||
|
urlOverrideRepository = directDI.instance(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invoke(
|
||||||
|
urlOverrideIds: Set<String>,
|
||||||
|
): IO<Unit> = performRemoveEmailRelay(
|
||||||
|
urlOverrideIds = urlOverrideIds,
|
||||||
|
).map { Unit }
|
||||||
|
|
||||||
|
private fun performRemoveEmailRelay(
|
||||||
|
urlOverrideIds: Set<String>,
|
||||||
|
) = urlOverrideRepository
|
||||||
|
.removeByIds(urlOverrideIds)
|
||||||
|
}
|
|
@ -83,6 +83,10 @@
|
||||||
<string name="organizations">Organizations</string>
|
<string name="organizations">Organizations</string>
|
||||||
<string name="organizations_empty_label">No organizations</string>
|
<string name="organizations_empty_label">No organizations</string>
|
||||||
<string name="misc">Miscellaneous</string>
|
<string name="misc">Miscellaneous</string>
|
||||||
|
<string name="command">Command</string>
|
||||||
|
<string name="execute_command">Execute</string>
|
||||||
|
<!-- Add (something) -->
|
||||||
|
<string name="add">Add</string>
|
||||||
<string name="add_integration">Add integration</string>
|
<string name="add_integration">Add integration</string>
|
||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
<string name="accounts">Accounts</string>
|
<string name="accounts">Accounts</string>
|
||||||
|
@ -161,6 +165,8 @@
|
||||||
<string name="downloads">Downloads</string>
|
<string name="downloads">Downloads</string>
|
||||||
<string name="downloads_empty_label">No downloads</string>
|
<string name="downloads_empty_label">No downloads</string>
|
||||||
<string name="duplicates_empty_label">No duplicates</string>
|
<string name="duplicates_empty_label">No duplicates</string>
|
||||||
|
<string name="duplicate">Duplicate</string>
|
||||||
|
<string name="regex">Regular expression</string>
|
||||||
<string name="encryption">Encryption</string>
|
<string name="encryption">Encryption</string>
|
||||||
<!-- Encryption key -->
|
<!-- Encryption key -->
|
||||||
<string name="encryption_key">Key</string>
|
<string name="encryption_key">Key</string>
|
||||||
|
@ -252,6 +258,8 @@
|
||||||
<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_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_unsecure">Unsecure</string>
|
<string name="uri_unsecure">Unsecure</string>
|
||||||
<string name="uri_match_app_title">Match app</string>
|
<string name="uri_match_app_title">Match app</string>
|
||||||
|
@ -458,6 +466,15 @@
|
||||||
<string name="emailrelay_integration_title">Email forwarder integration</string>
|
<string name="emailrelay_integration_title">Email forwarder integration</string>
|
||||||
<string name="emailrelay_empty_label">No email forwarders</string>
|
<string name="emailrelay_empty_label">No email forwarders</string>
|
||||||
|
|
||||||
|
<string name="urloverride_header_title">URL override</string>
|
||||||
|
<string name="urloverride_list_header_title">URL overrides</string>
|
||||||
|
<string name="urloverride_list_section_title">URL overrides</string>
|
||||||
|
<string name="urloverride_regex_note">The override will be applied to URLs that match the regular expression.</string>
|
||||||
|
<string name="urloverride_delete_one_confirmation_title">Delete URL override?</string>
|
||||||
|
<string name="urloverride_delete_many_confirmation_title">Delete URL overrides?</string>
|
||||||
|
<string name="urloverride_integration_title">Email forwarder integration</string>
|
||||||
|
<string name="urloverride_empty_label">No URL overrides</string>
|
||||||
|
|
||||||
<string name="setup_header_text">Create an encrypted vault where the local data will be stored.</string>
|
<string name="setup_header_text">Create an encrypted vault where the local data will be stored.</string>
|
||||||
<string name="setup_field_app_password_label">App password</string>
|
<string name="setup_field_app_password_label">App password</string>
|
||||||
<string name="setup_checkbox_biometric_auth">Biometric authentication</string>
|
<string name="setup_checkbox_biometric_auth">Biometric authentication</string>
|
||||||
|
@ -847,6 +864,7 @@
|
||||||
<string name="pref_item_persist_vault_key_note">Storing a vault key on a disk is a security risk. If the device\'s internal storage is compromised, the attacker will gain access to the local vault data.</string>
|
<string name="pref_item_persist_vault_key_note">Storing a vault key on a disk is a security risk. If the device\'s internal storage is compromised, the attacker will gain access to the local vault data.</string>
|
||||||
<string name="pref_item_permissions_title">Permissions</string>
|
<string name="pref_item_permissions_title">Permissions</string>
|
||||||
<string name="pref_item_features_overview_title">Features overview</string>
|
<string name="pref_item_features_overview_title">Features overview</string>
|
||||||
|
<string name="pref_item_url_override_title">URL overrides</string>
|
||||||
<string name="pref_item_biometric_unlock_title">Biometric unlock</string>
|
<string name="pref_item_biometric_unlock_title">Biometric unlock</string>
|
||||||
<!--
|
<!--
|
||||||
A title of the system popup that asks a user to use his biometric to later
|
A title of the system popup that asks a user to use his biometric to later
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import kotlinx.datetime.Instant;
|
||||||
|
|
||||||
|
CREATE TABLE urlOverride (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
regex TEXT NOT NULL,
|
||||||
|
command TEXT NOT NULL,
|
||||||
|
createdAt INTEGER AS Instant NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
update {
|
||||||
|
UPDATE urlOverride
|
||||||
|
SET
|
||||||
|
name = :name,
|
||||||
|
regex = :regex,
|
||||||
|
command = :command,
|
||||||
|
createdAt = :createdAt
|
||||||
|
WHERE
|
||||||
|
id = :id;
|
||||||
|
}
|
||||||
|
|
||||||
|
insert {
|
||||||
|
INSERT OR IGNORE INTO urlOverride(name, regex, command, createdAt)
|
||||||
|
VALUES (:name, :regex, :command, :createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
get:
|
||||||
|
SELECT *
|
||||||
|
FROM urlOverride
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT :limit;
|
||||||
|
|
||||||
|
deleteAll:
|
||||||
|
DELETE FROM urlOverride;
|
||||||
|
|
||||||
|
deleteByIds:
|
||||||
|
DELETE FROM urlOverride
|
||||||
|
WHERE id IN (:ids);
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE urlOverride (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
regex TEXT NOT NULL,
|
||||||
|
command TEXT NOT NULL,
|
||||||
|
createdAt INTEGER NOT NULL
|
||||||
|
);
|
|
@ -1,26 +0,0 @@
|
||||||
package com.artemchep.keyguard.copy
|
|
||||||
|
|
||||||
import com.artemchep.keyguard.common.io.IO
|
|
||||||
import com.artemchep.keyguard.common.io.ioEffect
|
|
||||||
import com.artemchep.keyguard.common.model.DSecret
|
|
||||||
import com.artemchep.keyguard.common.model.LinkInfoLaunch
|
|
||||||
import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
class LinkInfoExtractorLaunch(
|
|
||||||
) : LinkInfoExtractor<DSecret.Uri, LinkInfoLaunch> {
|
|
||||||
override val from: KClass<DSecret.Uri> get() = DSecret.Uri::class
|
|
||||||
|
|
||||||
override val to: KClass<LinkInfoLaunch> get() = LinkInfoLaunch::class
|
|
||||||
|
|
||||||
override fun extractInfo(
|
|
||||||
uri: DSecret.Uri,
|
|
||||||
): IO<LinkInfoLaunch> = ioEffect<LinkInfoLaunch> {
|
|
||||||
val apps = LinkInfoLaunch.Allow.AppInfo(
|
|
||||||
label = "App",
|
|
||||||
)
|
|
||||||
LinkInfoLaunch.Allow(listOf(apps))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(uri: DSecret.Uri): Boolean = true
|
|
||||||
}
|
|
|
@ -10,6 +10,9 @@ import com.artemchep.keyguard.common.service.deeplink.DeeplinkService
|
||||||
import com.artemchep.keyguard.common.service.deeplink.impl.DeeplinkServiceImpl
|
import com.artemchep.keyguard.common.service.deeplink.impl.DeeplinkServiceImpl
|
||||||
import com.artemchep.keyguard.common.service.download.DownloadService
|
import com.artemchep.keyguard.common.service.download.DownloadService
|
||||||
import com.artemchep.keyguard.common.service.download.DownloadServiceImpl
|
import com.artemchep.keyguard.common.service.download.DownloadServiceImpl
|
||||||
|
import com.artemchep.keyguard.common.service.execute.ExecuteCommand
|
||||||
|
import com.artemchep.keyguard.common.service.execute.impl.ExecuteCommandImpl
|
||||||
|
import com.artemchep.keyguard.common.service.extract.impl.LinkInfoExtractorExecute
|
||||||
import com.artemchep.keyguard.common.service.extract.impl.LinkInfoPlatformExtractor
|
import com.artemchep.keyguard.common.service.extract.impl.LinkInfoPlatformExtractor
|
||||||
import com.artemchep.keyguard.common.service.gpmprivapps.PrivilegedAppsService
|
import com.artemchep.keyguard.common.service.gpmprivapps.PrivilegedAppsService
|
||||||
import com.artemchep.keyguard.common.service.gpmprivapps.impl.PrivilegedAppsServiceImpl
|
import com.artemchep.keyguard.common.service.gpmprivapps.impl.PrivilegedAppsServiceImpl
|
||||||
|
@ -954,6 +957,11 @@ fun globalModuleJvm() = DI.Module(
|
||||||
directDI = this,
|
directDI = this,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
bindSingleton<ExecuteCommand> {
|
||||||
|
ExecuteCommandImpl(
|
||||||
|
directDI = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
bindSingleton<WordlistService> {
|
bindSingleton<WordlistService> {
|
||||||
WordlistServiceImpl(
|
WordlistServiceImpl(
|
||||||
directDI = this,
|
directDI = this,
|
||||||
|
@ -997,6 +1005,9 @@ fun globalModuleJvm() = DI.Module(
|
||||||
bindSingleton<LinkInfoPlatformExtractor> {
|
bindSingleton<LinkInfoPlatformExtractor> {
|
||||||
LinkInfoPlatformExtractor()
|
LinkInfoPlatformExtractor()
|
||||||
}
|
}
|
||||||
|
bindSingleton<LinkInfoExtractorExecute> {
|
||||||
|
LinkInfoExtractorExecute()
|
||||||
|
}
|
||||||
bindSingleton<SimilarityService> {
|
bindSingleton<SimilarityService> {
|
||||||
SimilarityServiceJvm(
|
SimilarityServiceJvm(
|
||||||
directDI = this,
|
directDI = this,
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Placeholders
|
||||||
|
|
||||||
|
Keyguard replaces placeholders when performing an action with the field (copying, opening in a browser and more). The feature is largely based on the [Keepass's specification](https://keepass.info/help/base/placeholders.html).
|
||||||
|
|
||||||
|
**At this moment placeholders are supported in**:
|
||||||
|
|
||||||
|
- URL field;
|
||||||
|
- URL override commands.
|
||||||
|
|
||||||
|
**Basics**:
|
||||||
|
|
||||||
|
- placeholders and their basics parameters are _case-insensitive_;
|
||||||
|
- placeholders are resolved using the shared constant time, all time-sensitive placeholders will be synced;
|
||||||
|
- if no value is found, the placeholder will be replaced with an empty string: `{otp}` will be replaced with an empty string if an entry doesn't have one-time password configured;
|
||||||
|
- if no placeholder is found, the placeholder will be kept in it's original form: `{keyguard}` will be replaced with `{keyguard}`.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
#### Entry Core
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :- | :---- |
|
||||||
|
| `uuid` | UUID |
|
||||||
|
| `title` | Title/Name |
|
||||||
|
| `username` | Username |
|
||||||
|
| `password` | Password |
|
||||||
|
| `otp` | One-time password |
|
||||||
|
| `notes` | Notes |
|
||||||
|
| `favorite` | Favorite |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
> https://example.com?user={username}
|
||||||
|
https://example.com?user=joe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Entry Custom Field
|
||||||
|
Custom strings can be referenced using `{s:name}`. For example, if you have a custom string named "Email", you can use the placeholder `{s:email}`.
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :- | :---- |
|
||||||
|
| `s:value` | First of the custom fields named 'value' |
|
||||||
|
|
||||||
|
_Example_:
|
||||||
|
```
|
||||||
|
> https://example.com?license={s:license}
|
||||||
|
https://example.com?license=12345678ABCD
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Entry URL
|
||||||
|
\***URL override specific**\*
|
||||||
|
|
||||||
|
This is useful in URL override command field. You can extract data from the URL for your new *command*.
|
||||||
|
|
||||||
|
Note: `{base}` supports exactly the same parts as `{url}` and is identical to it.
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :- | :---- |
|
||||||
|
| `url` | URL: `https://user:pw@keepass.info:80/path/example.php?q=e&s=t` |
|
||||||
|
| `url:rmvscm` | URL without scheme name: `user:pw@keepass.info:80/path/example.php?q=e&s=t` |
|
||||||
|
| `url:scm` | Scheme name: `https` |
|
||||||
|
| `url:host` | Host: `keepass.info` |
|
||||||
|
| `url:port` | Port: `80` |
|
||||||
|
| `url:path` | Path: `/path/example.php` |
|
||||||
|
| `url:query` | Query: `?q=e&s=t` |
|
||||||
|
| `url:userinfo` | User information: `user:pw` |
|
||||||
|
| `url:username` | Username: `user` |
|
||||||
|
| `url:password` | Password: `pw` |
|
||||||
|
|
||||||
|
#### Text transformation
|
||||||
|
|
||||||
|
Convert text to the other representation.
|
||||||
|
|
||||||
|
```
|
||||||
|
t-conv:/value/type/
|
||||||
|
```
|
||||||
|
the first symbol after `:` defines the separator. It may be any symbol except `{` and `}`. Trailing separator symbol is required.
|
||||||
|
|
||||||
|
- `u` or `upper` - transforms 'value' component into the uppercase basing on the English locale;
|
||||||
|
- `l` or `lower` - transforms 'value' component into the lowercase basing on the English locale;
|
||||||
|
- `base64` - encodes 'value' component into the Base64 (no padding, no wrap, URL safe) representation of the text;
|
||||||
|
- `hex` - encodes 'value' component into the HEX (lowercase) representation of the text;
|
||||||
|
- `uri` - encodes 'value' component into the URI representation of the text;
|
||||||
|
- `uri-dec` - decodes 'value' component from the URI representation of the text to the text;
|
||||||
|
|
||||||
|
_Example_:
|
||||||
|
```
|
||||||
|
> https://example.com?user={username}&password={t-conv:/{password}/uri/}
|
||||||
|
https://example.com?user=joe&password=Password1%21
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Date-time
|
||||||
|
##### Local
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :- | :---- |
|
||||||
|
| `dt_simple` | Current local date/time as a simple, sortable string. For example, for '2024-01-01 17:05:34' the value is `20240101170534`. |
|
||||||
|
| `dt_year` | Year component of the current local date/time |
|
||||||
|
| `dt_month` | Month component of the current local date/time |
|
||||||
|
| `dt_day` | Day component of the current local date/time |
|
||||||
|
| `dt_hour` | Hour component of the current local date/time |
|
||||||
|
| `dt_minute` | Minute component of the current local date/time |
|
||||||
|
| `dt_second` | Second component of the current local date/time |
|
||||||
|
|
||||||
|
##### UTC
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :- | :---- |
|
||||||
|
| `dt_utc_simple` | Current UTC date/time as a simple, sortable string. For example, for '2024-01-01 17:05:34' the value is `20240101170534`. |
|
||||||
|
| `dt_utc_year` | Year component of the current UTC date/time |
|
||||||
|
| `dt_utc_month` | Month component of the current UTC date/time |
|
||||||
|
| `dt_utc_day` | Day component of the current UTC date/time |
|
||||||
|
| `dt_utc_hour` | Hour component of the current UTC date/time |
|
||||||
|
| `dt_utc_minute` | Minute component of the current UTC date/time |
|
||||||
|
| `dt_utc_second` | Second component of the current UTC date/time |
|
||||||
|
|
||||||
|
##### Utility
|
||||||
|
|
||||||
|
| Placeholder | Description |
|
||||||
|
| :-- | :---- |
|
||||||
|
| `c:value` | Comment, removed upon transformation |
|
|
@ -0,0 +1,23 @@
|
||||||
|
# URL override
|
||||||
|
|
||||||
|
If you want to extend the default URL functionality, you can add URL overrides. An override has at least:
|
||||||
|
|
||||||
|
- **regex**: the override will be applied to URLs that match the regular expression;
|
||||||
|
- **command**: the new URL that will replace the old one, usually should contain [placeholders](PLACEHOLDERS.md).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
#### FileZilla FTP Client
|
||||||
|
Add a URL to the entry that we be overriden later:
|
||||||
|
```
|
||||||
|
ftp://{username}:{password}@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
this URL may already work if the FTP client correctly sets up URL protocol handlers. Otherwise, add the following URL override (Linux):
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
| :- |:------------------------|
|
||||||
|
| Regex | `^ftp://.*` |
|
||||||
|
| Command | `cmd://filezilla {url}` |
|
||||||
|
|
||||||
|
when done correctly, all matching URLs will have a button to execute the command, launching the FireZilla client.
|
||||||
|
|
Loading…
Reference in New Issue