Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import com.ichi2.anki.reviewreminders.ScheduleReminders
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.internationalization.toSentenceCase
import com.ichi2.anki.utils.ext.description
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.utils.HtmlUtils.convertNewlinesToHtml
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -608,11 +607,17 @@ class StudyOptionsFragment :
}

// Set deck description
@Language("HTML")
val desc: String =
if (isDynamic) {
resources.getString(R.string.dyn_deck_desc)
} else {
col.decks.current().description
val deck = col.decks.current()
if (deck.descriptionAsMarkdown) {
col.renderMarkdown(deck.description, sanitize = true)
} else {
deck.description
}
}
if (desc.isNotEmpty()) {
textDeckDescription.text = formatDescription(desc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,123 +16,175 @@

package com.ichi2.anki.dialogs

import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageButton
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.R
import com.ichi2.anki.StudyOptionsFragment
import com.ichi2.anki.launchCatchingTask
import com.ichi2.anki.dialogs.EditDeckDescriptionDialogViewModel.DismissType
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.utils.ext.description
import com.ichi2.anki.utils.ext.update
import com.ichi2.themes.Themes
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.utils.AndroidUiUtils.setFocusAndOpenKeyboard
import com.ichi2.utils.create
import com.ichi2.utils.negativeButton
import com.ichi2.utils.positiveButton
import com.ichi2.utils.show
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import timber.log.Timber

/**
* Allows a user to edit the [deck description][description]
* Allows a user to edit the [deck description][com.ichi2.anki.libanki.Deck.description]
*
* This is visible on [StudyOptionsFragment]
*/
class EditDeckDescriptionDialog : DialogFragment() {
private val deckId: DeckId
get() = requireArguments().getLong(ARG_DECK_ID)
private val viewModel: EditDeckDescriptionDialogViewModel by viewModels()

private lateinit var deckDescriptionInput: TextInputEditText
private lateinit var alertDialog: AlertDialog
private lateinit var dialogView: View

private var currentDescription
get() = deckDescriptionInput.text.toString()
set(value) {
deckDescriptionInput.setText(value)
}
private val deckDescriptionInput: TextInputEditText
get() = dialogView.findViewById(R.id.deck_description_input)

private val formatAsMarkdownInput: CheckBox
get() = dialogView.findViewById(R.id.format_as_markdown)

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
Themes.setTheme(requireContext())
return inflater.inflate(R.layout.dialog_deck_description, null).apply {
deckDescriptionInput = this.findViewById(R.id.deck_description_input)
launchCatchingTask {
currentDescription = getDescription()
private val toolbar: MaterialToolbar
get() = dialogView.findViewById(R.id.topAppBar)

private val onUnsavedChangesBackCallback =
object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
showDiscardChangesDialog()
}
findViewById<MaterialToolbar>(R.id.topAppBar)
.apply {
setNavigationOnClickListener {
onBack()
}
}

setOnMenuItemClickListener { menuItem ->
if (menuItem.itemId == R.id.action_save) {
saveAndExit()
true
} else {
false
}
}
}.also { toolbar ->
launchCatchingTask { toolbar.title = withCol { decks.getLegacy(deckId)!!.name } }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
this.dialogView = layoutInflater.inflate(R.layout.dialog_deck_description, null)
return MaterialAlertDialogBuilder(requireContext())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing that MaterialAlertDialogBuilder doesn't produce an ui like AlertDialog does we should settle which one we use going forward.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.create {
setView(dialogView)
positiveButton(R.string.save)
negativeButton(R.string.close)
}.apply {
alertDialog = this
setOnShowListener {
positiveButton.setOnClickListener { viewModel.saveAndExit() }
negativeButton.setOnClickListener { viewModel.onBackRequested() }
}
setFocusAndOpenKeyboard(deckDescriptionInput) { deckDescriptionInput.setSelection(deckDescriptionInput.text!!.length) }
}
setCanceledOnTouchOutside(false)
setCancelable(false)
onBackPressedDispatcher.addCallback(this, onUnsavedChangesBackCallback)
show()
setupDialogView(dialogView)
}
}

override fun onStart() {
super.onStart()
private fun setupDialogView(view: View) {
deckDescriptionInput.apply {
doOnTextChanged { text, _, _, _ ->
viewModel.description = text?.toString() ?: ""
}
}

formatAsMarkdownInput.apply {
setOnCheckedChangeListener { _, value -> viewModel.formatAsMarkdown = value }
}

// setup 'Format as Markdown' help
view.findViewById<ImageButton>(R.id.markdown_formatting_help).apply {
contentDescription =
getString(R.string.help_button_content_description, getString(R.string.format_deck_description_as_markdown))
setOnClickListener {
MaterialAlertDialogBuilder(requireContext()).show {
setTitle(formatAsMarkdownInput.text)
setIcon(R.drawable.ic_help_black_24dp)
// FIXME: the upstream string unexpectedly contains newlines
setMessage(TR.deckConfigDescriptionNewHandlingHint().replace("\n", " ").replace(" ", " "))
}
}
}

dialog!!.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
setupFlows()
}

private fun saveAndExit() =
launchCatchingTask {
setDescription(currentDescription)
Timber.i("closing deck description dialog")
dismiss()
private fun setupFlows() {
lifecycleScope.launch {
viewModel.flowOfDismissDialog
.filterNotNull()
.collect { dismissType ->
when (dismissType) {
DismissType.ClosedWithoutSaving -> dismiss()
DismissType.Saved -> {
dismiss()
showSnackbar(R.string.deck_description_saved)
}
}
}
}

private fun onBack() =
launchCatchingTask {
fun closeWithoutSaving() {
Timber.i("Closing dialog without saving")
dismiss()
lifecycleScope.launch {
viewModel.flowOfDescription.collect { desc ->
if (desc == deckDescriptionInput.text.toString()) return@collect
deckDescriptionInput.setText(desc)
}
}

if (getDescription() == currentDescription) {
closeWithoutSaving()
return@launchCatchingTask
lifecycleScope.launch {
viewModel.flowOfFormatAsMarkdown.collect {
formatAsMarkdownInput.isChecked = it
}
}

Timber.i("asking if user should discard changes")
DiscardChangesDialog.showDialog(requireContext()) {
closeWithoutSaving()
lifecycleScope.launch {
viewModel.flowOfInitCompleted.collect {
if (!it) return@collect
toolbar.title = viewModel.windowTitle
setFocusAndOpenKeyboard(deckDescriptionInput) { deckDescriptionInput.setSelection(deckDescriptionInput.text!!.length) }
}
}

private suspend fun getDescription() = withCol { decks.getLegacy(deckId)!!.description }
lifecycleScope.launch {
viewModel.flowOfShowDiscardChanges.collect {
showDiscardChangesDialog()
}
}

private suspend fun setDescription(value: String) {
Timber.i("updating deck description")
withCol { decks.update(deckId) { description = value } }
lifecycleScope.launch {
viewModel.flowOfHasChanges.collect {
alertDialog.positiveButton.isEnabled = it
onUnsavedChangesBackCallback.isEnabled = it
}
}
}

companion object {
private const val ARG_DECK_ID = "deckId"
fun showDiscardChangesDialog() {
Timber.i("asking if user should discard changes")
DiscardChangesDialog.showDialog(requireContext()) {
viewModel.closeWithoutSaving()
}
}

companion object {
fun newInstance(deckId: DeckId): EditDeckDescriptionDialog =
EditDeckDescriptionDialog().apply {
arguments =
bundleOf(
ARG_DECK_ID to deckId,
EditDeckDescriptionDialogViewModel.ARG_DECK_ID to deckId,
)
}
}
Expand Down
Loading