fix: Password color and entropy view

This commit is contained in:
J-Jamet
2024-11-15 19:25:00 +01:00
parent eee61db189
commit 11199b996c
10 changed files with 135 additions and 118 deletions

View File

@@ -6,7 +6,7 @@ KeePassDX(4.1.0)
* Fix date fields #1695 #1710 * Fix date fields #1695 #1710
* Fix distinct domain names #1105 #1820 * Fix distinct domain names #1105 #1820
* Support otpauth://steam/Steam link #1289 * Support otpauth://steam/Steam link #1289
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 * Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1490 #1355
KeePassDX(4.0.8) KeePassDX(4.0.8)
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850 * Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.password
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
@@ -253,51 +254,62 @@ class PasswordGenerator(private val resources: Resources) {
return charSet.toString() return charSet.toString()
} }
fun colorizedPassword(editable: Editable?) {
editable.toString().forEachIndexed { index, char ->
colorFromChar(char)?.let { color ->
editable?.setSpan(
ForegroundColorSpan(color),
index,
index + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
fun getColorizedPassword(password: String): Spannable { fun getColorizedPassword(password: String): Spannable {
val spannableString = SpannableStringBuilder() val spannableString = SpannableStringBuilder()
if (password.isNotEmpty()) { if (password.isNotEmpty()) {
password.forEach { password.forEach { char ->
when { colorFromChar(char)?.let { color ->
UPPERCASE_CHARS.contains(it)|| val spannableColorChar = SpannableString(char.toString())
LOWERCASE_CHARS.contains(it) -> { spannableColorChar.setSpan(
spannableString.append(it) ForegroundColorSpan(color),
} 0,
DIGIT_CHARS.contains(it) -> { 1,
// RED Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62))) )
} spannableString.append(spannableColorChar)
SPECIAL_CHARS.contains(it) -> { } ?: spannableString.append(char)
// Blue
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
}
MINUS_CHAR.contains(it)||
UNDERLINE_CHAR.contains(it)||
BRACKET_CHARS.contains(it) -> {
// Purple
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
}
extendedChars().contains(it) -> {
// Green
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
}
else -> {
spannableString.append(it)
}
}
} }
} }
return spannableString return spannableString
} }
private fun colorizeChar(char: Char, color: Int): Spannable { private fun colorFromChar(char: Char): Int? {
val spannableColorChar = SpannableString(char.toString()) return when {
spannableColorChar.setSpan( DIGIT_CHARS.contains(char) -> {
ForegroundColorSpan(color), // RED
0, Color.rgb(246, 79, 62)
1, }
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE SPECIAL_CHARS.contains(char) -> {
) // Blue
return spannableColorChar Color.rgb(39, 166, 228)
}
MINUS_CHAR.contains(char)||
UNDERLINE_CHAR.contains(char)||
BRACKET_CHARS.contains(char) -> {
// Purple
Color.rgb(185, 38, 209)
}
extendedChars().contains(char) -> {
// Green
Color.rgb(44, 181, 50)
}
else -> {
null
}
}
} }
} }
} }

View File

@@ -33,8 +33,8 @@ import android.widget.TextView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordEditView @JvmOverloads constructor(context: Context, class PasswordEditView @JvmOverloads constructor(context: Context,
@@ -46,7 +46,6 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
private val passwordInputLayout: TextInputLayout private val passwordInputLayout: TextInputLayout
private val passwordText: EditText private val passwordText: EditText
private var textModified = false
private val passwordStrengthProgress: LinearProgressIndicator private val passwordStrengthProgress: LinearProgressIndicator
private val passwordEntropy: TextView private val passwordEntropy: TextView
@@ -76,22 +75,22 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_password_edit, this) inflater?.inflate(R.layout.view_password_edit, this)
passwordInputLayout = findViewById(R.id.password_input_layout) passwordInputLayout = findViewById(R.id.password_edit_input_layout)
passwordInputLayout?.hint = mViewHint passwordInputLayout?.hint = mViewHint
passwordText = findViewById(R.id.password_text) passwordText = findViewById(R.id.password_edit_text)
if (mShowPassword) { if (mShowPassword) {
passwordText?.inputType = passwordText.inputType or passwordText?.inputType = passwordText.inputType or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} }
passwordText?.maxLines = mMaxLines passwordText?.maxLines = mMaxLines
passwordText?.applyFontVisibility() passwordText?.applyFontVisibility()
passwordStrengthProgress = findViewById(R.id.password_strength_progress) passwordStrengthProgress = findViewById(R.id.password_edit_strength_progress)
passwordStrengthProgress?.apply { passwordStrengthProgress?.apply {
setIndicatorColor(PasswordEntropy.Strength.RISKY.color) setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
progress = 0 progress = 0
max = 100 max = 100
} }
passwordEntropy = findViewById(R.id.password_entropy) passwordEntropy = findViewById(R.id.password_edit_entropy)
mPasswordEntropyCalculator = PasswordEntropy { mPasswordEntropyCalculator = PasswordEntropy {
passwordText?.text?.toString()?.let { firstPassword -> passwordText?.text?.toString()?.let { firstPassword ->
@@ -113,20 +112,11 @@ class PasswordEditView @JvmOverloads constructor(context: Context,
} }
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
/* Fixme 1686
if (textModified) {
textModified = false
} else {
textModified = true
val selectionStart = passwordText.selectionStart
val selectionEnd = passwordText.selectionEnd
passwordString = editable.toString()
passwordText.setSelection(selectionStart, selectionEnd)
}*/
mPasswordTextWatchers.forEach { mPasswordTextWatchers.forEach {
it.afterTextChanged(editable) it.afterTextChanged(editable)
} }
getEntropyStrength(editable.toString()) getEntropyStrength(editable.toString())
PasswordGenerator.colorizedPassword(editable)
} }
} }
passwordText?.addTextChangedListener(mPasswordTextWatcher) passwordText?.addTextChangedListener(mPasswordTextWatcher)

View File

@@ -2,13 +2,19 @@ package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.text.Spannable
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.widget.TextView import android.widget.TextView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.core.widget.TextViewCompat
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordTextEditFieldView @JvmOverloads constructor(context: Context, class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
@@ -16,15 +22,23 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
defStyle: Int = 0) defStyle: Int = 0)
: TextEditFieldView(context, attrs, defStyle) { : TextEditFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
valueView.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
}
}
private var isColorizedPasswordActivated = PreferencesUtil.colorizePassword(context)
private var passwordProgressViewId = ViewCompat.generateViewId() private var passwordProgressViewId = ViewCompat.generateViewId()
private var passwordEntropyViewId = ViewCompat.generateViewId() private var passwordEntropyViewId = ViewCompat.generateViewId()
private var mPasswordProgress = LinearProgressIndicator(context).apply { private var mPasswordProgress = LinearProgressIndicator(context).apply {
layoutParams = LayoutParams( layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT).apply { LayoutParams.WRAP_CONTENT
addRule(ALIGN_PARENT_BOTTOM) ).apply {
} addRule(ALIGN_PARENT_BOTTOM)
}
setPadding( setPadding(
TypedValue.applyDimension( TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
@@ -38,22 +52,30 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
} }
private val mPasswordEntropyView = TextView(context).apply { private val mPasswordEntropyView = TextView(context).apply {
LayoutParams( layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT).apply { LayoutParams.WRAP_CONTENT
addRule(ALIGN_PARENT_BOTTOM or ALIGN_PARENT_RIGHT) ).apply {
} addRule(ALIGN_PARENT_BOTTOM)
}
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
valueView.text?.toString()?.let { firstPassword ->
getEntropyStrength(firstPassword)
} }
setPadding(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
context.resources.displayMetrics
).toInt()
)
TextViewCompat.setTextAppearance(this, R.style.KeepassDXStyle_Text_Indicator)
} }
init { init {
buildViews() buildViews()
valueView.doAfterTextChanged { editable ->
getEntropyStrength(editable.toString())
PasswordGenerator.colorizedPassword(editable)
}
addView(mPasswordProgress) addView(mPasswordProgress)
addView(mPasswordEntropyView) addView(mPasswordEntropyView)
} }
@@ -71,9 +93,9 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
mPasswordEntropyView.apply { mPasswordEntropyView.apply {
id = passwordEntropyViewId id = passwordEntropyViewId
layoutParams = (layoutParams as LayoutParams?)?.also { layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, actionImageButtonId) it.addRule(ALIGN_RIGHT, passwordProgressViewId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.addRule(START_OF, actionImageButtonId) it.addRule(ALIGN_END, passwordProgressViewId)
} }
} }
} }
@@ -95,6 +117,15 @@ class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
} }
} }
override fun spannableValue(value: String?): Spannable? {
if (value == null)
return null
return if (isColorizedPasswordActivated)
PasswordGenerator.getColorizedPassword(value)
else
super.spannableValue(value)
}
override var label: String override var label: String
get() { get() {
return super.label return super.label

View File

@@ -17,6 +17,7 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -113,7 +114,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun buildLinearTextView(templateAttribute: TemplateAttribute, override fun buildLinearTextView(templateAttribute: TemplateAttribute,
field: Field): TextEditFieldView? { field: Field): TextEditFieldView? {
return context?.let { return context?.let {
(if (templateAttribute.label == TemplateField.LABEL_PASSWORD) (if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextEditFieldView(it) PasswordTextEditFieldView(it)
else TextEditFieldView(it)).apply { else TextEditFieldView(it)).apply {
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout // hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout

View File

@@ -8,23 +8,19 @@ import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
open class TextEditFieldView @JvmOverloads constructor(context: Context, open class TextEditFieldView @JvmOverloads constructor(context: Context,
@@ -36,17 +32,16 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
private var valueViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId()
protected var actionImageButtonId = ViewCompat.generateViewId() protected var actionImageButtonId = ViewCompat.generateViewId()
private var textModified = false private val labelView = TextInputLayout(context).apply {
private var isColorizedPasswordActivated = PreferencesUtil.colorizePassword(context)
protected val labelView = TextInputLayout(context).apply {
layoutParams = LayoutParams( layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT) LayoutParams.WRAP_CONTENT)
} }
protected val valueView = TextInputEditText( protected val valueView = TextInputEditText(
ContextThemeWrapper(getContext(), ContextThemeWrapper(
R.style.KeepassDXStyle_TextInputLayout) getContext(),
R.style.KeepassDXStyle_TextInputLayout
)
).apply { ).apply {
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
@@ -62,7 +57,10 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
maxLines = 1 maxLines = 1
} }
private var actionImageButton = AppCompatImageButton( private var actionImageButton = AppCompatImageButton(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { ContextThemeWrapper(
context,
R.style.KeepassDXStyle_ImageButton_Simple
), null, 0).apply {
layoutParams = LayoutParams( layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT).also { LayoutParams.WRAP_CONTENT).also {
@@ -83,21 +81,6 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
init { init {
// Manually write view to avoid view id bugs // Manually write view to avoid view id bugs
buildViews() buildViews()
// To change the password color dynamically
/* Fixme 1686
valueView.doAfterTextChanged { editable ->
editable?.let { text ->
if (textModified) {
textModified = false
} else {
textModified = true
val selectionStart = valueView.selectionStart
val selectionEnd = valueView.selectionEnd
value = text.toString()
valueView.setSelection(selectionStart, selectionEnd)
}
}
}*/
labelView.addView(valueView) labelView.addView(valueView)
addView(labelView) addView(labelView)
addView(actionImageButton) addView(actionImageButton)
@@ -130,15 +113,6 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
return actionImageButton return actionImageButton
} }
private fun spannableValue(value: String?): Spannable? {
if (value == null)
return null
return if (isColorizedPasswordActivated && TemplateField.isStandardPasswordName(context, label))
PasswordGenerator.getColorizedPassword(value)
else
SpannableString(value)
}
override var label: String override var label: String
get() { get() {
return labelView.hint?.toString() ?: "" return labelView.hint?.toString() ?: ""
@@ -152,6 +126,10 @@ open class TextEditFieldView @JvmOverloads constructor(context: Context,
buildViews() buildViews()
} }
protected open fun spannableValue(value: String?): Spannable? {
return SpannableString(value)
}
override var value: String override var value: String
get() { get() {
return valueView.text?.toString() ?: "" return valueView.text?.toString() ?: ""

View File

@@ -7,7 +7,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input_layout" android:id="@+id/password_edit_input_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -18,7 +18,7 @@
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_text" android:id="@+id/password_edit_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ems="10" android:ems="10"
@@ -31,23 +31,21 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/password_strength_progress" android:id="@+id/password_edit_strength_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="1dp" android:padding="1dp"
app:trackCornerRadius="8dp" app:trackCornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="@+id/password_input_layout"/> app:layout_constraintBottom_toBottomOf="@+id/password_edit_input_layout"/>
<TextView <TextView
android:id="@+id/password_entropy" android:id="@+id/password_edit_entropy"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Entropy: 72.50 bit" tools:text="Entropy: 72.50 bit"
android:textSize="11sp" style="@style/KeepassDXStyle.Text.Indicator"
android:layout_margin="4dp" android:padding="4dp"
android:textColor="?attr/colorSecondary" app:layout_constraintBottom_toBottomOf="@+id/password_edit_input_layout"
android:textStyle="bold" app:layout_constraintEnd_toEndOf="@+id/password_edit_input_layout" />
app:layout_constraintBottom_toBottomOf="@+id/password_input_layout"
app:layout_constraintEnd_toEndOf="@+id/password_input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -416,6 +416,13 @@
<item name="backgroundTint">?attr/colorSecondary</item> <item name="backgroundTint">?attr/colorSecondary</item>
</style> </style>
<!-- Indicator -->
<style name="KeepassDXStyle.Text.Indicator" parent="KeepassDXStyle.Text">
<item name="android:textSize">11sp</item>
<item name="android:textColor">?attr/colorSecondary</item>
<item name="android:textStyle">bold</item>
</style>
<!-- Nodes Text Style --> <!-- Nodes Text Style -->
<style name="KeepassDXStyle.Title.Entry" parent="KeepassDXStyle.Text"> <style name="KeepassDXStyle.Title.Entry" parent="KeepassDXStyle.Text">
<item name="android:textColor">@color/text_primary_color</item> <item name="android:textColor">@color/text_primary_color</item>

View File

@@ -5,4 +5,4 @@
* Fix date fields #1695 #1710 * Fix date fields #1695 #1710
* Fix distinct domain names #1105 #1820 * Fix distinct domain names #1105 #1820
* Support otpauth://steam/Steam link #1289 * Support otpauth://steam/Steam link #1289
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 * Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1490 #1355

View File

@@ -5,4 +5,4 @@
* Correction des champs date #1695 #1710 * Correction des champs date #1695 #1710
* Correction des noms de domaines #1105 #1820 * Correction des noms de domaines #1105 #1820
* Support des liens otpauth://steam/Steam #1289 * Support des liens otpauth://steam/Steam #1289
* Petites corrections #1711 #1831 #1780 #1821 #1863 * Petites corrections #1711 #1831 #1780 #1821 #1863 #1490 #1355