From b461aee72018c2cf2b292b9f0f567c20b0eca53c Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Fri, 13 Mar 2026 12:31:23 +0100 Subject: [PATCH] edit tags Signed-off-by: tobiasKaminsky --- .../nextcloud/client/di/ComponentsModule.java | 4 + .../nextcloud/client/di/ViewModelModule.kt | 8 +- .../com/nextcloud/ui/tags/TagListAdapter.kt | 129 ++ .../ui/tags/TagManagementBottomSheet.kt | 142 ++ .../ui/tags/TagManagementViewModel.kt | 169 +++ .../ui/fragment/FileDetailFragment.java | 88 +- .../main/res/drawable/ic_tag_color_dot.xml | 14 + app/src/main/res/layout/tag_list_item.xml | 38 + .../layout/tag_management_bottom_sheet.xml | 70 + app/src/main/res/values/strings.xml | 4 + gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 1296 +++++++++++++++++ 12 files changed, 1936 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt create mode 100644 app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt create mode 100644 app/src/main/res/drawable/ic_tag_color_dot.xml create mode 100644 app/src/main/res/layout/tag_list_item.xml create mode 100644 app/src/main/res/layout/tag_management_bottom_sheet.xml 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 c6a483ad3b05..68fc1a329a46 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; @@ -513,4 +514,7 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet(); + + @ContributesAndroidInjector + abstract TagManagementBottomSheet tagManagementBottomSheet(); } 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/TagListAdapter.kt b/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt new file mode 100644 index 000000000000..66a9c0ec5b44 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt @@ -0,0 +1,129 @@ +/* + * 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.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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 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 { + return 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) + } else { + val view = inflater.inflate(R.layout.tag_list_item, parent, false) + TagViewHolder(view) + } + } + + 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) + } + } + } + + inner class TagViewHolder(itemView: View) : 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 + } + } + } + + inner class CreateTagViewHolder(itemView: View) : 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/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt new file mode 100644 index 000000000000..efc024981319 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -0,0 +1,142 @@ +/* + * 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.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 TagManagementViewModel.TagUiState.Loading -> { + binding.loadingIndicator.visibility = View.VISIBLE + binding.tagList.visibility = View.GONE + } + is TagManagementViewModel.TagUiState.Loaded -> { + binding.loadingIndicator.visibility = View.GONE + binding.tagList.visibility = View.VISIBLE + tagAdapter.update(state.allTags, state.assignedTagIds, state.query) + } + is TagManagementViewModel.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 { + return 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..73cf3c02d5f2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -0,0 +1,169 @@ +/* + * 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.client.network.ClientFactory +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() { + + sealed interface TagUiState { + object Loading : TagUiState + data class Loaded( + val allTags: List, + val assignedTagIds: Set, + val query: String = "" + ) : TagUiState + + data class Error(val message: String) : TagUiState + } + + 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 assignedIds = currentTags.map { it.id }.toSet() + + viewModelScope.launch(Dispatchers.IO) { + try { + val client = clientFactory.create(currentAccountProvider.user) + val result = GetTagsRemoteOperation().execute(client) + + if (result.isSuccess) { + _uiState.update { + TagUiState.Loaded( + allTags = result.resultData, + assignedTagIds = assignedIds + ) + } + } else { + _uiState.update { TagUiState.Error("Failed to load tags") } + } + } catch (e: ClientFactory.CreationException) { + _uiState.update { TagUiState.Error("Failed to create client") } + } + } + } + + 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) { + // ignore + } + } + } + + 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/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 0cbf1aa8c700..096da6ba25d1 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().getFileId(), + 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 e4f1d21778ac..0e89d3db4683 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1001,6 +1001,10 @@ New 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. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f600c10b1ef..7fc6906e2df7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.1" androidGifDrawableVersion = "1.2.31" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="cbc5ff74fcf83a26d8bba454f8c82a9a786ba02e" +androidLibraryVersion ="1033fb117730a4dc5aa4276d15c4b0fe186fc25d" androidPluginVersion = "9.1.0" androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6e1117e07404..b682d5121195 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1288,6 +1288,9 @@ + + + @@ -1528,6 +1531,16 @@ + + + + + + + + + + @@ -1629,6 +1642,22 @@ + + + + + + + + + + + + + + + + @@ -1735,6 +1764,16 @@ + + + + + + + + + + @@ -1836,6 +1875,22 @@ + + + + + + + + + + + + + + + + @@ -1934,6 +1989,16 @@ + + + + + + + + + + @@ -2035,6 +2100,22 @@ + + + + + + + + + + + + + + + + @@ -2133,6 +2214,16 @@ + + + + + + + + + + @@ -2234,6 +2325,22 @@ + + + + + + + + + + + + + + + + @@ -2324,6 +2431,11 @@ + + + + + @@ -2385,6 +2497,14 @@ + + + + + + + + @@ -2531,6 +2651,16 @@ + + + + + + + + + + @@ -2632,6 +2762,22 @@ + + + + + + + + + + + + + + + + @@ -2791,6 +2937,16 @@ + + + + + + + + + + @@ -2905,6 +3061,22 @@ + + + + + + + + + + + + + + + + @@ -3003,6 +3175,16 @@ + + + + + + + + + + @@ -3064,6 +3246,22 @@ + + + + + + + + + + + + + + + + @@ -3122,6 +3320,16 @@ + + + + + + + + + + @@ -3146,6 +3354,22 @@ + + + + + + + + + + + + + + + + @@ -3172,6 +3396,16 @@ + + + + + + + + + + @@ -3286,6 +3520,22 @@ + + + + + + + + + + + + + + + + @@ -3392,6 +3642,16 @@ + + + + + + + + + + @@ -3506,6 +3766,22 @@ + + + + + + + + + + + + + + + + @@ -3612,6 +3888,16 @@ + + + + + + + + + + @@ -3721,6 +4007,22 @@ + + + + + + + + + + + + + + + + @@ -3827,6 +4129,16 @@ + + + + + + + + + + @@ -3936,6 +4248,22 @@ + + + + + + + + + + + + + + + + @@ -4074,6 +4402,16 @@ + + + + + + + + + + @@ -4183,6 +4521,22 @@ + + + + + + + + + + + + + + + + @@ -4273,6 +4627,11 @@ + + + + + @@ -4334,6 +4693,14 @@ + + + + + + + + @@ -4416,6 +4783,11 @@ + + + + + @@ -4477,6 +4849,14 @@ + + + + + + + + @@ -4562,6 +4942,11 @@ + + + + + @@ -4650,6 +5035,14 @@ + + + + + + + + @@ -4748,6 +5141,16 @@ + + + + + + + + + + @@ -4857,6 +5260,22 @@ + + + + + + + + + + + + + + + + @@ -4963,6 +5382,16 @@ + + + + + + + + + + @@ -5072,6 +5501,22 @@ + + + + + + + + + + + + + + + + @@ -5686,6 +6131,14 @@ + + + + + + + + @@ -5830,6 +6283,14 @@ + + + + + + + + @@ -5974,6 +6435,14 @@ + + + + + + + + @@ -6118,6 +6587,14 @@ + + + + + + + + @@ -6262,6 +6739,14 @@ + + + + + + + + @@ -6406,6 +6891,14 @@ + + + + + + + + @@ -6566,6 +7059,14 @@ + + + + + + + + @@ -12016,6 +12517,14 @@ + + + + + + + + @@ -12160,6 +12669,14 @@ + + + + + + + + @@ -12185,6 +12702,11 @@ + + + + + @@ -12329,6 +12851,14 @@ + + + + + + + + @@ -12354,6 +12884,11 @@ + + + + + @@ -12498,6 +13033,14 @@ + + + + + + + + @@ -12642,6 +13185,14 @@ + + + + + + + + @@ -12786,6 +13337,14 @@ + + + + + + + + @@ -12994,6 +13553,14 @@ + + + + + + + + @@ -13138,6 +13705,14 @@ + + + + + + + + @@ -13282,6 +13857,14 @@ + + + + + + + + @@ -13426,6 +14009,14 @@ + + + + + + + + @@ -13570,6 +14161,14 @@ + + + + + + + + @@ -13714,6 +14313,14 @@ + + + + + + + + @@ -13858,6 +14465,14 @@ + + + + + + + + @@ -14117,6 +14732,14 @@ + + + + + + + + @@ -14261,6 +14884,14 @@ + + + + + + + + @@ -14405,6 +15036,14 @@ + + + + + + + + @@ -14549,6 +15188,14 @@ + + + + + + + + @@ -14693,6 +15340,14 @@ + + + + + + + + @@ -14837,6 +15492,14 @@ + + + + + + + + @@ -14981,6 +15644,14 @@ + + + + + + + + @@ -15181,6 +15852,14 @@ + + + + + + + + @@ -15325,6 +16004,14 @@ + + + + + + + + @@ -15357,6 +16044,14 @@ + + + + + + + + @@ -15501,6 +16196,14 @@ + + + + + + + + @@ -15645,6 +16348,14 @@ + + + + + + + + @@ -15817,6 +16528,14 @@ + + + + + + + + @@ -15961,6 +16680,14 @@ + + + + + + + + @@ -16297,6 +17024,14 @@ + + + + + + + + @@ -16697,6 +17432,14 @@ + + + + + + + + @@ -16841,6 +17584,14 @@ + + + + + + + + @@ -16985,6 +17736,14 @@ + + + + + + + + @@ -17129,6 +17888,14 @@ + + + + + + + + @@ -17689,6 +18456,14 @@ + + + + + + + + @@ -17833,6 +18608,14 @@ + + + + + + + + @@ -17977,6 +18760,14 @@ + + + + + + + + @@ -18121,6 +18912,14 @@ + + + + + + + + @@ -18265,6 +19064,14 @@ + + + + + + + + @@ -18409,6 +19216,14 @@ + + + + + + + + @@ -18553,6 +19368,14 @@ + + + + + + + + @@ -18697,6 +19520,14 @@ + + + + + + + + @@ -18841,6 +19672,14 @@ + + + + + + + + @@ -18985,6 +19824,14 @@ + + + + + + + + @@ -19129,6 +19976,14 @@ + + + + + + + + @@ -19273,6 +20128,14 @@ + + + + + + + + @@ -19593,6 +20456,14 @@ + + + + + + + + @@ -19737,6 +20608,14 @@ + + + + + + + + @@ -19745,6 +20624,14 @@ + + + + + + + + @@ -19753,6 +20640,14 @@ + + + + + + + + @@ -19889,6 +20784,14 @@ + + + + + + + + @@ -20083,6 +20986,14 @@ + + + + + + + + @@ -20131,6 +21042,14 @@ + + + + + + + + @@ -20179,6 +21098,14 @@ + + + + + + + + @@ -20247,6 +21174,11 @@ + + + + + @@ -20302,6 +21234,11 @@ + + + + + @@ -20342,6 +21279,11 @@ + + + + + @@ -20387,6 +21329,14 @@ + + + + + + + + @@ -20825,6 +21775,14 @@ + + + + + + + + @@ -23514,6 +24472,14 @@ + + + + + + + + @@ -23833,6 +24799,14 @@ + + + + + + + + @@ -23921,6 +24895,14 @@ + + + + + + + + @@ -24009,6 +24991,14 @@ + + + + + + + + @@ -24097,6 +25087,14 @@ + + + + + + + + @@ -24185,6 +25183,14 @@ + + + + + + + + @@ -24273,6 +25279,14 @@ + + + + + + + + @@ -24369,6 +25383,14 @@ + + + + + + + + @@ -24459,6 +25481,11 @@ + + + + + @@ -24580,6 +25607,11 @@ + + + + + @@ -24849,6 +25881,14 @@ + + + + + + + + @@ -25073,6 +26113,14 @@ + + + + + + + + @@ -25209,6 +26257,14 @@ + + + + + + + + @@ -25235,6 +26291,11 @@ + + + + + @@ -25411,6 +26472,11 @@ + + + + + @@ -26144,6 +27210,16 @@ + + + + + + + + + + @@ -26176,6 +27252,19 @@ + + + + + + + + + + + + + @@ -26208,6 +27297,14 @@ + + + + + + + + @@ -26216,6 +27313,14 @@ + + + + + + + + @@ -26249,6 +27354,16 @@ + + + + + + + + + + @@ -26478,6 +27593,14 @@ + + + + + + + + @@ -26725,6 +27848,22 @@ + + + + + + + + + + + + + + + + @@ -27727,10 +28866,21 @@ + + + + + + + + + + + @@ -31809,6 +32959,14 @@ + + + + + + + + @@ -32101,6 +33259,14 @@ + + + + + + + + @@ -32121,6 +33287,11 @@ + + + + + @@ -32153,6 +33324,14 @@ + + + + + + + + @@ -32161,6 +33340,14 @@ + + + + + + + + @@ -32276,6 +33463,9 @@ + + + @@ -35202,6 +36392,14 @@ + + + + + + + + @@ -36323,6 +37521,14 @@ + + + + + + + + @@ -36509,6 +37715,14 @@ + + + + + + + + @@ -37925,6 +39139,14 @@ + + + + + + + + @@ -37941,6 +39163,14 @@ + + + + + + + + @@ -38252,6 +39482,14 @@ + + + + + + + + @@ -38273,6 +39511,22 @@ + + + + + + + + + + + + + + + + @@ -38305,6 +39559,14 @@ + + + + + + + + @@ -38313,16 +39575,34 @@ + + + + + + + + + + + + + + + + + + @@ -38807,6 +40087,14 @@ + + + + + + + + @@ -39107,6 +40395,14 @@ + + + + + + + +