diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 9a0b4193d292..1486bae7f35b 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -34,6 +34,7 @@ import com.nextcloud.ui.SetStatusMessageBottomSheet; import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; +import com.nextcloud.ui.tags.TagManagementBottomSheet; import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.MainApp; @@ -510,6 +511,9 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet(); + + @ContributesAndroidInjector + abstract TagManagementBottomSheet tagManagementBottomSheet(); @ContributesAndroidInjector abstract NavigatorActivity navigatorActivity(); diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt index eb3e98a6c570..4017ed61395a 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -13,8 +13,9 @@ import com.nextcloud.client.documentscan.DocumentScanViewModel import com.nextcloud.client.etm.EtmViewModel import com.nextcloud.client.logger.ui.LogsViewModel import com.nextcloud.ui.fileactions.FileActionsViewModel -import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel +import com.nextcloud.ui.tags.TagManagementViewModel import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel +import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel import dagger.Binds import dagger.Module @@ -57,6 +58,11 @@ abstract class ViewModelModule { @ViewModelKey(TrashbinFileActionsViewModel::class) abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(TagManagementViewModel::class) + abstract fun tagManagementViewModel(vm: TagManagementViewModel): ViewModel + @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory } diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt new file mode 100644 index 000000000000..db68508a2ab1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -0,0 +1,147 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.nextcloud.ui.tags.adapter.TagListAdapter +import com.nextcloud.ui.tags.model.TagUiState +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.di.Injectable +import com.nextcloud.client.di.ViewModelFactory +import com.owncloud.android.databinding.TagManagementBottomSheetBinding +import com.owncloud.android.lib.resources.tags.Tag +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.launch +import javax.inject.Inject + +class TagManagementBottomSheet : + BottomSheetDialogFragment(), + Injectable { + + @Inject + lateinit var vmFactory: ViewModelFactory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private var _binding: TagManagementBottomSheetBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: TagManagementViewModel + private lateinit var tagAdapter: TagListAdapter + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + viewModel = ViewModelProvider(this, vmFactory)[TagManagementViewModel::class.java] + _binding = TagManagementBottomSheetBinding.inflate(inflater, container, false) + + val bottomSheetDialog = dialog as BottomSheetDialog + bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetDialog.behavior.skipCollapsed = true + + viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) + + setupAdapter() + setupSearch() + observeState() + + val fileId = requireArguments().getLong(ARG_FILE_ID) + val currentTags = requireArguments().getParcelableArrayList(ARG_CURRENT_TAGS) ?: arrayListOf() + viewModel.load(fileId, currentTags) + + return binding.root + } + + private fun setupAdapter() { + tagAdapter = TagListAdapter( + onTagChecked = { tag, isChecked -> + if (isChecked) { + viewModel.assignTag(tag) + } else { + viewModel.unassignTag(tag) + } + }, + onCreateTag = { name -> + viewModel.createAndAssignTag(name) + binding.searchEditText.text?.clear() + } + ) + + binding.tagList.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = tagAdapter + } + } + + private fun setupSearch() { + binding.searchEditText.doAfterTextChanged { text -> + viewModel.setSearchQuery(text?.toString() ?: "") + } + } + + private fun observeState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + when (state) { + is TagUiState.Loading -> { + binding.loadingIndicator.visibility = View.VISIBLE + binding.tagList.visibility = View.GONE + } + + is TagUiState.Loaded -> { + binding.loadingIndicator.visibility = View.GONE + binding.tagList.visibility = View.VISIBLE + tagAdapter.update(state.allTags, state.assignedTagIds, state.query) + } + + is TagUiState.Error -> { + binding.loadingIndicator.visibility = View.GONE + binding.tagList.visibility = View.GONE + } + } + } + } + } + } + + override fun onDestroyView() { + val assignedTags = viewModel.getAssignedTags() + setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_TAGS to ArrayList(assignedTags))) + + super.onDestroyView() + _binding = null + } + + companion object { + const val REQUEST_KEY = "TAG_MANAGEMENT_REQUEST" + const val RESULT_KEY_TAGS = "RESULT_TAGS" + private const val ARG_FILE_ID = "ARG_FILE_ID" + private const val ARG_CURRENT_TAGS = "ARG_CURRENT_TAGS" + + fun newInstance(fileId: Long, currentTags: List): TagManagementBottomSheet = + TagManagementBottomSheet().apply { + arguments = bundleOf( + ARG_FILE_ID to fileId, + ARG_CURRENT_TAGS to ArrayList(currentTags) + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt new file mode 100644 index 000000000000..6928e7baf815 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.ui.tags.model.TagUiState +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.tags.CreateTagRemoteOperation +import com.owncloud.android.lib.resources.tags.DeleteTagRemoteOperation +import com.owncloud.android.lib.resources.tags.GetTagsRemoteOperation +import com.owncloud.android.lib.resources.tags.PutTagRemoteOperation +import com.owncloud.android.lib.resources.tags.Tag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +class TagManagementViewModel @Inject constructor( + private val clientFactory: ClientFactory, + private val currentAccountProvider: CurrentAccountProvider +) : ViewModel() { + + private val _uiState = MutableStateFlow(TagUiState.Loading) + val uiState: StateFlow = _uiState + + private var fileId: Long = -1 + + fun load(fileId: Long, currentTags: List) { + this.fileId = fileId + val assignedTagNames = currentTags.map { it.name }.toSet() + + viewModelScope.launch(Dispatchers.IO) { + try { + val client = clientFactory.create(currentAccountProvider.user) + val result = GetTagsRemoteOperation().execute(client) + + if (result.isSuccess) { + val assignedIds = result.resultData + .filter { it.name in assignedTagNames } + .map { it.id } + .toSet() + + _uiState.update { + TagUiState.Loaded( + allTags = result.resultData, + assignedTagIds = assignedIds + ) + } + } else { + _uiState.update { TagUiState.Error(R.string.failed_to_load_tags) } + } + } catch (e: ClientFactory.CreationException) { + _uiState.update { TagUiState.Error(R.string.failed_to_load_tags) } + } + } + } + + fun assignTag(tag: Tag) { + viewModelScope.launch(Dispatchers.IO) { + try { + val client = clientFactory.createNextcloudClient(currentAccountProvider.user) + val result = PutTagRemoteOperation(tag.id, fileId).execute(client) + + if (result.isSuccess) { + _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy(assignedTagIds = state.assignedTagIds + tag.id) + } else { + state + } + } + } + } catch (e: ClientFactory.CreationException) { + // ignore + } + } + } + + fun unassignTag(tag: Tag) { + viewModelScope.launch(Dispatchers.IO) { + try { + val client = clientFactory.createNextcloudClient(currentAccountProvider.user) + val result = DeleteTagRemoteOperation(tag.id, fileId).execute(client) + + if (result.isSuccess) { + _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy(assignedTagIds = state.assignedTagIds - tag.id) + } else { + state + } + } + } + } catch (e: ClientFactory.CreationException) { + // ignore + } + } + } + + fun createAndAssignTag(name: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + val nextcloudClient = clientFactory.createNextcloudClient(currentAccountProvider.user) + val createResult = CreateTagRemoteOperation(name).execute(nextcloudClient) + + if (createResult.isSuccess) { + val ownCloudClient = clientFactory.create(currentAccountProvider.user) + val tagsResult = GetTagsRemoteOperation().execute(ownCloudClient) + + if (tagsResult.isSuccess) { + val allTags = tagsResult.resultData + val newTag = allTags.find { it.name == name } + + if (newTag != null) { + PutTagRemoteOperation(newTag.id, fileId).execute(nextcloudClient) + + _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy( + allTags = allTags, + assignedTagIds = state.assignedTagIds + newTag.id + ) + } else { + TagUiState.Loaded( + allTags = allTags, + assignedTagIds = setOf(newTag.id) + ) + } + } + } + } + } + } catch (e: ClientFactory.CreationException) { + Log_OC.e("TagManagement", e.message) + } + } + } + + fun setSearchQuery(query: String) { + _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy(query = query) + } else { + state + } + } + } + + fun getAssignedTags(): List { + val state = _uiState.value + if (state is TagUiState.Loaded) { + return state.allTags.filter { it.id in state.assignedTagIds } + } + return emptyList() + } +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt new file mode 100644 index 000000000000..5ceda6037fe7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt @@ -0,0 +1,73 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.ui.tags.adapter.viewholder.CreateTagViewHolder +import com.nextcloud.ui.tags.adapter.viewholder.TagViewHolder +import com.owncloud.android.R +import com.owncloud.android.lib.resources.tags.Tag + +class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private val onCreateTag: (String) -> Unit) : + RecyclerView.Adapter() { + + private var tags: List = emptyList() + private var assignedTagIds: Set = emptySet() + private var query: String = "" + private var showCreateItem: Boolean = false + + companion object { + private const val VIEW_TYPE_TAG = 0 + private const val VIEW_TYPE_CREATE = 1 + } + + fun update(allTags: List, assignedIds: Set, searchQuery: String) { + this.assignedTagIds = assignedIds + this.query = searchQuery + + tags = if (searchQuery.isBlank()) { + allTags + } else { + allTags.filter { it.name.contains(searchQuery, ignoreCase = true) } + } + + showCreateItem = searchQuery.isNotBlank() && tags.none { it.name.equals(searchQuery, ignoreCase = true) } + + notifyDataSetChanged() + } + + override fun getItemCount(): Int = tags.size + if (showCreateItem) 1 else 0 + + override fun getItemViewType(position: Int): Int = + if (showCreateItem && position == tags.size) VIEW_TYPE_CREATE else VIEW_TYPE_TAG + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return if (viewType == VIEW_TYPE_CREATE) { + val view = inflater.inflate(R.layout.tag_list_item, parent, false) + CreateTagViewHolder(view, onCreateTag) + } else { + val view = inflater.inflate(R.layout.tag_list_item, parent, false) + TagViewHolder(view, onTagChecked) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is TagViewHolder -> { + val tag = tags[position] + holder.bind(tag, tag.id in assignedTagIds) + } + + is CreateTagViewHolder -> { + holder.bind(query) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/CreateTagViewHolder.kt b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/CreateTagViewHolder.kt new file mode 100644 index 000000000000..0aabeaebacb3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/CreateTagViewHolder.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags.adapter.viewholder + +import android.view.View +import android.widget.CheckBox +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R + +class CreateTagViewHolder( + itemView: View, + private val onCreateTag: (String) -> Unit +) : RecyclerView.ViewHolder(itemView) { + private val colorDot: View = itemView.findViewById(R.id.tag_color_dot) + private val tagName: TextView = itemView.findViewById(R.id.tag_name) + private val checkBox: CheckBox = itemView.findViewById(R.id.tag_checkbox) + + fun bind(name: String) { + colorDot.visibility = View.INVISIBLE + tagName.text = itemView.context.getString(R.string.create_tag_format, name) + checkBox.visibility = View.GONE + + itemView.setOnClickListener { + onCreateTag(name) + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt new file mode 100644 index 000000000000..18fa1541f6bd --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags.adapter.viewholder + +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.widget.CheckBox +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R +import com.owncloud.android.lib.resources.tags.Tag + +class TagViewHolder( + itemView: View, + private val onTagChecked: (Tag, Boolean) -> Unit +) : RecyclerView.ViewHolder(itemView) { + private val colorDot: View = itemView.findViewById(R.id.tag_color_dot) + private val tagName: TextView = itemView.findViewById(R.id.tag_name) + private val checkBox: CheckBox = itemView.findViewById(R.id.tag_checkbox) + + fun bind(tag: Tag, isAssigned: Boolean) { + tagName.text = tag.name + + if (tag.color != null) { + try { + val color = Color.parseColor(tag.color) + val background = colorDot.background + if (background is GradientDrawable) { + background.setColor(color) + } + colorDot.visibility = View.VISIBLE + } catch (e: IllegalArgumentException) { + colorDot.visibility = View.INVISIBLE + } + } else { + colorDot.visibility = View.INVISIBLE + } + + checkBox.setOnCheckedChangeListener(null) + checkBox.isChecked = isAssigned + checkBox.setOnCheckedChangeListener { _, isChecked -> + onTagChecked(tag, isChecked) + } + + itemView.setOnClickListener { + checkBox.isChecked = !checkBox.isChecked + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt b/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt new file mode 100644 index 000000000000..16e49f98a5ee --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags.model + +import androidx.annotation.StringRes +import com.owncloud.android.lib.resources.tags.Tag + +sealed interface TagUiState { + object Loading : TagUiState + data class Loaded(val allTags: List, val assignedTagIds: Set, val query: String = "") : TagUiState + data class Error(@StringRes val messageId: Int) : TagUiState +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 0cbf1aa8c700..f4a016ee5b91 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -32,11 +32,10 @@ import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.model.WorkerState; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; +import com.nextcloud.ui.tags.TagManagementBottomSheet; import com.nextcloud.utils.MenuUtils; -import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; @@ -84,7 +83,6 @@ import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.widget.ViewPager2; -import kotlin.Unit; /** * This Fragment is used to display the details about a file. @@ -244,29 +242,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, return null; } - if (getFile().getTags().isEmpty()) { - binding.tagsGroup.setVisibility(View.GONE); - } else { - for (Tag tag : getFile().getTags()) { - Chip chip = new Chip(context); - chip.setText(tag.getName()); - chip.setChipBackgroundColor(ColorStateList.valueOf(getResources().getColor(R.color.bg_default, - context.getTheme()))); - chip.setShapeAppearanceModel(chip.getShapeAppearanceModel().toBuilder().setAllCornerSizes((100.0f)) - .build()); - chip.setEnsureMinTouchTargetSize(false); - chip.setClickable(false); - viewThemeUtils.material.themeChipSuggestion(chip); - - if (tag.getColor() != null) { - int color = Color.parseColor(tag.getColor()); - chip.setChipStrokeColor(ColorStateList.valueOf(color)); - chip.setTextColor(color); - } - - binding.tagsGroup.addView(chip); - } - } + refreshTagChips(context); return view; } @@ -285,6 +261,22 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat updateFileDetails(false, false); } + + getChildFragmentManager().setFragmentResultListener( + TagManagementBottomSheet.REQUEST_KEY, + getViewLifecycleOwner(), + (requestKey, result) -> { + ArrayList updatedTags = result.getParcelableArrayList(TagManagementBottomSheet.RESULT_KEY_TAGS); + if (updatedTags != null) { + getFile().setTags(updatedTags); + storageManager.saveFile(getFile()); + Context ctx = getContext(); + if (ctx != null) { + refreshTagChips(ctx); + } + } + } + ); } @Override @@ -304,6 +296,50 @@ private void onOverflowIconClicked() { .show(fragmentManager, "actions"); } + private void refreshTagChips(Context context) { + binding.tagsGroup.removeAllViews(); + binding.tagsGroup.setVisibility(View.VISIBLE); + + for (Tag tag : getFile().getTags()) { + Chip chip = new Chip(context); + chip.setText(tag.getName()); + chip.setChipBackgroundColor(ColorStateList.valueOf(getResources().getColor(R.color.bg_default, + context.getTheme()))); + chip.setShapeAppearanceModel(chip.getShapeAppearanceModel().toBuilder().setAllCornerSizes((100.0f)) + .build()); + chip.setEnsureMinTouchTargetSize(false); + chip.setClickable(false); + viewThemeUtils.material.themeChipSuggestion(chip); + + if (tag.getColor() != null) { + int color = Color.parseColor(tag.getColor()); + chip.setChipStrokeColor(ColorStateList.valueOf(color)); + chip.setTextColor(color); + } + + binding.tagsGroup.addView(chip); + } + + Chip editChip = new Chip(context); + editChip.setChipIconResource(R.drawable.ic_edit); + editChip.setText(R.string.manage_tags); + editChip.setChipBackgroundColor(ColorStateList.valueOf(getResources().getColor(R.color.bg_default, + context.getTheme()))); + editChip.setShapeAppearanceModel(editChip.getShapeAppearanceModel().toBuilder().setAllCornerSizes(100.0f) + .build()); + editChip.setEnsureMinTouchTargetSize(false); + viewThemeUtils.material.themeChipSuggestion(editChip); + editChip.setOnClickListener(v -> { + TagManagementBottomSheet bottomSheet = TagManagementBottomSheet.Companion.newInstance( + getFile().getLocalId(), + getFile().getTags() + ); +// FileActionsBottomSheet bottomSheet = FileActionsBottomSheet.Companion.newInstance(getFile(), false); + bottomSheet.show(getChildFragmentManager(), "tag_management"); + }); + binding.tagsGroup.addView(editChip); + } + private void setupViewPager() { binding.tabLayout.removeAllTabs(); diff --git a/app/src/main/res/drawable/ic_tag_color_dot.xml b/app/src/main/res/drawable/ic_tag_color_dot.xml new file mode 100644 index 000000000000..ee7c3e0ea9d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_tag_color_dot.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/layout/tag_list_item.xml b/app/src/main/res/layout/tag_list_item.xml new file mode 100644 index 000000000000..f10a065a70e9 --- /dev/null +++ b/app/src/main/res/layout/tag_list_item.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/tag_management_bottom_sheet.xml b/app/src/main/res/layout/tag_management_bottom_sheet.xml new file mode 100644 index 000000000000..53ce84305ac9 --- /dev/null +++ b/app/src/main/res/layout/tag_management_bottom_sheet.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d4b90050f06..56fa33087e2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1012,6 +1012,10 @@ New encrypted folder Virus detected. Upload cannot be completed! Tags + Manage tags + Search tags + Create tag: \"%1$s\" + Error managing tags Unable to fetch sharees. Adding sharee failed Adding share failed. This file or folder has already been shared with this person or group. @@ -1514,6 +1518,7 @@ Cannot open file chooser Send copy to Failed to start action! + Failed to load tags Action triggered File upload conflicts diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60c978e0a371..72e0e7714e1b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.31" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="eacc5b6d375cacaae8efa27d35519b27a9ce5da2" +androidLibraryVersion ="0e8aab9b3f56426fd5e4ffc88242b1dcf44ac0db" androidPluginVersion = "9.1.0" androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9f383a77935f..37c401329935 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20830,6 +20830,14 @@ + + + + + + + +