瀏覽代碼

Show file lock info in actions menu instead of separate dialog

Requested by design team

Signed-off-by: Álvaro Brey Vilas <alvaro.brey@nextcloud.com>
Álvaro Brey Vilas 3 年之前
父節點
當前提交
d90083fc07

+ 94 - 0
app/src/main/java/com/nextcloud/android/files/FileActionsPopupMenu.kt

@@ -0,0 +1,94 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2022 Álvaro Brey Vilas
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.android.files
+
+import android.content.Context
+import android.graphics.Typeface
+import android.os.Build
+import android.text.style.StyleSpan
+import android.view.ContextThemeWrapper
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.widget.PopupMenu
+import com.nextcloud.utils.TimeConstants
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.files.FileMenuFilter
+import com.owncloud.android.lib.resources.files.model.FileLockType
+import com.owncloud.android.utils.DisplayUtils
+
+/**
+ * Wrapper around PopupMenu with file locking info
+ */
+class FileActionsPopupMenu(
+    private val context: Context,
+    anchor: View,
+    private val file: OCFile,
+    fileMenuFilter: FileMenuFilter
+) : PopupMenu(wrapContext(context), anchor) {
+
+    init {
+        this.inflate(R.menu.item_file)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            menu.setGroupDividerEnabled(true)
+        }
+        fileMenuFilter.filter(menu, true)
+
+        if (file.isLocked) {
+            menu.findItem(R.id.action_locked_by).title = getLockedByText()
+            showLockedUntil()
+        }
+    }
+
+    private fun showLockedUntil() {
+        val lockedUntil: MenuItem = menu.findItem(R.id.action_locked_until)
+        if (file.lockTimestamp == 0L || file.lockTimeout == 0L) {
+            lockedUntil.isVisible = false
+        } else {
+            lockedUntil.title = context.getString(R.string.lock_expiration_info, getExpirationRelativeText())
+            lockedUntil.isVisible = true
+        }
+    }
+
+    private fun getLockedByText(): CharSequence {
+        val username = file.lockOwnerDisplayName ?: file.lockOwnerId
+        val resource = when (file.lockType) {
+            FileLockType.COLLABORATIVE -> R.string.locked_by_app
+            else -> R.string.locked_by
+        }
+        return DisplayUtils.createTextWithSpan(
+            context.getString(resource, username),
+            username,
+            StyleSpan(Typeface.BOLD)
+        )
+    }
+
+    private fun getExpirationRelativeText(): CharSequence? {
+        val expirationTimestamp = (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND
+        return DisplayUtils.getRelativeTimestamp(context, expirationTimestamp, true)
+    }
+
+    companion object {
+        private fun wrapContext(context: Context): Context =
+            ContextThemeWrapper(context, R.style.Nextcloud_Widget_PopupMenu)
+    }
+}

+ 1 - 3
app/src/main/java/com/nextcloud/utils/TimeConstants.kt

@@ -21,8 +21,6 @@
 
 package com.nextcloud.utils
 
-@Suppress("MagicNumber")
 object TimeConstants {
-    @JvmStatic
-    val MILLIS_PER_SECOND = 1000
+    const val MILLIS_PER_SECOND = 1000
 }

+ 28 - 2
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -132,11 +132,23 @@ public class FileMenuFilter {
             filter(toShow, toHide, inSingleFileFragment);
 
             for (int i : toShow) {
-                showMenuItem(menu.findItem(i));
+                final MenuItem item = menu.findItem(i);
+                if (item != null) {
+                    showMenuItem(item);
+                } else {
+                    // group
+                    menu.setGroupVisible(i, true);
+                }
             }
 
             for (int i : toHide) {
-                hideMenuItem(menu.findItem(i));
+                final MenuItem item = menu.findItem(i);
+                if (item != null) {
+                    hideMenuItem(item);
+                } else {
+                    // group
+                    menu.setGroupVisible(i, false);
+                }
             }
         }
     }
@@ -210,6 +222,7 @@ public class FileMenuFilter {
         filterStream(toShow, toHide);
         filterLock(toShow, toHide);
         filterUnlock(toShow, toHide);
+        filterLockInfo(toShow, toHide);
     }
 
     private void filterShareFile(List<Integer> toShow, List<Integer> toHide, OCCapability capability) {
@@ -287,6 +300,19 @@ public class FileMenuFilter {
         }
     }
 
+    private void filterLockInfo(List<Integer> toShow, List<Integer> toHide) {
+        if (files.isEmpty() || !isSingleSelection()) {
+            toHide.add(R.id.menu_group_lock_info);
+        } else {
+            OCFile file = files.iterator().next();
+            if (file.isLocked()) {
+                toShow.add(R.id.menu_group_lock_info);
+            } else {
+                toHide.add(R.id.menu_group_lock_info);
+            }
+        }
+    }
+
     private void filterEncrypt(List<Integer> toShow, List<Integer> toHide, boolean endToEndEncryptionEnabled) {
         if (files.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder()
             || !endToEndEncryptionEnabled) {

+ 0 - 138
app/src/main/java/com/owncloud/android/ui/dialog/LockInfoDialogFragment.kt

@@ -1,138 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Álvaro Brey Vilas
- * Copyright (C) 2022 Álvaro Brey Vilas
- * Copyright (C) 2022 Nextcloud GmbH
- *
- * This program 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.
- *
- * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.ui.dialog
-
-import android.app.Dialog
-import android.graphics.Typeface
-import android.graphics.drawable.Drawable
-import android.os.Bundle
-import android.text.style.StyleSpan
-import android.view.View
-import android.widget.ImageView
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.nextcloud.client.account.User
-import com.nextcloud.utils.TimeConstants
-import com.owncloud.android.R
-import com.owncloud.android.databinding.LockInfoDialogBinding
-import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.utils.DisplayUtils
-import com.owncloud.android.utils.theme.ThemeButtonUtils
-
-/**
- * Dialog that shows lock information for a locked file
- */
-class LockInfoDialogFragment(private val user: User, private val file: OCFile) :
-    DialogFragment(),
-    DisplayUtils.AvatarGenerationListener {
-    private lateinit var binding: LockInfoDialogBinding
-
-    override fun onStart() {
-        super.onStart()
-        dialog?.let {
-            val alertDialog = it as AlertDialog
-            ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE))
-        }
-    }
-
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        // Inflate the layout for the dialog
-        val inflater = requireActivity().layoutInflater
-        binding = LockInfoDialogBinding.inflate(inflater, null, false)
-
-        val view = binding.root
-
-        // Setup layout
-        if (file.isLocked) {
-            binding.lockedByUsername.text = getUserAndTimestampText()
-            loadAvatar()
-            setRemainingTime()
-        } else {
-            setNotLocked()
-        }
-
-        // Build the dialog
-        val dialog = MaterialAlertDialogBuilder(requireActivity(), R.style.Theme_ownCloud_Dialog)
-            .setTitle(R.string.file_lock_dialog_title)
-            .setView(view)
-            .setPositiveButton(R.string.dismiss) { _, _ ->
-                dismiss()
-            }
-            .create()
-
-        return dialog
-    }
-
-    private fun getUserAndTimestampText(): CharSequence {
-        val username = file.lockOwnerDisplayName ?: file.lockOwnerId
-        val lockTimestampMillis = file.lockTimestamp * TimeConstants.MILLIS_PER_SECOND
-        return DisplayUtils.createTextWithSpan(
-            getString(
-                R.string.locked_by_user_date, username, DisplayUtils.unixTimeToHumanReadable(lockTimestampMillis)
-            ),
-            username,
-            StyleSpan(Typeface.BOLD)
-        )
-    }
-
-    private fun loadAvatar() {
-        DisplayUtils.setAvatar(
-            user,
-            file.lockOwnerId!!,
-            file.lockOwnerDisplayName,
-            this,
-            requireContext().resources.getDimension(R.dimen.list_item_avatar_icon_radius),
-            resources,
-            binding.lockedByAvatar,
-            context
-        )
-    }
-
-    private fun setRemainingTime() {
-        if (file.lockTimestamp == 0L || file.lockTimeout == 0L) {
-            binding.lockExpiration.visibility = View.GONE
-        } else {
-            val expirationText = getExpirationRelativeText()
-            binding.lockExpiration.text = getString(R.string.lock_expiration_info, expirationText)
-            binding.lockExpiration.visibility = View.VISIBLE
-        }
-    }
-
-    private fun getExpirationRelativeText(): CharSequence? {
-        val expirationTimestamp = (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND
-        return DisplayUtils.getRelativeTimestamp(context, expirationTimestamp, true)
-    }
-
-    private fun setNotLocked() {
-        binding.lockExpiration.visibility = View.GONE
-        binding.lockedByAvatar.visibility = View.GONE
-        binding.lockedByUsername.text = getString(R.string.file_not_locked)
-    }
-
-    override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any) {
-        (callContext as ImageView).setImageDrawable(avatarDrawable)
-    }
-
-    override fun shouldCallGeneratedCallback(tag: String, callContext: Any): Boolean {
-        return (callContext as ImageView).tag == tag
-    }
-}

+ 6 - 11
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -41,13 +41,13 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
-import android.widget.PopupMenu;
 import android.widget.Toast;
 
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
+import com.nextcloud.android.files.FileActionsPopupMenu;
 import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation;
 import com.nextcloud.android.lib.richWorkspace.RichWorkspaceDirectEditingRemoteOperation;
 import com.nextcloud.client.account.User;
@@ -86,7 +86,6 @@ import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment;
 import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
-import com.owncloud.android.ui.dialog.LockInfoDialogFragment;
 import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
 import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
 import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
@@ -573,19 +572,20 @@ public class OCFileListFragment extends ExtendedListFragment implements
     @Override
     public void onOverflowIconClicked(OCFile file, View view) {
         throttler.run("overflowClick", () -> {
-            PopupMenu popup = new PopupMenu(getActivity(), view);
-            popup.inflate(R.menu.item_file);
             FileMenuFilter mf = new FileMenuFilter(mAdapter.getFiles().size(),
                                                    Collections.singleton(file),
                                                    mContainerActivity, getActivity(),
                                                    true,
                                                    accountManager.getUser());
-            mf.filter(popup.getMenu(), true);
+
+            final FileActionsPopupMenu popup = new FileActionsPopupMenu(requireContext(), view, file, mf);
+
             popup.setOnMenuItemClickListener(item -> {
                 Set<OCFile> checkedFiles = new HashSet<>();
                 checkedFiles.add(file);
                 return onFileActionChosen(item, checkedFiles);
             });
+
             popup.show();
         });
     }
@@ -594,7 +594,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     public void newDocument() {
         ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile,
                                                               ChooseRichDocumentsTemplateDialogFragment.Type.DOCUMENT)
-                .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT);
+            .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT);
     }
 
     @Override
@@ -618,11 +618,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
     }
 
-    @Override
-    public void onLockIndicatorClicked(OCFile file) {
-        new LockInfoDialogFragment(accountManager.getUser(), file).show(requireActivity().getSupportFragmentManager(), DIALOG_LOCK_DETAILS);
-    }
-
     @Override
     public void showTemplate(Creator creator, String headline) {
         ChooseTemplateDialogFragment.newInstance(mFile, creator, headline).show(requireActivity().getSupportFragmentManager(),

+ 0 - 2
app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java

@@ -48,6 +48,4 @@ public interface OCFileListFragmentInterface {
     boolean isLoading();
 
     void onHeaderClicked();
-
-    void onLockIndicatorClicked(OCFile file);
 }

+ 25 - 0
app/src/main/res/color/popup_item_text_color.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Álvaro Brey Vilas
+  ~ Copyright (C) 2022 Álvaro Brey Vilas
+  ~ Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program 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.
+  ~
+  ~ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/text_color" android:state_enabled="true"/>
+    <item android:color="@color/disabled_text" />
+</selector>

+ 0 - 61
app/src/main/res/layout/lock_info_dialog.xml

@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Nextcloud Android client application
-  ~
-  ~ @author Álvaro Brey Vilas
-  ~ Copyright (C) 2022 Álvaro Brey Vilas
-  ~ Copyright (C) 2022 Nextcloud GmbH
-  ~
-  ~ This program 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.
-  ~
-  ~ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
-  -->
-<androidx.constraintlayout.widget.ConstraintLayout 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"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:paddingHorizontal="?dialogPreferredPadding">
-
-    <ImageView
-        android:id="@+id/locked_by_avatar"
-        android:layout_width="@dimen/file_icon_size"
-        android:layout_height="@dimen/file_icon_size"
-        android:importantForAccessibility="no"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:src="@drawable/all_files" />
-
-    <TextView
-        android:id="@+id/locked_by_username"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="@dimen/list_item_share_right_margin"
-        android:layout_marginTop="@dimen/list_item_share_right_margin"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/locked_by_avatar"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:text="Locked by Username Goes Here at 2022/01/02 13:00" />
-
-    <TextView
-        android:id="@+id/lock_expiration"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="@dimen/list_item_share_right_margin"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/locked_by_avatar"
-        app:layout_constraintTop_toBottomOf="@id/locked_by_username"
-        tools:text="Expires: In 20 minutes" />
-
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 40 - 15
app/src/main/res/menu/item_file.xml

@@ -18,9 +18,40 @@
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
 <menu 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:ignore="AppCompatResource">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="AppCompatResource">
+
+    <group
+        android:id="@+id/menu_group_lock_info"
+        android:visible="false"
+        android:enabled="false"
+        tools:visible="true">
+
+        <item
+            android:id="@+id/action_locked_by"
+            android:showAsAction="never"
+            android:title="Locked by %1$s"
+            android:enabled="false"
+            app:showAsAction="never"
+            tools:ignore="HardcodedText"
+            tools:title="Locked by Username With Surname at bla bla bla bla bla" />
+
+        <item
+            android:id="@+id/action_locked_until"
+            android:showAsAction="never"
+            android:enabled="false"
+            android:title="Lock expires: %1$s"
+            app:showAsAction="never"
+            tools:ignore="HardcodedText"
+            tools:title="Lock expires: in 20 minutes" />
+    </group>
+
+    <item
+        android:id="@+id/action_unlock_file"
+        android:showAsAction="never"
+        android:title="@string/unlock_file"
+        app:showAsAction="never" />
 
     <item
         android:id="@+id/action_edit"
@@ -40,24 +71,18 @@
         app:showAsAction="never"
         android:showAsAction="never" />
 
-    <item
-        android:id="@+id/action_lock_file"
-        android:title="@string/lock_file"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_unlock_file"
-        android:title="@string/unlock_file"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
     <item
         android:id="@+id/action_see_details"
         android:title="@string/actionbar_see_details"
         app:showAsAction="never"
         android:showAsAction="never" />
 
+    <item
+        android:id="@+id/action_lock_file"
+        android:showAsAction="never"
+        android:title="@string/lock_file"
+        app:showAsAction="never" />
+
     <item
         android:id="@+id/action_rename_file"
         android:title="@string/common_rename"

+ 4 - 3
app/src/main/res/values/strings.xml

@@ -1009,11 +1009,12 @@
     <string name="storage_permission_full_access">Full access</string>
     <string name="storage_permission_media_read_only">Media read-only</string>
     <string name="file_locked">This file is locked</string>
-    <string name="lock_file">Lock</string>
-    <string name="unlock_file">Unlock</string>
+    <string name="lock_file">Lock file</string>
+    <string name="unlock_file">Unlock file</string>
     <string name="error_file_lock">Error changing file lock status</string>
     <string name="file_lock_dialog_title">Lock details</string>
-    <string name="locked_by_user_date">Locked by %1$s at %2$s</string>
+    <string name="locked_by">Locked by %1$s</string>
+    <string name="locked_by_app">Locked by %1$s app</string>
     <string name="lock_expiration_info">Expires: %1$s</string>
     <string name="file_not_locked">File is not locked</string>
 </resources>

+ 4 - 0
app/src/main/res/values/styles.xml

@@ -386,6 +386,10 @@
         <item name="android:textColorPrimary">@color/text_color</item>
     </style>
 
+    <style name="Nextcloud.Widget.PopupMenu" parent="@style/Widget.AppCompat.PopupMenu">
+        <item name="android:textColor">@color/popup_item_text_color</item>
+    </style>
+
     <style name="MaterialListItemSingleLine">
         <item name="android:clickable">true</item>
         <item name="android:background">?android:selectableItemBackground</item>