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
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 lukstbit <52494258+lukstbit@users.noreply.github.com>
*
* 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.widget.ArrayAdapter
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.R
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.databinding.DialogDeckOptionsSelectionBinding
import com.ichi2.anki.pages.DeckOptionsEntry
import com.ichi2.anki.ui.windows.reviewer.ReviewerFragment
import com.ichi2.anki.ui.windows.reviewer.ReviewerViewModel
import com.ichi2.anki.utils.ext.usingStyledAttributes
import com.ichi2.utils.create
import com.ichi2.utils.customView
import com.ichi2.utils.title
import timber.log.Timber

/**
* Shows a list of decks from which the user can select one to display its deck options.
* @see ReviewerFragment
* @see ReviewerViewModel.emitDeckOptionsDestination
*/
@NeedsTest("verify null deck names => null entry")
fun Context.showDeckOptionsSelectionDialog(
options: List<DeckOptionsEntry>,
onDeckSelected: (DeckOptionsEntry) -> Unit,
) {
Timber.i("Showing deck options selection dialog")
val binding = DialogDeckOptionsSelectionBinding.inflate(LayoutInflater.from(this))
val normalDeckNameColor: Int =
usingStyledAttributes(null, intArrayOf(android.R.attr.textColor)) {
getColor(0, 0)
}
val dynamicDeckNameColor: Int =
usingStyledAttributes(null, intArrayOf(R.attr.dynDeckColor)) {
getColor(0, 0)
}
val dialog =
MaterialAlertDialogBuilder(this).create {
title(text = TR.deckConfigWhichDeck())
customView(binding.root)
}
binding.deckOptionsList.adapter =
object : ArrayAdapter<String>(
this,
R.layout.item_deck_option_selection,
options.map { it.name.toString() },
) {
override fun getView(
position: Int,
convertView: View?,
parent: ViewGroup,
): View {
val rowView = super.getView(position, convertView, parent) as TextView
rowView.setTextColor(if (options[position].isFiltered) dynamicDeckNameColor else normalDeckNameColor)
rowView.setOnClickListener {
dialog.dismiss()
onDeckSelected(options[position])
}
return rowView
}
}
dialog.show()
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ import com.ichi2.anki.FilteredDeckOptions
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.utils.Destination

class DeckOptionsDestination(
private val deckId: DeckId,
private val isFiltered: Boolean,
/**
* @param options the list of deck options to present to the user before going to deck options. This
* will contain the current selected deck([deckId]) plus any other possible deck targets(ex: decks
* of the current studied card)
*/
data class DeckOptionsDestination(
val deckId: DeckId,
val isFiltered: Boolean,
val options: List<DeckOptionsEntry> = emptyList(),
) : Destination {
override fun toIntent(context: Context): Intent =
if (isFiltered) {
Expand All @@ -52,3 +58,13 @@ class DeckOptionsDestination(
}
}
}

/**
* Information about a deck that appears in the list of possible deck targets when deck options are
* requested from the study screen.
*/
data class DeckOptionsEntry(
val deckId: DeckId,
val name: String?,
val isFiltered: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import android.content.Intent
import android.hardware.SensorManager
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
Expand Down Expand Up @@ -55,14 +54,15 @@ import com.ichi2.anki.R
import com.ichi2.anki.cardviewer.Gesture
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.databinding.Reviewer2Binding
import com.ichi2.anki.dialogs.showDeckOptionsSelectionDialog
import com.ichi2.anki.dialogs.tags.TagsDialog
import com.ichi2.anki.dialogs.tags.TagsDialogFactory
import com.ichi2.anki.dialogs.tags.TagsDialogListener
import com.ichi2.anki.model.CardStateFilter
import com.ichi2.anki.pages.DeckOptionsDestination
import com.ichi2.anki.preferences.reviewer.ViewerAction
import com.ichi2.anki.previewer.CardViewerActivity
import com.ichi2.anki.previewer.CardViewerFragment
import com.ichi2.anki.previewer.TypeAnswer
import com.ichi2.anki.previewer.setFrameStyle
import com.ichi2.anki.previewer.stdHtml
import com.ichi2.anki.reviewer.BindingMap
Expand All @@ -87,13 +87,11 @@ import com.ichi2.anki.workarounds.SafeWebViewLayout
import com.ichi2.themes.Themes
import com.ichi2.utils.dp
import com.ichi2.utils.show
import com.ichi2.utils.stripHtml
import com.squareup.seismic.ShakeDetector
import dev.androidbroadcast.vbpd.viewBinding
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import timber.log.Timber
import kotlin.math.max
import kotlin.math.roundToInt
Expand Down Expand Up @@ -200,6 +198,18 @@ class ReviewerFragment :
}

viewModel.destinationFlow.collectIn(lifecycleScope) { destination ->
if (destination is DeckOptionsDestination && destination.options.size > 1) {
requireContext().showDeckOptionsSelectionDialog(destination.options) { selectedOption ->
Timber.i("Deck options target selected: ${selectedOption.deckId}")
val updatedDestination =
destination.copy(
deckId = selectedOption.deckId,
isFiltered = selectedOption.isFiltered,
)
startActivity(updatedDestination.toIntent(requireContext()))
}
return@collectIn
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a need for this return?

}
startActivity(destination.toIntent(requireContext()))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import com.ichi2.anki.Reviewer
import com.ichi2.anki.asyncIO
import com.ichi2.anki.browser.BrowserDestination
import com.ichi2.anki.cardviewer.SingleCardSide
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.launchCatchingIO
import com.ichi2.anki.libanki.Card
import com.ichi2.anki.libanki.CardId
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.NoteId
import com.ichi2.anki.libanki.redoLabel
import com.ichi2.anki.libanki.sched.CurrentQueueState
Expand All @@ -43,6 +45,7 @@ import com.ichi2.anki.observability.undoableOp
import com.ichi2.anki.pages.AnkiServer
import com.ichi2.anki.pages.CardInfoDestination
import com.ichi2.anki.pages.DeckOptionsDestination
import com.ichi2.anki.pages.DeckOptionsEntry
import com.ichi2.anki.pages.PostRequestUri
import com.ichi2.anki.pages.StatisticsDestination
import com.ichi2.anki.preferences.reviewer.ViewerAction
Expand Down Expand Up @@ -278,14 +281,49 @@ class ReviewerViewModel(
destinationFlow.emit(destination)
}

@NeedsTest("verify that we show the proper deck option targets for the current card")
private suspend fun emitDeckOptionsDestination() {
val deckId = withCol { decks.getCurrentId() }
val isFiltered = withCol { decks.isFiltered(deckId) }
val destination = DeckOptionsDestination(deckId, isFiltered)
val card = currentCard.await()
val options = getDeckOptionsTargets(deckId, card)
val isFiltered = options.first { it.deckId == deckId }.isFiltered
val destination = DeckOptionsDestination(deckId, isFiltered, options)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this logic could be inside either: DeckOptionsDestination.create(deckId) or DeckOptionsDestination.from(deckId, options), one doing the isFiltered check, one extracting the logic

This feels like a general utility, rather than a responsibility of the study screen

Timber.i("Launching 'deck options' for deck %d", deckId)
destinationFlow.emit(destination)
}

/**
* Builds the valid(all [DeckOptionsEntry] that have a proper deck name) list of
* [DeckOptionsEntry] from which the user can select one to see its deck options.
* @param currentDeckId the [DeckId] of the currently selected deck
* @param card current card shown in the study screen
* See https://github.com/ankitects/anki/blob/b8884bac72aa50fa1189fe0a5079a71574bc5043/qt/aqt/deckoptions.py#L83-L100
* for backend implementation and ordering of the entries.
*/
private suspend fun getDeckOptionsTargets(
currentDeckId: DeckId,
card: Card,
): List<DeckOptionsEntry> {
val extraDeckIds = mutableListOf(currentDeckId)
if (card.oDid != 0L && card.oDid != currentDeckId) {
extraDeckIds.add(card.oDid)
}
if (card.did != currentDeckId) {
extraDeckIds.add(card.did)
}
return withCol {
extraDeckIds
.map { deckId ->
DeckOptionsEntry(
deckId = deckId,
name = decks.nameIfExists(deckId),
isFiltered = decks.isFiltered(deckId),
)
}.filter { it.name != null }
.sortedBy { it.isFiltered }
}
}

private suspend fun emitBrowseDestination() {
val deckId = withCol { decks.getCurrentId() }
val cardId = currentCard.await().id
Expand Down
10 changes: 10 additions & 0 deletions AnkiDroid/src/main/res/layout/dialog_deck_options_selection.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/deck_options_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp"
tools:listitem="@layout/item_deck_option_selection"
/>
13 changes: 13 additions & 0 deletions AnkiDroid/src/main/res/layout/item_deck_option_selection.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<com.ichi2.ui.FixedTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/deck_option"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeight"
android:paddingVertical="8dp"
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:gravity="center_vertical|start"
android:background="?attr/selectableItemBackground"
tools:text="Deck option target"
/>
Loading