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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,8 @@ abstract class AbstractFlashcardViewer :
} else if (resultCode == RESULT_CANCELED && !reloadRequired) {
// nothing was changed by the note editor so just redraw the card
redrawCard()
} else if (resultCode == NoteEditor.RESULT_UPDATED_IO_NOTE) {
displayCardQuestion(true)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(question, not a request)

Is an OpChanges exposed which we can use inside an undoableOp instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both adding and updating I/O notes return OpChanges, so that should be possible, but as the calls are currently of the Raw variant, the return value would need to be decoded as OpChangesOnly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be rebased on main, this line conflicts (parameter was removed in API changes)

}
}
}
Expand Down
153 changes: 140 additions & 13 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ package com.ichi2.anki

import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
Expand Down Expand Up @@ -49,11 +51,13 @@ import androidx.core.content.edit
import androidx.core.content.res.ResourcesCompat
import androidx.core.text.HtmlCompat
import anki.config.ConfigKey
import anki.notetypes.StockNotetype
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anim.ActivityTransitionAnimation
import com.ichi2.anim.ActivityTransitionAnimation.Direction.*
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.dialogs.ConfirmationDialog
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck
Expand All @@ -74,6 +78,7 @@ import com.ichi2.anki.noteeditor.FieldState.FieldChangeType
import com.ichi2.anki.noteeditor.Toolbar
import com.ichi2.anki.noteeditor.Toolbar.TextFormatListener
import com.ichi2.anki.noteeditor.Toolbar.TextWrapper
import com.ichi2.anki.pages.ImageOcclusion
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.receiver.SdCardReceiver
import com.ichi2.anki.servicelayer.LanguageHintService
Expand All @@ -95,6 +100,7 @@ import com.ichi2.libanki.exception.ConfirmModSchemaException
import com.ichi2.utils.*
import com.ichi2.widget.WidgetStatus
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import java.util.*
Expand Down Expand Up @@ -134,6 +140,10 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
private var mCardsButton: AppCompatButton? = null
private var mNoteTypeSpinner: Spinner? = null
private var mDeckSpinnerSelection: DeckSpinnerSelection? = null
private var mImageOcclusionButtonsContainer: LinearLayout? = null
private var mSelectImageForOcclusionButton: Button? = null
private var mEditOcclusionsButton: Button? = null
private var mPasteImaegOcclusionImageButton: Button? = null

// non-null after onCollectionLoaded
private var mEditorNote: Note? = null
Expand Down Expand Up @@ -170,6 +180,8 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
private var mToggleStickyText: HashMap<Int, String?> = HashMap()
private val mOnboarding = Onboarding.NoteEditor(this)

var clipboard: ClipboardManager? = null

private val requestAddLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
NoteEditorActivityResultCallback {
Expand Down Expand Up @@ -240,6 +252,30 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
}
)

private val requestIOEditorLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NoteEditorActivityResultCallback { result ->
if (result.resultCode != RESULT_CANCELED) {
ImportUtils.getFileCachedCopy(this@NoteEditor, result.data!!)?.let { path ->
setupImageOcclusionEditor(path)
}
}
}
)

private val requestIOEditorCloser = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
NoteEditorActivityResultCallback { result ->
if (result.resultCode != RESULT_CANCELED) {
changed = true
if (!addNote) {
mReloadRequired = true
closeNoteEditor(RESULT_UPDATED_IO_NOTE, null)
}
}
}
)

private inner class NoteEditorActivityResultCallback(private val callback: (result: ActivityResult) -> Unit) : ActivityResultCallback<ActivityResult> {
override fun onActivityResult(result: ActivityResult) {
Timber.d("onActivityResult() with result: %s", result.resultCode)
Expand Down Expand Up @@ -403,6 +439,17 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
Timber.i("NoteEditor:: Cards button pressed. Opening template editor")
showCardTemplateEditor()
}
mImageOcclusionButtonsContainer = findViewById(R.id.ImageOcclusionButtonsLayout)
mEditOcclusionsButton = findViewById(R.id.EditOcclusionsButton)
mSelectImageForOcclusionButton = findViewById(R.id.SelectImageForOcclusionButton)
mPasteImaegOcclusionImageButton = findViewById(R.id.PasteImageForOcclusionButton)

try {
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
} catch (e: Exception) {
Timber.w(e)
}

aedictIntent = false
mCurrentEditedCard = null
when (caller) {
Expand Down Expand Up @@ -447,6 +494,46 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
else -> {}
}

launchCatchingTask {
withCol {
addImageOcclusionNotetype()
}
}

if (addNote) {
mEditOcclusionsButton?.visibility = View.GONE
mSelectImageForOcclusionButton?.setOnClickListener {
val i = Intent().apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
launchActivityForResultWithAnimation(Intent.createChooser(i, resources.getString(R.string.choose_an_image)), requestIOEditorLauncher, START)
}
mPasteImaegOcclusionImageButton?.text = TR.notetypesIoPasteImageFromClipboard()
mPasteImaegOcclusionImageButton?.setOnClickListener {
if (ClipboardUtil.hasImage(clipboard)) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Clipboard support is not properly tested yet. I wrote this referencing similar features across the codebase. I've not managed to copy images to the clipboard for some reason (tested copying from Chrome).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just confirmed this is working. But maybe hasImage() should be extended to support all accepted types? https://github.com/ankitects/anki/blob/6f3550464d37aee1b8b784e431cbfce8382d3ce7/rslib/src/image_occlusion/imagedata.rs#L154

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be left as a TODO if you want

val uri = ClipboardUtil.getImageUri(clipboard)
val i = Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
clipData = ClipData.newUri(contentResolver, uri.toString(), uri)
}
ImportUtils.getFileCachedCopy(this, i)?.let { path ->
setupImageOcclusionEditor(path)
}
} else {
showSnackbar(TR.editingNoImageFoundOnClipboard())
}
}
} else {
mSelectImageForOcclusionButton?.visibility = View.GONE
mEditOcclusionsButton?.visibility = View.GONE
mEditOcclusionsButton?.setOnClickListener {
setupImageOcclusionEditor()
}
}

// Note type Selector
mNoteTypeSpinner = findViewById(R.id.note_type_spinner)
mAllModelIds = setupNoteTypeSpinner(this, mNoteTypeSpinner!!, col)
Expand Down Expand Up @@ -848,19 +935,21 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
mCurrentEditedCard!!.did = deckId
modified = true
}
// now load any changes to the fields from the form
for (f in mEditFields!!) {
modified = modified or updateField(f)
}
// added tag?
for (t in mSelectedTags!!) {
modified = modified || !mEditorNote!!.hasTag(t)
}
// removed tag?
modified = modified || mEditorNote!!.tags.size > mSelectedTags!!.size
if (modified) {
mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!))
changed = true
if (!currentNotetypeIsImageOcclusion()) {
// now load any changes to the fields from the form
for (f in mEditFields!!) {
modified = modified or updateField(f)
}
// added tag?
for (t in mSelectedTags!!) {
modified = modified || !mEditorNote!!.hasTag(t)
}
// removed tag?
modified = modified || mEditorNote!!.tags.size > mSelectedTags!!.size
if (modified) {
mEditorNote!!.setTagsFromStr(tagsAsString(mSelectedTags!!))
changed = true
}
}
closeNoteEditor()
}
Expand Down Expand Up @@ -1252,6 +1341,14 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
val editLines = mFieldState.loadFieldEditLines(type)
mFieldsLayoutContainer!!.removeAllViews()
mCustomViewIds.clear()
if (currentNotetypeIsImageOcclusion()) {
setImageOcclusionButton()
return
} else {
mImageOcclusionButtonsContainer?.visibility = View.GONE
mFieldsLayoutContainer?.visibility = View.VISIBLE
}

mEditFields = LinkedList()

var previous: FieldEditLine? = null
Expand Down Expand Up @@ -1926,6 +2023,34 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
val fieldsFromSelectedNote: Array<Array<String>>
get() = mEditorNote!!.items().map { it.requireNoNulls() }.toTypedArray()

private fun currentNotetypeIsImageOcclusion(): Boolean {
println("currentNotetypeIsImageOcclusion: ${currentlySelectedNotetype?.fieldsNames}")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer Timber.d

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(if this is something you feel would be useful moving forward, and not temporary debug info that should be removed)

try {
return currentlySelectedNotetype?.getInt("originalStockKind") == StockNotetype.OriginalStockKind.ORIGINAL_STOCK_KIND_IMAGE_OCCLUSION_VALUE
} catch (j: JSONException) {
return false
}
}

private fun setImageOcclusionButton() {
mImageOcclusionButtonsContainer?.visibility = View.VISIBLE
mFieldsLayoutContainer?.visibility = View.GONE
}

private fun setupImageOcclusionEditor(imagePath: String = "") {
val kind: String
val id: Long
if (addNote) {
kind = "add"
id = 0
} else {
kind = "edit"
id = mEditorNote?.id!!
}
val intent = ImageOcclusion.getIntent(this@NoteEditor, kind, id, imagePath)
launchActivityForResultWithAnimation(intent, requestIOEditorCloser, START)
}

// ----------------------------------------------------------------------------
// INNER CLASSES
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -2192,6 +2317,8 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
const val CALLER_NOTEEDITOR = 8
const val CALLER_NOTEEDITOR_INTENT_ADD = 10

const val RESULT_UPDATED_IO_NOTE = 11

// preferences keys
const val PREF_NOTE_EDITOR_SCROLL_TOOLBAR = "noteEditorScrollToolbar"
private const val PREF_NOTE_EDITOR_SHOW_TOOLBAR = "noteEditorShowToolbar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import com.ichi2.anki.R
import com.ichi2.anki.hideShowButtonCss

class AnkiPackageImporterFragment : PageFragment() {
override val title: Int
get() = R.string.menu_import
override val title: String
get() = resources.getString(R.string.menu_import)
override val pageName: String
get() = "import-page"
override lateinit var webViewClient: PageWebViewClient
Expand Down
23 changes: 23 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@

package com.ichi2.anki.pages

import android.app.Activity
import androidx.fragment.app.FragmentActivity
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.NoteEditor
import com.ichi2.anki.importCsvRaw
import com.ichi2.anki.importJsonFileRaw
import com.ichi2.anki.launchCatchingTask
import com.ichi2.anki.searchInBrowser
import com.ichi2.libanki.*
import com.ichi2.libanki.sched.computeFsrsWeightsRaw
Expand All @@ -30,6 +33,7 @@ import com.ichi2.libanki.sched.evaluateWeightsRaw
import com.ichi2.libanki.stats.*
import fi.iki.elonen.NanoHTTPD
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.io.ByteArrayInputStream
Expand Down Expand Up @@ -93,6 +97,25 @@ open class AnkiServer(
"setWantsAbort" -> CollectionManager.getBackend().setWantsAbortRaw(bytes)
"evaluateWeights" -> withCol { evaluateWeightsRaw(bytes) }
"latestProgress" -> CollectionManager.getBackend().latestProgressRaw(bytes)
"getImageForOcclusion" -> withCol { getImageForOcclusionRaw(bytes) }
"getImageOcclusionNote" -> withCol { getImageOcclusionNoteRaw(bytes) }
"getImageForOcclusionFields" -> withCol { getImageOcclusionFieldsRaw(bytes) }
"addImageOcclusionNote" -> withCol { addImageOcclusionNoteRaw(bytes) }.also {
activity.launchCatchingTask {
// Allow time for toast message to appear before closing editor
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Android toasts persist globally, as a future extension, we can move this to a native call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For context David, this current screen is a 'temporary' one: the desktop editor integrates the I/O editor directly, and this separate screen is provided just for the mobile clients. Hopefully they'll be able to switch to the desktop editor in the future, making this entrypoint obsolete.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dae does AnkiMobile show the toast messages? If so, how is this handled there?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It 'cheats': the screen is closed immediately, and it prints something else:

showPopup(tr.importingNoteAdded(count: 1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may be somewhat related - a crash showing JS dialogs/confirms when AnkiPages tried to open an AlertDialog after the AnkiPages object was destroyed (if my hypothesis there is correct) #14920 - I see some other discussion in this area and I haven't read all comments so this comment should be considered as an isolated bit of context just in case, and it may not be useful

delay(1000)
activity.setResult(Activity.RESULT_OK)
activity.finish()
}
}
"updateImageOcclusionNote" -> withCol { updateImageOcclusionNoteRaw(bytes) }.also {
activity.launchCatchingTask {
// Allow time for toast message to appear before closing editor
delay(1000)
activity.setResult(NoteEditor.RESULT_UPDATED_IO_NOTE)
activity.finish()
}
}
else -> { throw Exception("unhandled request: $methodName") }
}
}
Expand Down
4 changes: 3 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/pages/CardInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import com.ichi2.anki.R
import com.ichi2.libanki.CardId

class CardInfo : PageFragment() {
override val title = R.string.card_info_title
override val title: String
get() = resources.getString(R.string.card_info_title)

override val pageName = "card-info"
override lateinit var webViewClient: PageWebViewClient
override var webChromeClient = PageChromeClient()
Expand Down
4 changes: 3 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/pages/CsvImporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.ichi2.anki.R
* Anki page used to import text/csv files
*/
class CsvImporter : PageFragment() {
override val title = R.string.menu_import
override val title: String
get() = resources.getString(R.string.menu_import)

override val pageName = "import-csv"
override lateinit var webViewClient: PageWebViewClient
override var webChromeClient = PageChromeClient()
Expand Down
3 changes: 2 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class DeckOptions : PageFragment() {
override val title = R.string.menu__deck_options
override val title: String
get() = resources.getString(R.string.menu__deck_options)
override val pageName = "deck-options"
override lateinit var webViewClient: PageWebViewClient
override var webChromeClient = PageChromeClient()
Expand Down
Loading