From 55e530a250240b7153f42e5f3828f8afb3dc0209 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Fri, 13 Mar 2026 12:31:23 +0100 Subject: [PATCH 01/11] edit tags Signed-off-by: tobiasKaminsky # Conflicts: # gradle/libs.versions.toml --- .../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 d6f19299750b..f2730df49df4 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -33,6 +33,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; @@ -512,4 +513,7 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract CommunityFragment communityFragment(); + + @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 829e1b4dcc31..7ad0554284ba 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; @@ -241,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; } @@ -282,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 @@ -301,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 d662ed36f1bf..57e9d6e69876 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1016,6 +1016,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. From e62d4ce3b22a9dc8c56c87bc33dadbe6ce786b8b Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 17 Mar 2026 09:57:36 +0100 Subject: [PATCH 02/11] 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 7ad0554284ba..94e44dad791f 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 @@ -331,7 +331,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 58b69fb18ccc8ab13577a9a76b97edb9730ec0f6 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 18 Jun 2026 07:57:36 +0200 Subject: [PATCH 03/11] wip Signed-off-by: tobiasKaminsky --- .../com/nextcloud/ui/tags/TagChipsHelper.kt | 68 +++++++++++++++++++ .../ui/tags/TagManagementViewModel.kt | 47 ++++++------- .../ui/fragment/FileDetailFragment.java | 57 ++++------------ gradle/verification-metadata.xml | 8 +++ 4 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt b/app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt new file mode 100644 index 000000000000..1684eb1aebb1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui.tags + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.owncloud.android.R +import com.owncloud.android.lib.resources.tags.Tag +import com.owncloud.android.utils.theme.ViewThemeUtils + +class TagChipsHelper( + private val viewThemeUtils: ViewThemeUtils +) { + fun refresh( + context: Context, + chipGroup: ChipGroup, + tags: List, + onEditClicked: Runnable + ) { + chipGroup.removeAllViews() + chipGroup.visibility = android.view.View.VISIBLE + + for (tag in tags) { + val chip = Chip(context).apply { + text = tag.name + chipBackgroundColor = ColorStateList.valueOf( + context.resources.getColor(R.color.bg_default, context.theme) + ) + shapeAppearanceModel = shapeAppearanceModel.toBuilder() + .setAllCornerSizes(100.0f) + .build() + isClickable = false + } + chip.setEnsureMinTouchTargetSize(false) + viewThemeUtils.material.themeChipSuggestion(chip) + + if (tag.color != null) { + val color = Color.parseColor(tag.color) + chip.chipStrokeColor = ColorStateList.valueOf(color) + chip.setTextColor(color) + } + + chipGroup.addView(chip) + } + + val editChip = Chip(context).apply { + setChipIconResource(R.drawable.ic_edit) + setText(R.string.manage_tags) + chipBackgroundColor = ColorStateList.valueOf( + context.resources.getColor(R.color.bg_default, context.theme) + ) + shapeAppearanceModel = shapeAppearanceModel.toBuilder() + .setAllCornerSizes(100.0f) + .build() + setOnClickListener { onEditClicked.run() } + } + editChip.setEnsureMinTouchTargetSize(false) + viewThemeUtils.material.themeChipSuggestion(editChip) + chipGroup.addView(editChip) + } +} 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..cf4e820b0911 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -117,29 +117,30 @@ class TagManagementViewModel @Inject constructor( 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) - ) - } + if (!createResult.isSuccess) { + return@launch + } + 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) + ) } } } 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 94e44dad791f..2d073fb19820 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 @@ -10,9 +10,7 @@ package com.owncloud.android.ui.fragment; import android.content.Context; -import android.content.res.ColorStateList; import android.graphics.Bitmap; -import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -20,7 +18,6 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; -import com.google.android.material.chip.Chip; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; import com.nextcloud.client.account.User; @@ -34,6 +31,7 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; +import com.nextcloud.ui.tags.TagChipsHelper; import com.nextcloud.ui.tags.TagManagementBottomSheet; import com.nextcloud.utils.MenuUtils; import com.nextcloud.utils.extensions.BundleExtensionsKt; @@ -114,6 +112,8 @@ public class FileDetailFragment extends FileFragment implements OnClickListener, @Inject ViewThemeUtils viewThemeUtils; @Inject BackgroundJobManager backgroundJobManager; + private TagChipsHelper tagChipsHelper; + /** * Public factory method to create new FileDetailFragment instances. *

@@ -297,47 +297,18 @@ private void onOverflowIconClicked() { } 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); + new TagChipsHelper(viewThemeUtils).refresh( + context, + binding.tagsGroup, + getFile().getTags(), + () -> { + TagManagementBottomSheet bottomSheet = TagManagementBottomSheet.Companion.newInstance( + getFile().getLocalId(), + getFile().getTags() + ); + bottomSheet.show(getChildFragmentManager(), "tag_management"); } - - 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() { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1d6b2710697a..e67049413ec2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21109,6 +21109,14 @@ + + + + + + + + From a9fe1eaf3f7907fa2c2130324e84a25aa00b3493 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 08:56:59 +0200 Subject: [PATCH 04/11] separate inner classes Signed-off-by: alperozturk96 --- .../com/nextcloud/ui/tags/TagListAdapter.kt | 127 ------------------ .../ui/tags/TagManagementBottomSheet.kt | 8 +- .../ui/tags/TagManagementViewModel.kt | 8 +- .../ui/tags/adapter/TagListAdapter.kt | 76 +++++++++++ .../adapter/viewholder/CreateTagViewHolder.kt | 30 +++++ .../tags/adapter/viewholder/TagViewHolder.kt | 52 +++++++ .../com/nextcloud/ui/tags/model/TagUiState.kt | 17 +++ .../ui/tags/{ => util}/TagChipsHelper.kt | 10 +- .../ui/fragment/FileDetailFragment.java | 2 +- 9 files changed, 188 insertions(+), 142 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt 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 create mode 100644 app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt rename app/src/main/java/com/nextcloud/ui/tags/{ => util}/TagChipsHelper.kt (90%) diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt b/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt deleted file mode 100644 index cf822a9938f5..000000000000 --- a/app/src/main/java/com/nextcloud/ui/tags/TagListAdapter.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 = - 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 index a6958b7240c9..82aaa1e89b78 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -24,6 +24,8 @@ 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 @@ -99,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 cf4e820b0911..04e8515be855 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.network.ClientFactory +import com.nextcloud.ui.tags.model.TagUiState import com.owncloud.android.lib.resources.tags.CreateTagRemoteOperation import com.owncloud.android.lib.resources.tags.DeleteTagRemoteOperation import com.owncloud.android.lib.resources.tags.GetTagsRemoteOperation @@ -27,13 +28,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(val message: String) : TagUiState - } - private val _uiState = MutableStateFlow(TagUiState.Loading) val uiState: StateFlow = _uiState 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..94efda60b140 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/TagListAdapter.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.tags.adapter + +import android.annotation.SuppressLint +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 + } + + @SuppressLint("NotifyDataSetChanged") + 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) + } 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, onTagChecked) + } + + is CreateTagViewHolder -> { + holder.bind(query, onCreateTag) + } + } + } +} 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..0439dffbe539 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/CreateTagViewHolder.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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) : 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, onCreateTag: (String) -> Unit) { + 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..276c8751d934 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/adapter/viewholder/TagViewHolder.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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) : 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, onTagChecked: (Tag, Boolean) -> Unit) { + 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..2f2a6137e590 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.tags.model + +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(val message: String) : TagUiState +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt b/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt similarity index 90% rename from app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt rename to app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt index 1684eb1aebb1..5960b2b5e1df 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagChipsHelper.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt @@ -1,14 +1,16 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.ui.tags + +package com.nextcloud.ui.tags.util import android.content.Context import android.content.res.ColorStateList import android.graphics.Color +import android.view.View import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.owncloud.android.R @@ -25,7 +27,7 @@ class TagChipsHelper( onEditClicked: Runnable ) { chipGroup.removeAllViews() - chipGroup.visibility = android.view.View.VISIBLE + chipGroup.visibility = View.VISIBLE for (tag in tags) { val chip = Chip(context).apply { 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 2d073fb19820..102e068ea5e8 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 @@ -31,7 +31,7 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; -import com.nextcloud.ui.tags.TagChipsHelper; +import com.nextcloud.ui.tags.util.TagChipsHelper; import com.nextcloud.ui.tags.TagManagementBottomSheet; import com.nextcloud.utils.MenuUtils; import com.nextcloud.utils.extensions.BundleExtensionsKt; From 49f18748ec537e95f5dd22b3e2c7a298ffbd88cd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 09:00:35 +0200 Subject: [PATCH 05/11] remove unused error message string Signed-off-by: alperozturk96 --- .../main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt | 4 ++-- app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) 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 04e8515be855..e085a606f3e3 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -55,10 +55,10 @@ class TagManagementViewModel @Inject constructor( ) } } else { - _uiState.update { TagUiState.Error("Failed to load tags") } + _uiState.update { TagUiState.Error } } } catch (e: ClientFactory.CreationException) { - _uiState.update { TagUiState.Error("Failed to create client") } + _uiState.update { TagUiState.Error } } } } 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 index 2f2a6137e590..6e0426683720 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt @@ -12,6 +12,5 @@ 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(val message: String) : TagUiState + object Error : TagUiState } From cab5121a5a5ed6a4f57b3273388739b0da4296dc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 09:23:01 +0200 Subject: [PATCH 06/11] codacy fix Signed-off-by: alperozturk96 --- .../nextcloud/ui/tags/util/TagChipsHelper.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt b/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt index 5960b2b5e1df..bff9e19ecf91 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/util/TagChipsHelper.kt @@ -17,15 +17,12 @@ import com.owncloud.android.R import com.owncloud.android.lib.resources.tags.Tag import com.owncloud.android.utils.theme.ViewThemeUtils -class TagChipsHelper( - private val viewThemeUtils: ViewThemeUtils -) { - fun refresh( - context: Context, - chipGroup: ChipGroup, - tags: List, - onEditClicked: Runnable - ) { +class TagChipsHelper(private val viewThemeUtils: ViewThemeUtils) { + companion object { + private const val CORNER_SIZE = 100.0f + } + + fun refresh(context: Context, chipGroup: ChipGroup, tags: List, onEditClicked: Runnable) { chipGroup.removeAllViews() chipGroup.visibility = View.VISIBLE @@ -36,7 +33,7 @@ class TagChipsHelper( context.resources.getColor(R.color.bg_default, context.theme) ) shapeAppearanceModel = shapeAppearanceModel.toBuilder() - .setAllCornerSizes(100.0f) + .setAllCornerSizes(CORNER_SIZE) .build() isClickable = false } @@ -59,7 +56,7 @@ class TagChipsHelper( context.resources.getColor(R.color.bg_default, context.theme) ) shapeAppearanceModel = shapeAppearanceModel.toBuilder() - .setAllCornerSizes(100.0f) + .setAllCornerSizes(CORNER_SIZE) .build() setOnClickListener { onEditClicked.run() } } From 5f376216b5f764955a89924135533ed0b88e4bb9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 09:23:08 +0200 Subject: [PATCH 07/11] use bundle compat Signed-off-by: alperozturk96 --- .../java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 82aaa1e89b78..12633d39a67e 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.core.os.bundleOf import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.setFragmentResult @@ -63,7 +64,8 @@ class TagManagementBottomSheet : observeState() val fileId = requireArguments().getLong(ARG_FILE_ID) - val currentTags = requireArguments().getParcelableArrayList(ARG_CURRENT_TAGS) ?: arrayListOf() + val currentTags = BundleCompat.getParcelableArrayList(requireArguments(),ARG_CURRENT_TAGS, Tag::class.java) + ?: arrayListOf() viewModel.load(fileId, currentTags) return binding.root From 24d1e7862c502293b94e0fb15b62a75bbae9251c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 10:28:21 +0200 Subject: [PATCH 08/11] use repository design pattern Signed-off-by: alperozturk96 --- .../nextcloud/client/di/ViewModelModule.kt | 6 - .../ui/tags/TagManagementBottomSheet.kt | 27 +++- .../ui/tags/TagManagementViewModel.kt | 140 +++++------------- .../com/nextcloud/ui/tags/model/TagUiState.kt | 11 ++ .../repository/TagManagementRepository.kt | 20 +++ .../repository/TagManagementRepositoryImpl.kt | 100 +++++++++++++ 6 files changed, 193 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt create mode 100644 app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt 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 4017ed61395a..4426fcfd6c1a 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -13,7 +13,6 @@ 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.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 @@ -58,11 +57,6 @@ 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 index 12633d39a67e..0afb39726631 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -1,8 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.nextcloud.ui.tags @@ -15,7 +15,6 @@ 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 @@ -27,8 +26,11 @@ 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.nextcloud.ui.tags.repository.TagManagementRepositoryImpl +import com.nextcloud.utils.extensions.getTypedActivity import com.owncloud.android.databinding.TagManagementBottomSheetBinding import com.owncloud.android.lib.resources.tags.Tag +import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.launch import javax.inject.Inject @@ -49,8 +51,23 @@ class TagManagementBottomSheet : private lateinit var viewModel: TagManagementViewModel private lateinit var tagAdapter: TagListAdapter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val activity = getTypedActivity(BaseActivity::class.java) + lifecycleScope.launch { + val ocClient = + activity?.clientRepository?.getOwncloudClient() ?: throw Exception("oc client cannot constructed") + + val ncClient = + activity.clientRepository?.getNextcloudClient() ?: throw Exception("nc client cannot constructed") + + val repository = TagManagementRepositoryImpl(ocClient, ncClient) + viewModel = TagManagementViewModel(repository) + + } + } + 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 @@ -66,7 +83,7 @@ class TagManagementBottomSheet : val fileId = requireArguments().getLong(ARG_FILE_ID) val currentTags = BundleCompat.getParcelableArrayList(requireArguments(),ARG_CURRENT_TAGS, Tag::class.java) ?: arrayListOf() - viewModel.load(fileId, currentTags) + viewModel.fetch(fileId, currentTags) return binding.root } 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 e085a606f3e3..1820e3beee5b 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -1,146 +1,86 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ 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.nextcloud.ui.tags.model.TagUiState -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.nextcloud.ui.tags.model.toLoaded +import com.nextcloud.ui.tags.repository.TagManagementRepository 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() { +class TagManagementViewModel(private val repository: TagManagementRepository) : ViewModel() { private val _uiState = MutableStateFlow(TagUiState.Loading) val uiState: StateFlow = _uiState private var fileId: Long = -1 - fun load(fileId: Long, currentTags: List) { + fun fetch(fileId: Long, currentTags: List) { this.fileId = fileId - val assignedTagNames = currentTags.map { it.name }.toSet() + viewModelScope.launch { + val tags = repository.fetch(fileId, currentTags) - 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 } - } - } catch (e: ClientFactory.CreationException) { - _uiState.update { TagUiState.Error } + // TODO: handle error ui state + _uiState.update { + tags.toLoaded(currentTags) } } } 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 - } + viewModelScope.launch { + val result = repository.assignTag(fileId, tag) + if (result) { + _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 - } + viewModelScope.launch { + val result = repository.unassignTag(fileId, tag) + if (result) { + _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) { - return@launch - } - 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) - ) - } - } - } + viewModelScope.launch { + val (allTags, newTagId) = repository.createAndAssignTag(fileId, name) ?: return@launch + _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy( + allTags = allTags, + assignedTagIds = state.assignedTagIds + newTagId + ) + } else { + TagUiState.Loaded( + allTags = allTags, + assignedTagIds = setOf(newTagId) + ) } - } catch (e: ClientFactory.CreationException) { - // ignore } } } 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 index 6e0426683720..d1156f22c474 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/model/TagUiState.kt @@ -14,3 +14,14 @@ sealed interface TagUiState { data class Loaded(val allTags: List, val assignedTagIds: Set, val query: String = "") : TagUiState object Error : TagUiState } + +fun List.toLoaded(currentTags: List): TagUiState { + val assignedTagNames = currentTags.map { it.name }.toSet() + + val assignedIds = this + .filter { it.name in assignedTagNames } + .map { it.id } + .toSet() + + return TagUiState.Loaded(this, assignedIds) +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt new file mode 100644 index 000000000000..fccbbc869b24 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.tags.repository + +import com.owncloud.android.lib.resources.tags.Tag + +interface TagManagementRepository { + suspend fun fetch(fileId: Long, currentTags: List): List + suspend fun assignTag(fileId: Long, tag: Tag): Boolean + suspend fun unassignTag(fileId: Long, tag: Tag): Boolean + suspend fun createAndAssignTag( + fileId: Long, + name: String, + ): Pair,String>? +} diff --git a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt new file mode 100644 index 000000000000..94c42e787806 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt @@ -0,0 +1,100 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.tags.repository + +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.OwnCloudClient +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.withContext + +class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private val ncClient: NextcloudClient) : + TagManagementRepository { + + companion object { + private const val TAG = "TagManagementRepositoryImpl" + } + + override suspend fun fetch( + fileId: Long, + currentTags: List + ): List = withContext(Dispatchers.IO) { + return@withContext try { + val result = GetTagsRemoteOperation().execute(ocClient) + if (result.isSuccess) { + result.resultData + } else { + listOf() + } + } catch (e: Exception) { + Log_OC.e(TAG, "cannot fetch tags: $e") + listOf() + } + } + + override suspend fun assignTag(fileId: Long, tag: Tag): Boolean = withContext(Dispatchers.IO) { + return@withContext try { + val result = PutTagRemoteOperation(tag.id, fileId).execute(ncClient) + result.isSuccess + } catch (e: Exception) { + Log_OC.e(TAG, "cannot assign tag: $e") + false + } + } + + override suspend fun unassignTag(fileId: Long, tag: Tag): Boolean = withContext(Dispatchers.IO) { + return@withContext try { + val result = DeleteTagRemoteOperation(tag.id, fileId).execute(ncClient) + result.isSuccess + } catch (e: Exception) { + Log_OC.e(TAG, "cannot unassign tag: $e") + false + } + } + + override suspend fun createAndAssignTag( + fileId: Long, + name: String + ): Pair,String>? = + withContext(Dispatchers.IO) { + return@withContext try { + val createResult = CreateTagRemoteOperation(name).execute(ncClient) + if (!createResult.isSuccess) { + return@withContext null + } + + val tagsResult = GetTagsRemoteOperation().execute(ocClient) + if (!tagsResult.isSuccess) { + return@withContext null + } + + val allTags = tagsResult.resultData + val newTag = allTags.find { it.name == name } + + if (newTag == null) { + return@withContext null + } + + val result = PutTagRemoteOperation(newTag.id, fileId).execute(ncClient) + + if (result.isSuccess) { + allTags to newTag.id + } else { + null + } + } catch (e: Exception) { + Log_OC.e(TAG, "cannot create and assign tag: $e") + null + } + } +} From d4b1833d917dca3171d107139fa4cf47a76e5d70 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 10:39:38 +0200 Subject: [PATCH 09/11] wip Signed-off-by: alperozturk96 --- .../ui/tags/TagManagementBottomSheet.kt | 20 ++---- .../repository/TagManagementRepositoryImpl.kt | 62 ++++++++----------- 2 files changed, 30 insertions(+), 52 deletions(-) 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 0afb39726631..eebefe609de9 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -23,7 +23,6 @@ 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.nextcloud.ui.tags.repository.TagManagementRepositoryImpl @@ -39,9 +38,6 @@ class TagManagementBottomSheet : BottomSheetDialogFragment(), Injectable { - @Inject - lateinit var vmFactory: ViewModelFactory - @Inject lateinit var viewThemeUtils: ViewThemeUtils @@ -53,18 +49,10 @@ class TagManagementBottomSheet : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val activity = getTypedActivity(BaseActivity::class.java) - lifecycleScope.launch { - val ocClient = - activity?.clientRepository?.getOwncloudClient() ?: throw Exception("oc client cannot constructed") - - val ncClient = - activity.clientRepository?.getNextcloudClient() ?: throw Exception("nc client cannot constructed") - - val repository = TagManagementRepositoryImpl(ocClient, ncClient) - viewModel = TagManagementViewModel(repository) - - } + val clientRepository = getTypedActivity(BaseActivity::class.java)?.clientRepository + ?: error("clientRepository not available") + val repository = TagManagementRepositoryImpl(clientRepository) + viewModel = TagManagementViewModel(repository) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { diff --git a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt index 94c42e787806..d6c27e9f3334 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt @@ -7,8 +7,7 @@ package com.nextcloud.ui.tags.repository -import com.nextcloud.common.NextcloudClient -import com.owncloud.android.lib.common.OwnCloudClient +import com.nextcloud.repository.ClientRepository 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 @@ -18,8 +17,7 @@ import com.owncloud.android.lib.resources.tags.Tag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private val ncClient: NextcloudClient) : - TagManagementRepository { +class TagManagementRepositoryImpl(private val clientRepository: ClientRepository) : TagManagementRepository { companion object { private const val TAG = "TagManagementRepositoryImpl" @@ -30,12 +28,9 @@ class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private currentTags: List ): List = withContext(Dispatchers.IO) { return@withContext try { + val ocClient = clientRepository.getOwncloudClient() ?: return@withContext listOf() val result = GetTagsRemoteOperation().execute(ocClient) - if (result.isSuccess) { - result.resultData - } else { - listOf() - } + if (result.isSuccess) result.resultData else listOf() } catch (e: Exception) { Log_OC.e(TAG, "cannot fetch tags: $e") listOf() @@ -44,6 +39,7 @@ class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private override suspend fun assignTag(fileId: Long, tag: Tag): Boolean = withContext(Dispatchers.IO) { return@withContext try { + val ncClient = clientRepository.getNextcloudClient() ?: return@withContext false val result = PutTagRemoteOperation(tag.id, fileId).execute(ncClient) result.isSuccess } catch (e: Exception) { @@ -54,6 +50,7 @@ class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private override suspend fun unassignTag(fileId: Long, tag: Tag): Boolean = withContext(Dispatchers.IO) { return@withContext try { + val ncClient = clientRepository.getNextcloudClient() ?: return@withContext false val result = DeleteTagRemoteOperation(tag.id, fileId).execute(ncClient) result.isSuccess } catch (e: Exception) { @@ -65,36 +62,29 @@ class TagManagementRepositoryImpl(private val ocClient: OwnCloudClient, private override suspend fun createAndAssignTag( fileId: Long, name: String - ): Pair,String>? = - withContext(Dispatchers.IO) { - return@withContext try { - val createResult = CreateTagRemoteOperation(name).execute(ncClient) - if (!createResult.isSuccess) { - return@withContext null - } - - val tagsResult = GetTagsRemoteOperation().execute(ocClient) - if (!tagsResult.isSuccess) { - return@withContext null - } + ): Pair, String>? = withContext(Dispatchers.IO) { + return@withContext try { + val ncClient = clientRepository.getNextcloudClient() ?: return@withContext null + val ocClient = clientRepository.getOwncloudClient() ?: return@withContext null - val allTags = tagsResult.resultData - val newTag = allTags.find { it.name == name } + val createResult = CreateTagRemoteOperation(name).execute(ncClient) + if (!createResult.isSuccess) { + return@withContext null + } - if (newTag == null) { - return@withContext null - } + val tagsResult = GetTagsRemoteOperation().execute(ocClient) + if (!tagsResult.isSuccess) { + return@withContext null + } - val result = PutTagRemoteOperation(newTag.id, fileId).execute(ncClient) + val allTags = tagsResult.resultData + val newTag = allTags.find { it.name == name } ?: return@withContext null - if (result.isSuccess) { - allTags to newTag.id - } else { - null - } - } catch (e: Exception) { - Log_OC.e(TAG, "cannot create and assign tag: $e") - null - } + val result = PutTagRemoteOperation(newTag.id, fileId).execute(ncClient) + if (result.isSuccess) allTags to newTag.id else null + } catch (e: Exception) { + Log_OC.e(TAG, "cannot create and assign tag: $e") + null } + } } From 768b0241b247795ab98b5669dc97df723886abe6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 11:26:52 +0200 Subject: [PATCH 10/11] fix tag assigning Signed-off-by: alperozturk96 --- .../ui/tags/TagManagementViewModel.kt | 48 ++++++++++--------- .../ui/tags/adapter/TagListAdapter.kt | 8 ++++ 2 files changed, 34 insertions(+), 22 deletions(-) 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 1820e3beee5b..b748dd672586 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.ui.tags.model.TagUiState import com.nextcloud.ui.tags.model.toLoaded import com.nextcloud.ui.tags.repository.TagManagementRepository +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.tags.Tag import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -19,6 +20,10 @@ import kotlinx.coroutines.launch class TagManagementViewModel(private val repository: TagManagementRepository) : ViewModel() { + companion object { + private const val TAG = "TagManagementViewModel" + } + private val _uiState = MutableStateFlow(TagUiState.Loading) val uiState: StateFlow = _uiState @@ -36,32 +41,31 @@ class TagManagementViewModel(private val repository: TagManagementRepository) : } } - fun assignTag(tag: Tag) { - viewModelScope.launch { - val result = repository.assignTag(fileId, tag) - if (result) { - _uiState.update { state -> - if (state is TagUiState.Loaded) { - state.copy(assignedTagIds = state.assignedTagIds + tag.id) - } else { - state - } - } + fun assignTag(tag: Tag) = setTagAssigned(tag, assign = true) + + fun unassignTag(tag: Tag) = setTagAssigned(tag, assign = false) + + private fun setTagAssigned(tag: Tag, assign: Boolean) { + val loaded = _uiState.value as? TagUiState.Loaded ?: return + if ((tag.id in loaded.assignedTagIds) == assign) return + + fun apply(assigned: Boolean) = _uiState.update { state -> + if (state is TagUiState.Loaded) { + state.copy( + assignedTagIds = if (assigned) state.assignedTagIds + tag.id else state.assignedTagIds - tag.id + ) + } else { + state } } - } - fun unassignTag(tag: Tag) { + apply(assign) + viewModelScope.launch { - val result = repository.unassignTag(fileId, tag) - if (result) { - _uiState.update { state -> - if (state is TagUiState.Loaded) { - state.copy(assignedTagIds = state.assignedTagIds - tag.id) - } else { - state - } - } + val success = if (assign) repository.assignTag(fileId, tag) else repository.unassignTag(fileId, tag) + if (!success) { + Log_OC.e(TAG, "cannot ${if (assign) "assign" else "unassign"} tag") + apply(!assign) } } } 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 94efda60b140..519503f3c7b8 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 @@ -29,6 +29,10 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v private const val VIEW_TYPE_CREATE = 1 } + init { + setHasStableIds(true) + } + @SuppressLint("NotifyDataSetChanged") fun update(allTags: List, assignedIds: Set, searchQuery: String) { this.assignedTagIds = assignedIds @@ -45,6 +49,10 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v notifyDataSetChanged() } + override fun getItemId(position: Int): Long = + if (getItemViewType(position) == VIEW_TYPE_CREATE) Long.MIN_VALUE + else tags[position].id.hashCode().toLong() + override fun getItemCount(): Int = tags.size + if (showCreateItem) 1 else 0 override fun getItemViewType(position: Int): Int = From 924546bd9b3fedbeeba6fab6b51c2de19ba5fa14 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 19 Jun 2026 11:27:46 +0200 Subject: [PATCH 11/11] wip Signed-off-by: alperozturk96 --- .../ui/tags/TagManagementBottomSheet.kt | 2 +- .../ui/tags/TagManagementViewModel.kt | 1 + .../ui/tags/adapter/TagListAdapter.kt | 8 +-- .../repository/TagManagementRepository.kt | 5 +- .../repository/TagManagementRepositoryImpl.kt | 50 +++++++++---------- 5 files changed, 31 insertions(+), 35 deletions(-) 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 eebefe609de9..004fec2659ae 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementBottomSheet.kt @@ -69,7 +69,7 @@ class TagManagementBottomSheet : observeState() val fileId = requireArguments().getLong(ARG_FILE_ID) - val currentTags = BundleCompat.getParcelableArrayList(requireArguments(),ARG_CURRENT_TAGS, Tag::class.java) + val currentTags = BundleCompat.getParcelableArrayList(requireArguments(), ARG_CURRENT_TAGS, Tag::class.java) ?: arrayListOf() viewModel.fetch(fileId, 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 b748dd672586..eec2f5f231e6 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/TagManagementViewModel.kt @@ -45,6 +45,7 @@ class TagManagementViewModel(private val repository: TagManagementRepository) : fun unassignTag(tag: Tag) = setTagAssigned(tag, assign = false) + // TODO: handle error ui state private fun setTagAssigned(tag: Tag, assign: Boolean) { val loaded = _uiState.value as? TagUiState.Loaded ?: return if ((tag.id in loaded.assignedTagIds) == assign) return 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 519503f3c7b8..562a83ff7115 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 @@ -49,9 +49,11 @@ class TagListAdapter(private val onTagChecked: (Tag, Boolean) -> Unit, private v notifyDataSetChanged() } - override fun getItemId(position: Int): Long = - if (getItemViewType(position) == VIEW_TYPE_CREATE) Long.MIN_VALUE - else tags[position].id.hashCode().toLong() + override fun getItemId(position: Int): Long = if (getItemViewType(position) == VIEW_TYPE_CREATE) { + Long.MIN_VALUE + } else { + tags[position].id.hashCode().toLong() + } override fun getItemCount(): Int = tags.size + if (showCreateItem) 1 else 0 diff --git a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt index fccbbc869b24..47f42d64f224 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepository.kt @@ -13,8 +13,5 @@ interface TagManagementRepository { suspend fun fetch(fileId: Long, currentTags: List): List suspend fun assignTag(fileId: Long, tag: Tag): Boolean suspend fun unassignTag(fileId: Long, tag: Tag): Boolean - suspend fun createAndAssignTag( - fileId: Long, - name: String, - ): Pair,String>? + suspend fun createAndAssignTag(fileId: Long, name: String): Pair, String>? } diff --git a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt index d6c27e9f3334..30ab30629a93 100644 --- a/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/ui/tags/repository/TagManagementRepositoryImpl.kt @@ -17,16 +17,14 @@ import com.owncloud.android.lib.resources.tags.Tag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +@Suppress("TooGenericExceptionCaught") class TagManagementRepositoryImpl(private val clientRepository: ClientRepository) : TagManagementRepository { companion object { private const val TAG = "TagManagementRepositoryImpl" } - override suspend fun fetch( - fileId: Long, - currentTags: List - ): List = withContext(Dispatchers.IO) { + override suspend fun fetch(fileId: Long, currentTags: List): List = withContext(Dispatchers.IO) { return@withContext try { val ocClient = clientRepository.getOwncloudClient() ?: return@withContext listOf() val result = GetTagsRemoteOperation().execute(ocClient) @@ -59,32 +57,30 @@ class TagManagementRepositoryImpl(private val clientRepository: ClientRepository } } - override suspend fun createAndAssignTag( - fileId: Long, - name: String - ): Pair, String>? = withContext(Dispatchers.IO) { - return@withContext try { - val ncClient = clientRepository.getNextcloudClient() ?: return@withContext null - val ocClient = clientRepository.getOwncloudClient() ?: return@withContext null + override suspend fun createAndAssignTag(fileId: Long, name: String): Pair, String>? = + withContext(Dispatchers.IO) { + return@withContext try { + val ncClient = clientRepository.getNextcloudClient() ?: return@withContext null + val ocClient = clientRepository.getOwncloudClient() ?: return@withContext null - val createResult = CreateTagRemoteOperation(name).execute(ncClient) - if (!createResult.isSuccess) { - return@withContext null - } + val createResult = CreateTagRemoteOperation(name).execute(ncClient) + if (!createResult.isSuccess) { + return@withContext null + } - val tagsResult = GetTagsRemoteOperation().execute(ocClient) - if (!tagsResult.isSuccess) { - return@withContext null - } + val tagsResult = GetTagsRemoteOperation().execute(ocClient) + if (!tagsResult.isSuccess) { + return@withContext null + } - val allTags = tagsResult.resultData - val newTag = allTags.find { it.name == name } ?: return@withContext null + val allTags = tagsResult.resultData + val newTag = allTags.find { it.name == name } ?: return@withContext null - val result = PutTagRemoteOperation(newTag.id, fileId).execute(ncClient) - if (result.isSuccess) allTags to newTag.id else null - } catch (e: Exception) { - Log_OC.e(TAG, "cannot create and assign tag: $e") - null + val result = PutTagRemoteOperation(newTag.id, fileId).execute(ncClient) + if (result.isSuccess) allTags to newTag.id else null + } catch (e: Exception) { + Log_OC.e(TAG, "cannot create and assign tag: $e") + null + } } - } }