mirror of
https://github.com/ankidroid/Anki-Android.git
synced 2024-09-19 11:32:15 +02:00
Replace CRLF with LF
This commit is contained in:
parent
1feb8b31b8
commit
2c7c0c5bda
@ -1,208 +1,208 @@
|
||||
/*
|
||||
* Copyright (c) 2023
|
||||
*
|
||||
* 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
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.ichi2.anki.tests.InstrumentedTest
|
||||
import com.ichi2.anki.tests.libanki.RetryRule
|
||||
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
|
||||
import com.ichi2.anki.testutil.ThreadUtils
|
||||
import com.ichi2.anki.testutil.grantPermissions
|
||||
import com.ichi2.anki.testutil.notificationPermission
|
||||
import com.ichi2.libanki.Collection
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.AssertionError
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReviewerTest : InstrumentedTest() {
|
||||
|
||||
// Launch IntroductionActivity instead of DeckPicker activity because in CI
|
||||
// builds, it seems to create IntroductionActivity after the DeckPicker,
|
||||
// causing the DeckPicker activity to be destroyed. As a consequence, this
|
||||
// will throw RootViewWithoutFocusException when Espresso tries to interact
|
||||
// with an already destroyed activity. By launching IntroductionActivity, we
|
||||
// ensure that IntroductionActivity is launched first and navigate to the
|
||||
// DeckPicker -> Reviewer activities
|
||||
@get:Rule
|
||||
val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java)
|
||||
|
||||
@get:Rule
|
||||
val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission)
|
||||
|
||||
@get:Rule
|
||||
val retry = RetryRule(10)
|
||||
|
||||
@Test
|
||||
fun testCustomSchedulerWithCustomData() {
|
||||
col.cardStateCustomizer =
|
||||
"""
|
||||
states.good.normal.review.easeFactor = 3.0;
|
||||
states.good.normal.review.scheduledDays = 123;
|
||||
customData.good.c += 1;
|
||||
"""
|
||||
val note = addNoteUsingBasicModel("foo", "bar")
|
||||
val card = note.firstCard(col)
|
||||
val deck = col.decks.get(note.notetype.did)!!
|
||||
card.moveToReviewQueue()
|
||||
col.backend.updateCards(
|
||||
listOf(
|
||||
card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build()
|
||||
),
|
||||
true
|
||||
)
|
||||
|
||||
closeGetStartedScreenIfExists()
|
||||
closeBackupCollectionDialogIfExists()
|
||||
reviewDeckWithName(deck.name)
|
||||
|
||||
var cardFromDb = col.getCard(card.id).toBackendCard()
|
||||
assertThat(cardFromDb.easeFactor, equalTo(card.factor))
|
||||
assertThat(cardFromDb.interval, equalTo(card.ivl))
|
||||
assertThat(cardFromDb.customData, equalTo("""{"c":1}"""))
|
||||
|
||||
clickShowAnswerAndAnswerGood()
|
||||
|
||||
fun runAssertion() {
|
||||
cardFromDb = col.getCard(card.id).toBackendCard()
|
||||
assertThat(cardFromDb.easeFactor, equalTo(3000))
|
||||
assertThat(cardFromDb.interval, equalTo(123))
|
||||
assertThat(cardFromDb.customData, equalTo("""{"c":2}"""))
|
||||
}
|
||||
|
||||
try {
|
||||
runAssertion()
|
||||
} catch (e: Exception) {
|
||||
// Give separate threads a greater chance of doing the custom scheduling
|
||||
// if the card scheduling values aren't updated immediately
|
||||
ThreadUtils.sleep(2000)
|
||||
runAssertion()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomSchedulerWithRuntimeError() {
|
||||
// Issue 15035 - runtime errors weren't handled
|
||||
col.cardStateCustomizer = "states.this_is_not_defined.normal.review = 12;"
|
||||
addNoteUsingBasicModel()
|
||||
|
||||
closeGetStartedScreenIfExists()
|
||||
closeBackupCollectionDialogIfExists()
|
||||
reviewDeckWithName("Default")
|
||||
|
||||
clickShowAnswer()
|
||||
|
||||
ensureAnswerButtonsAreDisplayed()
|
||||
}
|
||||
|
||||
private fun clickOnDeckWithName(deckName: String) {
|
||||
onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName))))
|
||||
onView(withId(R.id.files)).perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(deckName)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun clickOnStudyButtonIfExists() {
|
||||
onView(withId(R.id.studyoptions_start))
|
||||
.withFailureHandler { _, _ -> }
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
private fun reviewDeckWithName(deckName: String) {
|
||||
clickOnDeckWithName(deckName)
|
||||
// Adding cards directly to the database while in the Deck Picker screen
|
||||
// will not update the page with correct card counts. Hence, clicking
|
||||
// on the deck will bring us to the study options page where we need to
|
||||
// click on the Study button. If we have added cards to the database
|
||||
// before the Deck Picker screen has fully loaded, then we skip clicking
|
||||
// the Study button
|
||||
clickOnStudyButtonIfExists()
|
||||
}
|
||||
|
||||
private fun clickShowAnswerAndAnswerGood() {
|
||||
clickShowAnswer()
|
||||
ensureAnswerButtonsAreDisplayed()
|
||||
try {
|
||||
// ...on the command line it has resource name "good_button"...
|
||||
onView(withResourceName("good_button")).perform(click())
|
||||
} catch (e: NoMatchingViewException) {
|
||||
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
|
||||
onView(withResourceName("flashcard_layout_ease3")).perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
private fun clickShowAnswer() {
|
||||
try {
|
||||
// ... on the command line, it has resource name "show_answer"...
|
||||
onView(withResourceName("show_answer")).perform(click())
|
||||
} catch (e: NoMatchingViewException) {
|
||||
// ... but in Android Studio it has resource name "flashcard_layout_flip" !?
|
||||
onView(withResourceName("flashcard_layout_flip")).perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureAnswerButtonsAreDisplayed() {
|
||||
// We need to wait for the card to fully load to allow enough time for
|
||||
// the messages to be passed in and out of the WebView when evaluating
|
||||
// the custom JS scheduler code. The ease buttons are hidden until the
|
||||
// custom scheduler has finished running
|
||||
try {
|
||||
// ...on the command line it has resource name "good_button"...
|
||||
onView(withResourceName("good_button")).checkWithTimeout(
|
||||
matches(isDisplayed()),
|
||||
100
|
||||
)
|
||||
} catch (e: AssertionError) {
|
||||
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
|
||||
onView(withResourceName("flashcard_layout_ease3")).checkWithTimeout(
|
||||
matches(isDisplayed()),
|
||||
100
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var Collection.cardStateCustomizer: String?
|
||||
get() = config.get("cardStateCustomizer")
|
||||
set(value) { config.set("cardStateCustomizer", value) }
|
||||
|
||||
fun closeGetStartedScreenIfExists() {
|
||||
onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click())
|
||||
}
|
||||
|
||||
fun closeBackupCollectionDialogIfExists() {
|
||||
onView(withText(R.string.button_backup_later))
|
||||
.withFailureHandler { _, _ -> }
|
||||
.perform(click())
|
||||
}
|
||||
/*
|
||||
* Copyright (c) 2023
|
||||
*
|
||||
* 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
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.ichi2.anki.tests.InstrumentedTest
|
||||
import com.ichi2.anki.tests.libanki.RetryRule
|
||||
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
|
||||
import com.ichi2.anki.testutil.ThreadUtils
|
||||
import com.ichi2.anki.testutil.grantPermissions
|
||||
import com.ichi2.anki.testutil.notificationPermission
|
||||
import com.ichi2.libanki.Collection
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.lang.AssertionError
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ReviewerTest : InstrumentedTest() {
|
||||
|
||||
// Launch IntroductionActivity instead of DeckPicker activity because in CI
|
||||
// builds, it seems to create IntroductionActivity after the DeckPicker,
|
||||
// causing the DeckPicker activity to be destroyed. As a consequence, this
|
||||
// will throw RootViewWithoutFocusException when Espresso tries to interact
|
||||
// with an already destroyed activity. By launching IntroductionActivity, we
|
||||
// ensure that IntroductionActivity is launched first and navigate to the
|
||||
// DeckPicker -> Reviewer activities
|
||||
@get:Rule
|
||||
val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java)
|
||||
|
||||
@get:Rule
|
||||
val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission)
|
||||
|
||||
@get:Rule
|
||||
val retry = RetryRule(10)
|
||||
|
||||
@Test
|
||||
fun testCustomSchedulerWithCustomData() {
|
||||
col.cardStateCustomizer =
|
||||
"""
|
||||
states.good.normal.review.easeFactor = 3.0;
|
||||
states.good.normal.review.scheduledDays = 123;
|
||||
customData.good.c += 1;
|
||||
"""
|
||||
val note = addNoteUsingBasicModel("foo", "bar")
|
||||
val card = note.firstCard(col)
|
||||
val deck = col.decks.get(note.notetype.did)!!
|
||||
card.moveToReviewQueue()
|
||||
col.backend.updateCards(
|
||||
listOf(
|
||||
card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build()
|
||||
),
|
||||
true
|
||||
)
|
||||
|
||||
closeGetStartedScreenIfExists()
|
||||
closeBackupCollectionDialogIfExists()
|
||||
reviewDeckWithName(deck.name)
|
||||
|
||||
var cardFromDb = col.getCard(card.id).toBackendCard()
|
||||
assertThat(cardFromDb.easeFactor, equalTo(card.factor))
|
||||
assertThat(cardFromDb.interval, equalTo(card.ivl))
|
||||
assertThat(cardFromDb.customData, equalTo("""{"c":1}"""))
|
||||
|
||||
clickShowAnswerAndAnswerGood()
|
||||
|
||||
fun runAssertion() {
|
||||
cardFromDb = col.getCard(card.id).toBackendCard()
|
||||
assertThat(cardFromDb.easeFactor, equalTo(3000))
|
||||
assertThat(cardFromDb.interval, equalTo(123))
|
||||
assertThat(cardFromDb.customData, equalTo("""{"c":2}"""))
|
||||
}
|
||||
|
||||
try {
|
||||
runAssertion()
|
||||
} catch (e: Exception) {
|
||||
// Give separate threads a greater chance of doing the custom scheduling
|
||||
// if the card scheduling values aren't updated immediately
|
||||
ThreadUtils.sleep(2000)
|
||||
runAssertion()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCustomSchedulerWithRuntimeError() {
|
||||
// Issue 15035 - runtime errors weren't handled
|
||||
col.cardStateCustomizer = "states.this_is_not_defined.normal.review = 12;"
|
||||
addNoteUsingBasicModel()
|
||||
|
||||
closeGetStartedScreenIfExists()
|
||||
closeBackupCollectionDialogIfExists()
|
||||
reviewDeckWithName("Default")
|
||||
|
||||
clickShowAnswer()
|
||||
|
||||
ensureAnswerButtonsAreDisplayed()
|
||||
}
|
||||
|
||||
private fun clickOnDeckWithName(deckName: String) {
|
||||
onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName))))
|
||||
onView(withId(R.id.files)).perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
hasDescendant(withText(deckName)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun clickOnStudyButtonIfExists() {
|
||||
onView(withId(R.id.studyoptions_start))
|
||||
.withFailureHandler { _, _ -> }
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
private fun reviewDeckWithName(deckName: String) {
|
||||
clickOnDeckWithName(deckName)
|
||||
// Adding cards directly to the database while in the Deck Picker screen
|
||||
// will not update the page with correct card counts. Hence, clicking
|
||||
// on the deck will bring us to the study options page where we need to
|
||||
// click on the Study button. If we have added cards to the database
|
||||
// before the Deck Picker screen has fully loaded, then we skip clicking
|
||||
// the Study button
|
||||
clickOnStudyButtonIfExists()
|
||||
}
|
||||
|
||||
private fun clickShowAnswerAndAnswerGood() {
|
||||
clickShowAnswer()
|
||||
ensureAnswerButtonsAreDisplayed()
|
||||
try {
|
||||
// ...on the command line it has resource name "good_button"...
|
||||
onView(withResourceName("good_button")).perform(click())
|
||||
} catch (e: NoMatchingViewException) {
|
||||
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
|
||||
onView(withResourceName("flashcard_layout_ease3")).perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
private fun clickShowAnswer() {
|
||||
try {
|
||||
// ... on the command line, it has resource name "show_answer"...
|
||||
onView(withResourceName("show_answer")).perform(click())
|
||||
} catch (e: NoMatchingViewException) {
|
||||
// ... but in Android Studio it has resource name "flashcard_layout_flip" !?
|
||||
onView(withResourceName("flashcard_layout_flip")).perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureAnswerButtonsAreDisplayed() {
|
||||
// We need to wait for the card to fully load to allow enough time for
|
||||
// the messages to be passed in and out of the WebView when evaluating
|
||||
// the custom JS scheduler code. The ease buttons are hidden until the
|
||||
// custom scheduler has finished running
|
||||
try {
|
||||
// ...on the command line it has resource name "good_button"...
|
||||
onView(withResourceName("good_button")).checkWithTimeout(
|
||||
matches(isDisplayed()),
|
||||
100
|
||||
)
|
||||
} catch (e: AssertionError) {
|
||||
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
|
||||
onView(withResourceName("flashcard_layout_ease3")).checkWithTimeout(
|
||||
matches(isDisplayed()),
|
||||
100
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var Collection.cardStateCustomizer: String?
|
||||
get() = config.get("cardStateCustomizer")
|
||||
set(value) { config.set("cardStateCustomizer", value) }
|
||||
|
||||
fun closeGetStartedScreenIfExists() {
|
||||
onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click())
|
||||
}
|
||||
|
||||
fun closeBackupCollectionDialogIfExists() {
|
||||
onView(withText(R.string.button_backup_later))
|
||||
.withFailureHandler { _, _ -> }
|
||||
.perform(click())
|
||||
}
|
||||
|
@ -1,99 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Abdo <abdo@abdnh.net>
|
||||
*
|
||||
* 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.pages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.os.bundleOf
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.SingleFragmentActivity
|
||||
import com.ichi2.anki.dialogs.DiscardChangesDialog
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
|
||||
class ImageOcclusion : PageFragment(R.layout.image_occlusion) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
with(requireActivity()) {
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
DiscardChangesDialog.showDialog(this@with) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
|
||||
if (it.itemId == R.id.action_save) {
|
||||
Timber.i("save item selected")
|
||||
webView.evaluateJavascript("anki.imageOcclusion.save()", null)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
|
||||
return object : PageWebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
val kind = requireArguments().getString(ARG_KEY_KIND)
|
||||
val noteOrNotetypeId = requireArguments().getLong(ARG_KEY_ID)
|
||||
val imagePath = requireArguments().getString(ARG_KEY_PATH)
|
||||
|
||||
val options = JSONObject()
|
||||
options.put("kind", kind)
|
||||
if (kind == "add") {
|
||||
options.put("imagePath", imagePath)
|
||||
options.put("notetypeId", noteOrNotetypeId)
|
||||
} else {
|
||||
options.put("noteId", noteOrNotetypeId)
|
||||
}
|
||||
|
||||
view?.evaluateJavascript("globalThis.anki.imageOcclusion.mode = $options") {
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_KEY_KIND = "kind"
|
||||
private const val ARG_KEY_ID = "id"
|
||||
private const val ARG_KEY_PATH = "imagePath"
|
||||
|
||||
fun getIntent(context: Context, kind: String, noteOrNotetypeId: Long, imagePath: String?): Intent {
|
||||
val suffix = if (kind == "edit") {
|
||||
"/$noteOrNotetypeId"
|
||||
} else {
|
||||
imagePath
|
||||
}
|
||||
val arguments = bundleOf(
|
||||
ARG_KEY_KIND to kind,
|
||||
ARG_KEY_ID to noteOrNotetypeId,
|
||||
ARG_KEY_PATH to imagePath,
|
||||
PATH_ARG_KEY to "image-occlusion$suffix"
|
||||
)
|
||||
return SingleFragmentActivity.getIntent(context, ImageOcclusion::class, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright (c) 2023 Abdo <abdo@abdnh.net>
|
||||
*
|
||||
* 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.pages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.os.bundleOf
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.SingleFragmentActivity
|
||||
import com.ichi2.anki.dialogs.DiscardChangesDialog
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
|
||||
class ImageOcclusion : PageFragment(R.layout.image_occlusion) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
with(requireActivity()) {
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
DiscardChangesDialog.showDialog(this@with) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
|
||||
if (it.itemId == R.id.action_save) {
|
||||
Timber.i("save item selected")
|
||||
webView.evaluateJavascript("anki.imageOcclusion.save()", null)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
|
||||
return object : PageWebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
val kind = requireArguments().getString(ARG_KEY_KIND)
|
||||
val noteOrNotetypeId = requireArguments().getLong(ARG_KEY_ID)
|
||||
val imagePath = requireArguments().getString(ARG_KEY_PATH)
|
||||
|
||||
val options = JSONObject()
|
||||
options.put("kind", kind)
|
||||
if (kind == "add") {
|
||||
options.put("imagePath", imagePath)
|
||||
options.put("notetypeId", noteOrNotetypeId)
|
||||
} else {
|
||||
options.put("noteId", noteOrNotetypeId)
|
||||
}
|
||||
|
||||
view?.evaluateJavascript("globalThis.anki.imageOcclusion.mode = $options") {
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_KEY_KIND = "kind"
|
||||
private const val ARG_KEY_ID = "id"
|
||||
private const val ARG_KEY_PATH = "imagePath"
|
||||
|
||||
fun getIntent(context: Context, kind: String, noteOrNotetypeId: Long, imagePath: String?): Intent {
|
||||
val suffix = if (kind == "edit") {
|
||||
"/$noteOrNotetypeId"
|
||||
} else {
|
||||
imagePath
|
||||
}
|
||||
val arguments = bundleOf(
|
||||
ARG_KEY_KIND to kind,
|
||||
ARG_KEY_ID to noteOrNotetypeId,
|
||||
ARG_KEY_PATH to imagePath,
|
||||
PATH_ARG_KEY to "image-occlusion$suffix"
|
||||
)
|
||||
return SingleFragmentActivity.getIntent(context, ImageOcclusion::class, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Brayan Oliveira <brayandso.dev@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.preferences
|
||||
|
||||
import com.ichi2.anki.R
|
||||
|
||||
/**
|
||||
* Fragment with preferences related to notifications
|
||||
*/
|
||||
class AccessibilitySettingsFragment : SettingsFragment() {
|
||||
override val preferenceResource: Int
|
||||
get() = R.xml.preferences_accessibility
|
||||
override val analyticsScreenNameConstant: String
|
||||
get() = "prefs.accessibility"
|
||||
|
||||
override fun initSubscreen() {
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright (c) 2022 Brayan Oliveira <brayandso.dev@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.preferences
|
||||
|
||||
import com.ichi2.anki.R
|
||||
|
||||
/**
|
||||
* Fragment with preferences related to notifications
|
||||
*/
|
||||
class AccessibilitySettingsFragment : SettingsFragment() {
|
||||
override val preferenceResource: Int
|
||||
get() = R.xml.preferences_accessibility
|
||||
override val analyticsScreenNameConstant: String
|
||||
get() = "prefs.accessibility"
|
||||
|
||||
override fun initSubscreen() {
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2024 WPum <27683756+WPum@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
|
||||
|
||||
import com.ichi2.anki.notifications.NotificationId
|
||||
import com.ichi2.anki.worker.UniqueWorkNames
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class ConstantUniquenessTest {
|
||||
|
||||
@Test
|
||||
fun testConstantUniqueness() {
|
||||
assertConstantUniqueness(NotificationId::class)
|
||||
assertConstantUniqueness(UniqueWorkNames::class)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* To check whether all PUBLIC CONST values in an object are unique.
|
||||
*/
|
||||
fun <T : Any> assertConstantUniqueness(clazz: KClass<T>) {
|
||||
assertNotNull(clazz.objectInstance, "Can only check objects for uniqueness")
|
||||
val valueSet = HashSet<Any?>()
|
||||
for (prop in clazz.declaredMemberProperties) {
|
||||
if (!prop.isConst || prop.visibility != KVisibility.PUBLIC) {
|
||||
continue
|
||||
}
|
||||
// use .call() since clazz represents an object
|
||||
val value = prop.call()
|
||||
assertFalse(valueSet.contains(value), "Duplicate value ('$value') for constant in ${clazz.qualifiedName}")
|
||||
valueSet.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright (c) 2024 WPum <27683756+WPum@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
|
||||
|
||||
import com.ichi2.anki.notifications.NotificationId
|
||||
import com.ichi2.anki.worker.UniqueWorkNames
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class ConstantUniquenessTest {
|
||||
|
||||
@Test
|
||||
fun testConstantUniqueness() {
|
||||
assertConstantUniqueness(NotificationId::class)
|
||||
assertConstantUniqueness(UniqueWorkNames::class)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* To check whether all PUBLIC CONST values in an object are unique.
|
||||
*/
|
||||
fun <T : Any> assertConstantUniqueness(clazz: KClass<T>) {
|
||||
assertNotNull(clazz.objectInstance, "Can only check objects for uniqueness")
|
||||
val valueSet = HashSet<Any?>()
|
||||
for (prop in clazz.declaredMemberProperties) {
|
||||
if (!prop.isConst || prop.visibility != KVisibility.PUBLIC) {
|
||||
continue
|
||||
}
|
||||
// use .call() since clazz represents an object
|
||||
val value = prop.call()
|
||||
assertFalse(valueSet.contains(value), "Duplicate value ('$value') for constant in ${clazz.qualifiedName}")
|
||||
valueSet.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Brayan Oliveira <brayandso.dev@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.previewer
|
||||
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PreviewerViewModelTest {
|
||||
@Test
|
||||
fun `type answer fields are removed in questions`() {
|
||||
assertThat(
|
||||
PreviewerViewModel.typeAnsQuestionFilter("creu [[type:leu]]"),
|
||||
equalTo("creu ")
|
||||
)
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Copyright (c) 2024 Brayan Oliveira <brayandso.dev@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.previewer
|
||||
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PreviewerViewModelTest {
|
||||
@Test
|
||||
fun `type answer fields are removed in questions`() {
|
||||
assertThat(
|
||||
PreviewerViewModel.typeAnsQuestionFilter("creu [[type:leu]]"),
|
||||
equalTo("creu ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
30
jitpack.yml
30
jitpack.yml
@ -1,15 +1,15 @@
|
||||
# We want control over the SDK used to build since we need newer JDKs
|
||||
# Use the latest LTS supported by temurin with version format xx.yy.zz-tem (ignore sub-version)
|
||||
# You may find versions e.g. for JDK21 like so https://adoptium.net/temurin/archive/?version=21
|
||||
# You may verify that a JDK is installed by looking at the "sdk list java" output from, for example:
|
||||
# https://jitpack.io/com/github/ankidroid/Anki-Android/v2.18alpha7/build.log (just use a recent tag,
|
||||
# if it has not built yet you will have to wait for it to build then on refresh the build log should exist)
|
||||
before_install:
|
||||
- sdk update
|
||||
- sdk list java
|
||||
- sdk install java 21.0.2-tem
|
||||
- sdk use java 21.0.2-tem
|
||||
|
||||
# We can do the absolute minimum to build the API module, no need to build AnkiDroid module
|
||||
install:
|
||||
- ./gradlew :api:publishToMavenLocal
|
||||
# We want control over the SDK used to build since we need newer JDKs
|
||||
# Use the latest LTS supported by temurin with version format xx.yy.zz-tem (ignore sub-version)
|
||||
# You may find versions e.g. for JDK21 like so https://adoptium.net/temurin/archive/?version=21
|
||||
# You may verify that a JDK is installed by looking at the "sdk list java" output from, for example:
|
||||
# https://jitpack.io/com/github/ankidroid/Anki-Android/v2.18alpha7/build.log (just use a recent tag,
|
||||
# if it has not built yet you will have to wait for it to build then on refresh the build log should exist)
|
||||
before_install:
|
||||
- sdk update
|
||||
- sdk list java
|
||||
- sdk install java 21.0.2-tem
|
||||
- sdk use java 21.0.2-tem
|
||||
|
||||
# We can do the absolute minimum to build the API module, no need to build AnkiDroid module
|
||||
install:
|
||||
- ./gradlew :api:publishToMavenLocal
|
||||
|
Loading…
Reference in New Issue
Block a user