mirror of
https://github.com/nextcloud/android.git
synced 2024-12-18 15:01:57 +03:00
Merge pull request #13982 from nextcloud/trashbin-multiple-selection
Add Bulk Action Support to Trashbin
This commit is contained in:
commit
3e656486c7
12 changed files with 849 additions and 37 deletions
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -123,6 +124,7 @@ import com.owncloud.android.ui.preview.PreviewTextFileFragment;
|
||||||
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
import com.owncloud.android.ui.preview.PreviewTextFragment;
|
||||||
import com.owncloud.android.ui.preview.PreviewTextStringFragment;
|
import com.owncloud.android.ui.preview.PreviewTextStringFragment;
|
||||||
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
|
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
|
||||||
|
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
|
||||||
import com.owncloud.android.ui.trashbin.TrashbinActivity;
|
import com.owncloud.android.ui.trashbin.TrashbinActivity;
|
||||||
|
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
@ -225,6 +227,9 @@ abstract class ComponentsModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract TrashbinActivity trashbinActivity();
|
abstract TrashbinActivity trashbinActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract TrashbinFileActionsBottomSheet trashbinFileActionsBottomSheet();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract UploadFilesActivity uploadFilesActivity();
|
abstract UploadFilesActivity uploadFilesActivity();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -13,6 +14,7 @@ import com.nextcloud.client.etm.EtmViewModel
|
||||||
import com.nextcloud.client.logger.ui.LogsViewModel
|
import com.nextcloud.client.logger.ui.LogsViewModel
|
||||||
import com.nextcloud.ui.fileactions.FileActionsViewModel
|
import com.nextcloud.ui.fileactions.FileActionsViewModel
|
||||||
import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel
|
import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel
|
||||||
|
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel
|
||||||
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
|
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -50,6 +52,11 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(DocumentScanViewModel::class)
|
@ViewModelKey(DocumentScanViewModel::class)
|
||||||
abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel
|
abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(TrashbinFileActionsViewModel::class)
|
||||||
|
abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
|
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.nextcloud.ui.trashbinFileActions
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.owncloud.android.R
|
||||||
|
|
||||||
|
enum class TrashbinFileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) {
|
||||||
|
DELETE_PERMANENTLY(R.id.action_delete, R.string.trashbin_file_remove, R.drawable.ic_delete),
|
||||||
|
RESTORE(R.id.restore, R.string.restore_item, R.drawable.ic_history),
|
||||||
|
SELECT_ALL(R.id.action_select_all_action_menu, R.string.select_all, R.drawable.ic_select_all),
|
||||||
|
SELECT_NONE(R.id.action_deselect_all_action_menu, R.string.deselect_all, R.drawable.ic_select_none);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* All file actions, in the order they should be displayed
|
||||||
|
*/
|
||||||
|
@JvmField
|
||||||
|
val SORTED_VALUES = listOf(
|
||||||
|
DELETE_PERMANENTLY,
|
||||||
|
RESTORE,
|
||||||
|
SELECT_ALL,
|
||||||
|
SELECT_NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.nextcloud.ui.trashbinFileActions
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.isEmpty
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||||
|
import com.nextcloud.client.account.CurrentAccountProvider
|
||||||
|
import com.nextcloud.client.di.Injectable
|
||||||
|
import com.nextcloud.client.di.ViewModelFactory
|
||||||
|
import com.nextcloud.utils.extensions.toOCFile
|
||||||
|
import com.owncloud.android.R
|
||||||
|
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
|
||||||
|
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
|
||||||
|
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||||
|
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||||
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager
|
||||||
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
|
||||||
|
import com.owncloud.android.utils.DisplayUtils
|
||||||
|
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class TrashbinFileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewThemeUtils: ViewThemeUtils
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var vmFactory: ViewModelFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var currentUserProvider: CurrentAccountProvider
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var storageManager: FileDataStorageManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var syncedFolderProvider: SyncedFolderProvider
|
||||||
|
|
||||||
|
private lateinit var viewModel: TrashbinFileActionsViewModel
|
||||||
|
|
||||||
|
private var _binding: FileActionsBottomSheetBinding? = null
|
||||||
|
val binding
|
||||||
|
get() = _binding!!
|
||||||
|
|
||||||
|
private val thumbnailAsyncTasks = mutableListOf<ThumbnailsCacheManager.ThumbnailGenerationTask>()
|
||||||
|
|
||||||
|
fun interface ResultListener {
|
||||||
|
fun onResult(@IdRes actionId: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
viewModel = ViewModelProvider(this, vmFactory)[TrashbinFileActionsViewModel::class.java]
|
||||||
|
_binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
viewModel.uiState.observe(viewLifecycleOwner, this::handleState)
|
||||||
|
|
||||||
|
viewModel.clickActionId.observe(viewLifecycleOwner) { id ->
|
||||||
|
dispatchActionClick(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.load(requireArguments())
|
||||||
|
|
||||||
|
val bottomSheetDialog = dialog as BottomSheetDialog
|
||||||
|
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
bottomSheetDialog.behavior.skipCollapsed = true
|
||||||
|
|
||||||
|
viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleState(state: TrashbinFileActionsViewModel.UiState) {
|
||||||
|
toggleLoadingOrContent(state)
|
||||||
|
when (state) {
|
||||||
|
is TrashbinFileActionsViewModel.UiState.LoadedForSingleFile -> {
|
||||||
|
loadFileThumbnail(state.titleFile)
|
||||||
|
displayActions(state.actions)
|
||||||
|
displayTitle(state.titleFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TrashbinFileActionsViewModel.UiState.LoadedForMultipleFiles -> {
|
||||||
|
setMultipleFilesThumbnail()
|
||||||
|
displayActions(state.actions)
|
||||||
|
displayTitle(state.fileCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrashbinFileActionsViewModel.UiState.Loading -> {}
|
||||||
|
TrashbinFileActionsViewModel.UiState.Error -> {
|
||||||
|
context?.let {
|
||||||
|
Toast.makeText(it, R.string.error_file_actions, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFileThumbnail(titleFile: TrashbinFile?) {
|
||||||
|
titleFile?.let {
|
||||||
|
DisplayUtils.setThumbnail(
|
||||||
|
it.toOCFile(),
|
||||||
|
binding.thumbnailLayout.thumbnail,
|
||||||
|
currentUserProvider.user,
|
||||||
|
storageManager,
|
||||||
|
thumbnailAsyncTasks,
|
||||||
|
false,
|
||||||
|
context,
|
||||||
|
binding.thumbnailLayout.thumbnailShimmer,
|
||||||
|
syncedFolderProvider.preferences,
|
||||||
|
viewThemeUtils,
|
||||||
|
syncedFolderProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setMultipleFilesThumbnail() {
|
||||||
|
context?.let {
|
||||||
|
val drawable = viewThemeUtils.platform.tintDrawable(it, R.drawable.file_multiple, ColorRole.PRIMARY)
|
||||||
|
binding.thumbnailLayout.thumbnail.setImageDrawable(drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResultListener(
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: ResultListener
|
||||||
|
): TrashbinFileActionsBottomSheet {
|
||||||
|
fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
|
||||||
|
@IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
|
||||||
|
if (actionId != -1) {
|
||||||
|
listener.onResult(actionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleLoadingOrContent(state: TrashbinFileActionsViewModel.UiState) {
|
||||||
|
if (state is TrashbinFileActionsViewModel.UiState.Loading) {
|
||||||
|
binding.bottomSheetLoading.isVisible = true
|
||||||
|
binding.bottomSheetHeader.isVisible = false
|
||||||
|
viewThemeUtils.platform.colorCircularProgressBar(binding.bottomSheetLoading, ColorRole.PRIMARY)
|
||||||
|
} else {
|
||||||
|
binding.bottomSheetLoading.isVisible = false
|
||||||
|
binding.bottomSheetHeader.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayActions(actions: List<TrashbinFileAction>) {
|
||||||
|
if (binding.fileActionsList.isEmpty()) {
|
||||||
|
actions.forEach { action ->
|
||||||
|
val view = inflateActionView(action)
|
||||||
|
binding.fileActionsList.addView(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayTitle(titleFile: TrashbinFile?) {
|
||||||
|
titleFile?.fileName?.let {
|
||||||
|
binding.title.text = it
|
||||||
|
} ?: { binding.title.isVisible = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayTitle(fileCount: Int) {
|
||||||
|
binding.title.text = resources.getQuantityString(R.plurals.trashbin_list__footer__file, fileCount, fileCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inflateActionView(action: TrashbinFileAction): View {
|
||||||
|
val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
|
||||||
|
.apply {
|
||||||
|
root.setOnClickListener {
|
||||||
|
viewModel.onClick(action)
|
||||||
|
}
|
||||||
|
text.setText(action.title)
|
||||||
|
if (action.icon != null) {
|
||||||
|
val drawable =
|
||||||
|
viewThemeUtils.platform.tintDrawable(
|
||||||
|
requireContext(),
|
||||||
|
AppCompatResources.getDrawable(requireContext(), action.icon)!!
|
||||||
|
)
|
||||||
|
icon.setImageDrawable(drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemBinding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispatchActionClick(id: Int?) {
|
||||||
|
if (id != null) {
|
||||||
|
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
|
||||||
|
parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
|
||||||
|
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(numberOfAllFiles: Int, files: Collection<TrashbinFile>): TrashbinFileActionsBottomSheet {
|
||||||
|
return TrashbinFileActionsBottomSheet().apply {
|
||||||
|
val argsBundle = bundleOf(
|
||||||
|
TrashbinFileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles,
|
||||||
|
TrashbinFileActionsViewModel.ARG_FILES to ArrayList<TrashbinFile>(files)
|
||||||
|
)
|
||||||
|
arguments = argsBundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.nextcloud.ui.trashbinFileActions
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.nextcloud.client.logger.Logger
|
||||||
|
import com.nextcloud.ui.fileactions.FileActionsViewModel
|
||||||
|
import com.owncloud.android.R
|
||||||
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class TrashbinFileActionsViewModel @Inject constructor(
|
||||||
|
private val logger: Logger
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
sealed interface UiState {
|
||||||
|
data object Loading : UiState
|
||||||
|
data object Error : UiState
|
||||||
|
data class LoadedForSingleFile(
|
||||||
|
val actions: List<TrashbinFileAction>,
|
||||||
|
val titleFile: TrashbinFile?
|
||||||
|
) : UiState
|
||||||
|
|
||||||
|
data class LoadedForMultipleFiles(val actions: List<TrashbinFileAction>, val fileCount: Int) : UiState
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState: MutableLiveData<UiState> = MutableLiveData(UiState.Loading)
|
||||||
|
val uiState: LiveData<UiState>
|
||||||
|
get() = _uiState
|
||||||
|
|
||||||
|
private val _clickActionId: MutableLiveData<Int?> = MutableLiveData(null)
|
||||||
|
val clickActionId: LiveData<Int?>
|
||||||
|
@IdRes
|
||||||
|
get() = _clickActionId
|
||||||
|
|
||||||
|
fun load(arguments: Bundle) {
|
||||||
|
val files: List<TrashbinFile>? = arguments.getParcelableArrayList(ARG_FILES)
|
||||||
|
val numberOfAllFiles: Int = arguments.getInt(FileActionsViewModel.ARG_ALL_FILES_COUNT, 1)
|
||||||
|
|
||||||
|
if (files.isNullOrEmpty()) {
|
||||||
|
logger.d(TAG, "No valid files argument for loading actions")
|
||||||
|
_uiState.postValue(UiState.Error)
|
||||||
|
} else {
|
||||||
|
load(files.toList(), numberOfAllFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(files: Collection<TrashbinFile>, numberOfAllFiles: Int?) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val toHide = getHiddenActions(numberOfAllFiles, files)
|
||||||
|
val availableActions = getActionsToShow(toHide)
|
||||||
|
updateStateLoaded(files, availableActions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getHiddenActions(numberOfAllFiles: Int?, files: Collection<TrashbinFile>): List<Int> {
|
||||||
|
numberOfAllFiles?.let {
|
||||||
|
if (files.size >= it) {
|
||||||
|
return listOf(R.id.action_select_all_action_menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActionsToShow(toHide: List<Int>) = TrashbinFileAction.SORTED_VALUES.filter { it.id !in toHide }
|
||||||
|
|
||||||
|
private fun updateStateLoaded(files: Collection<TrashbinFile>, availableActions: List<TrashbinFileAction>) {
|
||||||
|
val state: UiState = when (files.size) {
|
||||||
|
1 -> {
|
||||||
|
val file = files.first()
|
||||||
|
UiState.LoadedForSingleFile(availableActions, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> UiState.LoadedForMultipleFiles(availableActions, files.size)
|
||||||
|
}
|
||||||
|
_uiState.postValue(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClick(action: TrashbinFileAction) {
|
||||||
|
_clickActionId.value = action.id
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ARG_ALL_FILES_COUNT = "ALL_FILES_COUNT"
|
||||||
|
const val ARG_FILES = "FILES"
|
||||||
|
|
||||||
|
private val TAG = TrashbinFileActionsViewModel::class.simpleName!!
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
* SPDX-FileCopyrightText: 2023 Tobias Kaminsky <tobias@kaminsky.me>
|
||||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
|
@ -17,6 +18,8 @@ import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import com.owncloud.android.datamodel.OCFile
|
||||||
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -79,3 +82,12 @@ fun Long.getFormattedStringDate(format: String): String {
|
||||||
val simpleDateFormat = SimpleDateFormat(format, Locale.getDefault())
|
val simpleDateFormat = SimpleDateFormat(format, Locale.getDefault())
|
||||||
return simpleDateFormat.format(Date(this))
|
return simpleDateFormat.format(Date(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TrashbinFile.toOCFile(): OCFile {
|
||||||
|
val ocFile = OCFile(this.remotePath)
|
||||||
|
ocFile.mimeType = this.mimeType
|
||||||
|
ocFile.fileLength = this.fileLength
|
||||||
|
ocFile.remoteId = this.remoteId
|
||||||
|
ocFile.fileName = this.fileName
|
||||||
|
return ocFile
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky
|
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky
|
||||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
|
@ -16,12 +17,15 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
|
||||||
import com.nextcloud.client.account.User;
|
import com.nextcloud.client.account.User;
|
||||||
import com.nextcloud.client.preferences.AppPreferences;
|
import com.nextcloud.client.preferences.AppPreferences;
|
||||||
|
import com.nextcloud.utils.extensions.ViewExtensionsKt;
|
||||||
import com.owncloud.android.R;
|
import com.owncloud.android.R;
|
||||||
import com.owncloud.android.databinding.ListFooterBinding;
|
import com.owncloud.android.databinding.ListFooterBinding;
|
||||||
import com.owncloud.android.databinding.TrashbinItemBinding;
|
import com.owncloud.android.databinding.TrashbinItemBinding;
|
||||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||||
|
import com.owncloud.android.datamodel.SyncedFolderProvider;
|
||||||
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
|
import com.owncloud.android.datamodel.ThumbnailsCacheManager;
|
||||||
import com.owncloud.android.lib.common.utils.Log_OC;
|
import com.owncloud.android.lib.common.utils.Log_OC;
|
||||||
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
||||||
|
@ -32,11 +36,16 @@ import com.owncloud.android.utils.MimeTypeUtil;
|
||||||
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
import com.owncloud.android.utils.theme.ViewThemeUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import static com.nextcloud.utils.extensions.ViewExtensionsKt.createRoundedOutline;
|
||||||
import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
|
import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
|
||||||
import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
|
import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
|
||||||
|
|
||||||
|
@ -57,11 +66,16 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
private final AppPreferences preferences;
|
private final AppPreferences preferences;
|
||||||
private final List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
|
private final List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
|
||||||
private final ViewThemeUtils viewThemeUtils;
|
private final ViewThemeUtils viewThemeUtils;
|
||||||
|
private final SyncedFolderProvider syncedFolderProvider;
|
||||||
|
|
||||||
|
private final Set<TrashbinFile> checkedFiles = new HashSet<>();
|
||||||
|
private boolean isMultiSelect = false;
|
||||||
|
|
||||||
public TrashbinListAdapter(
|
public TrashbinListAdapter(
|
||||||
TrashbinActivityInterface trashbinActivityInterface,
|
TrashbinActivityInterface trashbinActivityInterface,
|
||||||
FileDataStorageManager storageManager,
|
FileDataStorageManager storageManager,
|
||||||
AppPreferences preferences,
|
AppPreferences preferences,
|
||||||
|
SyncedFolderProvider syncedFolderProvider,
|
||||||
Context context,
|
Context context,
|
||||||
User user,
|
User user,
|
||||||
ViewThemeUtils viewThemeUtils
|
ViewThemeUtils viewThemeUtils
|
||||||
|
@ -72,6 +86,7 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.syncedFolderProvider = syncedFolderProvider;
|
||||||
this.viewThemeUtils = viewThemeUtils;
|
this.viewThemeUtils = viewThemeUtils;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +125,7 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
|
|
||||||
// layout
|
// layout
|
||||||
trashbinFileViewHolder.binding.ListItemLayout.setOnClickListener(v -> trashbinActivityInterface.onItemClicked(file));
|
trashbinFileViewHolder.binding.ListItemLayout.setOnClickListener(v -> trashbinActivityInterface.onItemClicked(file));
|
||||||
|
trashbinFileViewHolder.binding.ListItemLayout.setOnLongClickListener(v -> trashbinActivityInterface.onLongItemClicked(file));
|
||||||
|
|
||||||
// thumbnail
|
// thumbnail
|
||||||
trashbinFileViewHolder.binding.thumbnail.setTag(file.getRemoteId());
|
trashbinFileViewHolder.binding.thumbnail.setTag(file.getRemoteId());
|
||||||
|
@ -136,15 +152,44 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
file.getDeletionTimestamp() * 1000));
|
file.getDeletionTimestamp() * 1000));
|
||||||
|
|
||||||
// checkbox
|
// checkbox
|
||||||
trashbinFileViewHolder.binding.customCheckbox.setVisibility(View.GONE);
|
if (isCheckedFile(file)) {
|
||||||
|
trashbinFileViewHolder.binding.customCheckbox.setImageDrawable(
|
||||||
|
viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY));
|
||||||
|
} else {
|
||||||
|
trashbinFileViewHolder.binding.customCheckbox.setImageResource(R.drawable.ic_checkbox_blank_outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
trashbinFileViewHolder.binding.customCheckbox.setVisibility(isMultiSelect ? View.VISIBLE : View.GONE);
|
||||||
|
trashbinFileViewHolder.binding.restore.setVisibility(isMultiSelect ? View.GONE : View.VISIBLE);
|
||||||
|
ViewExtensionsKt.setVisibleIf(trashbinFileViewHolder.binding.overflowMenu, !isMultiSelect());
|
||||||
|
|
||||||
// overflow menu
|
// overflow menu
|
||||||
trashbinFileViewHolder.binding.overflowMenu.setOnClickListener(v ->
|
trashbinFileViewHolder.binding.overflowMenu.setOnClickListener(v ->
|
||||||
trashbinActivityInterface.onOverflowIconClicked(file, v));
|
trashbinActivityInterface.onOverflowIconClicked(file, v));
|
||||||
|
|
||||||
// restore button
|
// restore button
|
||||||
trashbinFileViewHolder.binding.restore.setOnClickListener(v ->
|
trashbinFileViewHolder.binding.restore.setOnClickListener(v -> trashbinActivityInterface.onRestoreIconClicked(file));
|
||||||
trashbinActivityInterface.onRestoreIconClicked(file, v));
|
|
||||||
|
float cornerRadius = context.getResources().getDimension(R.dimen.selected_grid_container_radius);
|
||||||
|
|
||||||
|
boolean isDarkModeActive = (syncedFolderProvider.getPreferences().isDarkModeEnabled());
|
||||||
|
int selectedItemBackgroundColorId;
|
||||||
|
if (isDarkModeActive) {
|
||||||
|
selectedItemBackgroundColorId = R.color.action_mode_background;
|
||||||
|
} else {
|
||||||
|
selectedItemBackgroundColorId = R.color.selected_item_background;
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemLayoutBackgroundColorId;
|
||||||
|
if (isCheckedFile(file)) {
|
||||||
|
itemLayoutBackgroundColorId = selectedItemBackgroundColorId;
|
||||||
|
} else {
|
||||||
|
itemLayoutBackgroundColorId = R.color.bg_default;
|
||||||
|
}
|
||||||
|
|
||||||
|
trashbinFileViewHolder.binding.ListItemLayout.setOutlineProvider(createRoundedOutline(context, cornerRadius));
|
||||||
|
trashbinFileViewHolder.binding.ListItemLayout.setClipToOutline(true);
|
||||||
|
trashbinFileViewHolder.binding.ListItemLayout.setBackgroundColor(ContextCompat.getColor(context, itemLayoutBackgroundColorId));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
TrashbinFooterViewHolder trashbinFooterViewHolder = (TrashbinFooterViewHolder) holder;
|
TrashbinFooterViewHolder trashbinFooterViewHolder = (TrashbinFooterViewHolder) holder;
|
||||||
|
@ -275,6 +320,18 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
return files.size() + 1;
|
return files.size() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFilesCount() {
|
||||||
|
return files.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyItemChanged(@NonNull TrashbinFile file) {
|
||||||
|
notifyItemChanged(getItemPosition(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getItemPosition(@NonNull TrashbinFile file) {
|
||||||
|
return files.indexOf(file);
|
||||||
|
}
|
||||||
|
|
||||||
public void cancelAllPendingTasks() {
|
public void cancelAllPendingTasks() {
|
||||||
for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
|
for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
|
@ -295,6 +352,49 @@ public class TrashbinListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
public void setMultiSelect(boolean bool) {
|
||||||
|
isMultiSelect = bool;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultiSelect() {
|
||||||
|
return isMultiSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCheckedFile(TrashbinFile file) {
|
||||||
|
return checkedFiles.contains(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCheckedFile(TrashbinFile file) {
|
||||||
|
checkedFiles.add(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCheckedFile(TrashbinFile file) {
|
||||||
|
checkedFiles.remove(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addToCheckedFiles(@Nullable List<TrashbinFile> files) {
|
||||||
|
checkedFiles.addAll(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<TrashbinFile> getCheckedItems() {
|
||||||
|
return checkedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCheckedItem(@Nullable Set<TrashbinFile> files) {
|
||||||
|
checkedFiles.clear();
|
||||||
|
checkedFiles.addAll(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCheckedItems() {
|
||||||
|
checkedFiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllFilesToCheckedFiles() {
|
||||||
|
addToCheckedFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
public class TrashbinFileViewHolder extends RecyclerView.ViewHolder {
|
public class TrashbinFileViewHolder extends RecyclerView.ViewHolder {
|
||||||
protected TrashbinItemBinding binding;
|
protected TrashbinItemBinding binding;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
||||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
|
@ -10,11 +11,10 @@ package com.owncloud.android.ui.interfaces;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
|
||||||
import com.owncloud.android.ui.adapter.OCFileListAdapter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for communication between {@link com.owncloud.android.ui.fragment.OCFileListFragment}
|
* Interface for communication between {@link com.owncloud.android.ui.trashbin.TrashbinActivity}
|
||||||
* and {@link OCFileListAdapter}
|
* and {@link com.owncloud.android.ui.adapter.TrashbinListAdapter}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public interface TrashbinActivityInterface {
|
public interface TrashbinActivityInterface {
|
||||||
|
@ -22,5 +22,7 @@ public interface TrashbinActivityInterface {
|
||||||
|
|
||||||
void onItemClicked(TrashbinFile file);
|
void onItemClicked(TrashbinFile file);
|
||||||
|
|
||||||
void onRestoreIconClicked(TrashbinFile file, View view);
|
boolean onLongItemClicked(TrashbinFile file);
|
||||||
|
|
||||||
|
void onRestoreIconClicked(TrashbinFile file);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||||
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
||||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
||||||
|
@ -10,23 +11,32 @@ package com.owncloud.android.ui.trashbin
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.AbsListView
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.nextcloud.client.account.CurrentAccountProvider
|
import com.nextcloud.client.account.CurrentAccountProvider
|
||||||
import com.nextcloud.client.di.Injectable
|
import com.nextcloud.client.di.Injectable
|
||||||
import com.nextcloud.client.network.ClientFactory
|
import com.nextcloud.client.network.ClientFactory
|
||||||
import com.nextcloud.client.preferences.AppPreferences
|
import com.nextcloud.client.preferences.AppPreferences
|
||||||
|
import com.nextcloud.client.utils.Throttler
|
||||||
|
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet
|
||||||
import com.owncloud.android.R
|
import com.owncloud.android.R
|
||||||
import com.owncloud.android.databinding.TrashbinActivityBinding
|
import com.owncloud.android.databinding.TrashbinActivityBinding
|
||||||
|
import com.owncloud.android.datamodel.SyncedFolderProvider
|
||||||
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
|
import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
|
||||||
import com.owncloud.android.ui.activity.DrawerActivity
|
import com.owncloud.android.ui.activity.DrawerActivity
|
||||||
import com.owncloud.android.ui.adapter.TrashbinListAdapter
|
import com.owncloud.android.ui.adapter.TrashbinListAdapter
|
||||||
|
@ -51,6 +61,9 @@ class TrashbinActivity :
|
||||||
@Inject
|
@Inject
|
||||||
var preferences: AppPreferences? = null
|
var preferences: AppPreferences? = null
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var syncedFolderProvider: SyncedFolderProvider
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@Inject
|
@Inject
|
||||||
var accountProvider: CurrentAccountProvider? = null
|
var accountProvider: CurrentAccountProvider? = null
|
||||||
|
@ -59,9 +72,8 @@ class TrashbinActivity :
|
||||||
@Inject
|
@Inject
|
||||||
var clientFactory: ClientFactory? = null
|
var clientFactory: ClientFactory? = null
|
||||||
|
|
||||||
@JvmField
|
|
||||||
@Inject
|
@Inject
|
||||||
var viewThemeUtils: ViewThemeUtils? = null
|
lateinit var throttler: Throttler
|
||||||
|
|
||||||
private var trashbinListAdapter: TrashbinListAdapter? = null
|
private var trashbinListAdapter: TrashbinListAdapter? = null
|
||||||
|
|
||||||
|
@ -71,6 +83,8 @@ class TrashbinActivity :
|
||||||
private var active = false
|
private var active = false
|
||||||
lateinit var binding: TrashbinActivityBinding
|
lateinit var binding: TrashbinActivityBinding
|
||||||
|
|
||||||
|
private var mMultiChoiceModeListener: MultiChoiceModeListener? = null
|
||||||
|
|
||||||
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
trashbinPresenter?.navigateUp()
|
trashbinPresenter?.navigateUp()
|
||||||
|
@ -134,6 +148,7 @@ class TrashbinActivity :
|
||||||
this,
|
this,
|
||||||
storageManager,
|
storageManager,
|
||||||
preferences,
|
preferences,
|
||||||
|
syncedFolderProvider,
|
||||||
this,
|
this,
|
||||||
user.orElse(accountProvider!!.user),
|
user.orElse(accountProvider!!.user),
|
||||||
viewThemeUtils
|
viewThemeUtils
|
||||||
|
@ -161,6 +176,13 @@ class TrashbinActivity :
|
||||||
loadFolder()
|
loadFolder()
|
||||||
|
|
||||||
handleOnBackPressed()
|
handleOnBackPressed()
|
||||||
|
|
||||||
|
mMultiChoiceModeListener = MultiChoiceModeListener(
|
||||||
|
this,
|
||||||
|
trashbinListAdapter,
|
||||||
|
viewThemeUtils
|
||||||
|
) { filesCount, checkedFiles -> openActionsMenu(filesCount, checkedFiles) }
|
||||||
|
addDrawerListener(mMultiChoiceModeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnBackPressed() {
|
private fun handleOnBackPressed() {
|
||||||
|
@ -171,6 +193,8 @@ class TrashbinActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFolder(onComplete: () -> Unit = {}, onError: () -> Unit = {}) {
|
fun loadFolder(onComplete: () -> Unit = {}, onError: () -> Unit = {}) {
|
||||||
|
// exit action mode on data refresh
|
||||||
|
mMultiChoiceModeListener?.exitSelectionMode()
|
||||||
trashbinListAdapter?.let {
|
trashbinListAdapter?.let {
|
||||||
if (it.itemCount > EMPTY_LIST_COUNT) {
|
if (it.itemCount > EMPTY_LIST_COUNT) {
|
||||||
binding.swipeContainingList.isRefreshing = true
|
binding.swipeContainingList.isRefreshing = true
|
||||||
|
@ -205,20 +229,50 @@ class TrashbinActivity :
|
||||||
val popup = PopupMenu(this, view)
|
val popup = PopupMenu(this, view)
|
||||||
popup.inflate(R.menu.item_trashbin)
|
popup.inflate(R.menu.item_trashbin)
|
||||||
popup.setOnMenuItemClickListener {
|
popup.setOnMenuItemClickListener {
|
||||||
trashbinPresenter?.removeTrashbinFile(file)
|
onFileActionChosen(it.itemId, setOf(file))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
popup.show()
|
popup.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClicked(file: TrashbinFile) {
|
override fun onItemClicked(file: TrashbinFile) {
|
||||||
if (file.isFolder) {
|
if (trashbinListAdapter?.isMultiSelect == true) {
|
||||||
|
toggleItemToCheckedList(file)
|
||||||
|
} else if (file.isFolder) {
|
||||||
trashbinPresenter?.enterFolder(file.remotePath)
|
trashbinPresenter?.enterFolder(file.remotePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreIconClicked(file: TrashbinFile, view: View) {
|
override fun onRestoreIconClicked(file: TrashbinFile) {
|
||||||
trashbinPresenter?.restoreTrashbinFile(file)
|
trashbinPresenter?.restoreTrashbinFile(listOf(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongItemClicked(file: TrashbinFile): Boolean {
|
||||||
|
// Create only once instance of action mode
|
||||||
|
if (mMultiChoiceModeListener?.mActiveActionMode != null) {
|
||||||
|
toggleItemToCheckedList(file)
|
||||||
|
} else {
|
||||||
|
startActionMode(mMultiChoiceModeListener)
|
||||||
|
trashbinListAdapter?.addCheckedFile(file)
|
||||||
|
}
|
||||||
|
mMultiChoiceModeListener?.updateActionModeFile(file)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will toggle a file selection status from the action mode
|
||||||
|
*
|
||||||
|
* @param file The concerned TrashbinFile by the selection/deselection
|
||||||
|
*/
|
||||||
|
private fun toggleItemToCheckedList(file: TrashbinFile) {
|
||||||
|
trashbinListAdapter?.run {
|
||||||
|
if (isCheckedFile(file)) {
|
||||||
|
removeCheckedFile(file)
|
||||||
|
} else {
|
||||||
|
addCheckedFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mMultiChoiceModeListener?.updateActionModeFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
@ -315,7 +369,262 @@ class TrashbinActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openActionsMenu(filesCount: Int, checkedFiles: Set<TrashbinFile>) {
|
||||||
|
throttler.run("overflowClick") {
|
||||||
|
val supportFragmentManager = supportFragmentManager
|
||||||
|
|
||||||
|
TrashbinFileActionsBottomSheet.newInstance(filesCount, checkedFiles)
|
||||||
|
.setResultListener(
|
||||||
|
supportFragmentManager,
|
||||||
|
this
|
||||||
|
) { id: Int ->
|
||||||
|
onFileActionChosen(
|
||||||
|
id,
|
||||||
|
checkedFiles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.show(supportFragmentManager, "actions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set<TrashbinFile>): Boolean {
|
||||||
|
if (checkedFiles.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
when (itemId) {
|
||||||
|
R.id.action_delete -> {
|
||||||
|
trashbinPresenter?.removeTrashbinFile(checkedFiles)
|
||||||
|
mMultiChoiceModeListener?.exitSelectionMode()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.restore -> {
|
||||||
|
trashbinPresenter?.restoreTrashbinFile(checkedFiles)
|
||||||
|
mMultiChoiceModeListener?.exitSelectionMode()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.action_select_all_action_menu -> {
|
||||||
|
selectAllFiles(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.action_deselect_all_action_menu -> {
|
||||||
|
selectAllFiles(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-/select all elements in the current list view.
|
||||||
|
*
|
||||||
|
* @param select `true` to select all, `false` to deselect all
|
||||||
|
*/
|
||||||
|
private fun selectAllFiles(select: Boolean) {
|
||||||
|
trashbinListAdapter?.let {
|
||||||
|
if (select) {
|
||||||
|
it.addAllFilesToCheckedFiles()
|
||||||
|
} else {
|
||||||
|
it.clearCheckedItems()
|
||||||
|
}
|
||||||
|
for (i in 0 until it.itemCount) {
|
||||||
|
it.notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
mMultiChoiceModeListener?.invalidateActionMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EMPTY_LIST_COUNT = 1
|
const val EMPTY_LIST_COUNT = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for multiple selection mode.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Manages input from the user when one or more files or folders are selected in the list.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Also listens to changes in navigation drawer to hide and recover multiple selection when it's opened and closed.
|
||||||
|
*/
|
||||||
|
internal class MultiChoiceModeListener(
|
||||||
|
val activity: TrashbinActivity,
|
||||||
|
val adapter: TrashbinListAdapter?,
|
||||||
|
val viewThemeUtils: ViewThemeUtils,
|
||||||
|
val openActionsMenu: (Int, Set<TrashbinFile>) -> Unit
|
||||||
|
) : AbsListView.MultiChoiceModeListener, DrawerLayout.DrawerListener {
|
||||||
|
|
||||||
|
var mActiveActionMode: ActionMode? = null
|
||||||
|
private var mIsActionModeNew = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when action mode is finished because the drawer was opened
|
||||||
|
*/
|
||||||
|
private var mActionModeClosedByDrawer = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected items in list when action mode is closed by drawer
|
||||||
|
*/
|
||||||
|
private val mSelectionWhenActionModeClosedByDrawer: MutableSet<TrashbinFile> = HashSet()
|
||||||
|
|
||||||
|
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the navigation drawer is closed, action mode is recovered in the same state as was when the drawer was
|
||||||
|
* (started to be) opened.
|
||||||
|
*
|
||||||
|
* @param drawerView Navigation drawer just closed.
|
||||||
|
*/
|
||||||
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
if (mActionModeClosedByDrawer && mSelectionWhenActionModeClosedByDrawer.size > 0) {
|
||||||
|
activity.startActionMode(this)
|
||||||
|
|
||||||
|
adapter?.setCheckedItem(mSelectionWhenActionModeClosedByDrawer)
|
||||||
|
|
||||||
|
mActiveActionMode?.invalidate()
|
||||||
|
|
||||||
|
mSelectionWhenActionModeClosedByDrawer.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the action mode is active when the navigation drawer starts to move, the action mode is closed and the
|
||||||
|
* selection stored to be recovered when the drawer is closed.
|
||||||
|
*
|
||||||
|
* @param newState One of STATE_IDLE, STATE_DRAGGING or STATE_SETTLING.
|
||||||
|
*/
|
||||||
|
override fun onDrawerStateChanged(newState: Int) {
|
||||||
|
if (DrawerLayout.STATE_DRAGGING == newState && mActiveActionMode != null) {
|
||||||
|
adapter?.let {
|
||||||
|
mSelectionWhenActionModeClosedByDrawer.addAll(
|
||||||
|
it.checkedItems
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mActiveActionMode?.finish()
|
||||||
|
mActionModeClosedByDrawer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update action mode bar when an item is selected / unselected in the list
|
||||||
|
*/
|
||||||
|
override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) {
|
||||||
|
// nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load menu and customize UI when action mode is started.
|
||||||
|
*/
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mActiveActionMode = mode
|
||||||
|
// Determine if actionMode is "new" or not (already affected by item-selection)
|
||||||
|
mIsActionModeNew = true
|
||||||
|
|
||||||
|
// fake menu to be able to use bottom sheet instead
|
||||||
|
val inflater: MenuInflater = activity.menuInflater
|
||||||
|
inflater.inflate(R.menu.custom_menu_placeholder, menu)
|
||||||
|
val item = menu.findItem(R.id.custom_menu_placeholder_item)
|
||||||
|
item.icon?.let {
|
||||||
|
item.setIcon(
|
||||||
|
viewThemeUtils.platform.colorDrawable(
|
||||||
|
it,
|
||||||
|
ContextCompat.getColor(activity, R.color.white)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode.invalidate()
|
||||||
|
|
||||||
|
// set actionMode color
|
||||||
|
viewThemeUtils.platform.colorStatusBar(
|
||||||
|
activity,
|
||||||
|
ContextCompat.getColor(activity, R.color.action_mode_background)
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter?.setMultiSelect(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates available action in menu depending on current selection.
|
||||||
|
*/
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
val checkedFiles: Set<TrashbinFile> = adapter?.checkedItems ?: emptySet()
|
||||||
|
val checkedCount = checkedFiles.size
|
||||||
|
val title: String =
|
||||||
|
activity.getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount)
|
||||||
|
mode.title = title
|
||||||
|
|
||||||
|
// Determine if we need to finish the action mode because there are no items selected
|
||||||
|
if (checkedCount == 0 && !mIsActionModeNew) {
|
||||||
|
exitSelectionMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exits the multi file selection mode.
|
||||||
|
*/
|
||||||
|
fun exitSelectionMode() {
|
||||||
|
mActiveActionMode?.run {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will update (invalidate) the action mode adapter/mode to refresh an item selection change
|
||||||
|
*
|
||||||
|
* @param file The concerned TrashbinFile to refresh in adapter
|
||||||
|
*/
|
||||||
|
fun updateActionModeFile(file: TrashbinFile) {
|
||||||
|
mIsActionModeNew = false
|
||||||
|
mActiveActionMode?.let {
|
||||||
|
it.invalidate()
|
||||||
|
adapter?.notifyItemChanged(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateActionMode() {
|
||||||
|
mActiveActionMode?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the corresponding action when a menu item is tapped by the user.
|
||||||
|
*/
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
adapter?.let {
|
||||||
|
val checkedFiles: Set<TrashbinFile> = it.checkedItems
|
||||||
|
if (item.itemId == R.id.custom_menu_placeholder_item) {
|
||||||
|
openActionsMenu(it.filesCount, checkedFiles)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores UI.
|
||||||
|
*/
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
mActiveActionMode = null
|
||||||
|
|
||||||
|
viewThemeUtils.platform.resetStatusBar(activity)
|
||||||
|
|
||||||
|
adapter?.setMultiSelect(false)
|
||||||
|
adapter?.clearCheckedItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
||||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
|
@ -28,8 +29,8 @@ interface TrashbinContract {
|
||||||
fun loadFolder(onCompleted: () -> Unit = {}, onError: () -> Unit = {})
|
fun loadFolder(onCompleted: () -> Unit = {}, onError: () -> Unit = {})
|
||||||
fun navigateUp()
|
fun navigateUp()
|
||||||
fun enterFolder(folder: String?)
|
fun enterFolder(folder: String?)
|
||||||
fun restoreTrashbinFile(file: TrashbinFile?)
|
fun restoreTrashbinFile(files: Collection<TrashbinFile?>)
|
||||||
fun removeTrashbinFile(file: TrashbinFile?)
|
fun removeTrashbinFile(files: Collection<TrashbinFile?>)
|
||||||
fun emptyTrashbin()
|
fun emptyTrashbin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Nextcloud - Android Client
|
* Nextcloud - Android Client
|
||||||
*
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
|
||||||
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
* SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
|
||||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
|
||||||
|
@ -61,34 +62,38 @@ class TrashbinPresenter(
|
||||||
trashbinView.atRoot(isRoot)
|
trashbinView.atRoot(isRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restoreTrashbinFile(file: TrashbinFile?) {
|
override fun restoreTrashbinFile(files: Collection<TrashbinFile?>) {
|
||||||
trashbinRepository.restoreFile(
|
for (file in files) {
|
||||||
file,
|
trashbinRepository.restoreFile(
|
||||||
object : TrashbinRepository.OperationCallback {
|
file,
|
||||||
override fun onResult(success: Boolean) {
|
object : TrashbinRepository.OperationCallback {
|
||||||
if (success) {
|
override fun onResult(success: Boolean) {
|
||||||
trashbinView.removeFile(file)
|
if (success) {
|
||||||
} else {
|
trashbinView.removeFile(file)
|
||||||
trashbinView.showSnackbarError(R.string.trashbin_file_not_restored, file)
|
} else {
|
||||||
|
trashbinView.showSnackbarError(R.string.trashbin_file_not_restored, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeTrashbinFile(file: TrashbinFile?) {
|
override fun removeTrashbinFile(files: Collection<TrashbinFile?>) {
|
||||||
trashbinRepository.removeTrashbinFile(
|
for (file in files) {
|
||||||
file,
|
trashbinRepository.removeTrashbinFile(
|
||||||
object : TrashbinRepository.OperationCallback {
|
file,
|
||||||
override fun onResult(success: Boolean) {
|
object : TrashbinRepository.OperationCallback {
|
||||||
if (success) {
|
override fun onResult(success: Boolean) {
|
||||||
trashbinView.removeFile(file)
|
if (success) {
|
||||||
} else {
|
trashbinView.removeFile(file)
|
||||||
trashbinView.showSnackbarError(R.string.trashbin_file_not_deleted, file)
|
} else {
|
||||||
|
trashbinView.showSnackbarError(R.string.trashbin_file_not_deleted, file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun emptyTrashbin() {
|
override fun emptyTrashbin() {
|
||||||
|
|
|
@ -584,6 +584,10 @@
|
||||||
<item quantity="one">%1$d file</item>
|
<item quantity="one">%1$d file</item>
|
||||||
<item quantity="other">%1$d files</item>
|
<item quantity="other">%1$d files</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="trashbin_list__footer__file">
|
||||||
|
<item quantity="one">%1$d item</item>
|
||||||
|
<item quantity="other">%1$d items</item>
|
||||||
|
</plurals>
|
||||||
<string name="set_picture_as">Use picture as</string>
|
<string name="set_picture_as">Use picture as</string>
|
||||||
<string name="set_as">Set As</string>
|
<string name="set_as">Set As</string>
|
||||||
|
|
||||||
|
@ -880,6 +884,7 @@
|
||||||
<string name="first_run_4_text">Screensharing, online meetings and web conferences</string>
|
<string name="first_run_4_text">Screensharing, online meetings and web conferences</string>
|
||||||
<string name="restore_button_description">Restore deleted file</string>
|
<string name="restore_button_description">Restore deleted file</string>
|
||||||
<string name="restore">Restore file</string>
|
<string name="restore">Restore file</string>
|
||||||
|
<string name="restore_item">Restore</string>
|
||||||
<string name="new_version_was_created">New version was created</string>
|
<string name="new_version_was_created">New version was created</string>
|
||||||
<string name="new_comment">New comment…</string>
|
<string name="new_comment">New comment…</string>
|
||||||
<string name="error_comment_file">Error commenting file</string>
|
<string name="error_comment_file">Error commenting file</string>
|
||||||
|
|
Loading…
Reference in a new issue