mirror of
https://github.com/ankidroid/Anki-Android.git
synced 2024-09-19 19:42:17 +02:00
Implemention of Deck Picker Widget !Add Deck Picker Widget to display deck statistics
This commit introduces the Deck Picker Widget, which displays a list of decks along with the number of cards that are new, in learning, and due for review. It is a display-only widget. Features: - Displays deck names and statistics (new, learning, and review counts). - Retrieves selected decks from shared preferences. - Can be reconfigured by holding the widget. This widget provides users with a quick overview of their decks without needing to open the app.
This commit is contained in:
parent
f9ea4df68e
commit
44143f00c4
@ -520,6 +520,33 @@
|
||||
/>
|
||||
</receiver>
|
||||
|
||||
<!-- A widget that displays a few decks's name and number of cards to review on the Android home screen.
|
||||
The way to add it depends on the phone. It usually consists in a long press on the screen, followed by finding a "widget" button"-->
|
||||
<receiver
|
||||
android:name="com.ichi2.widget.DeckPickerWidget"
|
||||
android:label="@string/deck_picker_widget_description"
|
||||
android:exported="false"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_provider_deck_picker" />
|
||||
</receiver>
|
||||
|
||||
<!-- Configuration view for the DeckPickerWidget above.
|
||||
It is opened when adding a new deck picker widget and
|
||||
by configuration button which appears when the widget is hold or resized.-->
|
||||
<activity
|
||||
android:name="com.ichi2.widget.DeckPickerWidgetConfig"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="com.ichi2.widget.WidgetPermissionReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
285
AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
Normal file
285
AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.RemoteViews
|
||||
import com.ichi2.anki.AnkiDroidApp
|
||||
import com.ichi2.anki.CollectionManager.withCol
|
||||
import com.ichi2.anki.CrashReportService
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.Reviewer
|
||||
import com.ichi2.anki.analytics.UsageAnalytics
|
||||
import com.ichi2.anki.pages.DeckOptions
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
typealias DeckId = Long
|
||||
typealias AppWidgetId = Int
|
||||
|
||||
/**
|
||||
* Data class representing the data for a deck displayed in the widget.
|
||||
*
|
||||
* @property deckId The ID of the deck.
|
||||
* @property name The name of the deck.
|
||||
* @property reviewCount The number of cards due for review.
|
||||
* @property learnCount The number of cards in the learning phase.
|
||||
* @property newCount The number of new cards.
|
||||
*/
|
||||
data class DeckPickerWidgetData(
|
||||
val deckId: DeckId,
|
||||
val name: String,
|
||||
val reviewCount: Int,
|
||||
val learnCount: Int,
|
||||
val newCount: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* This widget displays a list of decks with their respective new, learning, and review card counts.
|
||||
* It updates every minute.
|
||||
* It can be resized vertically & horizontally.
|
||||
* It allows user to open the reviewer directly by clicking on the deck same as deckpicker.
|
||||
* There is only one way to configure the widget i.e. while adding it on home screen,
|
||||
*/
|
||||
class DeckPickerWidget : AnalyticsWidgetProvider() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Action identifier to trigger updating the app widget.
|
||||
* This constant is used to trigger the update of all widgets by the AppWidgetManager.
|
||||
*/
|
||||
const val ACTION_APPWIDGET_UPDATE = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
|
||||
/**
|
||||
* Custom action to update the widget.
|
||||
* This constant is used to trigger the widget update via a custom broadcast intent.
|
||||
*/
|
||||
const val ACTION_UPDATE_WIDGET = "com.ichi2.widget.ACTION_UPDATE_WIDGET"
|
||||
|
||||
/**
|
||||
* Key used for passing the selected deck IDs in the intent extras.
|
||||
*/
|
||||
const val EXTRA_SELECTED_DECK_IDS = "deck_picker_widget_selected_deck_ids"
|
||||
|
||||
/**
|
||||
* Updates the widget with the deck data.
|
||||
*
|
||||
* This method replaces the entire view content with entries for each deck ID
|
||||
* provided in the `deckIds` array. If any decks are deleted,
|
||||
* they will be ignored, and only the rest of the decks will be displayed.
|
||||
*
|
||||
* @param context the context of the application
|
||||
* @param appWidgetManager the AppWidgetManager instance
|
||||
* @param appWidgetId the ID of the app widget
|
||||
* @param deckIds the array of deck IDs to be displayed in the widget.
|
||||
* Each ID corresponds to a specific deck, and the view will
|
||||
* contain exactly the decks whose IDs are in this list.
|
||||
*
|
||||
* TODO: If the deck is completely empty (no cards at all), display a Snackbar or Toast message
|
||||
* saying "The deck is empty" instead of opening any activity.
|
||||
*
|
||||
*/
|
||||
fun updateWidget(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: AppWidgetId,
|
||||
deckIds: LongArray
|
||||
) {
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_deck_picker_large)
|
||||
|
||||
AnkiDroidApp.applicationScope.launch {
|
||||
val deckData = getDeckNameAndStats(deckIds.toList())
|
||||
|
||||
remoteViews.removeAllViews(R.id.deckCollection)
|
||||
|
||||
for (deck in deckData) {
|
||||
val deckView = RemoteViews(context.packageName, R.layout.widget_item_deck_main)
|
||||
|
||||
deckView.setTextViewText(R.id.deckName, deck.name)
|
||||
deckView.setTextViewText(R.id.deckNew, deck.newCount.toString())
|
||||
deckView.setTextViewText(R.id.deckDue, deck.reviewCount.toString())
|
||||
deckView.setTextViewText(R.id.deckLearn, deck.learnCount.toString())
|
||||
|
||||
val isEmptyDeck = deck.newCount == 0 && deck.reviewCount == 0 && deck.learnCount == 0
|
||||
|
||||
if (!isEmptyDeck) {
|
||||
val intent = Intent(context, Reviewer::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra("deckId", deck.deckId)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
deck.deckId.toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
deckView.setOnClickPendingIntent(R.id.deckName, pendingIntent)
|
||||
} else {
|
||||
val intent = DeckOptions.getIntent(context, deck.deckId)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
deck.deckId.toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
deckView.setOnClickPendingIntent(R.id.deckName, pendingIntent)
|
||||
}
|
||||
|
||||
remoteViews.addView(R.id.deckCollection, deckView)
|
||||
}
|
||||
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun performUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
usageAnalytics: UsageAnalytics
|
||||
) {
|
||||
Timber.d("Performing widget update for appWidgetIds: ${appWidgetIds.joinToString(", ")}")
|
||||
|
||||
val widgetPreferences = WidgetPreferences(context)
|
||||
|
||||
for (widgetId in appWidgetIds) {
|
||||
Timber.d("Updating widget with ID: $widgetId")
|
||||
val selectedDeckIds = widgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(widgetId)
|
||||
|
||||
/**Explanation of behavior when selectedDeckIds is empty
|
||||
* If selectedDeckIds is empty, the widget will retain the previous deck list.
|
||||
* This behavior ensures that the widget does not display an empty view, which could be
|
||||
* confusing to the user. Instead, it maintains the last known state until a new valid
|
||||
* list of deck IDs is provided. This approach prioritizes providing a consistent
|
||||
* user experience over showing an empty or default state.
|
||||
*/
|
||||
if (selectedDeckIds.isNotEmpty()) {
|
||||
Timber.d("Selected deck IDs: ${selectedDeckIds.joinToString(", ")} for widget ID: $widgetId")
|
||||
updateWidget(context, appWidgetManager, widgetId, selectedDeckIds)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Widget update process completed for appWidgetIds: ${appWidgetIds.joinToString(", ")}")
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (context == null || intent == null) {
|
||||
Timber.e("Context or intent is null in onReceive")
|
||||
return
|
||||
}
|
||||
super.onReceive(context, intent)
|
||||
|
||||
val widgetPreferences = WidgetPreferences(context)
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_APPWIDGET_UPDATE -> {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
|
||||
// Retrieve the widget ID from the intent
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
val selectedDeckIds = intent.getLongArrayExtra(EXTRA_SELECTED_DECK_IDS)
|
||||
|
||||
Timber.d("Received ACTION_APPWIDGET_UPDATE with widget ID: $appWidgetId and selectedDeckIds: ${selectedDeckIds?.joinToString(", ")}")
|
||||
|
||||
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && selectedDeckIds != null) {
|
||||
Timber.d("Updating widget with ID: $appWidgetId")
|
||||
updateWidget(context, appWidgetManager, appWidgetId, selectedDeckIds)
|
||||
Timber.d("Widget update process completed for widget ID: $appWidgetId")
|
||||
}
|
||||
}
|
||||
// This custom action is received to update a specific widget.
|
||||
// It is triggered by the setRecurringAlarm method to refresh the widget's data periodically.
|
||||
ACTION_UPDATE_WIDGET -> {
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
Timber.d("Received ACTION_UPDATE_WIDGET for widget ID: $appWidgetId")
|
||||
}
|
||||
}
|
||||
AppWidgetManager.ACTION_APPWIDGET_DELETED -> {
|
||||
Timber.d("ACTION_APPWIDGET_DELETED received")
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
Timber.d("Deleting widget with ID: $appWidgetId")
|
||||
widgetPreferences.deleteDeckPickerWidgetData(appWidgetId)
|
||||
} else {
|
||||
Timber.e("Invalid widget ID received in ACTION_APPWIDGET_DELETED")
|
||||
}
|
||||
}
|
||||
AppWidgetManager.ACTION_APPWIDGET_ENABLED -> {
|
||||
Timber.d("Widget enabled")
|
||||
}
|
||||
AppWidgetManager.ACTION_APPWIDGET_DISABLED -> {
|
||||
Timber.d("Widget disabled")
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Unexpected action received: ${intent.action}")
|
||||
CrashReportService.sendExceptionReport(
|
||||
Exception("Unexpected action received: ${intent.action}"),
|
||||
"DeckPickerWidget - onReceive",
|
||||
null,
|
||||
onlyIfSilent = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
|
||||
if (context == null) {
|
||||
Timber.e("Context is null in onDeleted")
|
||||
return
|
||||
}
|
||||
|
||||
val widgetPreferences = WidgetPreferences(context)
|
||||
|
||||
appWidgetIds?.forEach { widgetId ->
|
||||
widgetPreferences.deleteDeckPickerWidgetData(widgetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map deck id to the associated DeckPickerWidgetData. Omits any id that does not correspond to a deck.
|
||||
*
|
||||
* Note: This operation may be slow, as it involves processing the entire deck collection.
|
||||
*
|
||||
* @param deckIds the list of deck IDs to retrieve data for
|
||||
* @return a list of DeckPickerWidgetData objects containing deck names and statistics
|
||||
*/
|
||||
suspend fun getDeckNameAndStats(deckIds: List<DeckId>): List<DeckPickerWidgetData> {
|
||||
val result = mutableListOf<DeckPickerWidgetData>()
|
||||
|
||||
val deckTree = withCol { sched.deckDueTree() }
|
||||
|
||||
deckTree.forEach { node ->
|
||||
if (node.did !in deckIds) return@forEach
|
||||
result.add(
|
||||
DeckPickerWidgetData(
|
||||
deckId = node.did,
|
||||
name = node.lastDeckNameComponent,
|
||||
reviewCount = node.revCount,
|
||||
learnCount = node.lrnCount,
|
||||
newCount = node.newCount
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val deckIdToData = result.associateBy { it.deckId }
|
||||
return deckIds.mapNotNull { deckIdToData[it] }
|
||||
}
|
@ -0,0 +1,489 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.ichi2.anki.AnkiActivity
|
||||
import com.ichi2.anki.CollectionManager.withCol
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.dialogs.DeckSelectionDialog
|
||||
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
|
||||
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck
|
||||
import com.ichi2.anki.dialogs.DiscardChangesDialog
|
||||
import com.ichi2.anki.showThemedToast
|
||||
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
|
||||
import com.ichi2.anki.snackbar.SnackbarBuilder
|
||||
import com.ichi2.anki.snackbar.showSnackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Activity for configuring the Deck Picker Widget.
|
||||
* This activity allows the user to select decks from deck selection dialog to be displayed in the widget.
|
||||
* User can Select up to 5 decks.
|
||||
* User Can remove, reorder decks and reconfigure by holding the widget.
|
||||
*/
|
||||
class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener, BaseSnackbarBuilderProvider {
|
||||
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
lateinit var deckAdapter: WidgetConfigScreenAdapter
|
||||
private lateinit var deckPickerWidgetPreferences: WidgetPreferences
|
||||
|
||||
/**
|
||||
* Maximum number of decks allowed in the widget.
|
||||
*/
|
||||
private val MAX_DECKS_ALLOWED = 5
|
||||
private var hasUnsavedChanges = false
|
||||
private var isAdapterObserverRegistered = false
|
||||
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (showedActivityFailedScreen(savedInstanceState)) {
|
||||
return
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!ensureStoragePermissions()) {
|
||||
return
|
||||
}
|
||||
|
||||
setContentView(R.layout.widget_deck_picker_config)
|
||||
|
||||
deckPickerWidgetPreferences = WidgetPreferences(this)
|
||||
|
||||
appWidgetId = intent.extras?.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
Timber.v("Invalid App Widget ID")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the collection is empty before proceeding and if the collection is empty, show a toast instead of the configuration view.
|
||||
lifecycleScope.launch {
|
||||
if (isCollectionEmpty()) {
|
||||
showThemedToast(
|
||||
this@DeckPickerWidgetConfig,
|
||||
R.string.app_not_initialized_new,
|
||||
false
|
||||
)
|
||||
finish()
|
||||
return@launch
|
||||
}
|
||||
|
||||
initializeUIComponents()
|
||||
}
|
||||
}
|
||||
|
||||
fun showSnackbar(message: CharSequence) {
|
||||
showSnackbar(
|
||||
message,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
|
||||
fun showSnackbar(messageResId: Int) {
|
||||
showSnackbar(getString(messageResId))
|
||||
}
|
||||
|
||||
fun initializeUIComponents() {
|
||||
deckAdapter = WidgetConfigScreenAdapter { deck, position ->
|
||||
deckAdapter.removeDeck(deck.deckId)
|
||||
showSnackbar(R.string.deck_removed_from_widget)
|
||||
updateViewVisibility()
|
||||
updateFabVisibility()
|
||||
updateDoneButtonVisibility()
|
||||
hasUnsavedChanges = true
|
||||
setUnsavedChanges(true)
|
||||
}
|
||||
|
||||
findViewById<RecyclerView>(R.id.recyclerViewSelectedDecks).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = this@DeckPickerWidgetConfig.deckAdapter
|
||||
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
|
||||
itemTouchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
setupDoneButton()
|
||||
|
||||
// TODO: Implement multi-select functionality so that user can select desired decks in once.
|
||||
findViewById<FloatingActionButton>(R.id.fabWidgetDeckPicker).setOnClickListener {
|
||||
showDeckSelectionDialog()
|
||||
}
|
||||
|
||||
updateViewWithSavedPreferences()
|
||||
|
||||
// Update the visibility of the "no decks" placeholder and the widget configuration container
|
||||
updateViewVisibility()
|
||||
|
||||
registerReceiver(widgetRemovedReceiver, IntentFilter(AppWidgetManager.ACTION_APPWIDGET_DELETED))
|
||||
|
||||
onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (hasUnsavedChanges) {
|
||||
DiscardChangesDialog.showDialog(
|
||||
context = this@DeckPickerWidgetConfig,
|
||||
positiveMethod = {
|
||||
// Set flag to indicate that changes are discarded
|
||||
hasUnsavedChanges = false
|
||||
finish()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
||||
|
||||
// Register the AdapterDataObserver if not already registered
|
||||
if (!isAdapterObserverRegistered) {
|
||||
deckAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
updateDoneButtonVisibility() // Update visibility when data changes
|
||||
}
|
||||
})
|
||||
isAdapterObserverRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallbackState() {
|
||||
onBackPressedCallback.isEnabled = hasUnsavedChanges
|
||||
}
|
||||
|
||||
// Call this method when there are unsaved changes
|
||||
private fun setUnsavedChanges(unsaved: Boolean) {
|
||||
hasUnsavedChanges = unsaved
|
||||
updateCallbackState()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiverSilently(widgetRemovedReceiver)
|
||||
}
|
||||
|
||||
override val baseSnackbarBuilder: SnackbarBuilder = {
|
||||
anchorView = findViewById<FloatingActionButton>(R.id.fabWidgetDeckPicker)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the "Done" button based on the number of selected decks.
|
||||
*
|
||||
* If no decks are selected: The button is hidden.
|
||||
* If decks are selected: The button is visible with the text "Save".
|
||||
* When clicked, the selected decks are saved, the widget is updated,
|
||||
* and the activity is finished.
|
||||
*/
|
||||
private fun setupDoneButton() {
|
||||
val doneButton = findViewById<Button>(R.id.submit_button)
|
||||
val saveText = getString(R.string.save).uppercase()
|
||||
|
||||
// Set the button text and click listener only once during initialization
|
||||
doneButton.text = saveText
|
||||
doneButton.setOnClickListener {
|
||||
saveSelectedDecksToPreferencesDeckPickerWidget()
|
||||
hasUnsavedChanges = false
|
||||
setUnsavedChanges(false)
|
||||
|
||||
val selectedDeckIds = deckPickerWidgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(appWidgetId)
|
||||
|
||||
val appWidgetManager = AppWidgetManager.getInstance(this)
|
||||
DeckPickerWidget.updateWidget(this, appWidgetManager, appWidgetId, selectedDeckIds)
|
||||
|
||||
val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
|
||||
sendBroadcast(Intent(this, DeckPickerWidget::class.java))
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
// Initially set the visibility based on the number of selected decks
|
||||
updateDoneButtonVisibility()
|
||||
}
|
||||
|
||||
private fun updateDoneButtonVisibility() {
|
||||
val doneButton = findViewById<Button>(R.id.submit_button)
|
||||
doneButton.isVisible = deckAdapter.itemCount != 0
|
||||
}
|
||||
|
||||
/** Updates the visibility of the FloatingActionButton based on the number of selected decks */
|
||||
private fun updateFabVisibility() {
|
||||
lifecycleScope.launch {
|
||||
val defaultDeckEmpty = isDefaultDeckEmpty()
|
||||
|
||||
val totalSelectableDecks = getTotalSelectableDecks()
|
||||
|
||||
// Adjust totalSelectableDecks if the default deck is empty
|
||||
var adjustedTotalSelectableDecks = totalSelectableDecks
|
||||
if (defaultDeckEmpty) {
|
||||
adjustedTotalSelectableDecks -= 1
|
||||
}
|
||||
|
||||
val selectedDeckCount = deckAdapter.itemCount
|
||||
|
||||
val fab = findViewById<FloatingActionButton>(R.id.fabWidgetDeckPicker)
|
||||
fab.isVisible = !(selectedDeckCount >= MAX_DECKS_ALLOWED || selectedDeckCount >= adjustedTotalSelectableDecks)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of selectable decks.
|
||||
*
|
||||
* The operation involves accessing the collection, which might be a time-consuming
|
||||
* I/O-bound task. Hence, we switch to the IO dispatcher
|
||||
* to avoid blocking the main thread and ensure a smooth user experience.
|
||||
*/
|
||||
private suspend fun getTotalSelectableDecks(): Int {
|
||||
return withContext(Dispatchers.IO) {
|
||||
SelectableDeck.fromCollection(includeFiltered = false).size
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun isDefaultDeckEmpty(): Boolean {
|
||||
val defaultDeckId = 1L
|
||||
val decks = withContext(Dispatchers.IO) {
|
||||
withCol { decks }
|
||||
}
|
||||
val deckIds = decks.deckAndChildIds(defaultDeckId)
|
||||
val totalCardCount = decks.cardCount(*deckIds.toLongArray(), includeSubdecks = true)
|
||||
return totalCardCount == 0
|
||||
}
|
||||
|
||||
/** Updates the view according to the saved preference for appWidgetId.*/
|
||||
fun updateViewWithSavedPreferences() {
|
||||
val selectedDeckIds = deckPickerWidgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(appWidgetId)
|
||||
if (selectedDeckIds.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
val decks = fetchDecks()
|
||||
val selectedDecks = decks.filter { it.deckId in selectedDeckIds }
|
||||
selectedDecks.forEach { deckAdapter.addDeck(it) }
|
||||
updateViewVisibility()
|
||||
updateFabVisibility()
|
||||
setupDoneButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Asynchronously displays the list of deck in the selection dialog. */
|
||||
private fun showDeckSelectionDialog() {
|
||||
lifecycleScope.launch {
|
||||
val decks = fetchDecks()
|
||||
displayDeckSelectionDialog(decks)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the list of standard deck. */
|
||||
private suspend fun fetchDecks(): List<SelectableDeck> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
SelectableDeck.fromCollection(includeFiltered = false)
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays the deck selection dialog with the provided list of decks. */
|
||||
private fun displayDeckSelectionDialog(decks: List<SelectableDeck>) {
|
||||
val dialog = DeckSelectionDialog.newInstance(
|
||||
title = getString(R.string.select_deck_title),
|
||||
summaryMessage = null,
|
||||
keepRestoreDefaultButton = false,
|
||||
decks = decks
|
||||
)
|
||||
dialog.show(supportFragmentManager, "DeckSelectionDialog")
|
||||
}
|
||||
|
||||
/** Called when a deck is selected from the deck selection dialog. */
|
||||
override fun onDeckSelected(deck: SelectableDeck?) {
|
||||
if (deck == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val isDeckAlreadySelected = deckAdapter.deckIds.contains(deck.deckId)
|
||||
|
||||
if (isDeckAlreadySelected) {
|
||||
// TODO: Eventually, ensure that the user can't select a deck that is already selected.
|
||||
showSnackbar(getString(R.string.deck_already_selected_message))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the deck is being added to a fully occupied selection
|
||||
if (deckAdapter.itemCount >= MAX_DECKS_ALLOWED) {
|
||||
// Snackbar will only be shown when adding the 5th deck
|
||||
if (deckAdapter.itemCount == MAX_DECKS_ALLOWED) {
|
||||
showSnackbar(resources.getQuantityString(R.plurals.deck_limit_reached, MAX_DECKS_ALLOWED, MAX_DECKS_ALLOWED))
|
||||
}
|
||||
// The FAB visibility should be handled in updateFabVisibility()
|
||||
} else {
|
||||
// Add the deck and update views
|
||||
deckAdapter.addDeck(deck)
|
||||
updateViewVisibility()
|
||||
updateFabVisibility()
|
||||
setupDoneButton()
|
||||
hasUnsavedChanges = true
|
||||
setUnsavedChanges(true)
|
||||
|
||||
// Show snackbar if the deck is the 5th deck
|
||||
if (deckAdapter.itemCount == MAX_DECKS_ALLOWED) {
|
||||
showSnackbar(resources.getQuantityString(R.plurals.deck_limit_reached, MAX_DECKS_ALLOWED, MAX_DECKS_ALLOWED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the visibility of the "no decks" placeholder and the widget configuration container */
|
||||
fun updateViewVisibility() {
|
||||
val noDecksPlaceholder = findViewById<View>(R.id.no_decks_placeholder)
|
||||
val widgetConfigContainer = findViewById<View>(R.id.widgetConfigContainer)
|
||||
|
||||
if (deckAdapter.itemCount > 0) {
|
||||
noDecksPlaceholder.visibility = View.GONE
|
||||
widgetConfigContainer.visibility = View.VISIBLE
|
||||
} else {
|
||||
noDecksPlaceholder.visibility = View.VISIBLE
|
||||
widgetConfigContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
/** ItemTouchHelper callback for handling drag and drop of decks. */
|
||||
private val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
0
|
||||
) {
|
||||
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
val selectedDeckCount = deckAdapter.itemCount
|
||||
return if (selectedDeckCount > 1) {
|
||||
super.getDragDirs(recyclerView, viewHolder)
|
||||
} else {
|
||||
0 // Disable drag if there's only one item
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val fromPosition = viewHolder.bindingAdapterPosition
|
||||
val toPosition = target.bindingAdapterPosition
|
||||
deckAdapter.moveDeck(fromPosition, toPosition)
|
||||
hasUnsavedChanges = true
|
||||
setUnsavedChanges(true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
// No swipe action
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the selected deck IDs to SharedPreferences and triggers a widget update.
|
||||
*
|
||||
* This function retrieves the selected decks from the `deckAdapter`, converts their IDs
|
||||
* to a comma-separated string, and stores it in SharedPreferences.
|
||||
* It then sends a broadcast to update the widget with the new deck selection.
|
||||
*/
|
||||
fun saveSelectedDecksToPreferencesDeckPickerWidget() {
|
||||
val selectedDecks = deckAdapter.deckIds.map { it }
|
||||
deckPickerWidgetPreferences.saveSelectedDecks(appWidgetId, selectedDecks.map { it.toString() })
|
||||
|
||||
val updateIntent = Intent(this, DeckPickerWidget::class.java).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
|
||||
|
||||
putExtra("deck_picker_widget_selected_deck_ids", selectedDecks.toList().toLongArray())
|
||||
}
|
||||
|
||||
sendBroadcast(updateIntent)
|
||||
}
|
||||
|
||||
/** BroadcastReceiver to handle widget removal. */
|
||||
private val widgetRemovedReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action != AppWidgetManager.ACTION_APPWIDGET_DELETED) {
|
||||
return
|
||||
}
|
||||
|
||||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
return
|
||||
}
|
||||
|
||||
context?.let { deckPickerWidgetPreferences.deleteDeckPickerWidgetData(appWidgetId) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the deck picker displays any deck.
|
||||
* Technically, it means that there is a non default deck, or that the default deck is non-empty.
|
||||
*
|
||||
* This function is specifically implemented to address an issue where the default deck
|
||||
* isn't handled correctly when a second deck is added to the
|
||||
* collection. In this case, the deck tree may incorrectly appear as non-empty when it contains
|
||||
* only the default deck and no other cards.
|
||||
*
|
||||
*/
|
||||
private suspend fun isCollectionEmpty(): Boolean {
|
||||
val tree = withCol { sched.deckDueTree() }
|
||||
if (tree.children.size == 1 && tree.children[0].did == 1L) {
|
||||
return isDefaultDeckEmpty()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a broadcast receiver from the context silently.
|
||||
*
|
||||
* This extension function attempts to unregister a broadcast receiver from the context
|
||||
* without throwing an exception if the receiver is not registered.
|
||||
* It catches the `IllegalArgumentException` that is thrown when attempting to unregister
|
||||
* a receiver that is not registered, allowing the operation to fail gracefully without crashing.
|
||||
*
|
||||
* @param receiver The broadcast receiver to be unregistered.
|
||||
*
|
||||
* @see ContextWrapper.unregisterReceiver
|
||||
* @see IllegalArgumentException
|
||||
*/
|
||||
fun ContextWrapper.unregisterReceiverSilently(receiver: BroadcastReceiver) {
|
||||
try {
|
||||
unregisterReceiver(receiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.d(e, "unregisterReceiverSilently")
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.ichi2.anki.CollectionManager.withCol
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Adapter class for displaying and managing a list of selectable decks in a RecyclerView.
|
||||
*
|
||||
* @property decks the list of selectable decks to display
|
||||
* @property onDeleteDeck a function to call when a deck is removed
|
||||
*/
|
||||
class WidgetConfigScreenAdapter(
|
||||
private val onDeleteDeck: (SelectableDeck, Int) -> Unit
|
||||
) : RecyclerView.Adapter<WidgetConfigScreenAdapter.DeckViewHolder>() {
|
||||
|
||||
private val decks: MutableList<SelectableDeck> = mutableListOf()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
// Property to get the list of deck IDs
|
||||
val deckIds: List<Long> get() = decks.map { it.deckId }
|
||||
|
||||
class DeckViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val deckNameTextView: TextView = itemView.findViewById(R.id.deck_name)
|
||||
val removeButton: ImageButton = itemView.findViewById(R.id.action_button_remove_deck)
|
||||
}
|
||||
|
||||
/** Creates and inflates the view for each item in the RecyclerView
|
||||
* @param parent the parent ViewGroup
|
||||
* @param viewType the type of the view
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeckViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.widget_item_deck_config, parent, false)
|
||||
return DeckViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DeckViewHolder, position: Int) {
|
||||
val deck = decks[position]
|
||||
|
||||
coroutineScope.launch {
|
||||
val deckName = withContext(Dispatchers.IO) {
|
||||
withCol { decks.get(deck.deckId)!!.name }
|
||||
}
|
||||
holder.deckNameTextView.text = deckName
|
||||
}
|
||||
|
||||
holder.removeButton.setOnClickListener {
|
||||
onDeleteDeck(deck, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = decks.size
|
||||
|
||||
fun addDeck(deck: SelectableDeck) {
|
||||
decks.add(deck)
|
||||
notifyItemInserted(decks.size - 1)
|
||||
}
|
||||
|
||||
fun removeDeck(deckId: Long) {
|
||||
// Find the position of the deck with the given ID
|
||||
val position = decks.indexOfFirst { it.deckId == deckId }
|
||||
if (position != -1) {
|
||||
decks.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDeck(fromPosition: Int, toPosition: Int) {
|
||||
val deck = decks.removeAt(fromPosition)
|
||||
decks.add(toPosition, deck)
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* This class is currently used for the Deck Picker Widget but is designed to be extendable
|
||||
* for use with other widgets, such as the Card Analysis Widget, in the future.
|
||||
*
|
||||
* @param context the context used to access the shared preferences
|
||||
*/
|
||||
class WidgetPreferences(context: Context) {
|
||||
|
||||
/**
|
||||
* Prefix for the SharedPreferences key used to store the selected decks for the DeckPickerWidget.
|
||||
* The full key is constructed by appending the appWidgetId to this prefix, ensuring that each
|
||||
* widget instance has a unique key. This approach helps prevent typos and ensures consistency
|
||||
* across the codebase when accessing or modifying the stored deck selections.
|
||||
*/
|
||||
|
||||
private val deckPickerSharedPreferences = context.getSharedPreferences("DeckPickerWidgetPrefs", Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* Deletes the selected deck IDs from the shared preferences for the given widget ID.
|
||||
*/
|
||||
fun deleteDeckPickerWidgetData(appWidgetId: Int) {
|
||||
deckPickerSharedPreferences.edit {
|
||||
remove(getDeckPickerWidgetKey(appWidgetId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the selected deck IDs from the shared preferences for the given widget ID.
|
||||
* Note: There's no guarantee that these IDs still represent decks that exist at the time of execution.
|
||||
*/
|
||||
fun getSelectedDeckIdsFromPreferencesDeckPickerWidget(appWidgetId: Int): LongArray {
|
||||
val selectedDecksString = deckPickerSharedPreferences.getString(getDeckPickerWidgetKey(appWidgetId), "")
|
||||
return if (!selectedDecksString.isNullOrEmpty()) {
|
||||
selectedDecksString.split(",").map { it.toLong() }.toLongArray()
|
||||
} else {
|
||||
longArrayOf()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the selected deck IDs to the shared preferences for the given widget ID.
|
||||
*/
|
||||
fun saveSelectedDecks(appWidgetId: Int, selectedDecks: List<String>) {
|
||||
deckPickerSharedPreferences.edit {
|
||||
putString(getDeckPickerWidgetKey(appWidgetId), selectedDecks.joinToString(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the key for the shared preferences for the given widget ID.
|
||||
*/
|
||||
private fun getDeckPickerWidgetKey(appWidgetId: Int): String {
|
||||
return "deck_picker_widget_selected_decks_$appWidgetId"
|
||||
}
|
BIN
AnkiDroid/src/main/res/drawable/widget_deck_picker_drawable.jpg
Normal file
BIN
AnkiDroid/src/main/res/drawable/widget_deck_picker_drawable.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
79
AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml
Normal file
79
AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml
Normal file
@ -0,0 +1,79 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.DayNight">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/no_decks_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:visibility="visible"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.ichi2.ui.FixedTextView
|
||||
style="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="7dp"
|
||||
android:gravity="center"
|
||||
android:padding="30dp"
|
||||
android:text="@string/no_selected_deck_placeholder_title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widgetConfigContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerViewSelectedDecks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/buttonBackgroundLayer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="45dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
>
|
||||
|
||||
<!-- Save / Cancel -->
|
||||
<Button
|
||||
android:id="@+id/submit_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="@dimen/abc_text_size_button_material"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:singleLine="true"
|
||||
android:padding="3dp"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/save" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabWidgetDeckPicker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:src="@drawable/ic_add_white" />
|
||||
</RelativeLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This file is used for preview purposes only and
|
||||
is not actually utilized in the widget's implementation. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/colorPrimaryContainer"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.DayNight">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_weight="3"
|
||||
android:height="48dp"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/deck1Name_deck_picker_widget"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="22sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="50"
|
||||
android:textColor="@color/flag_reviewer_blue"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="52"
|
||||
android:textColor="@color/material_red_A700"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="50"
|
||||
android:textColor="@color/flag_reviewer_green"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_weight="3"
|
||||
android:height="48dp"
|
||||
android:paddingBottom="1dp"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/deck2Name_deck_picker_widget"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="22sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="30"
|
||||
android:textColor="@color/flag_reviewer_blue"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="20"
|
||||
android:textColor="@color/material_red_A700"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="10"
|
||||
android:textColor="@color/flag_reviewer_green"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_weight="3"
|
||||
android:height="48dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/deck3Name_deck_picker_widget"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="22sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="25"
|
||||
android:textColor="@color/flag_reviewer_blue"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="15"
|
||||
android:textColor="@color/material_red_A700"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="58"
|
||||
android:textColor="@color/flag_reviewer_green"
|
||||
android:textSize="20sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
20
AnkiDroid/src/main/res/layout/widget_deck_picker_large.xml
Normal file
20
AnkiDroid/src/main/res/layout/widget_deck_picker_large.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<RelativeLayout
|
||||
android:id="@+id/widget_deck_picker"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:clickable="true"
|
||||
android:focusable="false"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.DayNight">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/deckCollection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
28
AnkiDroid/src/main/res/layout/widget_item_deck_config.xml
Normal file
28
AnkiDroid/src/main/res/layout/widget_item_deck_config.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.DayNight">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deck_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="10dp"
|
||||
tools:text="Deck Name"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_button_remove_deck"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:paddingEnd="10dp"
|
||||
android:src="@drawable/ic_delete_white" />
|
||||
|
||||
</LinearLayout>
|
59
AnkiDroid/src/main/res/layout/widget_item_deck_main.xml
Normal file
59
AnkiDroid/src/main/res/layout/widget_item_deck_main.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.DayNight">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deckName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="43dp"
|
||||
android:layout_weight="3"
|
||||
android:height="48dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="10dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="22sp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
tools:text="Deck1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deckNew"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:textColor="@color/flag_reviewer_blue"
|
||||
android:textSize="20sp"
|
||||
android:text=""
|
||||
tools:text="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deckLearn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:textColor="@color/material_red_A700"
|
||||
android:textSize="20sp"
|
||||
android:text=""
|
||||
tools:text="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/deckDue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingEnd="5dp"
|
||||
android:textColor="@color/flag_reviewer_green"
|
||||
android:textSize="20sp"
|
||||
android:text=""
|
||||
tools:text="50" />
|
||||
|
||||
</LinearLayout>
|
@ -38,4 +38,16 @@
|
||||
</plurals>
|
||||
|
||||
<string name="widget_add_note_button">Add new AnkiDroid note</string>
|
||||
<string name="deck_picker_widget_description">Deck Picker Widget</string>
|
||||
|
||||
<!-- Strings to explain usage in Deck Picker Widget Configuration screen -->
|
||||
<string name="select_deck_title" comment="Title for Deck Selection Dialog">Select decks</string>
|
||||
<string name="no_selected_deck_placeholder_title" comment="Placeholder title when no decks are selected">Select decks to display in the widget. Select decks with the + icon.</string>
|
||||
<string name="deck_removed_from_widget" comment="Snackbar when deck is removed from widget">Deck Removed</string>
|
||||
<string name="deck_already_selected_message" comment="Snackbar when user try to select the same deck again">This deck is already selected</string>
|
||||
<plurals name="deck_limit_reached">
|
||||
<item quantity="one">You can select up to %d deck.</item>
|
||||
<item quantity="other">You can select up to %d decks.</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
||||
|
@ -291,6 +291,11 @@
|
||||
<item>@string/pref_cat_plugins</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Sample deck names and for Deck Picker Widget layout in widget picker screen
|
||||
These strings are marked as `translatable=false` because translating them would defeat the purpose of the example, which relies on specific language usage.-->
|
||||
<string name="deck1Name_deck_picker_widget" translatable="false" comment="Example of a Deck name. Android's widget provider displays it as an example of what the Deck Picker widget may look like">English</string>
|
||||
<string name="deck2Name_deck_picker_widget" translatable="false" comment="Example of a Deck name. Android's widget provider displays it as an example of what the Deck Picker widget may look like">Español</string>
|
||||
<string name="deck3Name_deck_picker_widget" translatable="false" comment="Example of a Deck name. Android's widget provider displays it as an example of what the Deck Picker widget may look like">日本語</string>
|
||||
|
||||
<string name="answer_buttons_position_preference" translatable="false">answerButtonPosition</string>
|
||||
<string-array name="answer_buttons_position" translatable="false">
|
||||
|
28
AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml
Normal file
28
AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<!--JPG type of file used in previewImage property because the SVG format is not supported on all devices.
|
||||
|
||||
The widths and heights parameters are determined as follow.
|
||||
The default is 3 cells in width and 2 in height
|
||||
The height is between 1 and 5 cells.
|
||||
The width is between 3 and 4 cells.
|
||||
Following https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size
|
||||
we used the portrait mode cell size for the width and the landscape mode cell size for the height. Leading to:
|
||||
* height between 50 and 315
|
||||
* width 203 and 276-->
|
||||
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialKeyguardLayout="@layout/widget_deck_picker_large"
|
||||
android:initialLayout="@layout/widget_deck_picker_large"
|
||||
android:configure="com.ichi2.widget.DeckPickerWidgetConfig"
|
||||
android:widgetFeatures="reconfigurable"
|
||||
android:minWidth="203dp"
|
||||
android:minHeight="50dp"
|
||||
android:minResizeWidth="203dp"
|
||||
android:minResizeHeight="50dp"
|
||||
android:maxResizeWidth="276dp"
|
||||
android:maxResizeHeight="315dp"
|
||||
android:previewImage="@drawable/widget_deck_picker_drawable"
|
||||
android:previewLayout="@layout/widget_deck_picker_drawable_v31"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:targetCellHeight="2"
|
||||
android:targetCellWidth="4"
|
||||
android:widgetCategory="home_screen" />
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.ichi2.anki.RobolectricTest
|
||||
import com.ichi2.widget.getDeckNameAndStats
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DeckNameAndStatsTest : RobolectricTest() {
|
||||
|
||||
@Test
|
||||
fun testGetDeckNameAndStats_withTopLevelDecks() = runTest {
|
||||
val deck1Id = addDeck("Deck 1")
|
||||
val deck2Id = addDeck("Deck 2")
|
||||
val deckIds = listOf(deck1Id, deck2Id)
|
||||
|
||||
val result = getDeckNameAndStats(deckIds)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("Deck 1", result[0].name)
|
||||
assertEquals(deck1Id, result[0].deckId)
|
||||
assertEquals("Deck 2", result[1].name)
|
||||
assertEquals(deck2Id, result[1].deckId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetDeckNameAndStats_ordering() = runTest {
|
||||
val deckAId = addDeck("Deck A")
|
||||
val deckBId = addDeck("Deck B")
|
||||
val deckCId = addDeck("Deck C")
|
||||
val deckIds = listOf(deckCId, deckAId, deckBId)
|
||||
|
||||
val result = getDeckNameAndStats(deckIds)
|
||||
|
||||
assertEquals(3, result.size)
|
||||
assertEquals("Deck C", result[0].name)
|
||||
assertEquals(deckCId, result[0].deckId)
|
||||
assertEquals("Deck A", result[1].name)
|
||||
assertEquals(deckAId, result[1].deckId)
|
||||
assertEquals("Deck B", result[2].name)
|
||||
assertEquals(deckBId, result[2].deckId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetDeckNameAndStats_withChildDecks() = runTest {
|
||||
val deck1Id = addDeck("Deck 1")
|
||||
val child1Id = addDeck("Deck 1::Child 1")
|
||||
val deckIds = listOf(deck1Id, child1Id)
|
||||
|
||||
val result = getDeckNameAndStats(deckIds)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("Deck 1", result[0].name)
|
||||
assertEquals(deck1Id, result[0].deckId)
|
||||
assertEquals("Child 1", result[1].name) // Changed to truncated name
|
||||
assertEquals(child1Id, result[1].deckId)
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Anoop <xenonnn4w@gmail.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.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.RobolectricTest
|
||||
import com.ichi2.anki.dialogs.DeckSelectionDialog
|
||||
import com.ichi2.widget.DeckPickerWidgetConfig
|
||||
import com.ichi2.widget.WidgetPreferences
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DeckPickerWidgetConfigTest : RobolectricTest() {
|
||||
|
||||
private lateinit var activity: DeckPickerWidgetConfig
|
||||
private lateinit var lifecycle: LifecycleRegistry
|
||||
private lateinit var widgetPreferences: WidgetPreferences
|
||||
|
||||
/**
|
||||
* Sets up the test environment before each test.
|
||||
*
|
||||
* Initializes the `DeckPickerWidgetConfig` activity and associated components like
|
||||
* `LifecycleRegistry` and `WidgetPreferences`. This setup is executed before each test method.
|
||||
*/
|
||||
@Before
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
val intent = Intent(ApplicationProvider.getApplicationContext(), DeckPickerWidgetConfig::class.java).apply {
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 1)
|
||||
}
|
||||
|
||||
activity = Robolectric.buildActivity(DeckPickerWidgetConfig::class.java, intent)
|
||||
.create()
|
||||
.start()
|
||||
.resume()
|
||||
.get()
|
||||
|
||||
lifecycle = LifecycleRegistry(activity)
|
||||
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
|
||||
widgetPreferences = WidgetPreferences(ApplicationProvider.getApplicationContext())
|
||||
|
||||
// Ensure deckAdapter is initialized
|
||||
activity.initializeUIComponents()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the functionality of saving selected decks to preferences.
|
||||
*
|
||||
* This test adds a deck to the adapter and verifies if it gets correctly saved to the
|
||||
* `WidgetPreferences`.
|
||||
*/
|
||||
@Test
|
||||
fun testSaveSelectedDecksToPreferences() {
|
||||
// Add decks to adapter
|
||||
val deck1 = DeckSelectionDialog.SelectableDeck(1, "Deck 1")
|
||||
activity.deckAdapter.addDeck(deck1)
|
||||
|
||||
// Save selected decks
|
||||
activity.saveSelectedDecksToPreferencesDeckPickerWidget()
|
||||
|
||||
// Verify saved decks
|
||||
val selectedDeckIds = widgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(1)
|
||||
assert(selectedDeckIds.contains(deck1.deckId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the loading of saved preferences into the activity's view.
|
||||
*
|
||||
* This test saves decks to preferences, then loads them into the activity and checks if the
|
||||
* `RecyclerView` displays the correct number of items based on the saved preferences.
|
||||
*/
|
||||
@Test
|
||||
fun testLoadSavedPreferences() {
|
||||
// Save decks to preferences
|
||||
val deckIds = listOf(1L)
|
||||
widgetPreferences.saveSelectedDecks(1, deckIds.map { it.toString() })
|
||||
|
||||
// Load preferences
|
||||
activity.updateViewWithSavedPreferences()
|
||||
|
||||
// Ensure all tasks on the UI thread are completed
|
||||
Robolectric.flushForegroundThreadScheduler()
|
||||
|
||||
// Get the RecyclerView and its adapter
|
||||
val recyclerView = activity.findViewById<RecyclerView>(R.id.recyclerViewSelectedDecks)
|
||||
val adapter = recyclerView.adapter
|
||||
|
||||
// Verify the adapter has the correct item count
|
||||
assert(adapter != null && adapter.itemCount == deckIds.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the visibility of different views based on the selected decks.
|
||||
*
|
||||
* This test checks the visibility of the placeholder and configuration container views
|
||||
* before and after adding a deck.
|
||||
*/
|
||||
@Test
|
||||
fun testUpdateViewVisibility() {
|
||||
val noDecksPlaceholder = activity.findViewById<View>(R.id.no_decks_placeholder)
|
||||
val widgetConfigContainer = activity.findViewById<View>(R.id.widgetConfigContainer)
|
||||
|
||||
// Initially, no decks should be selected
|
||||
activity.updateViewVisibility()
|
||||
assert(noDecksPlaceholder.visibility == View.VISIBLE)
|
||||
assert(widgetConfigContainer.visibility == View.GONE)
|
||||
|
||||
// Add a deck and update view visibility
|
||||
val deck = DeckSelectionDialog.SelectableDeck(1, "Deck 1")
|
||||
activity.deckAdapter.addDeck(deck)
|
||||
activity.updateViewVisibility()
|
||||
|
||||
assert(noDecksPlaceholder.visibility == View.GONE)
|
||||
assert(widgetConfigContainer.visibility == View.VISIBLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the selection of a deck.
|
||||
*
|
||||
* This test verifies that when a deck is selected, it gets added to the adapter and displayed
|
||||
* in the `RecyclerView`.
|
||||
*/
|
||||
@Test
|
||||
fun testOnDeckSelected() {
|
||||
val deck = DeckSelectionDialog.SelectableDeck(1, "Deck 1")
|
||||
activity.onDeckSelected(deck)
|
||||
|
||||
// Verify deck is added to adapter
|
||||
val recyclerView = activity.findViewById<RecyclerView>(R.id.recyclerViewSelectedDecks)
|
||||
assert(recyclerView.adapter?.itemCount == 1)
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ import com.ichi2.anki.services.ReminderService.Companion.getReviewDeckIntent
|
||||
import com.ichi2.anki.ui.windows.managespace.ManageSpaceActivity
|
||||
import com.ichi2.anki.ui.windows.permissions.PermissionsActivity
|
||||
import com.ichi2.testutils.ActivityList.ActivityLaunchParam.Companion.get
|
||||
import com.ichi2.widget.DeckPickerWidgetConfig
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.android.controller.ActivityController
|
||||
import java.util.function.Function
|
||||
@ -90,7 +91,8 @@ object ActivityList {
|
||||
get(SingleFragmentActivity::class.java),
|
||||
get(CardViewerActivity::class.java),
|
||||
get(InstantNoteEditorActivity::class.java),
|
||||
get(MultimediaActivity::class.java)
|
||||
get(MultimediaActivity::class.java),
|
||||
get(DeckPickerWidgetConfig::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user