Add & edit custom icon name #976

This commit is contained in:
J-Jamet
2021-09-28 18:12:49 +02:00
parent 9b847a0561
commit f0fdd4a537
14 changed files with 256 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
KeePassDX(3.1.0) KeePassDX(3.1.0)
* Change default Argon2 parameters #1098 * Change default Argon2 parameters #1098
* Add & edit custom icon name #976
KeePassDX(3.0.2) KeePassDX(3.0.2)
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong) * Samsung DeX mode #1114 #245 (Thx @chenxiaolong)

View File

@@ -33,6 +33,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
@@ -139,6 +140,16 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
uploadButton.isEnabled = true uploadButton.isEnabled = true
} }
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
iconCustomUpdated.errorConsumed = true
}
iconCustomUpdated.iconCustom?.let {
mDatabase?.updateCustomIcon(it)
}
iconPickerViewModel.deselectAllCustomIcons()
}
} }
override fun viewToInvalidateTimeout(): View? { override fun viewToInvalidateTimeout(): View? {
@@ -197,6 +208,10 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean { override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.menu_edit)?.apply {
isEnabled = mIconsSelected.size == 1
isVisible = isEnabled
}
menu?.findItem(R.id.menu_delete)?.apply { menu?.findItem(R.id.menu_delete)?.apply {
isEnabled = mCustomIconsSelectionMode isEnabled = mCustomIconsSelectionMode
isVisible = isEnabled isVisible = isEnabled
@@ -213,6 +228,9 @@ class IconPickerActivity : DatabaseLockActivity() {
onBackPressed() onBackPressed()
} }
} }
R.id.menu_edit -> {
updateCustomIcon(mIconsSelected[0])
}
R.id.menu_delete -> { R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove -> mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove) removeCustomIcon(iconToRemove)
@@ -277,6 +295,11 @@ class IconPickerActivity : DatabaseLockActivity() {
} }
} }
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
IconEditDialogFragment.update(iconImageCustom)
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) { private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false uploadButton.isEnabled = false
iconPickerViewModel.deselectAllCustomIcons() iconPickerViewModel.deselectAllCustomIcons()

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2021 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/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
class IconEditDialogFragment : DatabaseDialogFragment() {
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
private lateinit var iconView: ImageView
private lateinit var nameTextLayoutView: TextInputLayout
private lateinit var nameTextView: TextView
private var mCustomIcon: IconImageCustom? = null
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
mPopulateIconMethod = { imageView, icon ->
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
}
mCustomIcon?.let { customIcon ->
populateViewsWithCustomIcon(customIcon)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
iconView = root.findViewById(R.id.icon_edit_image)
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
nameTextView = root.findViewById(R.id.icon_edit_name)
if (savedInstanceState != null
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
} else {
arguments?.apply {
if (containsKey(KEY_CUSTOM_ICON_ID)) {
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
}
}
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok) { _, _ ->
retrieveIconInfoFromViews()
mCustomIcon?.let { customIcon ->
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(customIcon, false)
)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do nothing
mIconPickerViewModel.updateCustomIcon(
IconPickerViewModel.IconCustomState(null, false)
)
}
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
nameTextView.text = customIcon.name
}
private fun retrieveIconInfoFromViews() {
mCustomIcon?.name = nameTextView.text.toString()
mCustomIcon?.lastModificationTime = DateInstant()
}
override fun onSaveInstanceState(outState: Bundle) {
retrieveIconInfoFromViews()
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
super.onSaveInstanceState(outState)
}
companion object {
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
val bundle = Bundle()
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
val fragment = IconEditDialogFragment()
fragment.arguments = bundle
return fragment
}
}
}

View File

@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
iconCustomAdded?.iconCustom?.let { icon -> iconCustomAdded?.iconCustom?.let { icon ->
iconPickerAdapter.addIcon(icon) iconPickerAdapter.addIcon(icon)
iconCustomAdded.iconCustom = null iconCustomAdded.iconCustom = null
try {
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} catch (ignore: Exception) {}
} }
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
} }
} }
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved -> iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
} }
} }
} }
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
if (!iconCustomUpdated.error) {
iconCustomUpdated?.iconCustom?.let { icon ->
iconPickerAdapter.updateIcon(icon)
iconCustomUpdated.iconCustom = null
}
}
}
} }
override fun onIconClickListener(icon: IconImageCustom) { override fun onIconClickListener(icon: IconImageCustom) {

View File

@@ -147,6 +147,10 @@ class Database {
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
} }
fun updateCustomIcon(customIcon: IconImageCustom) {
iconsManager.getIcon(customIcon.uuid).updateWith(customIcon)
}
fun getTemplates(templateCreation: Boolean): List<Template> { fun getTemplates(templateCreation: Boolean): List<Template> {
return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf() return mDatabaseKDBX?.getTemplates(templateCreation) ?: listOf()
} }

View File

@@ -31,6 +31,10 @@ class CustomIconPool(private val binaryCache: BinaryCache) : BinaryPool<UUID>(bi
return newUUID return newUUID
} }
fun getCustomIcon(key: UUID): IconImageCustom? {
return customIcons[key]
}
fun any(predicate: (IconImageCustom)-> Boolean): Boolean { fun any(predicate: (IconImageCustom)-> Boolean): Boolean {
return customIcons.any { predicate(it.value) } return customIcons.any { predicate(it.value) }
} }

View File

@@ -32,6 +32,16 @@ class IconImageCustom : IconImageDraw {
var name: String = "" var name: String = ""
var lastModificationTime: DateInstant? = null var lastModificationTime: DateInstant? = null
fun updateWith(icon: IconImageCustom) {
this.name = icon.name
this.lastModificationTime = icon.lastModificationTime
}
constructor(copy: IconImageCustom) {
this.uuid = copy.uuid
updateWith(copy)
}
constructor(name: String = "", lastModificationTime: DateInstant? = null) { constructor(name: String = "", lastModificationTime: DateInstant? = null) {
this.uuid = DatabaseVersioned.UUID_ZERO this.uuid = DatabaseVersioned.UUID_ZERO
this.name = name this.name = name

View File

@@ -65,7 +65,7 @@ class IconsManager(binaryCache: BinaryCache) {
} }
fun getIcon(iconUuid: UUID): IconImageCustom { fun getIcon(iconUuid: UUID): IconImageCustom {
return IconImageCustom(iconUuid) return customCache.getCustomIcon(iconUuid) ?: IconImageCustom(iconUuid)
} }
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {

View File

@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.viewmodels
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -30,6 +31,10 @@ class IconPickerViewModel: ViewModel() {
MutableLiveData<IconCustomState>() MutableLiveData<IconCustomState>()
} }
val customIconUpdated : MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
fun pickStandardIcon(icon: IconImageStandard) { fun pickStandardIcon(icon: IconImageStandard) {
standardIconPicked.value = icon standardIconPicked.value = icon
} }
@@ -54,6 +59,10 @@ class IconPickerViewModel: ViewModel() {
customIconRemoved.value = customIcon customIconRemoved.value = customIcon
} }
fun updateCustomIcon(customIcon: IconCustomState) {
customIconUpdated.value = customIcon
}
data class IconCustomState(var iconCustom: IconImageCustom? = null, data class IconCustomState(var iconCustom: IconImageCustom? = null,
var error: Boolean = true, var error: Boolean = true,
var errorStringId: Int = -1, var errorStringId: Int = -1,

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 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.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/default_margin"
android:importantForAutofill="noExcludeDescendants"
tools:targetApi="o">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon_edit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginRight="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"
android:src="@drawable/ic_blank_32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/icon_edit_name_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/icon_edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:inputType="text"
android:maxLines="1"
android:singleLine="true"
android:hint="@string/hint_icon_name"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -19,6 +19,12 @@
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_edit"
android:icon="@drawable/ic_mode_edit_white_24dp"
android:title="@string/menu_edit"
android:orderInCategory="5"
app:iconTint="?attr/colorControlNormal"
app:showAsAction="ifRoom" />
<item android:id="@+id/menu_delete" <item android:id="@+id/menu_delete"
android:icon="@drawable/ic_delete_forever_white_24dp" android:icon="@drawable/ic_delete_forever_white_24dp"
android:title="@string/menu_delete" android:title="@string/menu_delete"

View File

@@ -182,6 +182,7 @@
<string name="hint_conf_pass">Confirm password</string> <string name="hint_conf_pass">Confirm password</string>
<string name="hint_generated_password">Generated password</string> <string name="hint_generated_password">Generated password</string>
<string name="hint_group_name">Group name</string> <string name="hint_group_name">Group name</string>
<string name="hint_icon_name">Icon name</string>
<string name="hint_keyfile">Keyfile</string> <string name="hint_keyfile">Keyfile</string>
<string name="hint_length">Length</string> <string name="hint_length">Length</string>
<string name="hint_pass">Password</string> <string name="hint_pass">Password</string>

View File

@@ -1 +1,2 @@
* Change default Argon2 parameters #1098 * Change default Argon2 parameters #1098
* Add & edit custom icon name #976

View File

@@ -1 +1,2 @@
* Changement des paramètres Argon2 par défaut #1098 * Changement des paramètres Argon2 par défaut #1098
* Ajout & édition du nom d'icone customisé #976