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
19 changes: 12 additions & 7 deletions AnkiDroid/src/main/java/com/ichi2/anki/ankiquest/AnkiquestAki.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal object AnkiquestAki {
@DrawableRes val image: Int,
@StringRes val message: Int,
) {
WELCOME(R.drawable.aki_welcome, R.string.aki_welcome),
WELCOME(R.drawable.aki_welcome, R.string.aki_review),
REVIEW(R.drawable.aki_review, R.string.aki_review),
CELEBRATE(R.drawable.aki_celebrate, R.string.aki_complete),
STREAK(R.drawable.aki_streak, R.string.aki_streak),
Expand Down Expand Up @@ -60,7 +60,7 @@ internal object AnkiquestAki {
size: Int = 80,
): ImageView =
ImageView(context).apply {
setImageResource(resource)
AnkiquestCompanion.show(this, resource)
scaleType = ImageView.ScaleType.FIT_CENTER
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
layoutParams = LinearLayout.LayoutParams(dp(context, size), dp(context, size))
Expand All @@ -71,6 +71,7 @@ internal object AnkiquestAki {
mood: Mood,
): LinearLayout =
LinearLayout(context).apply {
visibility = if (AnkiquestCompanion.visible()) View.VISIBLE else View.GONE
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(0, dp(context, 8), 0, dp(context, 8))
Expand All @@ -87,11 +88,15 @@ internal object AnkiquestAki {
}

fun notificationIcon(context: Context): Bitmap? =
BitmapFactory.decodeResource(
context.resources,
R.drawable.aki_face,
BitmapFactory.Options().apply { inSampleSize = 8 },
)
if (!AnkiquestCompanion.visible()) {
null
} else {
BitmapFactory.decodeResource(
context.resources,
AnkiquestCompanion.resource(R.drawable.aki_face),
BitmapFactory.Options().apply { inSampleSize = 8 },
)
}

private fun dp(
context: Context,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: AGPL-3.0-only

package com.ichi2.anki.ankiquest

import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.core.content.edit
import androidx.core.view.isVisible
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.R

/** Account-scoped artwork cache. The server owns the choice; offline UI uses its last answer. */
internal object AnkiquestCompanion {
private const val KEY_PREFIX = "ankiquestCompanion:"
private val choices = setOf("aki", "ankilope", "none")

fun normalize(value: String?) = value?.takeIf { it in choices } ?: "aki"

fun selected(): String {
val prefs = AnkiDroidApp.sharedPrefsOrNull() ?: return "aki"
val settings = prefs.all
val account = HomeAccount.from(settings, (settings["username"] as? String).orEmpty()) ?: return "aki"
return normalize(prefs.getString(KEY_PREFIX + account.scope, "aki"))
}

fun remember(
account: HomeAccount,
value: String,
): Boolean {
if (AnkiquestHomeData.account()?.scope != account.scope) return false
AnkiDroidApp.sharedPrefs().edit { putString(KEY_PREFIX + account.scope, normalize(value)) }
return true
}

fun visible() = selected() != "none"

@DrawableRes
fun resource(
@DrawableRes aki: Int,
): Int =
if (selected() != "ankilope") {
aki
} else {
when (aki) {
R.drawable.aki_face -> R.drawable.ankilope_face
R.drawable.aki_welcome -> R.drawable.ankilope_welcome
R.drawable.aki_celebrate, R.drawable.aki_winner -> R.drawable.ankilope_celebrate
R.drawable.aki_review, R.drawable.aki_streak, R.drawable.aki_freeze -> R.drawable.ankilope_study
else -> aki
}
}

fun show(
view: ImageView,
@DrawableRes aki: Int,
) {
view.isVisible = visible()
if (view.isVisible) view.setImageResource(resource(aki))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
Expand Down Expand Up @@ -176,6 +177,20 @@ class AnkiquestHomeActivity : AnkiActivity(R.layout.activity_ankiquest_home) {
try {
val result = AnkiquestHomeData.profile(current)
if (turn == generation && AnkiquestHomeData.account()?.scope == current.scope) profile = result
if (current.token.isNotEmpty()) {
try {
val companion = AnkiquestHomeData.companion(current)
if (turn == generation && AnkiquestHomeData.account()?.scope == current.scope &&
AnkiquestCompanion.selected() != companion && AnkiquestCompanion.remember(current, companion)
) {
AnkiquestWidget.requestUpdate(this@AnkiquestHomeActivity)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.w(e, "AnkiQuest companion could not be refreshed")
}
}
null
} catch (e: CancellationException) {
throw e
Expand Down Expand Up @@ -223,6 +238,7 @@ class AnkiquestHomeActivity : AnkiActivity(R.layout.activity_ankiquest_home) {
if (!::content.isInitialized) return
val position = scroll.scrollY
content.removeAllViews()
AnkiquestCompanion.show(findViewById<ImageView>(R.id.aq_home_companion), R.drawable.aki_face)
findViewById<View>(R.id.aq_home_loading).isVisible = loading
renderToday()
scroll.post { scroll.scrollTo(0, position) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.net.URLEncoder
Expand Down Expand Up @@ -110,6 +112,38 @@ internal object AnkiquestHomeData {
}
}

suspend fun companion(account: HomeAccount): String = companionRequest(account, null)

suspend fun saveCompanion(
account: HomeAccount,
choice: String,
): String {
require(AnkiquestCompanion.normalize(choice) == choice)
return companionRequest(account, choice)
}

private suspend fun companionRequest(
account: HomeAccount,
choice: String?,
): String =
withContext(Dispatchers.IO) {
check(account.token.isNotEmpty()) { "Set your AnkiQuest token to choose a companion." }
val request =
Request
.Builder()
.url(account.url("api/companion/${account.encodedUser}"))
.header("Authorization", "Bearer ${account.token}")
.apply {
if (choice != null) {
post(JSONObject().put("companion", choice).toString().toRequestBody("application/json".toMediaType()))
}
}.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Ankiquest.HttpStatusException(response.code)
AnkiquestCompanion.normalize(JSONObject(response.body.string()).getString("companion"))
}
}

suspend fun local(): HomeLocal =
CollectionManager.withCol {
val now =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ open class AnkiquestWidget : AppWidgetProvider() {
period: String = "week",
): RemoteViews {
val views = RemoteViews(context.packageName, style.layout)
views.setViewVisibility(R.id.ankiquest_widget_companion, if (AnkiquestCompanion.visible()) View.VISIBLE else View.GONE)
if (AnkiquestCompanion.visible()) {
views.setImageViewResource(
R.id.ankiquest_widget_companion,
AnkiquestCompanion.resource(R.drawable.aki_face),
)
}
views.setTextViewText(
R.id.ankiquest_widget_period,
AnkiquestLanguage.context(context).getString(PERIODS.first { it.name == period }.label),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,16 @@ class ReviewHeatmapView
if (ReviewHeatmap.searchForDay(currentData, date) != null) onDaySelected?.invoke(currentData, date)
}
setLoading()
findViewById<View>(R.id.review_heatmap_aki).isVisible =
AnkiDroidApp.sharedPrefsOrNull() != null && AnkiquestNavigation.enabled()
val companion = findViewById<android.widget.ImageView>(R.id.review_heatmap_aki)
companion.isVisible = AnkiDroidApp.sharedPrefsOrNull() != null && AnkiquestNavigation.enabled() &&
com.ichi2.anki.ankiquest.AnkiquestCompanion
.visible()
if (companion.isVisible) {
companion.setImageResource(
com.ichi2.anki.ankiquest.AnkiquestCompanion
.resource(R.drawable.aki_streak),
)
}
}

fun setData(data: ReviewHeatmapData) = setData(data, state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ import com.ichi2.anki.ankiquest.Ankiquest
import com.ichi2.anki.ankiquest.AnkiquestActivity
import com.ichi2.anki.ankiquest.AnkiquestAvatarEditor
import com.ichi2.anki.ankiquest.AnkiquestAvatars
import com.ichi2.anki.ankiquest.AnkiquestCompanion
import com.ichi2.anki.ankiquest.AnkiquestDeckAdapter
import com.ichi2.anki.ankiquest.AnkiquestDeckTree
import com.ichi2.anki.ankiquest.AnkiquestHomeData
import com.ichi2.anki.ankiquest.AnkiquestNotificationHealth
import com.ichi2.anki.ankiquest.AnkiquestNotifier
import com.ichi2.anki.ankiquest.AnkiquestPoll
import com.ichi2.anki.ankiquest.AnkiquestUpdater
import com.ichi2.anki.ankiquest.AnkiquestWidget
import com.ichi2.preferences.VersatileTextPreference
import com.ichi2.utils.Permissions.openAppSettingsScreen
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -76,13 +79,15 @@ open class AnkiquestSettingsFragment : SettingsFragment() {
override fun initSubscreen() {
refreshServerSettings.clear()
bindDashboard(R.string.ankiquest_dashboard_key)
updateCompanionIcon()
}

protected fun bindAccount() {
requirePreference<VersatileTextPreference>(R.string.ankiquest_url_key).continuousValidator =
VersatileTextPreference.Validator { value ->
if (value.isNotEmpty()) value.toHttpUrl()
}
bindCompanion()
requirePreference<Preference>(R.string.ankiquest_avatar_key).setOnPreferenceClickListener {
pictureAction {
val account = checkNotNull(AnkiquestAvatars.account()) { getString(R.string.ankiquest_nudges_unavailable) }
Expand Down Expand Up @@ -209,6 +214,97 @@ open class AnkiquestSettingsFragment : SettingsFragment() {
}
}

private fun updateCompanionIcon() {
preferenceScreen.findPreference<Preference>(getString(R.string.ankiquest_dashboard_key))?.icon =
if (AnkiquestCompanion.visible()) {
ContextCompat.getDrawable(requireContext(), AnkiquestCompanion.resource(R.drawable.aki_face))
} else {
null
}
}

private fun bindCompanion() {
val preference = requirePreference<ListPreference>(R.string.ankiquest_companion_key)
var generation = 0
var saving = false
var refreshPending = false
preference.isPersistent = false
preference.isEnabled = false
val refresh = refresh@{
val request = ++generation
preference.isEnabled = false
val account = AnkiquestHomeData.account()?.takeIf { it.token.isNotEmpty() }
if (account == null) {
preference.summary = getString(R.string.ankiquest_check_unconfigured)
return@refresh
}
preference.summary = getString(R.string.ankiquest_check_running)
if (saving) {
refreshPending = true
return@refresh
}
lifecycleScope.launch {
try {
val choice = AnkiquestHomeData.companion(account)
if (request != generation || AnkiquestHomeData.account()?.scope != account.scope || !isAdded) return@launch
AnkiquestCompanion.remember(account, choice)
preference.value = choice
preference.summary = preference.entry
preference.isEnabled = true
updateCompanionIcon()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
if (request == generation &&
isAdded
) {
preference.summary = getString(R.string.ankiquest_check_failed, e.message ?: e.javaClass.simpleName)
}
}
}
}
refreshServerSettings.add { refresh() }
preference.setOnPreferenceChangeListener { _, value ->
val account = AnkiquestHomeData.account() ?: return@setOnPreferenceChangeListener false
val request = ++generation
saving = true
preference.isEnabled = false
lifecycleScope.launch {
try {
val choice = AnkiquestHomeData.saveCompanion(account, value.toString())
if (request != generation || AnkiquestHomeData.account()?.scope != account.scope || !isAdded) return@launch
AnkiquestCompanion.remember(account, choice)
preference.value = choice
preference.summary = preference.entry
preference.isEnabled = true
updateCompanionIcon()
AnkiquestWidget.requestUpdate(requireContext())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
if (request == generation && isAdded) {
AlertDialog
.Builder(requireContext())
.setTitle(preference.title)
.setMessage(getString(R.string.ankiquest_check_failed, e.message ?: e.javaClass.simpleName))
.setPositiveButton(android.R.string.ok, null)
.show()
}
} finally {
saving = false
if (refreshPending && isAdded) {
refreshPending = false
refresh()
} else if (request == generation && isAdded && !preference.isEnabled) {
refresh()
}
}
}
false
}
updateCompanionIcon()
}

private fun bindAlertSettings(
key: Int,
nudge: Boolean,
Expand Down Expand Up @@ -578,6 +674,7 @@ open class AnkiquestSettingsFragment : SettingsFragment() {

override fun onResume() {
super.onResume()
updateCompanionIcon()
refreshServerSettings.forEach { it() }
// Deliver pending messages promptly after returning from Android alert settings.
AnkiquestPoll.refreshNow(requireContext())
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/layout/activity_ankiquest_home.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
android:paddingStart="16dp"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/aq_home_companion"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/layout/widget_ankiquest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
android:paddingBottom="4dp">

<ImageView
android:id="@+id/ankiquest_widget_companion"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="6dp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
android:paddingBottom="4dp">

<ImageView
android:id="@+id/ankiquest_widget_companion"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="6dp"
Expand Down
7 changes: 7 additions & 0 deletions AnkiDroid/src/main/res/values-de/ankiquest_companion.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
<resources>
<string name="ankiquest_companion_title">Lernbegleiter</string>
<string name="ankiquest_companion_summary">Wähle Aki, Ankilope oder keinen Begleiter. Auf allen Geräten synchronisiert.</string>
<string name="ankiquest_companion_none">Kein Begleiter</string>
</resources>
Loading
Loading