diff --git a/CHANGELOG b/CHANGELOG index 87f201e2b..49f6a960a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +KeePassDX(3.1.0) + * Add breadcrumb + * Add path in search results #1148 + KeePassDX(3.0.4) * Fix autofill inline bugs #1173 #1165 * Small UI change diff --git a/app/build.gradle b/app/build.gradle index 4256d0b6d..78b9b567c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 30 - versionCode = 91 - versionName = "3.0.4" + versionCode = 92 + versionName = "3.1.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index f71724b17..5faa47d7d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -25,7 +25,6 @@ import android.app.TimePickerDialog import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.* import android.util.Log import android.view.Menu @@ -41,12 +40,15 @@ import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.fragments.GroupFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity +import com.kunzisoft.keepass.adapters.BreadcrumbAdapter import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper @@ -84,18 +86,23 @@ class GroupActivity : DatabaseLockActivity(), private var coordinatorLayout: CoordinatorLayout? = null private var lockView: View? = null private var toolbar: Toolbar? = null + private var databaseNameView: TextView? = null + private var searchContainer: ViewGroup? = null + private var searchNumbers: TextView? = null + private var searchString: TextView? = null + private var toolbarBreadcrumb: Toolbar? = null private var searchTitleView: View? = null private var toolbarAction: ToolbarAction? = null - private var iconView: ImageView? = null private var numberChildrenView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null - private var groupNameView: TextView? = null - private var groupMetaView: TextView? = null + private var breadcrumbListView: RecyclerView? = null private var loadingView: ProgressBar? = null private val mGroupViewModel: GroupViewModel by viewModels() private val mGroupEditViewModel: GroupEditViewModel by viewModels() + private var mBreadcrumbAdapter: BreadcrumbAdapter? = null + private var mGroupFragment: GroupFragment? = null private var mRecyclingBinEnabled = false private var mRecyclingBinIsCurrentGroup = false @@ -123,8 +130,6 @@ class GroupActivity : DatabaseLockActivity(), AutofillHelper.buildActivityResultLauncher(this) else null - private var mIconColor: Int = 0 - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -134,13 +139,16 @@ class GroupActivity : DatabaseLockActivity(), // Initialize views rootContainerView = findViewById(R.id.activity_group_container_view) coordinatorLayout = findViewById(R.id.group_coordinator) - iconView = findViewById(R.id.group_icon) numberChildrenView = findViewById(R.id.group_numbers) addNodeButtonView = findViewById(R.id.add_node_button) toolbar = findViewById(R.id.toolbar) + databaseNameView = findViewById(R.id.database_name) + searchContainer = findViewById(R.id.search_container) + searchNumbers = findViewById(R.id.search_numbers) + searchString = findViewById(R.id.search_string) + toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb) searchTitleView = findViewById(R.id.search_title) - groupNameView = findViewById(R.id.group_name) - groupMetaView = findViewById(R.id.group_meta) + breadcrumbListView = findViewById(R.id.breadcrumb_list) toolbarAction = findViewById(R.id.toolbar_action) lockView = findViewById(R.id.lock_button) loadingView = findViewById(R.id.loading) @@ -152,10 +160,18 @@ class GroupActivity : DatabaseLockActivity(), toolbar?.title = "" setSupportActionBar(toolbar) - // Retrieve the textColor to tint the icon - val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) - mIconColor = taTextColor.getColor(0, Color.WHITE) - taTextColor.recycle() + mBreadcrumbAdapter = BreadcrumbAdapter(this).apply { + // Open group on breadcrumb click + onItemClickListener = { node, _ -> + finishNodeAction() + mDatabase?.let { database -> + onNodeClick(database, node) + } + } + } + breadcrumbListView?.apply { + adapter = mBreadcrumbAdapter + } // Retrieve group if defined at launch manageIntent(intent) @@ -211,6 +227,12 @@ class GroupActivity : DatabaseLockActivity(), // Update last access time. currentGroup.touch(modified = false, touchParents = false) + // Add breadcrumb + mBreadcrumbAdapter?.apply { + setNode(currentGroup) + breadcrumbListView?.scrollToPosition(itemCount -1) + } + // Add listeners to the add buttons addNodeButtonView?.setAddGroupClickListener { GroupEditDialogFragment.create(GroupInfo().apply { @@ -371,7 +393,9 @@ class GroupActivity : DatabaseLockActivity(), // Search suggestion database?.let { + databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database) mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it) + mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory mOnSuggestionListener = object : SearchView.OnSuggestionListener { override fun onSuggestionClick(position: Int): Boolean { mSearchSuggestionAdapter?.let { searchAdapter -> @@ -455,7 +479,8 @@ class GroupActivity : DatabaseLockActivity(), finishNodeAction() - refreshNumberOfChildren(mCurrentGroup) + // Refresh breadcrumb + mBreadcrumbAdapter?.setNode(mCurrentGroup) } /** @@ -502,58 +527,28 @@ class GroupActivity : DatabaseLockActivity(), private fun assignGroupViewElements(group: Group?) { // Assign title - if (group != null) { - if (groupNameView != null) { - val title = group.title - groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root) - groupNameView?.invalidate() - } - if (groupMetaView != null) { - val meta = group.nodeId.toString() - groupMetaView?.text = meta - if (meta.isNotEmpty() - && !group.isVirtual - && PreferencesUtil.showUUID(this)) { - groupMetaView?.visibility = View.VISIBLE - } else { - groupMetaView?.visibility = View.GONE - } - groupMetaView?.invalidate() - } - } - if (group?.isVirtual == true) { - searchTitleView?.visibility = View.VISIBLE - if (toolbar != null) { - toolbar?.navigationIcon = null - } - iconView?.visibility = View.GONE + searchContainer?.visibility = View.VISIBLE + val title = group.title + searchString?.text = if (title.isNotEmpty()) title else "" + searchNumbers?.text = group.numberOfChildEntries.toString() + databaseNameView?.visibility = View.GONE + toolbarBreadcrumb?.navigationIcon = null + toolbarBreadcrumb?.collapse() } else { - searchTitleView?.visibility = View.GONE - // Assign the group icon depending of IconPack or custom icon - iconView?.visibility = View.VISIBLE - group?.let { currentGroup -> - iconView?.let { imageView -> - mIconDrawableFactory?.assignDatabaseIcon( - imageView, - currentGroup.icon, - mIconColor - ) - } - - if (toolbar != null) { - if (group.containsParent()) - toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp) - else { - toolbar?.navigationIcon = null - } + searchContainer?.visibility = View.GONE + databaseNameView?.visibility = View.VISIBLE + // Refresh breadcrumb + if (toolbarBreadcrumb?.isVisible != true) { + mBreadcrumbAdapter?.setNode(null) + toolbarBreadcrumb?.expand { + mBreadcrumbAdapter?.setNode(group) } + } else { + mBreadcrumbAdapter?.setNode(group) } } - // Assign number of children - refreshNumberOfChildren(group) - // Hide button initAddButton(group) } @@ -577,18 +572,6 @@ class GroupActivity : DatabaseLockActivity(), } } - private fun refreshNumberOfChildren(group: Group?) { - numberChildrenView?.apply { - if (PreferencesUtil.showNumberEntries(context)) { - group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context)) - text = group?.numberOfChildEntries?.toString() ?: "" - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - } - override fun onScrolled(dy: Int) { if (actionNodeMode == null) addNodeButtonView?.hideOrShowButtonOnScrollListener(dy) @@ -1034,7 +1017,7 @@ class GroupActivity : DatabaseLockActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { - onBackPressed() + // TODO change database return true } R.id.menu_search -> diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt new file mode 100644 index 000000000..0bee7269a --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt @@ -0,0 +1,143 @@ +package com.kunzisoft.keepass.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.icons.IconDrawableFactory +import com.kunzisoft.keepass.settings.PreferencesUtil + +class BreadcrumbAdapter(val context: Context) + : RecyclerView.Adapter() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + var iconDrawableFactory: IconDrawableFactory? = null + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + notifyDataSetChanged() + } + private var mNodeBreadcrumb: MutableList = mutableListOf() + var onItemClickListener: ((item: Node, position: Int)->Unit)? = null + + private var mShowNumberEntries = false + private var mShowUUID = false + private var mIconColor: Int = 0 + + init { + mShowNumberEntries = PreferencesUtil.showNumberEntries(context) + mShowUUID = PreferencesUtil.showUUID(context) + + // Retrieve the textColor to tint the icon + val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse)) + mIconColor = taTextColor.getColor(0, Color.WHITE) + taTextColor.recycle() + } + + @SuppressLint("NotifyDataSetChanged") + fun setNode(node: Node?) { + mNodeBreadcrumb.clear() + node?.let { + var currentNode = it + mNodeBreadcrumb.add(0, currentNode) + while (currentNode.containsParent()) { + currentNode.parent?.let { parent -> + currentNode = parent + mNodeBreadcrumb.add(0, currentNode) + } + } + } + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int { + return when (position) { + mNodeBreadcrumb.size - 1 -> 0 + else -> 1 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder { + return BreadcrumbGroupViewHolder(inflater.inflate( + when (viewType) { + 0 -> R.layout.item_group + else -> R.layout.item_breadcrumb + }, parent, false) + ) + } + + override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) { + val node = mNodeBreadcrumb[position] + + holder.groupNameView.apply { + text = when { + node == null -> "" + node.title.isEmpty() -> context.getString(R.string.root) + else -> node.title + } + } + + holder.itemView.setOnClickListener { + node?.let { + onItemClickListener?.invoke(it, position) + } + } + + if (node?.type == Type.GROUP) { + (node as Group).let { group -> + + holder.groupIconView?.let { imageView -> + iconDrawableFactory?.assignDatabaseIcon( + imageView, + group.icon, + mIconColor + ) + } + + holder.groupNumbersView?.apply { + if (mShowNumberEntries) { + group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context)) + text = group.numberOfChildEntries.toString() + visibility = View.VISIBLE + } else { + visibility = View.GONE + } + } + + holder.groupMetaView?.apply { + val meta = group.nodeId.toString() + text = meta + visibility = if (meta.isNotEmpty() + && !group.isVirtual + && mShowUUID + ) { + View.VISIBLE + } else { + View.GONE + } + } + } + } + + } + + override fun getItemCount(): Int { + return mNodeBreadcrumb.size + } + + inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon) + var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers) + var groupNameView: TextView = itemView.findViewById(R.id.group_name) + var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index e7b1164be..301244b0b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -79,6 +79,8 @@ class NodeAdapter (private val context: Context, private var mShowOTP: Boolean = false private var mShowUUID: Boolean = false private var mEntryFilters = arrayOf() + private var mOldVirtualGroup = false + private var mVirtualGroup = false private var mActionNodesList = LinkedList() private var mNodeClickCallback: NodeClickCallback? = null @@ -145,6 +147,8 @@ class NodeAdapter (private val context: Context, * Rebuild the list by clear and build children from the group */ fun rebuildList(group: Group) { + mOldVirtualGroup = mVirtualGroup + mVirtualGroup = group.isVirtual assignPreferences() mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters)) } @@ -155,6 +159,8 @@ class NodeAdapter (private val context: Context, } override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { + if (mOldVirtualGroup != mVirtualGroup) + return false var typeContentTheSame = true if (oldItem is Entry && newItem is Entry) { typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle() @@ -356,6 +362,15 @@ class NodeAdapter (private val context: Context, visibility = View.GONE } } + // Add path to virtual group + if (mVirtualGroup) { + holder.path?.apply { + text = subNode.getPathString() + visibility = View.VISIBLE + } + } else { + holder.path?.visibility = View.GONE + } // Specific elements for entry if (subNode.type == Type.ENTRY) { @@ -497,6 +512,7 @@ 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 path: TextView? = itemView.findViewById(R.id.node_path) var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/node/Node.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/node/Node.kt index 036ab5580..f43db7eb0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/node/Node.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/node/Node.kt @@ -32,6 +32,19 @@ interface Node: NodeVersionedInterface { fun removeParent() { parent = null } + + fun getPathString(): String { + val pathNodes = mutableListOf() + var currentNode = this + pathNodes.add(0, currentNode) + while (currentNode.containsParent()) { + currentNode.parent?.let { parent -> + currentNode = parent + pathNodes.add(0, currentNode) + } + } + return pathNodes.joinToString("/") { it.title } + } } /** diff --git a/app/src/main/res/layout/activity_group.xml b/app/src/main/res/layout/activity_group.xml index 1ba069e24..b477a88f4 100644 --- a/app/src/main/res/layout/activity_group.xml +++ b/app/src/main/res/layout/activity_group.xml @@ -31,11 +31,66 @@ android:layout_height="wrap_content" android:theme="?attr/toolbarSpecialAppearance" /> + + + + + + + + + - - - - - - - - - - - - - + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:orientation="horizontal" /> @@ -159,6 +152,7 @@ + + android:layout_marginTop="?attr/actionBarSize" + android:background="?attr/colorPrimary"> + + + + + + diff --git a/app/src/main/res/layout/item_group.xml b/app/src/main/res/layout/item_group.xml new file mode 100644 index 000000000..9bfc67eb3 --- /dev/null +++ b/app/src/main/res/layout/item_group.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_list_nodes_entry.xml b/app/src/main/res/layout/item_list_nodes_entry.xml index cac133ba5..4d250ab6b 100644 --- a/app/src/main/res/layout/item_list_nodes_entry.xml +++ b/app/src/main/res/layout/item_list_nodes_entry.xml @@ -101,6 +101,15 @@ android:lines="1" android:singleLine="true" tools:text="7543A7EAB2EA7CFD1394F1615EBEB08C" /> + + + diff --git a/fastlane/metadata/android/en-US/changelogs/92.txt b/fastlane/metadata/android/en-US/changelogs/92.txt new file mode 100644 index 000000000..5449f6d6c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/92.txt @@ -0,0 +1,2 @@ + * Add breadcrumb + * Add path in search results #1148 \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/92.txt b/fastlane/metadata/android/fr-FR/changelogs/92.txt new file mode 100644 index 000000000..638178863 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/92.txt @@ -0,0 +1,2 @@ + * Ajout d'un fil d'ariane + * Ajout du chemin pour les résultats de recherche #1148 \ No newline at end of file