Skip to content

Commit

Permalink
feat(card-browser): rename flags
Browse files Browse the repository at this point in the history
Done in the Card Browser options

Fixes 16205
  • Loading branch information
criticalAY authored and lukstbit committed Jun 17, 2024
1 parent 4e161d1 commit 0439663
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 0 deletions.
16 changes: 16 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ enum class Flag(val code: Int, @DrawableRes val drawableRes: Int, @ColorRes val
PURPLE -> TR.actionsFlagPurple()
}

/**
* Renames the flag
*
* @param newName The new name for the flag.
*/
suspend fun rename(newName: String) {
val labels = FlagLabels.loadFromColConfig()
labels.updateName(this, newName)
}

companion object {
fun fromCode(code: Int): Flag {
return entries.first { it.code == code }
Expand Down Expand Up @@ -85,6 +95,12 @@ private value class FlagLabels(val value: JSONObject) {
* This is not supported for [Flag.NONE] and is validated outside this method
*/
fun getLabel(flag: Flag): String? = value.getStringOrNull(flag.code.toString())
suspend fun updateName(flag: Flag, newName: String) {
value.put(flag.ordinal.toString(), newName)
withCol {
config.set("flagLabels", value)
}
}

companion object {
suspend fun loadFromColConfig() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.annotation.IdRes
Expand All @@ -30,6 +31,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.ichi2.anki.R
import com.ichi2.anki.browser.CardBrowserViewModel
import com.ichi2.anki.model.CardsOrNotes
import timber.log.Timber

class BrowserOptionsDialog(private val cardsOrNotes: CardsOrNotes, private val isTruncated: Boolean) : AppCompatDialogFragment() {
private lateinit var dialogView: View
Expand Down Expand Up @@ -61,6 +63,13 @@ class BrowserOptionsDialog(private val cardsOrNotes: CardsOrNotes, private val i

dialogView.findViewById<CheckBox>(R.id.truncate_checkbox).isChecked = isTruncated

dialogView.findViewById<LinearLayout>(R.id.action_rename_flag).setOnClickListener {
Timber.d("Rename flag clicked")
val flagRenameDialog = FlagRenameDialog()
flagRenameDialog.show(parentFragmentManager, "FlagRenameDialog")
dismiss()
}

return MaterialAlertDialogBuilder(requireContext()).run {
this.setView(dialogView)
this.setTitle(getString(R.string.browser_options_dialog_heading))
Expand Down
136 changes: 136 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2024 Ashish Yadav <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.dialogs

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
import com.ichi2.anki.Flag
import com.ichi2.anki.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
* Adapter for the RecyclerView displaying flag items.
*
* @param lifecycleScope The CoroutineScope used for launching coroutines.
*/
class FlagAdapter(private val lifecycleScope: CoroutineScope) :
ListAdapter<FlagItem, FlagAdapter.FlagViewHolder>(FlagItemDiffCallback()) {

inner class FlagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val flagImageView: ImageView = itemView.findViewById(R.id.ic_flag)
val flagNameText: TextView = itemView.findViewById(R.id.flag_name)
val flagNameEdit: TextInputEditText = itemView.findViewById(R.id.flag_name_edit_text)
val editButton: MaterialButton = itemView.findViewById(R.id.action_edit_flag)
val saveButton: MaterialButton = itemView.findViewById(R.id.action_save_flag_name)
val cancelButton: MaterialButton = itemView.findViewById(R.id.action_cancel_flag_rename)

val flagNameViewLayout: LinearLayout = itemView.findViewById(R.id.flag_name_view_layout)
val flagNameEditLayout: LinearLayout = itemView.findViewById(R.id.edit_flag_name_layout)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlagViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.edit_flag_item, parent, false)
return FlagViewHolder(view)
}

override fun onBindViewHolder(holder: FlagViewHolder, position: Int) {
val flagItem = getItem(position)

holder.flagImageView.setImageResource(flagItem.icon)

holder.flagNameEditLayout.visibility = View.GONE

holder.flagNameText.text = flagItem.title
holder.flagNameEdit.setText(flagItem.title)

holder.editButton.setOnClickListener {
flagItem.isInEditMode = true
holder.flagNameViewLayout.visibility = View.GONE
holder.flagNameEditLayout.visibility = View.VISIBLE
holder.flagNameEdit.requestFocus()
holder.flagNameEdit.text?.let { text -> holder.flagNameEdit.setSelection(text.length) }
val inputMethodManager = holder.flagNameEdit.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(holder.flagNameEdit, InputMethodManager.SHOW_IMPLICIT)
}

holder.saveButton.setOnClickListener {
val updatedTextName = holder.flagNameEdit.text.toString()
holder.flagNameViewLayout.visibility = View.VISIBLE
holder.flagNameEditLayout.visibility = View.GONE
val updatedFlagItem = flagItem.copy(title = updatedTextName)
val updatedDataset = currentList.toMutableList()
lifecycleScope.launch {
flagItem.renameTo(updatedTextName)
}
updatedFlagItem.isInEditMode = false
updatedDataset[position] = updatedFlagItem
submitList(updatedDataset)
}

holder.cancelButton.setOnClickListener {
holder.flagNameViewLayout.visibility = View.VISIBLE
holder.flagNameEditLayout.visibility = View.GONE
flagItem.isInEditMode = false
}
}

class FlagItemDiffCallback : DiffUtil.ItemCallback<FlagItem>() {
override fun areItemsTheSame(oldItem: FlagItem, newItem: FlagItem): Boolean {
return oldItem.ordinal == newItem.ordinal
}

override fun areContentsTheSame(oldItem: FlagItem, newItem: FlagItem): Boolean {
return oldItem.title == newItem.title
}
}
}

/**
* Data class representing a flag item.
*
* @property ordinal The ordinal value of the flag.
* @property title The title or name of the flag.
* @property icon The icon resource ID of the flag.
* @property isInEditMode Whether the flag is being edited.
*/
data class FlagItem(
val ordinal: Int,
val title: String,
val icon: Int,
var isInEditMode: Boolean = false
) {
/**
* Renames the flag
*
* @param newName The new name for the flag.
*/
suspend fun renameTo(newName: String) = Flag.fromCode(ordinal).rename(newName)
}
113 changes: 113 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagRenameDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2024 Ashish Yadav <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.dialogs

import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.ichi2.anki.Flag
import com.ichi2.anki.R
import com.ichi2.anki.showThemedToast
import com.ichi2.utils.customView
import com.ichi2.utils.negativeButton
import com.ichi2.utils.positiveButton
import com.ichi2.utils.title
import kotlinx.coroutines.launch
import timber.log.Timber

/**
* A DialogFragment for renaming flags through a RecyclerView.
*/
class FlagRenameDialog : DialogFragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var flagAdapter: FlagAdapter

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialogView = requireActivity().layoutInflater.inflate(R.layout.rename_flag_layout, null)
val builder = AlertDialog.Builder(requireContext()).apply {
customView(view = dialogView, 4, 4, 4, 4)
title(R.string.rename_flag)
// positiveButton is set in onResume so dialog is not always dismissed
positiveButton(R.string.dialog_ok, click = null)
negativeButton(R.string.dialog_cancel)
}
val dialog = builder.create()

recyclerView = dialogView.findViewById(R.id.recyclerview_flags)
setupRecyclerView()
return dialog
}

override fun onResume() {
super.onResume()
(dialog as AlertDialog).positiveButton.setOnClickListener {
// TODO: Extract pending changes from the adapter and save them
if (!::flagAdapter.isInitialized) return@setOnClickListener
val pendingChanges = flagAdapter.currentList.filter { it.isInEditMode }
if (pendingChanges.any()) {
Timber.i("Attempted to close with %d pending changes", pendingChanges.size)
showThemedToast(R.string.confirm_before_saving, true)
return@setOnClickListener
}

Timber.i("Closing dialog", pendingChanges.size)
activity?.invalidateOptionsMenu()
dismiss()
}
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
activity?.invalidateOptionsMenu()
}

override fun onStart() {
super.onStart()
dialog?.window?.clearFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
)
}

private fun setupRecyclerView() = requireActivity().lifecycleScope.launch {
val flagItems = createFlagList()
flagAdapter = FlagAdapter(lifecycleScope = lifecycleScope)
recyclerView.adapter = flagAdapter
flagAdapter.submitList(flagItems)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
}

private suspend fun createFlagList(): List<FlagItem> {
Timber.d("Creating flag list")
return Flag.queryDisplayNames()
.filter { it.key != Flag.NONE }
.map { (flag, displayName) ->
FlagItem(
ordinal = flag.code,
title = displayName,
icon = flag.drawableRes
)
}
}
}
36 changes: 36 additions & 0 deletions AnkiDroid/src/main/res/layout/browser_options_dialog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">

<LinearLayout
Expand Down Expand Up @@ -74,6 +75,41 @@

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="8dp">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/menu_flag"
android:textStyle="bold"
android:layout_marginHorizontal="16dp" />

<LinearLayout
android:gravity="center"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:id="@+id/action_rename_flag"
android:background="?attr/selectableItemBackground"
android:layout_marginHorizontal="16dp"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="match_parent">

<ImageView
android:layout_width="wrap_content"
android:src="@drawable/ic_flag_transparent"
android:layout_height="match_parent"/>

<com.ichi2.ui.FixedTextView
android:layout_marginStart="4dp"
android:layout_width="match_parent"
android:text="@string/rename_flag"
android:layout_height="wrap_content"/>
</LinearLayout>

</LinearLayout>

</LinearLayout>
Loading

0 comments on commit 0439663

Please sign in to comment.