Merge branch 'Kunzisoft:develop' into develop

This commit is contained in:
Uli
2021-08-29 10:50:08 +02:00
committed by GitHub
65 changed files with 1073 additions and 667 deletions

View File

@@ -1,9 +1,10 @@
KeePassDX(3.0.0)
* Add / Manage dynamic templates #191
* Manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655
* Fix timeout in dialogs #716
* Check URI permissions #626
* Small changes #1035 #1043 #942
* Improvements #1035 #1043 #942 #1021 #1027
KeePassDX(2.10.5)
* Increase the saving speed of database #1028

View File

@@ -30,6 +30,7 @@ package com.igreenwood.loupe
import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
@@ -108,6 +109,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
// drag distance threshold in dp for swipe to dismiss
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
// on view touched
var onViewTouchedListener: View.OnTouchListener? = null
// on view translate listener
var onViewTranslateListener: OnViewTranslateListener? = null
// on scale changed
@@ -272,7 +275,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
onViewTouchedListener?.onTouch(view, event)
event ?: return false
val imageView = imageViewRef.get() ?: return false
val container = containerRef.get() ?: return false

View File

@@ -157,11 +157,6 @@ class EntryActivity : DatabaseLockActivity() {
invalidateOptionsMenu()
}
mEntryViewModel.url.observe(this) { url ->
this.mUrl = url
invalidateOptionsMenu()
}
mEntryViewModel.entryInfo.observe(this) { entryInfo ->
// Manage entry copy to start notification if allowed (at the first start)
if (savedInstanceState == null) {
@@ -184,6 +179,8 @@ class EntryActivity : DatabaseLockActivity() {
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
mUrl = entryInfo.url
// Refresh Menu
invalidateOptionsMenu()

View File

@@ -143,13 +143,13 @@ class EntryEditActivity : DatabaseLockActivity(),
// Entry is retrieve, it's an entry to update
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
intent.removeExtra(KEY_ENTRY)
//intent.removeExtra(KEY_ENTRY)
mEntryId = entryToUpdate
}
// Parent is retrieve, it's a new entry to create
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
intent.removeExtra(KEY_PARENT)
//intent.removeExtra(KEY_PARENT)
mParentId = parent
}
@@ -166,7 +166,37 @@ class EntryEditActivity : DatabaseLockActivity(),
// Save button
validateButton?.setOnClickListener { saveEntry() }
mEntryEditViewModel.entryInfo.observe(this) {
mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry ->
// Change template dynamically
templatesEntry?.templates?.let { templates ->
val defaultTemplate = templatesEntry.defaultTemplate
templateSelectorSpinner?.apply {
// Build template selector
if (templates.isNotEmpty()) {
adapter = TemplatesSelectorAdapter(
this@EntryEditActivity,
mIconDrawableFactory,
templates
)
setSelection(templates.indexOf(defaultTemplate))
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
mEntryEditViewModel.changeTemplate(templates[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
} else {
visibility = View.GONE
}
}
}
loadingView?.hideByFading()
}
@@ -238,27 +268,6 @@ class EntryEditActivity : DatabaseLockActivity(),
mAttachmentFileBinderManager?.removeBinaryAttachment(it)
}
// Change template dynamically
mEntryEditViewModel.templates.observe(this) { templatesLoaded ->
val templates = templatesLoaded.templates
val defaultTemplate = templatesLoaded.defaultTemplate
templateSelectorSpinner?.apply {
// Build template selector
if (templates.isNotEmpty()) {
adapter = TemplatesSelectorAdapter(this@EntryEditActivity, mIconDrawableFactory, templates)
setSelection(templates.indexOf(defaultTemplate))
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
mEntryEditViewModel.changeTemplate(templates[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
} else {
visibility = View.GONE
}
}
}
// Build new entry from the entry info retrieved
mEntryEditViewModel.onEntrySaved.observe(this) { entrySave ->
// Open a progress dialog and save entry

View File

@@ -19,6 +19,7 @@
*/
package com.kunzisoft.keepass.activities
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -43,6 +44,7 @@ class ImageViewerActivity : DatabaseLockActivity() {
private lateinit var imageView: ImageView
private lateinit var progressView: View
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -52,12 +54,21 @@ class ImageViewerActivity : DatabaseLockActivity() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
toolbar.setOnTouchListener { _, _ ->
resetAppTimeout()
false
}
imageContainerView = findViewById(R.id.image_viewer_container)
imageView = findViewById(R.id.image_viewer_image)
progressView = findViewById(R.id.image_viewer_progress)
Loupe.create(imageView, imageContainerView!!) {
onViewTouchedListener = View.OnTouchListener { _, _ ->
// to reset timeout when Loupe image view touched
resetAppTimeout()
false
}
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
override fun onStart(view: ImageView) {
@@ -81,7 +92,8 @@ class ImageViewerActivity : DatabaseLockActivity() {
}
override fun viewToInvalidateTimeout(): View? {
return imageContainerView
// Null to manually manage events
return null
}
override fun finishActivityIfReloadRequested(): Boolean {

View File

@@ -4,9 +4,10 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
@@ -19,7 +20,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
mDatabaseViewModel.database.observe(this) { database ->
this.mDatabase = database
resetAppTimeoutWhenViewFocusedOrChanged()
resetAppTimeoutOnTouchOrFocus()
onDatabaseRetrieved(database)
}
@@ -31,7 +32,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
resetAppTimeoutWhenViewFocusedOrChanged()
resetAppTimeoutOnTouchOrFocus()
}
override fun onDatabaseRetrieved(database: Database?) {
@@ -46,9 +47,25 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
// Can be overridden by a subclass
}
private fun resetAppTimeoutWhenViewFocusedOrChanged() {
fun resetAppTimeout() {
context?.let {
dialog?.window?.decorView?.resetAppTimeoutWhenViewFocusedOrChanged(it, mDatabase?.loaded)
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
mDatabase?.loaded ?: false)
}
}
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return false
}
private fun resetAppTimeoutOnTouchOrFocus() {
if (!overrideTimeoutTouchAndFocusEvents()) {
context?.let {
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
it,
mDatabase?.loaded
)
}
}
}
}

View File

@@ -4,8 +4,10 @@ import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class DatePickerFragment : DatabaseDialogFragment() {
// Not as DatabaseDialogFragment because crash on KitKat
class DatePickerFragment : DialogFragment() {
private var mDefaultYear: Int = 2000
private var mDefaultMonth: Int = 1

View File

@@ -19,11 +19,11 @@
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential

View File

@@ -79,11 +79,15 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
if (!isFocus)
mManualEvent = true
else
resetAppTimeout()
}
@SuppressLint("ClickableViewAccessibility")
private var mOnTouchListener = View.OnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mManualEvent = true
resetAppTimeout()
}
}
false
@@ -94,6 +98,10 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
private var mPeriodWellFormed = false
private var mDigitsWellFormed = false
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
return true
}
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
@@ -224,8 +232,11 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(activity)
builder.apply {
setView(root)
.setPositiveButton(android.R.string.ok) {_, _ -> }
.setPositiveButton(android.R.string.ok) { _, _ ->
resetAppTimeout()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
resetAppTimeout()
}
}

View File

@@ -6,8 +6,10 @@ import android.app.TimePickerDialog
import android.content.Context
import android.os.Bundle
import android.text.format.DateFormat
import androidx.fragment.app.DialogFragment
class TimePickerFragment : DatabaseDialogFragment() {
// Not as DatabaseDialogFragment because crash on KitKat
class TimePickerFragment : DialogFragment() {
private var defaultHour: Int = 0
private var defaultMinute: Int = 0

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.activityViewModels
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData
@@ -33,7 +33,7 @@ abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
context?.let {
view?.resetAppTimeoutWhenViewFocusedOrChanged(it, mDatabase?.loaded)
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
}
}

View File

@@ -118,10 +118,11 @@ class EntryEditFragment: DatabaseFragment() {
templateView.setTemplate(template)
}
mEntryEditViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo ->
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
templateView.setTemplate(templateEntry.defaultTemplate)
// Load entry info only the first time to keep change locally
if (savedInstanceState == null) {
assignEntryInfo(entryInfo)
assignEntryInfo(templateEntry.entryInfo)
}
// To prevent flickering
rootView.showByFading()

View File

@@ -219,6 +219,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
activity?.intent?.let {
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
}
rebuildList()
}
override fun onPause() {

View File

@@ -169,7 +169,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
// Focus view to reinitialize timeout,
// view is not necessary loaded so retry later in resume
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewFocusedOrChanged(this, database?.loaded)
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
database?.let {
// check timeout
@@ -383,7 +383,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
// Invalidate timeout by touch
mDatabase?.let { database ->
viewToInvalidateTimeout()
?.resetAppTimeoutWhenViewFocusedOrChanged(this, database.loaded)
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
}
invalidateOptionsMenu()
@@ -422,6 +422,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
sendBroadcast(Intent(LOCK_ACTION))
}
fun resetAppTimeout() {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
mDatabase?.loaded ?: false)
}
override fun onBackPressed() {
if (mTimeoutEnable) {
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
@@ -451,12 +456,12 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
* To reset the app timeout when a view is focused or changed
*/
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context, databaseLoaded: Boolean?) {
// Log.d(LockingActivity.TAG, "View prepared to reset app timeout")
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
@@ -464,13 +469,13 @@ fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context, databaseLoade
false
}
setOnFocusChangeListener { _, _ ->
// Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
databaseLoaded ?: false)
}
if (this is ViewGroup) {
for (i in 0..childCount) {
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context, databaseLoaded)
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
}
}
}

View File

@@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
holder.lastModifiedView.text = entryHistory.lastModificationTime.getDateTimeString(context.resources)
holder.titleView.text = entryHistory.title
holder.usernameView.text = entryHistory.username
holder.urlView.text = entryHistory.url
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryHistory, position)
@@ -64,6 +63,5 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHist
var lastModifiedView: TextView = itemView.findViewById(R.id.entry_history_last_modified)
var titleView: TextView = itemView.findViewById(R.id.entry_history_title)
var usernameView: TextView = itemView.findViewById(R.id.entry_history_username)
var urlView: TextView = itemView.findViewById(R.id.entry_history_url)
}
}

View File

@@ -26,6 +26,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
@@ -40,6 +41,8 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.view.setTextSize
import com.kunzisoft.keepass.view.strikeOut
@@ -68,6 +71,7 @@ class NodeAdapter (private val context: Context,
private var mShowUserNames: Boolean = true
private var mShowNumberEntries: Boolean = true
private var mShowOTP: Boolean = false
private var mShowUUID: Boolean = false
private var mEntryFilters = arrayOf<Group.ChildFilter>()
@@ -122,6 +126,7 @@ class NodeAdapter (private val context: Context,
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
this.mShowOTP = PreferencesUtil.showOTPToken(context)
this.mShowUUID = PreferencesUtil.showUUID(context)
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
@@ -148,6 +153,7 @@ class NodeAdapter (private val context: Context,
if (oldItem is Entry && newItem is Entry) {
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
&& oldItem.username == newItem.username
&& oldItem.getOtpElement() == newItem.getOtpElement()
&& oldItem.containsAttachment() == newItem.containsAttachment()
} else if (oldItem is Group && newItem is Group) {
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
@@ -357,6 +363,25 @@ class NodeAdapter (private val context: Context,
}
}
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
&& mShowOTP
&& otpElement.token.isNotEmpty()) {
// Execute runnable to show progress
holder.otpRunnable.action = {
populateOtpView(holder, otpElement)
}
if (otpElement.type == OtpType.TOTP) {
holder.otpRunnable.postDelayed()
}
populateOtpView(holder, otpElement)
holder.otpContainer?.visibility = View.VISIBLE
} else {
holder.otpContainer?.visibility = View.GONE
}
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
@@ -387,6 +412,40 @@ class NodeAdapter (private val context: Context,
}
}
private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) {
when (otpElement?.type) {
OtpType.HOTP -> {
holder?.otpCounter?.text = otpElement.counter.toString()
holder?.otpProgress?.apply {
max = 100
progress = 100
}
}
OtpType.TOTP -> {
holder?.otpCounter?.text = otpElement.secondsRemaining.toString()
holder?.otpProgress?.apply {
max = otpElement.period
progress = otpElement.secondsRemaining
}
}
}
holder?.otpToken?.text = otpElement?.token
}
class OtpRunnable(val view: View?): Runnable {
var action: (() -> Unit)? = null
override fun run() {
action?.invoke()
postDelayed()
}
fun postDelayed() {
view?.postDelayed(this, 1000)
}
}
override fun getItemCount(): Int {
return mNodeSortedList.size()
}
@@ -413,6 +472,11 @@ class NodeAdapter (private val context: Context,
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var meta: TextView = itemView.findViewById(R.id.node_meta)
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
var otpCounter: TextView? = itemView.findViewById(R.id.node_otp_counter)
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
}

View File

@@ -56,7 +56,7 @@ class OtpModel() : Parcelable {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OtpElement
other as OtpModel
if (type != other.type) return false
// Token type is important only if it's a TOTP

View File

@@ -185,6 +185,19 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) {
return secondsRemaining == otpModel.period
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is OtpElement) return false
if (otpModel != other.otpModel) return false
return true
}
override fun hashCode(): Int {
return otpModel.hashCode()
}
companion object {
const val MIN_HOTP_COUNTER = 0
const val MAX_HOTP_COUNTER = Long.MAX_VALUE

View File

@@ -454,7 +454,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
// To reload group when appearence settings are modified
// To reload group when appearance settings are modified
when (preference?.key) {
getString(R.string.setting_style_key),
getString(R.string.setting_style_brightness_key),
@@ -464,6 +464,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
getString(R.string.list_size_key),
getString(R.string.monospace_font_fields_enable_key),
getString(R.string.hide_expired_entries_key),
getString(R.string.show_otp_token_key),
getString(R.string.show_uuid_key),
getString(R.string.enable_education_screens_key),
getString(R.string.reset_education_screens_key) -> {

View File

@@ -32,7 +32,7 @@ import com.kunzisoft.androidclearchroma.ChromaUtil
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewFocusedOrChanged
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database
@@ -74,7 +74,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
mDatabase = database
view.resetAppTimeoutWhenViewFocusedOrChanged(requireContext(), database?.loaded)
view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded)
onDatabaseRetrieved(database)
}

View File

@@ -128,6 +128,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.hide_expired_entries_default))
}
fun showOTPToken(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.show_otp_token_key),
context.resources.getBoolean(R.bool.show_otp_token_default))
}
fun showUUID(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.show_uuid_key),
@@ -644,6 +650,7 @@ object PreferencesUtil {
context.getString(R.string.list_size_key) -> editor.putString(name, value)
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.timeout
import android.app.Activity
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@@ -30,6 +29,7 @@ import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.exception.ClipboardException
import com.kunzisoft.keepass.settings.PreferencesUtil

View File

@@ -239,8 +239,23 @@ object UriUtil {
}
fun openExternalApp(context: Context, packageName: String) {
var launchIntent: Intent? = null
try {
context.startActivity(context.applicationContext.packageManager.getLaunchIntentForPackage(packageName))
launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
} catch (ignored: Exception) {
}
try {
if (launchIntent == null) {
context.startActivity(
Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))
)
} else {
context.startActivity(launchIntent)
}
} catch (e: Exception) {
Log.e(TAG, "App cannot be open", e)
}

View File

@@ -39,7 +39,7 @@ abstract class TemplateAbstractView<
private var mTemplate: Template? = null
protected var mEntryInfo: EntryInfo? = null
protected var mCustomFieldIds = mutableListOf<FieldId>()
private var mViewFields = mutableListOf<ViewField>()
protected var mFontInVisibility: Boolean = PreferencesUtil.fieldFontIsInVisibility(context)
protected var mHideProtectedValue: Boolean = PreferencesUtil.hideProtectedValue(context)
@@ -48,7 +48,7 @@ abstract class TemplateAbstractView<
protected var entryIconView: ImageView
private var titleContainerView: ViewGroup
protected var templateContainerView: ViewGroup
protected var customFieldsContainerView: SectionView
private var customFieldsContainerView: SectionView
private var notReferencedFieldsContainerView: SectionView
init {
@@ -95,7 +95,7 @@ abstract class TemplateAbstractView<
}
}
fun buildTemplate() {
private fun buildTemplate() {
// Retrieve preferences
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
@@ -104,7 +104,7 @@ abstract class TemplateAbstractView<
templateContainerView.removeAllViews()
customFieldsContainerView.removeAllViews()
notReferencedFieldsContainerView.removeAllViews()
mCustomFieldIds.clear()
mViewFields.clear()
mTemplate?.let { template ->
@@ -200,7 +200,7 @@ abstract class TemplateAbstractView<
TemplateAttributeType.TEXT,
field.protectedValue.isProtected,
TemplateAttributeOption().apply {
setNumberLines(3)
setNumberLines(20)
},
TemplateAttributeAction.CUSTOM_EDITION
).apply {
@@ -231,13 +231,13 @@ abstract class TemplateAbstractView<
// Add new custom view id to the custom field list
if (fieldTag == FIELD_CUSTOM_TAG) {
val indexOldItem = indexCustomFieldIdByName(field.name)
val indexOldItem = getIndexViewFieldByName(field.name)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mViewFields.removeAt(indexOldItem)
if (itemView?.id != null) {
mCustomFieldIds.add(
FieldId(
itemView.id,
mViewFields.add(
ViewField(
itemView,
field
)
)
@@ -320,7 +320,7 @@ abstract class TemplateAbstractView<
/**
* Return empty custom fields
*/
protected open fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<FieldId> {
protected open fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
mEntryInfo?.let { entryInfo ->
populateEntryFieldView(FIELD_TITLE_TAG,
@@ -350,15 +350,14 @@ abstract class TemplateAbstractView<
showEmptyFields)
customFieldsContainerView.removeAllViews()
val emptyCustomFields = mutableListOf<FieldId>().also { it.addAll(mCustomFieldIds) }
val emptyCustomFields = mutableListOf<ViewField>().also { it.addAll(mViewFields) }
entryInfo.customFields.forEach { customField ->
val indexFieldViewId = indexCustomFieldIdByName(customField.name)
val indexFieldViewId = getIndexViewFieldByName(customField.name)
if (indexFieldViewId >= 0) {
// Template contains the custom view
val customFieldId = mCustomFieldIds[indexFieldViewId]
emptyCustomFields.remove(customFieldId)
templateContainerView.findViewById<View>(customFieldId.viewId)
?.let { customView ->
val viewField = mViewFields[indexFieldViewId]
emptyCustomFields.remove(viewField)
viewField.view.let { customView ->
if (customView is GenericTextFieldView) {
customView.value = customField.protectedValue.stringValue
customView.applyFontVisibility(mFontInVisibility)
@@ -459,22 +458,22 @@ abstract class TemplateAbstractView<
* -------------
*/
protected data class FieldId(var viewId: Int, var field: Field)
protected data class ViewField(var view: View, var field: Field)
private fun isStandardFieldName(name: String): Boolean {
return TemplateField.isStandardFieldName(name)
}
protected fun customFieldIdByName(name: String): FieldId? {
return mCustomFieldIds.find { it.field.name.equals(name, true) }
protected fun getViewFieldByName(name: String): ViewField? {
return mViewFields.find { it.field.name.equals(name, true) }
}
protected fun indexCustomFieldIdByName(name: String): Int {
return mCustomFieldIds.indexOfFirst { it.field.name.equals(name, true) }
private fun getIndexViewFieldByName(name: String): Int {
return mViewFields.indexOfFirst { it.field.name.equals(name, true) }
}
private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) {
mEntryInfo?.customFields = mCustomFieldIds.mapNotNull {
mEntryInfo?.customFields = mViewFields.mapNotNull {
getCustomField(it.field.name, templateFieldNotEmpty)
}.toMutableList()
}
@@ -484,10 +483,9 @@ abstract class TemplateAbstractView<
?: Field(fieldName, ProtectedString(false))
}
protected fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
customFieldIdByName(fieldName)?.let { fieldId ->
val editView: View? = templateContainerView.findViewById(fieldId.viewId)
?: customFieldsContainerView.findViewById(fieldId.viewId)
private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? {
getViewFieldByName(fieldName)?.let { fieldId ->
val editView: View? = fieldId.view
if (editView is GenericFieldView) {
// Do not return field with a default value
val defaultViewValue = if (editView.value == editView.default) "" else editView.value
@@ -514,20 +512,22 @@ abstract class TemplateAbstractView<
return if (!isStandardFieldName(customField.name)) {
customFieldsContainerView.visibility = View.VISIBLE
if (indexCustomFieldIdByName(customField.name) >= 0) {
replaceCustomField(customField, customField, focus)
if (getIndexViewFieldByName(customField.name) >= 0) {
// Update a custom field with a new value,
// new field name must be the same as old field name
replaceCustomField(customField, customField, false, focus)
} else {
val newCustomView = buildViewForCustomField(customField)
newCustomView?.let {
customFieldsContainerView.addView(newCustomView)
val fieldId = FieldId(
newCustomView.id,
val fieldId = ViewField(
newCustomView,
customField
)
val indexOldItem = indexCustomFieldIdByName(fieldId.field.name)
val indexOldItem = getIndexViewFieldByName(fieldId.field.name)
if (indexOldItem >= 0)
mCustomFieldIds.removeAt(indexOldItem)
mCustomFieldIds.add(indexOldItem, fieldId)
mViewFields.removeAt(indexOldItem)
mViewFields.add(indexOldItem, fieldId)
if (focus)
newCustomView.requestFocus()
}
@@ -544,32 +544,35 @@ abstract class TemplateAbstractView<
return put
}
/**
* Update a custom field and keep the old value
*/
private fun replaceCustomField(oldField: Field, newField: Field, focus: Boolean): Boolean {
private fun replaceCustomField(oldField: Field,
newField: Field,
keepOldValue: Boolean,
focus: Boolean): Boolean {
if (!isStandardFieldName(newField.name)) {
customFieldIdByName(oldField.name)?.viewId?.let { viewId ->
customFieldsContainerView.findViewById<View>(viewId)?.let { viewToReplace ->
getViewFieldByName(oldField.name)?.view?.let { viewToReplace ->
val oldValue = getCustomField(oldField.name).protectedValue.toString()
val parentGroup = viewToReplace.parent as ViewGroup
val indexInParent = parentGroup.indexOfChild(viewToReplace)
parentGroup.removeView(viewToReplace)
val newCustomFieldWithValue = Field(newField.name,
ProtectedString(newField.protectedValue.isProtected, oldValue))
val oldPosition = indexCustomFieldIdByName(oldField.name)
val newCustomFieldWithValue = if (keepOldValue)
Field(newField.name,
ProtectedString(newField.protectedValue.isProtected, oldValue)
)
else
newField
val oldPosition = getIndexViewFieldByName(oldField.name)
if (oldPosition >= 0)
mCustomFieldIds.removeAt(oldPosition)
mViewFields.removeAt(oldPosition)
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
newCustomView?.let {
parentGroup.addView(newCustomView, indexInParent)
mCustomFieldIds.add(
mViewFields.add(
oldPosition,
FieldId(
newCustomView.id,
ViewField(
newCustomView,
newCustomFieldWithValue
)
)
@@ -579,23 +582,25 @@ abstract class TemplateAbstractView<
return true
}
}
}
return false
}
/**
* Update a custom field and keep the old value
*/
fun replaceCustomField(oldField: Field, newField: Field): Boolean {
val replace = replaceCustomField(oldField, newField, true)
val replace = replaceCustomField(oldField, newField, keepOldValue = true, focus = true)
retrieveCustomFieldsFromView()
return replace
}
fun removeCustomField(oldCustomField: Field) {
val indexOldField = indexCustomFieldIdByName(oldCustomField.name)
val indexOldField = getIndexViewFieldByName(oldCustomField.name)
if (indexOldField >= 0) {
mCustomFieldIds[indexOldField].viewId.let { viewId ->
customFieldsContainerView.removeViewById(viewId)
mViewFields[indexOldField].let { fieldView ->
customFieldsContainerView.removeViewById(fieldView.view.id)
}
mCustomFieldIds.removeAt(indexOldField)
mViewFields.removeAt(indexOldField)
}
retrieveCustomFieldsFromView()
}

View File

@@ -192,7 +192,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
}
}
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<FieldId> {
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
refreshIcon()
return super.populateViewsWithEntryInfo(showEmptyFields)
}

View File

@@ -56,18 +56,20 @@ class TemplateView @JvmOverloads constructor(context: Context,
setMaxLines(templateAttribute.options.getNumberLines())
// TODO Linkify
value = field.protectedValue.stringValue
// Here the value is often empty
if (field.protectedValue.isProtected) {
if (mFirstTimeAskAllowCopyProtectedFields) {
setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE)
setCopyButtonClickListener {
setCopyButtonClickListener { _, _ ->
mOnAskCopySafeClickListener?.invoke()
}
} else {
if (mAllowCopyProtectedFields) {
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener {
mOnCopyActionClickListener?.invoke(field)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value)))
}
} else {
setCopyButtonState(TextFieldView.ButtonState.GONE)
@@ -76,8 +78,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
}
} else {
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener {
mOnCopyActionClickListener?.invoke(field)
setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value)))
}
}
}
@@ -114,22 +117,22 @@ class TemplateView @JvmOverloads constructor(context: Context,
return findViewWithTag<TextFieldView?>(FIELD_PASSWORD_TAG)?.getCopyButtonView()
}
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<FieldId> {
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
val emptyCustomFields = super.populateViewsWithEntryInfo(false)
// Hide empty custom fields
emptyCustomFields.forEach { customFieldId ->
templateContainerView.findViewById<View?>(customFieldId.viewId)
?.isVisible = false
customFieldId.view.isVisible = false
}
removeOtpRunnable()
mEntryInfo?.let { entryInfo ->
// Assign specific OTP dynamic view
removeOtpRunnable()
entryInfo.otpModel?.let {
assignOtp(it)
}
}
return emptyCustomFields
}
@@ -146,11 +149,10 @@ class TemplateView @JvmOverloads constructor(context: Context,
private var mOnOtpElementUpdated: ((OtpElement?) -> Unit)? = null
private fun getOtpTokenView(): TextFieldView? {
val indexFieldViewId = indexCustomFieldIdByName(OTP_TOKEN_FIELD)
if (indexFieldViewId >= 0) {
// Template contains the custom view
val customFieldId = mCustomFieldIds[indexFieldViewId]
return findViewById(customFieldId.viewId)
getViewFieldByName(OTP_TOKEN_FIELD)?.let { viewField ->
val view = viewField.view
if (view is TextFieldView)
return view
}
return null
}
@@ -166,7 +168,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
label = otpElement.type.name
value = otpElement.token
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener {
setCopyButtonClickListener { _, _ ->
mOnCopyActionClickListener?.invoke(Field(
otpElement.type.name,
ProtectedString(false, otpElement.token)))

View File

@@ -1,6 +1,7 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Build
import android.text.InputFilter
import android.text.InputType
import android.util.AttributeSet
@@ -41,11 +42,11 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT)
inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
}
maxLines = 1
@@ -61,7 +62,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
resources.displayMetrics
).toInt()
it.addRule(ALIGN_PARENT_RIGHT)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.addRule(ALIGN_PARENT_END)
}
}
@@ -82,7 +83,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, actionImageButtonId)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, actionImageButtonId)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
@@ -20,16 +20,20 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Build
import android.text.InputFilter
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
@@ -39,22 +43,130 @@ import com.kunzisoft.keepass.utils.UriUtil
class TextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle), GenericTextFieldView {
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
private val labelView: TextView
private val valueView: TextView
private val showButtonView: ImageView
private val copyButtonView: ImageView
private var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId()
private val labelView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT
).also {
it.leftMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.marginStart = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
}
}
}
private val valueView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextEntryItem)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT).also {
it.topMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
it.leftMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.marginStart = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
}
}
setTextIsSelectable(true)
}
private var showButton = AppCompatImageButton(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT)
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_visibility_state))
contentDescription = context.getString(R.string.menu_showpass)
}
private var copyButton = AppCompatImageButton(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT)
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_content_copy_white_24dp))
contentDescription = context.getString(R.string.menu_copy)
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_field, this)
buildViews()
addView(copyButton)
addView(showButton)
addView(labelView)
addView(valueView)
}
labelView = findViewById(R.id.entry_field_label)
valueView = findViewById(R.id.entry_field_value)
showButtonView = findViewById(R.id.entry_field_show)
copyButtonView = findViewById(R.id.entry_field_copy)
copyButtonView.visibility = View.GONE
private fun buildViews() {
copyButton.apply {
id = copyButtonId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END)
}
}
}
showButton.apply {
id = showButtonId
layoutParams = (layoutParams as LayoutParams?).also {
if (copyButton.isVisible) {
it?.addRule(LEFT_OF, copyButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, copyButtonId)
}
} else {
it?.addRule(ALIGN_PARENT_RIGHT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(ALIGN_PARENT_END)
}
}
}
}
labelView.apply {
id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId)
}
}
}
valueView.apply {
id = valueViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, showButtonId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, showButtonId)
}
it?.addRule(BELOW, labelViewId)
}
}
}
override fun applyFontVisibility(fontInVisibility: Boolean) {
@@ -115,19 +227,20 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) {
showButtonView.isVisible = protection
showButtonView.isSelected = hiddenProtectedValue
showButtonView.setOnClickListener {
showButtonView.isSelected = !showButtonView.isSelected
showButton.isVisible = protection
showButton.isSelected = hiddenProtectedValue
showButton.setOnClickListener {
showButton.isSelected = !showButton.isSelected
changeProtectedValueParameters()
}
changeProtectedValueParameters()
invalidate()
}
private fun changeProtectedValueParameters() {
valueView.apply {
if (showButtonView.isVisible) {
applyHiddenStyle(showButtonView.isSelected)
if (showButton.isVisible) {
applyHiddenStyle(showButton.isSelected)
} else {
linkify()
}
@@ -138,11 +251,11 @@ class TextFieldView @JvmOverloads constructor(context: Context,
when {
labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> {
val packageName = valueView.text.toString()
if (UriUtil.isExternalAppInstalled(context, packageName)) {
// TODO #996 if (UriUtil.isExternalAppInstalled(context, packageName)) {
valueView.customLink {
UriUtil.openExternalApp(context, packageName)
}
}
//}
}
else -> {
LinkifyCompat.addLinks(valueView, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES)
@@ -151,8 +264,8 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
fun getCopyButtonView(): View? {
if (copyButtonView.isVisible) {
return copyButtonView
if (copyButton.isVisible) {
return copyButton
}
return null
}
@@ -160,7 +273,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
fun setCopyButtonState(buttonState: ButtonState) {
when (buttonState) {
ButtonState.ACTIVATE -> {
copyButtonView.apply {
copyButton.apply {
visibility = VISIBLE
isActivated = false
}
@@ -170,7 +283,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
}
ButtonState.DEACTIVATE -> {
copyButtonView.apply {
copyButton.apply {
visibility = VISIBLE
// Reverse because isActivated show custom color and allow click
isActivated = true
@@ -181,7 +294,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
}
ButtonState.GONE -> {
copyButtonView.apply {
copyButton.apply {
visibility = GONE
setOnClickListener(null)
}
@@ -191,18 +304,24 @@ class TextFieldView @JvmOverloads constructor(context: Context,
}
}
}
invalidate()
}
fun setCopyButtonClickListener(onActionClickListener: OnClickListener?) {
setOnActionClickListener(onActionClickListener, null)
fun setCopyButtonClickListener(onActionClickListener: ((label: String, value: String) -> Unit)?) {
val clickListener = if (onActionClickListener != null)
OnClickListener { onActionClickListener.invoke(label, value) }
else
null
setOnActionClickListener(clickListener, null)
}
override fun setOnActionClickListener(
onActionClickListener: OnClickListener?,
actionImageId: Int?
) {
copyButtonView.setOnClickListener(onActionClickListener)
copyButtonView.isVisible = onActionClickListener != null
copyButton.setOnClickListener(onActionClickListener)
copyButton.isVisible = onActionClickListener != null
invalidate()
}
override var isFieldVisible: Boolean
@@ -213,6 +332,11 @@ class TextFieldView @JvmOverloads constructor(context: Context,
isVisible = value
}
override fun invalidate() {
super.invalidate()
buildViews()
}
enum class ButtonState {
ACTIVATE, DEACTIVATE, GONE
}

View File

@@ -2,21 +2,27 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Build
import android.text.InputType
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
class TextSelectFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
@@ -28,31 +34,55 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
private var actionImageButtonId = ViewCompat.generateViewId()
private var mDefaultPosition = 0
private val labelView = AppCompatTextView(context).apply {
setTextAppearance(context, R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT).also {
val leftMargin = 4f
it.leftMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
leftMargin,
resources.displayMetrics
).toInt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.marginStart = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
leftMargin,
resources.displayMetrics
).toInt()
}
}
}
private val valueSpinnerView = Spinner(context).apply {
private val labelView = TextInputLayout(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT)
}
private val valueView = TextInputEditText(
ContextThemeWrapper(context,
R.style.KeepassDXStyle_TextInputLayout)
).apply {
layoutParams = LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT)
inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
}
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_white_24dp)
?.apply {
mutate().colorFilter = BlendModeColorFilterCompat
.createBlendModeColorFilterCompat(currentTextColor, BlendModeCompat.SRC_IN)
}
setCompoundDrawablesWithIntrinsicBounds(
null,
null,
drawable,
null
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
drawable,
null
)
}
isFocusable = false
inputType = InputType.TYPE_NULL
maxLines = 1
}
private val valueSpinnerView = Spinner(context).apply {
layoutParams = LayoutParams(
0,
0
)
}
private var actionImageButton = AppCompatImageButton(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
layoutParams = LayoutParams(
@@ -64,7 +94,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
resources.displayMetrics
).toInt()
it.addRule(ALIGN_PARENT_RIGHT)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it.addRule(ALIGN_PARENT_END)
}
}
@@ -75,15 +105,22 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
init {
// Manually write view to avoid view id bugs
buildViews()
labelView.addView(valueView)
addView(labelView)
addView(valueSpinnerView)
addView(actionImageButton)
valueView.apply {
setOnClickListener {
valueSpinnerView.performClick()
}
}
valueSpinnerView.apply {
adapter = valueSpinnerAdapter
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
valueSpinnerAdapter.getItem(position)
val stringValue = valueSpinnerAdapter.getItem(position)
valueView.setText(stringValue)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
@@ -95,7 +132,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
id = labelViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, actionImageButtonId)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, actionImageButtonId)
}
}
@@ -104,7 +141,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context,
id = valueViewId
layoutParams = (layoutParams as LayoutParams?).also {
it?.addRule(LEFT_OF, actionImageButtonId)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
it?.addRule(START_OF, actionImageButtonId)
}
it?.addRule(BELOW, labelViewId)

View File

@@ -5,29 +5,23 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.otp.OtpElement
import java.util.*
class EntryEditViewModel: NodeEditViewModel() {
private val mTempAttachments = mutableListOf<EntryAttachmentState>()
val entryInfo : LiveData<EntryInfo> get() = _entryInfo
private val _entryInfo = MutableLiveData<EntryInfo>()
val templatesEntry : LiveData<TemplatesEntry> get() = _templatesEntry
private val _templatesEntry = MutableLiveData<TemplatesEntry>()
val requestEntryInfoUpdate : LiveData<EntryUpdate> get() = _requestEntryInfoUpdate
private val _requestEntryInfoUpdate = SingleLiveEvent<EntryUpdate>()
val onEntrySaved : LiveData<EntrySave> get() = _onEntrySaved
private val _onEntrySaved = SingleLiveEvent<EntrySave>()
val templates : LiveData<TemplatesLoad> get() = _templates
private val _templates = MutableLiveData<TemplatesLoad>()
val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged
private val _onTemplateChanged = SingleLiveEvent<Template>()
@@ -69,15 +63,6 @@ class EntryEditViewModel: NodeEditViewModel() {
{
val templates = database.getTemplates(isTemplate)
val entryTemplate = entry?.let { database.getTemplate(it) } ?: Template.STANDARD
TemplatesLoad(templates, entryTemplate)
},
{ templatesLoad ->
// Call templates init before populate entry info
_templates.value = templatesLoad
changeTemplate(templatesLoad!!.defaultTemplate)
IOActionTask(
{
var entryInfo: EntryInfo? = null
// Decode the entry / load entry info
entry?.let {
@@ -95,12 +80,10 @@ class EntryEditViewModel: NodeEditViewModel() {
}
}
}
entryInfo
TemplatesEntry(templates, entryTemplate, entryInfo)
},
{ entryInfo ->
_entryInfo.value = entryInfo
}
).execute()
{ templatesEntry ->
_templatesEntry.value = templatesEntry
}
).execute()
}
@@ -239,7 +222,7 @@ class EntryEditViewModel: NodeEditViewModel() {
_onBinaryPreviewLoaded.value = AttachmentPosition(entryAttachmentState, viewPosition)
}
data class TemplatesLoad(val templates: List<Template>, val defaultTemplate: Template)
data class TemplatesEntry(val templates: List<Template>, val defaultTemplate: Template, val entryInfo: EntryInfo?)
data class EntryUpdate(val database: Database?, val entry: Entry?, val parent: Group?)
data class EntrySave(val oldEntry: Entry, val newEntry: Entry, val parent: Group?)
data class FieldEdition(val oldField: Field?, val newField: Field?)

View File

@@ -45,9 +45,6 @@ class EntryViewModel: ViewModel() {
val historyPosition : LiveData<Int> get() = _historyPosition
private val _historyPosition = MutableLiveData<Int>()
val url : LiveData<String?> get() = _url
private val _url = MutableLiveData<String?>()
val entryInfo : LiveData<EntryInfo> get() = _entryInfo
private val _entryInfo = MutableLiveData<EntryInfo>()
@@ -66,34 +63,24 @@ class EntryViewModel: ViewModel() {
private val _historySelected = SingleLiveEvent<EntryHistory>()
fun loadEntry(database: Database?, entryId: NodeId<UUID>?, historyPosition: Int) {
if (entryId != null) {
if (database != null && entryId != null) {
IOActionTask(
{
database?.getEntryById(entryId)
},
{ mainEntry ->
// Manage current version and history
_mainEntryId.value = mainEntry?.nodeId
_historyPosition.value = historyPosition
val mainEntry = database.getEntryById(entryId)
val currentEntry = if (historyPosition > -1) {
mainEntry?.getHistory()?.get(historyPosition)
} else {
mainEntry
}
_url.value = currentEntry?.url
IOActionTask(
{
val entryTemplate = currentEntry?.let {
database?.getTemplate(it)
database.getTemplate(it)
} ?: Template.STANDARD
// To simplify template field visibility
currentEntry?.let { entry ->
// Add mainEntry to check the parent and define the template state
database?.decodeEntryWithTemplateConfiguration(entry, mainEntry)
?.let {
database.decodeEntryWithTemplateConfiguration(entry, mainEntry).let {
// To update current modification time
it.touch(modified = false, touchParents = false)
@@ -103,6 +90,7 @@ class EntryViewModel: ViewModel() {
}
EntryInfoHistory(
mainEntry!!.nodeId,
entryTemplate,
it.getEntryInfo(database),
entryInfoHistory
@@ -112,6 +100,8 @@ class EntryViewModel: ViewModel() {
},
{ entryInfoHistory ->
if (entryInfoHistory != null) {
_mainEntryId.value = entryInfoHistory.mainEntryId
_historyPosition.value = historyPosition
_template.value = entryInfoHistory.template
_entryInfo.value = entryInfoHistory.entryInfo
_entryHistory.value = entryInfoHistory.entryHistory
@@ -119,8 +109,6 @@ class EntryViewModel: ViewModel() {
}
).execute()
}
).execute()
}
}
fun updateEntry(database: Database?) {
@@ -143,7 +131,8 @@ class EntryViewModel: ViewModel() {
_historySelected.value = EntryHistory(NodeIdUUID(item.id), null, item, position)
}
data class EntryInfoHistory(val template: Template,
data class EntryInfoHistory(var mainEntryId: NodeId<UUID>,
val template: Template,
val entryInfo: EntryInfo,
val entryHistory: List<EntryInfo>)
// Custom data class to manage entry to retrieve and define is it's an history item (!= -1)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccent" />
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_selected="true" android:color="@color/white"/>
<item android:color="?android:attr/textColorSecondary"/>
<item android:color="@color/grey"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white_dark" />
</selector>

View File

@@ -7,6 +7,6 @@
android:innerRadiusRatio="2.5"
android:thickness="2dp"
android:useLevel="true">
<solid android:color="@color/orange" />
<solid android:color="@color/progress_color" />
</shape>
</rotate>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="270"
android:toDegrees="270">
@@ -8,6 +7,6 @@
android:innerRadiusRatio="2.5"
android:thickness="2dp"
android:useLevel="true">
<solid android:color="?attr/colorAccent" />
<solid android:color="@color/list_secondary_color" />
</shape>
</rotate>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601754 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844607 8.7519531 19.306641 C 5.4810648 17.911181 3.4461927 14.150571 4.109375 10.648438 C 4.6649663 7.2806973 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801388 19.308004 7.2166098 19.861328 10.574219 C 20.123351 12.069186 19.935392 13.632673 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671735 12.714066 22.12099 8.4920873 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995703 13 2.0644531 z M 11 7.0644531 L 11 12.064453 L 7 12.064453 L 12 17.064453 L 17 12.064453 L 13 12.064453 L 13 7.0644531 L 11 7.0644531 z" />
</vector>

View File

@@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24" >
<path android:fillColor="#FFFFFF" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601753 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844605 8.7519531 19.306641 C 5.481064 17.911182 3.4461934 14.150571 4.109375 10.648438 C 4.6649664 7.2806974 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801387 19.308002 7.2166101 19.861328 10.574219 C 20.123352 12.069186 19.935393 13.632674 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671737 12.714066 22.120988 8.4920871 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995704 13 2.0644531 z M 8.1113281 7 C 7.4946611 7 7 7.5002173 7 8.1113281 L 7 15.888672 C 7 16.499784 7.4946611 17 8.1113281 17 L 15.888672 17 C 16.499783 17 17 16.499784 17 15.888672 L 17 9.2226562 L 14.777344 7 L 8.1113281 7 z M 8.1113281 8.1113281 L 13.666016 8.1113281 L 13.666016 10.333984 L 8.1113281 10.333984 L 8.1113281 8.1113281 z M 12 12.554688 C 12.922223 12.554688 13.666016 13.300434 13.666016 14.222656 C 13.666016 15.144878 12.922223 15.888672 12 15.888672 C 11.077779 15.888672 10.333984 15.144878 10.333984 14.222656 C 10.333984 13.300434 11.077779 12.554687 12 12.554688 z" />
</vector>

View File

@@ -70,16 +70,16 @@
tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar>
<ProgressBar
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/entry_progress"
android:visibility="gone"
style="?android:attr/progressBarStyleHorizontal"
android:indeterminate="false"
app:indicatorColor="?attr/colorAccent"
android:progress="10"
android:max="30"
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="4dp" />
android:layout_height="wrap_content" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -29,15 +29,14 @@
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/toolbarSpecialAppearance"
app:layout_constraintTop_toTopOf="parent" />
android:theme="?attr/toolbarSpecialAppearance" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/group_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toTopOf="@+id/toolbar_action">
android:layout_height="match_parent"
android:layout_below="@+id/special_mode_view"
android:layout_above="@+id/toolbar_action">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -47,14 +46,6 @@
android:elevation="4dp"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:titleEnabled="false"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/app_name"
@@ -82,8 +73,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:gravity="center_vertical"
android:baselineAligned="false">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
@@ -94,26 +86,21 @@
android:layout_gravity="end|center_vertical"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:scaleType="fitXY" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="3"
style="@style/KeepassDXStyle.TextAppearance.Info"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
style="@style/KeepassDXStyle.TextAppearance.Info" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="start|center_vertical"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginLeft="14dp"
android:layout_marginStart="14dp"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/group_name"
@@ -136,10 +123,9 @@
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
<RelativeLayout
android:id="@+id/node_list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -151,7 +137,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground" />
</LinearLayout>
</RelativeLayout>
<com.kunzisoft.keepass.view.AddNodeButtonView
android:id="@+id/add_node_button"
@@ -167,8 +153,7 @@
android:layout_height="?attr/actionBarSize"
android:visibility="gone"
android:theme="?attr/toolbarActionAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
android:layout_alignParentBottom="true" />
<FrameLayout
android:layout_width="match_parent"
@@ -185,7 +170,6 @@
layout="@layout/view_button_lock"
android:layout_width="@dimen/lock_button_size"
android:layout_height="@dimen/lock_button_size"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
android:layout_alignParentBottom="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>

View File

@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -29,17 +29,16 @@
android:id="@+id/special_mode_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/toolbarSpecialAppearance"
app:layout_constraintTop_toTopOf="parent" />
android:theme="?attr/toolbarSpecialAppearance" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/activity_password_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="match_parent"
android:background="@drawable/background_repeat"
android:backgroundTint="?android:attr/textColor"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
app:layout_constraintBottom_toTopOf="@+id/activity_password_footer">
android:layout_below="@+id/special_mode_view"
android:layout_above="@+id/activity_password_footer">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
@@ -111,12 +110,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:elevation="4dp"
android:paddingTop="@dimen/default_margin"
android:paddingLeft="@dimen/default_margin"
android:paddingStart="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
android:paddingEnd="@dimen/default_margin"
android:paddingBottom="@dimen/default_margin"
android:paddingTop="6dp"
android:paddingLeft="12dp"
android:paddingStart="12dp"
android:paddingRight="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp"
android:background="?android:attr/windowBackground"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
@@ -214,7 +213,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent">
android:layout_alignParentBottom="true">
<LinearLayout
android:id="@+id/activity_password_info_container"
android:layout_width="match_parent"
@@ -250,4 +249,4 @@
android:text="@string/menu_open" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>

View File

@@ -17,28 +17,24 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:targetApi="o"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<com.kunzisoft.keepass.view.TemplateEditView
android:id="@+id/template_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/entry_attachments_container"/>
android:layout_height="wrap_content"/>
<androidx.cardview.widget.CardView
android:id="@+id/entry_attachments_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/cardViewStyle"
app:layout_constraintTop_toBottomOf="@+id/template_view"
app:layout_constraintBottom_toBottomOf="parent">
style="?attr/cardViewStyle">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
@@ -62,4 +58,4 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -38,10 +38,11 @@
android:text="@string/entry_history"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="3"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp">
@@ -49,51 +50,30 @@
android:id="@+id/entry_history_last_modified"
android:text="@string/entry_modified"
style="@style/KeepassDXStyle.TextAppearance.LabelTableTextStyle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_title"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_weight="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_title"
android:text="@string/entry_title"
style="@style/KeepassDXStyle.TextAppearance.LabelTableTextStyle"
app:layout_constraintStart_toEndOf="@+id/entry_history_last_modified"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_username"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_weight="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_username"
android:text="@string/entry_user_name"
style="@style/KeepassDXStyle.TextAppearance.LabelTableTextStyle"
app:layout_constraintStart_toEndOf="@+id/entry_history_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_url"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_url"
android:text="@string/entry_url"
style="@style/KeepassDXStyle.TextAppearance.LabelTableTextStyle"
app:layout_constraintStart_toEndOf="@+id/entry_history_username"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
android:layout_weight="1" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_history_list"

View File

@@ -18,7 +18,9 @@
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
@@ -55,9 +57,9 @@
android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.WarningTextStyle"/>
<ProgressBar
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar"
style="?android:attr/progressBarStyleHorizontal"
app:indicatorColor="?attr/colorAccent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"

View File

@@ -38,43 +38,40 @@
android:background="?android:attr/windowBackground"
android:scaleType="fitStart" />
<androidx.constraintlayout.widget.ConstraintLayout
<RelativeLayout
android:id="@+id/item_attachment_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="48dp"
android:layout_alignBottom="@+id/item_attachment_thumbnail"
android:background="?attr/cardBackgroundTransparentColor">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_broken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@string/entry_attachments"
android:src="@drawable/ic_attach_file_broken_white_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:src="@drawable/ic_attach_file_broken_white_24dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_title"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintStart_toEndOf="@+id/item_attachment_broken"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toStartOf="@+id/item_attachment_size_container"
android:layout_toLeftOf="@+id/item_attachment_size_container"
android:layout_toEndOf="@+id/item_attachment_broken"
android:layout_toRightOf="@+id/item_attachment_broken"
android:gravity="center_vertical"
tools:text="BinaryFile.attach" />
<LinearLayout
android:id="@+id/item_attachment_size_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_height="match_parent"
android:gravity="center_vertical|end"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_action_container"
app:layout_constraintTop_toTopOf="parent">
android:layout_toLeftOf="@+id/item_attachment_action_container"
android:layout_toStartOf="@+id/item_attachment_action_container">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_attachment_size"
@@ -101,10 +98,9 @@
<FrameLayout
android:id="@+id/item_attachment_action_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_attachment_delete_button"
@@ -150,5 +146,5 @@
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</RelativeLayout>

View File

@@ -17,65 +17,43 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="3"
android:background="?android:attr/selectableItemBackground">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_last_modified"
tools:text = "Last Modified"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_title"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_title"
tools:text = "Title"
app:layout_constraintStart_toEndOf="@+id/entry_history_last_modified"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_username"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_username"
tools:text = "Username"
app:layout_constraintStart_toEndOf="@+id/entry_history_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_history_url"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_url"
tools:text = "URL"
app:layout_constraintStart_toEndOf="@+id/entry_history_username"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
android:layout_weight="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_title"
tools:text = "Title"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_history_username"
tools:text = "Username"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
android:maxLines="6"
android:ellipsize="end"
android:gravity="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -25,15 +25,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Selectable.Item">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:minHeight="56dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?android:attr/selectableItemBackground" >
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_icon"
android:layout_width="32dp"
@@ -44,67 +49,129 @@
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginRight="@dimen/image_list_margin_vertical"
android:layout_marginEnd="@dimen/image_list_margin_vertical"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/ic_blank_32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<LinearLayout
android:id="@+id/node_container_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="2dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginRight="@dimen/image_list_margin_vertical"
android:layout_marginEnd="@dimen/image_list_margin_vertical"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/node_icon"
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintEnd_toStartOf="@+id/node_attachment_icon"
app:layout_constraintRight_toLeftOf="@+id/node_attachment_icon">
app:layout_constraintEnd_toStartOf="@+id/node_options"
app:layout_constraintRight_toLeftOf="@+id/node_options">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_text"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.Entry.Title"
android:layout_width="wrap_content"
tools:text="Node Title"
android:maxLines="2"
android:layout_height="wrap_content"
android:ellipsize="end"
style="@style/KeepassDXStyle.TextAppearance.Entry.Title" />
android:maxLines="2"
tools:text="Node Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_subtext"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
tools:text="Node SubTitle"
android:lines="1"
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle" />
tools:text="Node SubTitle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_meta"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.Entry.Meta"
android:layout_width="wrap_content"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C"
android:layout_height="wrap_content"
android:lines="1"
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Entry.Meta" />
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_attachment_icon"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/node_options"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/node_otp_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/node_attachment_icon"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/node_otp_token"
style="@style/KeepassDXStyle.TextAppearance.Entry.Info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="5136" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp">
<ProgressBar
android:id="@+id/node_otp_progress"
style="@style/KeepassDXStyle.ProgressBar.Circle.Secondary"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:max="100"
android:progress="60" />
<TextView
android:id="@+id/node_otp_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="@style/KeepassDXStyle.TextAppearance.Entry.Info"
android:textSize="11sp"
tools:text="70" />
</FrameLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_attachment_icon"
style="@style/KeepassDXStyle.TextAppearance.Entry.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/image_list_margin_vertical"
android:layout_marginStart="@dimen/image_list_margin_vertical"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:src="@drawable/ic_attach_file_white_24dp"
style="@style/KeepassDXStyle.TextAppearance.Entry.Icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/node_otp_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -25,12 +25,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Selectable.Item">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:minHeight="56dp"
android:maxHeight="72dp"
app:layout_constraintWidth_percent="@dimen/content_percent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?android:attr/selectableItemBackground" >
@@ -66,14 +70,14 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/node_icon"
android:layout_toRightOf="@+id/node_icon"
android:layout_toStartOf="@+id/node_image_identifier"
android:layout_toLeftOf="@+id/node_image_identifier"
android:orientation="vertical"
android:paddingTop="2dp"
android:paddingBottom="4dp">
android:layout_toEndOf="@+id/node_icon"
android:layout_toRightOf="@+id/node_icon"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_text"
@@ -84,25 +88,27 @@
android:gravity="center_vertical"
android:maxLines="2"
tools:text="Node Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_subtext"
style="@style/KeepassDXStyle.TextAppearance.Group.SubTitle"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:gravity="center_vertical"
android:lines="1"
android:singleLine="true"
android:visibility="gone"
tools:text="Node SubTitle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/node_meta"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.Group.Meta"
android:layout_width="wrap_content"
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C"
android:layout_height="wrap_content"
android:lines="1"
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Group.Meta" />
tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView

View File

@@ -27,10 +27,10 @@
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="2"
android:paddingStart="6dp"
android:paddingLeft="6dp"
android:paddingEnd="6dp"
android:paddingRight="6dp"
android:paddingStart="@dimen/default_margin"
android:paddingLeft="@dimen/default_margin"
android:paddingEnd="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
style="@style/KeepassDXStyle.TextAppearance.Large"
tools:text="Test" />
</FrameLayout>

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="2dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/entry_field_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="title"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<TextView
android:id="@+id/entry_field_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_field_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="value"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_field_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_field_copy"
android:src="@drawable/ic_visibility_state"
android:contentDescription="@string/menu_showpass"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_field_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,22 +17,20 @@
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:targetApi="o"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/template_header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/cardViewStyle"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/template_fields_container">
android:visibility="gone">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
@@ -61,8 +59,6 @@
android:id="@+id/template_fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/template_header_container"
app:layout_constraintBottom_toTopOf="@+id/not_referenced_fields_container"
android:orientation="vertical" />
<com.kunzisoft.keepass.view.SectionView
@@ -70,16 +66,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/cardViewStyle"
android:layout_marginBottom="@dimen/card_view_margin_vertical"
app:layout_constraintTop_toBottomOf="@+id/template_fields_container"
app:layout_constraintBottom_toTopOf="@+id/custom_fields_container"/>
android:layout_marginBottom="@dimen/card_view_margin_vertical"/>
<com.kunzisoft.keepass.view.SectionView
android:id="@+id/custom_fields_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/cardViewStyle"
app:layout_constraintTop_toBottomOf="@+id/not_referenced_fields_container"
app:layout_constraintBottom_toBottomOf="parent"/>
style="?attr/cardViewStyle"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -20,13 +20,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_save_database"
android:icon="@drawable/ic_save_white_24dp"
android:icon="@drawable/ic_saving_white_24dp"
android:title="@string/menu_save_database"
android:orderInCategory="95"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
<item android:id="@+id/menu_reload_database"
android:icon="@drawable/ic_reload_white_24dp"
android:icon="@drawable/ic_downloading_white_24dp"
android:title="@string/menu_reload_database"
android:orderInCategory="96"
app:iconTint="?attr/colorControlNormal"

View File

@@ -33,5 +33,5 @@
<dimen name="hidden_lock_button_size">0dp</dimen>
<dimen name="content_percent">1</dimen>
<dimen name="toolbar_parallax_height">160dp</dimen>
<integer name="animation_duration">320</integer>
<integer name="animation_duration">260</integer>
</resources>

View File

@@ -190,6 +190,8 @@
<bool name="monospace_font_fields_enable_default" translatable="false">true</bool>
<string name="hide_expired_entries_key" translatable="false">hide_expired_entries_key</string>
<bool name="hide_expired_entries_default" translatable="false">false</bool>
<string name="show_otp_token_key" translatable="false">show_otp_token_key</string>
<bool name="show_otp_token_default" translatable="false">true</bool>
<string name="show_uuid_key" translatable="false">show_uuid_key</string>
<bool name="show_uuid_default" translatable="false">false</bool>
<string name="enable_education_screens_key" translatable="false">enable_education_screens_key</string>

View File

@@ -628,5 +628,7 @@
<string name="hide_expired_entries_title">Hide expired entries</string>
<string name="hide_expired_entries_summary">Expired entries are not shown</string>
<string name="show_uuid_title">Show UUID</string>
<string name="show_uuid_summary">Displays the UUID linked to an entry</string>
<string name="show_uuid_summary">Displays the UUID linked to an entry or a group</string>
<string name="show_otp_token_title">Show OTP Token</string>
<string name="show_otp_token_summary">Displays OTP tokens in the list of entries</string>
</resources>

View File

@@ -245,8 +245,8 @@
<item name="colorControlActivated">?attr/colorAccent</item>
</style>
<style name="KeepassDXStyle.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="android:paddingTop">1dp</item>
<item name="android:paddingBottom">1dp</item>
<item name="android:paddingTop">2dp</item>
<item name="android:paddingBottom">2dp</item>
<item name="boxStrokeColor">@color/edit_text_stroke_color</item>
<item name="hintTextColor">?attr/colorAccent</item>
<item name="materialThemeOverlay">@style/ThemeOverlay.KeepassDXTheme.TextInputEditText</item>
@@ -352,35 +352,41 @@
<!-- Nodes Text Style -->
<style name="KeepassDXStyle.TextAppearance.Entry.Title" parent="KeepassDXStyle.TextAppearance">
<item name="android:textColor">@color/entry_title_color</item>
<item name="android:tint">@color/entry_title_color</item>
<item name="android:textColor">@color/list_color</item>
<item name="android:tint">@color/list_color</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">16sp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Entry.SubTitle" parent="KeepassDXStyle.TextAppearance.Small">
<item name="android:textColor">@color/entry_subtitle_color</item>
<item name="android:tint">@color/entry_subtitle_color</item>
<item name="android:textColor">@color/list_secondary_color</item>
<item name="android:tint">@color/list_secondary_color</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Entry.Meta" parent="KeepassDXStyle.TextAppearance.Tiny">
<item name="android:textColor">@color/entry_title_color</item>
<item name="android:tint">@color/entry_title_color</item>
<item name="android:textColor">@color/list_color</item>
<item name="android:tint">@color/list_color</item>
<item name="android:textSize">11sp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Entry.Info" parent="KeepassDXStyle.TextAppearance.Small">
<item name="android:textColor">@color/list_secondary_color</item>
<item name="android:tint">@color/list_secondary_color</item>
<item name="android:textSize">16sp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Entry.Icon" parent="KeepassDXStyle.TextAppearance.Small">
<item name="tint">@color/entry_subtitle_color</item>
<item name="tint">@color/list_secondary_color</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Group.Title" parent="KeepassDXStyle.TextAppearance">
<item name="android:textColor">@color/group_title_color</item>
<item name="android:tint">@color/group_title_color</item>
<item name="android:textColor">@color/list_primary_color</item>
<item name="android:tint">@color/list_primary_color</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">16sp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Group.SubTitle" parent="KeepassDXStyle.TextAppearance.Small">
<item name="android:textColor">@color/group_subtitle_color</item>
<item name="android:tint">@color/group_subtitle_color</item>
<item name="android:textColor">@color/list_secondary_color</item>
<item name="android:tint">@color/list_secondary_color</item>
</style>
<style name="KeepassDXStyle.TextAppearance.Group.Meta" parent="KeepassDXStyle.TextAppearance.Tiny">
<item name="android:textColor">@color/group_title_color</item>
<item name="android:tint">@color/group_title_color</item>
<item name="android:textColor">@color/list_primary_color</item>
<item name="android:tint">@color/list_primary_color</item>
<item name="android:textSize">11sp</item>
</style>
@@ -463,18 +469,29 @@
<item name="android:textColor">?attr/colorAccent</item>
<item name="android:textSize">12sp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingStart">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:paddingEnd">4dp</item>
</style>
<style name="KeepassDXStyle.TextAppearance.LabelTableTextStyle" parent="KeepassDXStyle.TextAppearance">
<item name="android:textSize">12sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="KeepassDXStyle.TextAppearance.TextEntryItem" parent="KeepassDXStyle.TextAppearance">
<item name="android:padding">5sp</item>
<item name="android:layout_marginTop">4dp</item>
<item name="android:layout_marginBottom">4dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingStart">8dp</item>
<item name="android:paddingRight">8dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:textSize">16sp</item>
</style>
<!-- Progress bar -->
<style name="KeepassDXStyle.ProgressBar.Circle.Secondary" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:progressDrawable">@drawable/foreground_progress_circle_secondary</item>
<item name="android:background">@null</item>
</style>
<style name="KeepassDXStyle.ProgressBar.Circle" parent="Widget.AppCompat.ProgressBar.Horizontal">
<item name="android:progressDrawable">@drawable/foreground_progress_circle</item>
<item name="android:background">@drawable/background_progress_circle</item>

View File

@@ -80,6 +80,11 @@
android:title="@string/hide_expired_entries_title"
android:summary="@string/hide_expired_entries_summary"
android:defaultValue="@bool/hide_expired_entries_default"/>
<SwitchPreference
android:key="@string/show_otp_token_key"
android:title="@string/show_otp_token_title"
android:summary="@string/show_otp_token_summary"
android:defaultValue="@bool/show_otp_token_default"/>
<SwitchPreference
android:key="@string/show_uuid_key"
android:title="@string/show_uuid_title"

59
art/reload_database.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg4"
sodipodi:docname="Save.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#c8c8c8"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0.28235294"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview6"
showgrid="true"
inkscape:zoom="11.313709"
inkscape:cx="10.124302"
inkscape:cy="14.525681"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4">
<inkscape:grid
type="xygrid"
id="grid818" />
</sodipodi:namedview>
<path
style="fill:#ffffff"
d="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601754 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844607 8.7519531 19.306641 C 5.4810648 17.911181 3.4461927 14.150571 4.109375 10.648438 C 4.6649663 7.2806973 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801388 19.308004 7.2166098 19.861328 10.574219 C 20.123351 12.069186 19.935392 13.632673 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671735 12.714066 22.12099 8.4920873 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995703 13 2.0644531 z M 11 7.0644531 L 11 12.064453 L 7 12.064453 L 12 17.064453 L 17 12.064453 L 13 12.064453 L 13 7.0644531 L 11 7.0644531 z "
id="path868" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

59
art/save_database.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg4"
sodipodi:docname="Save2.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#c8c8c8"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0.28235294"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview6"
showgrid="true"
inkscape:zoom="11.313709"
inkscape:cx="-2.2942603"
inkscape:cy="14.525681"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4">
<inkscape:grid
type="xygrid"
id="grid818" />
</sodipodi:namedview>
<path
style="fill:#ffffff"
d="M 11 2.0566406 C 6.762335 2.4220229 3.0067094 5.7987155 2.203125 9.9785156 C 1.3601753 13.960549 3.1781148 18.394742 6.7089844 20.480469 C 9.6237318 22.368157 13.514425 22.492178 16.582031 20.892578 C 17.959775 20.180473 19.316015 19.099467 20.087891 17.808594 L 18.402344 16.791016 C 16.277892 19.724364 12.039121 20.844605 8.7519531 19.306641 C 5.481064 17.911182 3.4461934 14.150571 4.109375 10.648438 C 4.6649664 7.2806974 7.5784749 4.4226117 11 4.0839844 L 11 2.0566406 z M 13 2.0644531 L 13 4.09375 C 16.367309 4.4801387 19.308002 7.2166101 19.861328 10.574219 C 20.123352 12.069186 19.935393 13.632674 19.367188 15.037109 C 19.94644 15.387646 20.527063 15.73602 21.105469 16.087891 C 22.671737 12.714066 22.120988 8.4920871 19.708984 5.6542969 C 18.063396 3.6246553 15.604973 2.2995704 13 2.0644531 z M 8.1113281 7 C 7.4946611 7 7 7.5002173 7 8.1113281 L 7 15.888672 C 7 16.499784 7.4946611 17 8.1113281 17 L 15.888672 17 C 16.499783 17 17 16.499784 17 15.888672 L 17 9.2226562 L 14.777344 7 L 8.1113281 7 z M 8.1113281 8.1113281 L 13.666016 8.1113281 L 13.666016 10.333984 L 8.1113281 10.333984 L 8.1113281 8.1113281 z M 12 12.554688 C 12.922223 12.554688 13.666016 13.300434 13.666016 14.222656 C 13.666016 15.144878 12.922223 15.888672 12 15.888672 C 11.077779 15.888672 10.333984 15.144878 10.333984 14.222656 C 10.333984 13.300434 11.077779 12.554687 12 12.554688 z "
id="path868" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,5 +1,6 @@
* Add / Manage dynamic templates #191
* Allow to manually select RecycleBin group and Templates group #191
* Setting to display OTP Token in list #655
* Fix timeout in dialogs #716
* Check URI permissions #626
* Small changes #1035 #1043 #942
* Improvements #1035 #1043 #942 #1021 #1027

View File

@@ -1,5 +1,6 @@
* Ajout / Gestion dynamique des templates #191
* Sélection manuelle des groupes de la corbeille et des templates #191
* Paramètres pour afficher les jetons OTP dans la liste #655
* Correction du délai d'expiration dans les dialogues #716
* Vérification des permissions URI #626
* Petits changements #1035 #1043 #942
* Améliorations #1035 #1043 #942 #1021 #1027