-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Integrate image occlusion editor #14884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
0a794d6
8b58483
474b077
8e084e4
d10c1e3
834ec12
1fcaf64
d8027cd
8facba1
ba94433
47b1247
c09efa4
2014063
385f59e
6ba377f
d92941d
c2fd7f0
e3615ff
d1de5e9
84ba4b7
3e2047e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -94,6 +99,7 @@ import com.ichi2.libanki.Notetypes.Companion.NOT_FOUND_NOTE_TYPE | |
| 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.* | ||
|
|
@@ -133,6 +139,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 | ||
|
|
@@ -169,6 +179,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 { | ||
|
|
@@ -239,6 +251,29 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags | |
| } | ||
| ) | ||
|
|
||
| private val ioEditorLauncher = registerForActivityResult( | ||
| ActivityResultContracts.GetContent() | ||
| ) { uri -> | ||
| if (uri !== null) { | ||
BrayanDSO marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ImportUtils.getFileCachedCopy(this@NoteEditor, uri)?.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) | ||
|
|
@@ -402,6 +437,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) { | ||
|
|
@@ -446,6 +492,42 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags | |
| else -> {} | ||
| } | ||
|
|
||
| launchCatchingTask { | ||
| withCol { | ||
| addImageOcclusionNotetype() | ||
david-allison marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (addNote) { | ||
| mEditOcclusionsButton?.visibility = View.GONE | ||
| mSelectImageForOcclusionButton?.setOnClickListener { | ||
| ioEditorLauncher.launch("image/*") | ||
| } | ||
| mPasteImaegOcclusionImageButton?.text = TR.notetypesIoPasteImageFromClipboard() | ||
| mPasteImaegOcclusionImageButton?.setOnClickListener { | ||
| if (ClipboardUtil.hasImage(clipboard)) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| mPasteImaegOcclusionImageButton?.visibility = View.GONE | ||
| mEditOcclusionsButton?.visibility = View.VISIBLE | ||
| mEditOcclusionsButton?.text = resources.getString(R.string.edit_occlusions) | ||
| mEditOcclusionsButton?.setOnClickListener { | ||
| setupImageOcclusionEditor() | ||
| } | ||
| } | ||
|
|
||
| // Note type Selector | ||
| mNoteTypeSpinner = findViewById(R.id.note_type_spinner) | ||
| mAllModelIds = setupNoteTypeSpinner(this, mNoteTypeSpinner!!, col) | ||
|
|
@@ -845,19 +927,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() | ||
| } | ||
|
|
@@ -1224,6 +1308,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 | ||
|
|
@@ -1897,6 +1989,33 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags | |
| val fieldsFromSelectedNote: Array<Array<String>> | ||
| get() = mEditorNote!!.items() | ||
|
|
||
| private fun currentNotetypeIsImageOcclusion(): Boolean { | ||
| 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) | ||
BrayanDSO marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| // INNER CLASSES | ||
| // ---------------------------------------------------------------------------- | ||
|
|
@@ -2163,6 +2282,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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,11 +17,15 @@ | |
|
|
||
| package com.ichi2.anki.pages | ||
|
|
||
| import android.app.Activity | ||
| import androidx.fragment.app.FragmentActivity | ||
| import anki.collection.OpChanges | ||
| 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 | ||
|
|
@@ -30,6 +34,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 | ||
|
|
@@ -93,6 +98,35 @@ 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" -> { | ||
| val data = withCol { | ||
| addImageOcclusionNoteRaw(bytes) | ||
| } | ||
| undoableOp { OpChanges.parseFrom(data) } | ||
| activity.launchCatchingTask { | ||
| // Allow time for toast message to appear before closing editor | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It 'cheats': the screen is closed immediately, and it prints something else:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| } | ||
| data | ||
| } | ||
| "updateImageOcclusionNote" -> { | ||
| val data = withCol { | ||
| updateImageOcclusionNoteRaw(bytes) | ||
| } | ||
| undoableOp { OpChanges.parseFrom(data) } | ||
| activity.launchCatchingTask { | ||
| // Allow time for toast message to appear before closing editor | ||
| delay(1000) | ||
| activity.setResult(NoteEditor.RESULT_UPDATED_IO_NOTE) | ||
| activity.finish() | ||
| } | ||
| data | ||
| } | ||
| "congratsInfo" -> withCol { congratsInfoRaw(bytes) } | ||
| else -> { throw Exception("unhandled request: $methodName") } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.