diff --git a/CHANGELOG.md b/CHANGELOG.md index e8a033fe..c5bde777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Added - Fast scroller with alphabetic hints +- UI button to create new folders ### Changed - Logging is now enabled in release builds diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index 61437731..22bad566 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -70,12 +70,30 @@ class PasswordFragment : Fragment() { recyclerView.adapter = recyclerAdapter // Setup fast scroller FastScrollerBuilder(recyclerView).build() - val fab: FloatingActionButton = view.findViewById(R.id.fab) - fab.setOnClickListener { (requireActivity() as PasswordStore).createPassword() } + val fab = view.findViewById(R.id.fab) + fab.setOnClickListener { + toggleFabExpand(fab) + } + + view.findViewById(R.id.create_folder).setOnClickListener { + (requireActivity() as PasswordStore).createFolder() + toggleFabExpand(fab) + } + + view.findViewById(R.id.create_password).setOnClickListener { + (requireActivity() as PasswordStore).createPassword() + toggleFabExpand(fab) + } registerForContextMenu(recyclerView) return view } + private fun toggleFabExpand(fab: FloatingActionButton) = with(fab) { + isExpanded = !isExpanded + isActivated = isExpanded + animate().rotationBy(if (isExpanded) -45f else 45f).setDuration(100).start() + } + override fun onAttach(context: Context) { super.onAttach(context) try { diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index f0302d83..087eb42d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -38,6 +38,7 @@ import com.zeapo.pwdstore.git.GitActivity import com.zeapo.pwdstore.git.GitAsyncTask import com.zeapo.pwdstore.git.GitOperation import com.zeapo.pwdstore.ui.adapters.PasswordRecyclerAdapter +import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment import com.zeapo.pwdstore.utils.PasswordItem import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository.Companion.closeRepository @@ -435,13 +436,13 @@ class PasswordStore : AppCompatActivity() { startActivityForResult(intent, REQUEST_CODE_EDIT) } - fun createPassword() { + private fun validateState(): Boolean { if (!isInitialized) { MaterialAlertDialogBuilder(this) .setMessage(this.resources.getString(R.string.creation_dialog_text)) .setPositiveButton(this.resources.getString(R.string.dialog_ok), null) .show() - return + return false } if (settings.getStringSet("openpgp_key_ids_set", HashSet()).isNullOrEmpty()) { MaterialAlertDialogBuilder(this) @@ -452,8 +453,13 @@ class PasswordStore : AppCompatActivity() { startActivity(intent) } .show() - return + return false } + return true + } + + fun createPassword() { + if (!validateState()) return val currentDir = currentDir Timber.tag(TAG).i("Adding file to : ${currentDir!!.absolutePath}") val intent = Intent(this, PgpActivity::class.java) @@ -463,6 +469,11 @@ class PasswordStore : AppCompatActivity() { startActivityForResult(intent, REQUEST_CODE_ENCRYPT) } + fun createFolder() { + if (!validateState()) return + FolderCreationDialogFragment.newInstance(currentDir!!.path).show(supportFragmentManager, null) + } + // deletes passwords in order from top to bottom fun deletePasswords(adapter: PasswordRecyclerAdapter, selectedItems: MutableSet) { val it: MutableIterator<*> = selectedItems.iterator() diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt new file mode 100644 index 00000000..8584aeaf --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -0,0 +1,45 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.ui.dialogs + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.zeapo.pwdstore.PasswordStore +import com.zeapo.pwdstore.R +import java.io.File + +class FolderCreationDialogFragment : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + alertDialogBuilder.setView(R.layout.folder_creation_dialog_fragment) + alertDialogBuilder.setPositiveButton(getString(R.string.button_create)) { _: DialogInterface, _: Int -> + createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!) + } + return alertDialogBuilder.create() + } + + private fun createDirectory(currentDir: String) { + val dialog = requireDialog() + val materialTextView = dialog.findViewById(R.id.folder_name_text) + val folderName = materialTextView.text.toString() + File("$currentDir/$folderName").mkdir() + (requireActivity() as PasswordStore).updateListAdapter() + } + + companion object { + private const val CURRENT_DIR_EXTRA = "CURRENT_DIRECTORY" + fun newInstance(startingDirectory: String): FolderCreationDialogFragment { + val extras = bundleOf(CURRENT_DIR_EXTRA to startingDirectory) + val fragment = FolderCreationDialogFragment() + fragment.arguments = extras + return fragment + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/widget/fab/EmitExpandableTransformationBehaviour.kt b/app/src/main/java/com/zeapo/pwdstore/widget/fab/EmitExpandableTransformationBehaviour.kt new file mode 100644 index 00000000..0b05fb8f --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/widget/fab/EmitExpandableTransformationBehaviour.kt @@ -0,0 +1,141 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.widget.fab + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.animation.addListener +import androidx.core.view.children +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.transformation.ExpandableTransformationBehavior +import java.util.ArrayList + +/** + * Taken from Mao Yufeng's excellent example at https://git.io/Jvml9, all credits to him for this. + * It's hard to create per-file copyright rules for Spotless so I'm choosing to credit him here. + */ +class EmitExpandableTransformationBehavior @JvmOverloads constructor( + context: Context? = null, + attrs: AttributeSet? = null +) : ExpandableTransformationBehavior(context, attrs) { + + companion object { + private const val EXPAND_DELAY = 60L + private const val EXPAND_DURATION = 150L + private const val COLLAPSE_DELAY = 60L + private const val COLLAPSE_DURATION = 150L + } + + override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + return dependency is FloatingActionButton && child is ViewGroup + } + + override fun onCreateExpandedStateChangeAnimation( + dependency: View, + child: View, + expanded: Boolean, + isAnimating: Boolean + ): AnimatorSet { + + if (child !is ViewGroup) { + return AnimatorSet() + } + + val animations = ArrayList() + + if (expanded) { + createExpandAnimation(child, isAnimating, animations) + } else { + createCollapseAnimation(child, animations) + } + + val set = AnimatorSet() + set.playTogether(animations) + set.addListener( + onStart = { + if (expanded) { + child.isVisible = true + } + }, + onEnd = { + if (!expanded) { + child.isInvisible = true + } + } + ) + return set + } + + private fun createExpandAnimation( + child: ViewGroup, + currentlyAnimating: Boolean, + animations: MutableList + ) { + if (!currentlyAnimating) { + child.children.forEach { + it.alpha = 0f + it.scaleX = 0.4f + it.scaleY = 0.4f + } + } + val delays = List(child.childCount) { + it * EXPAND_DELAY + }.reversed().asSequence() + val scaleXHolder = PropertyValuesHolder.ofFloat(View.SCALE_X, 1f) + val scaleYHolder = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f) + val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 1f) + val animators = child.children.zip(delays) { view, delay -> + ObjectAnimator.ofPropertyValuesHolder( + view, + scaleXHolder, + scaleYHolder, + alphaHolder + ).apply { + duration = EXPAND_DURATION + startDelay = delay + } + }.toList() + val animatorSet = AnimatorSet().apply { + playTogether(animators) + } + animations.add(animatorSet) + } + + private fun createCollapseAnimation( + child: ViewGroup, + animations: MutableList + ) { + val delays = List(child.childCount) { + it * COLLAPSE_DELAY + }.asSequence() + val scaleXHolder = PropertyValuesHolder.ofFloat(View.SCALE_X, 0.4f) + val scaleYHolder = PropertyValuesHolder.ofFloat(View.SCALE_Y, 0.4f) + val alphaHolder = PropertyValuesHolder.ofFloat(View.ALPHA, 0f) + val animators = child.children.zip(delays) { view, delay -> + ObjectAnimator.ofPropertyValuesHolder( + view, + scaleXHolder, + scaleYHolder, + alphaHolder + ).apply { + duration = COLLAPSE_DURATION + startDelay = delay + } + }.toList() + val animatorSet = AnimatorSet().apply { + playTogether(animators) + } + animations.add(animatorSet) + } +} diff --git a/app/src/main/res/drawable/ic_action_new_folder.xml b/app/src/main/res/drawable/ic_action_new_folder.xml new file mode 100644 index 00000000..44666001 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_new_folder.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_new_password.xml b/app/src/main/res/drawable/ic_action_new_password.xml new file mode 100644 index 00000000..38350bd3 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_new_password.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_secure_24dp.xml b/app/src/main/res/drawable/ic_action_secure_24dp.xml index 2968c4a0..ec7b4c21 100644 --- a/app/src/main/res/drawable/ic_action_secure_24dp.xml +++ b/app/src/main/res/drawable/ic_action_secure_24dp.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/passwordIconColor"> + android:tint="?attr/colorOnPrimary"> diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml index 882ab73a..ede2c488 100644 --- a/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml +++ b/app/src/main/res/drawable/ic_keyboard_arrow_right_24dp.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/passwordIconColor"> + android:tint="?attr/colorOnPrimary"> diff --git a/app/src/main/res/drawable/ic_multiple_files_24dp.xml b/app/src/main/res/drawable/ic_multiple_files_24dp.xml index 1aadafbc..a504b9cc 100644 --- a/app/src/main/res/drawable/ic_multiple_files_24dp.xml +++ b/app/src/main/res/drawable/ic_multiple_files_24dp.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?passwordIconColor"> + android:tint="?colorOnPrimary"> diff --git a/app/src/main/res/layout/folder_creation_dialog_fragment.xml b/app/src/main/res/layout/folder_creation_dialog_fragment.xml new file mode 100644 index 00000000..9d5dfda2 --- /dev/null +++ b/app/src/main/res/layout/folder_creation_dialog_fragment.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml index 07596335..f58543ac 100644 --- a/app/src/main/res/layout/password_recycler_view.xml +++ b/app/src/main/res/layout/password_recycler_view.xml @@ -1,11 +1,14 @@ - + + tools:itemCount="20" /> + + + + + + + - + android:layout_alignParentEnd="true" + app:backgroundTint="?attr/colorSecondary" + app:rippleColor="?attr/colorSecondary" /> + diff --git a/app/src/main/res/layout/password_row_layout.xml b/app/src/main/res/layout/password_row_layout.xml index 4339e019..0b7112b6 100644 --- a/app/src/main/res/layout/password_row_layout.xml +++ b/app/src/main/res/layout/password_row_layout.xml @@ -25,7 +25,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:textColor="?android:attr/textColor" android:textSize="18sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/type_image" diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 7fda5217..00000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/values-v21/dimens.xml b/app/src/main/res/values-v21/dimens.xml deleted file mode 100644 index 3a8d4266..00000000 --- a/app/src/main/res/values-v21/dimens.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 16dp - diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml deleted file mode 100644 index 63fc8164..00000000 --- a/app/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 64dp - diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index aa02723f..913b7a4d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,8 +1,5 @@ - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ec75b5b6..48d1a7a3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,6 +9,7 @@ #c63f17 #212121 #ffffff + #ffffffff #eceff1 @@ -17,4 +18,5 @@ @color/primary_text_color #FFFFFF #668eacbb + @color/primary_dark_color diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8a517840..15c6287c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,5 +2,6 @@ 16dp 16dp - 0dp + 16dp + 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7ffa3e9..c1827ac9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Add password for %1$s using android password store. "Edit %1$s using android password store." + "Folder %1$s created using android password store." "Remove %1$s from store." "Rename %1$s to %2$s." "Increment HOTP counter for %1$s." @@ -278,4 +279,6 @@ Error while trying to generate the ssh-key Show hidden folders Include hidden directories in the password list + Create folder + Create diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ad6a61ff..e6312175 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + -