From c826703a8401b469f3df4cb46977eb530aa87469 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Fri, 13 Mar 2026 12:31:23 +0100 Subject: [PATCH 1/6] 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 | 85 ++++++--- .../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 + 10 files changed, 639 insertions(+), 24 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 bf93c43f4f2a..fe4e2ca82e21 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.albumItemActions.AlbumItemActionsBottomSheet; 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; @@ -512,6 +513,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/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 c45076cd078e..a78d33d8099a 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 @@ -34,6 +34,7 @@ import com.nextcloud.client.preferences.AppPreferences; 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.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; @@ -242,29 +243,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; } @@ -283,6 +262,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 @@ -302,6 +297,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 83482af7f956..ac41fe335ba9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1050,6 +1050,10 @@ 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. From ab0c803107dc9c982b6cdc0351495db603ba5cd1 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 17 Mar 2026 09:57:36 +0100 Subject: [PATCH 2/6] wip Signed-off-by: tobiasKaminsky --- .../java/com/nextcloud/ui/tags/TagListAdapter.kt | 12 +++++------- .../nextcloud/ui/tags/TagManagementBottomSheet.kt | 11 +++++++---- .../com/nextcloud/ui/tags/TagManagementViewModel.kt | 13 +++++++------ .../android/ui/fragment/FileDetailFragment.java | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt b/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt index 66a9c0ec5b44..cf822a9938f5 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt @@ -17,10 +17,8 @@ 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() { +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() @@ -49,9 +47,8 @@ class TagListAdapter( 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 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) @@ -70,6 +67,7 @@ class TagListAdapter( 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/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt index efc024981319..a6958b7240c9 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -30,7 +30,9 @@ import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.launch import javax.inject.Inject -class TagManagementBottomSheet : BottomSheetDialogFragment(), Injectable { +class TagManagementBottomSheet : + BottomSheetDialogFragment(), + Injectable { @Inject lateinit var vmFactory: ViewModelFactory @@ -101,11 +103,13 @@ class TagManagementBottomSheet : BottomSheetDialogFragment(), Injectable { 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 @@ -130,13 +134,12 @@ class TagManagementBottomSheet : BottomSheetDialogFragment(), Injectable { 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 { + 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 index 73cf3c02d5f2..d45e6eb421b8 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -29,11 +29,7 @@ class TagManagementViewModel @Inject constructor( sealed interface TagUiState { object Loading : TagUiState - data class Loaded( - val allTags: List, - val assignedTagIds: Set, - val query: String = "" - ) : TagUiState + data class Loaded(val allTags: List, val assignedTagIds: Set, val query: String = "") : TagUiState data class Error(val message: String) : TagUiState } @@ -45,7 +41,7 @@ class TagManagementViewModel @Inject constructor( fun load(fileId: Long, currentTags: List) { this.fileId = fileId - val assignedIds = currentTags.map { it.id }.toSet() + val assignedTagNames = currentTags.map { it.name }.toSet() viewModelScope.launch(Dispatchers.IO) { try { @@ -53,6 +49,11 @@ class TagManagementViewModel @Inject constructor( 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, 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 a78d33d8099a..21fa6e2fd793 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 @@ -332,7 +332,7 @@ private void refreshTagChips(Context context) { viewThemeUtils.material.themeChipSuggestion(editChip); editChip.setOnClickListener(v -> { TagManagementBottomSheet bottomSheet = TagManagementBottomSheet.Companion.newInstance( - getFile().getFileId(), + getFile().getLocalId(), getFile().getTags() ); // FileActionsBottomSheet bottomSheet = FileActionsBottomSheet.Companion.newInstance(getFile(), false); From a33c9e3984e6ce10059cb6d406542725769ed460 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 14 Apr 2026 10:33:27 +0200 Subject: [PATCH 3/6] wip Signed-off-by: tobiasKaminsky --- .../ui/tags/TagManagementBottomSheet.kt | 1 + .../ui/tags/TagManagementViewModel.kt | 8 ++- .../ui/tags/{ => adapter}/TagListAdapter.kt | 64 ++----------------- .../adapter/viewholder/CreateTagViewHolder.kt | 32 ++++++++++ .../tags/adapter/viewholder/TagViewHolder.kt | 54 ++++++++++++++++ app/src/main/res/values/strings.xml | 1 + gradle/verification-metadata.xml | 8 +++ 7 files changed, 106 insertions(+), 62 deletions(-) rename app/src/main/java/com/nextcloud/ui/tags/{ => adapter}/TagListAdapter.kt (50%) create mode 100644 app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/CreateTagViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt index a6958b7240c9..207905c4f4bb 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -18,6 +18,7 @@ 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.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt index d45e6eb421b8..2f7f94a12c74 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -6,10 +6,12 @@ */ package com.nextcloud.ui.tags +import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.R import com.owncloud.android.lib.resources.tags.CreateTagRemoteOperation import com.owncloud.android.lib.resources.tags.DeleteTagRemoteOperation import com.owncloud.android.lib.resources.tags.GetTagsRemoteOperation @@ -31,7 +33,7 @@ class TagManagementViewModel @Inject constructor( object Loading : TagUiState data class Loaded(val allTags: List, val assignedTagIds: Set, val query: String = "") : TagUiState - data class Error(val message: String) : TagUiState + data class Error(@StringRes val messageId: Int) : TagUiState } private val _uiState = MutableStateFlow(TagUiState.Loading) @@ -61,10 +63,10 @@ class TagManagementViewModel @Inject constructor( ) } } else { - _uiState.update { TagUiState.Error("Failed to load tags") } + _uiState.update { TagUiState.Error(R.string.failed_to_load_tags) } } } catch (e: ClientFactory.CreationException) { - _uiState.update { TagUiState.Error("Failed to create client") } + _uiState.update { TagUiState.Error(R.string.failed_to_load_tags) } } } } diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt similarity index 50% rename from app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt rename to app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt index cf822a9938f5..5ceda6037fe7 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt @@ -4,16 +4,13 @@ * 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 +package com.nextcloud.ui.tags.adapter -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.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 @@ -54,10 +51,10 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v 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) + CreateTagViewHolder(view, onCreateTag) } else { val view = inflater.inflate(R.layout.tag_list_item, parent, false) - TagViewHolder(view) + TagViewHolder(view, onTagChecked) } } @@ -73,55 +70,4 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v } } } - - 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/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/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac41fe335ba9..644a28463818 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1577,6 +1577,7 @@ Android System WebView is missing or disabled on this device Send copy to Failed to start action! + Failed to load tags Action triggered File upload conflicts diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6e00639df0b7..a2add94a937b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -20977,6 +20977,14 @@ + + + + + + + + From 081fd2de3e4a6adc4382209a3d7d66eee3dfa78d Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 15 Apr 2026 08:11:35 +0200 Subject: [PATCH 4/6] create TagUiState Signed-off-by: tobiasKaminsky --- .../ui/tags/TagManagementBottomSheet.kt | 7 ++++--- .../nextcloud/ui/tags/TagManagementViewModel.kt | 12 +++--------- .../com/nextcloud/ui/tags/model/TagUiState.kt | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt index 207905c4f4bb..db68508a2ab1 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -19,6 +19,7 @@ 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 @@ -100,18 +101,18 @@ class TagManagementBottomSheet : viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> when (state) { - is TagManagementViewModel.TagUiState.Loading -> { + is TagUiState.Loading -> { binding.loadingIndicator.visibility = View.VISIBLE binding.tagList.visibility = View.GONE } - is TagManagementViewModel.TagUiState.Loaded -> { + is TagUiState.Loaded -> { binding.loadingIndicator.visibility = View.GONE binding.tagList.visibility = View.VISIBLE tagAdapter.update(state.allTags, state.assignedTagIds, state.query) } - is TagManagementViewModel.TagUiState.Error -> { + is TagUiState.Error -> { binding.loadingIndicator.visibility = View.GONE binding.tagList.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt index 2f7f94a12c74..6928e7baf815 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -6,12 +6,13 @@ */ package com.nextcloud.ui.tags -import androidx.annotation.StringRes 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 @@ -29,13 +30,6 @@ class TagManagementViewModel @Inject constructor( 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(@StringRes val messageId: Int) : TagUiState - } - private val _uiState = MutableStateFlow(TagUiState.Loading) val uiState: StateFlow = _uiState @@ -147,7 +141,7 @@ class TagManagementViewModel @Inject constructor( } } } catch (e: ClientFactory.CreationException) { - // ignore + Log_OC.e("TagManagement", e.message) } } } 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 +} From 9b72cb711d33e48b86ad160c5b8183ff4c85ce84 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Wed, 2 Sep 2026 10:04:30 +0200 Subject: [PATCH 5/6] Tag: sort case-insensitive Signed-off-by: tobiasKaminsky --- .../java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt | 3 ++- .../ui/tags/adapter/viewholder/CreateTagViewHolder.kt | 6 ++---- .../nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt | 6 ++---- 3 files changed, 6 insertions(+), 9 deletions(-) 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 index 5ceda6037fe7..d87d2bd38be6 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt @@ -8,6 +8,7 @@ package com.nextcloud.ui.tags.adapter import android.view.LayoutInflater import android.view.ViewGroup +import androidx.compose.ui.text.toLowerCase import androidx.recyclerview.widget.RecyclerView import com.nextcloud.ui.tags.adapter.viewholder.CreateTagViewHolder import com.nextcloud.ui.tags.adapter.viewholder.TagViewHolder @@ -35,7 +36,7 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v allTags } else { allTags.filter { it.name.contains(searchQuery, ignoreCase = true) } - } + }.sortedBy { it.name.lowercase() } showCreateItem = searchQuery.isNotBlank() && tags.none { it.name.equals(searchQuery, ignoreCase = true) } 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 index 0aabeaebacb3..1246a1aecce5 100644 --- 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 @@ -12,10 +12,8 @@ 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) { +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) 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 index 18fa1541f6bd..a62d9de07f04 100644 --- 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 @@ -15,10 +15,8 @@ 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) { +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) From e966354e00785784b8e7afe5c40d4102f24c9605 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 2 Sep 2026 11:24:35 +0200 Subject: [PATCH 6/6] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/di/ComponentsModule.java | 2 +- .../java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt | 4 ++-- .../java/com/nextcloud/ui/tags/TagManagementViewModel.kt | 2 +- .../java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt | 3 ++- .../nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt | 7 ++++--- .../owncloud/android/ui/fragment/FileDetailFragment.java | 1 - app/src/main/res/values/strings.xml | 1 - 7 files changed, 10 insertions(+), 10 deletions(-) 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 fe4e2ca82e21..e4dc34170343 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -513,7 +513,7 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet(); - + @ContributesAndroidInjector abstract TagManagementBottomSheet tagManagementBottomSheet(); diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt index db68508a2ab1..82aaa1e89b78 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -18,14 +18,14 @@ 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.nextcloud.ui.tags.adapter.TagListAdapter +import com.nextcloud.ui.tags.model.TagUiState import com.owncloud.android.databinding.TagManagementBottomSheetBinding import com.owncloud.android.lib.resources.tags.Tag import com.owncloud.android.utils.theme.ViewThemeUtils diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt index 6928e7baf815..9011e6519fb7 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -9,8 +9,8 @@ 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.nextcloud.ui.tags.model.TagUiState import com.owncloud.android.R import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.tags.CreateTagRemoteOperation 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 index d87d2bd38be6..23e276ff034f 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt @@ -6,9 +6,9 @@ */ package com.nextcloud.ui.tags.adapter +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup -import androidx.compose.ui.text.toLowerCase import androidx.recyclerview.widget.RecyclerView import com.nextcloud.ui.tags.adapter.viewholder.CreateTagViewHolder import com.nextcloud.ui.tags.adapter.viewholder.TagViewHolder @@ -28,6 +28,7 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v private const val VIEW_TYPE_CREATE = 1 } + @SuppressLint("NotifyDataSetChanged") fun update(allTags: List, assignedIds: Set, searchQuery: String) { this.assignedTagIds = assignedIds this.query = searchQuery 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 index a62d9de07f04..6f61f65ac5e0 100644 --- 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 @@ -6,11 +6,11 @@ */ 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.core.graphics.toColorInt import androidx.recyclerview.widget.RecyclerView import com.owncloud.android.R import com.owncloud.android.lib.resources.tags.Tag @@ -24,9 +24,10 @@ class TagViewHolder(itemView: View, private val onTagChecked: (Tag, Boolean) -> fun bind(tag: Tag, isAssigned: Boolean) { tagName.text = tag.name - if (tag.color != null) { + val tagColor = tag.color + if (tagColor != null) { try { - val color = Color.parseColor(tag.color) + val color = tagColor.toColorInt() val background = colorDot.background if (background is GradientDrawable) { background.setColor(color) 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 21fa6e2fd793..344958fbf8d0 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 @@ -335,7 +335,6 @@ private void refreshTagChips(Context context) { getFile().getLocalId(), getFile().getTags() ); -// FileActionsBottomSheet bottomSheet = FileActionsBottomSheet.Companion.newInstance(getFile(), false); bottomSheet.show(getChildFragmentManager(), "tag_management"); }); binding.tagsGroup.addView(editChip); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 644a28463818..9f21f2174191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1053,7 +1053,6 @@ 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.