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