Skip to content
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

Porting card info #23

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Porting card info #23

wants to merge 7 commits into from

Conversation

krmanik
Copy link
Owner

@krmanik krmanik commented Jun 3, 2022

Pull Request template

Purpose / Description

Trying to port the card info

Fixes

Approach

  1. Setup build and clone Anki repository
sudo apt install bash grep findutils curl gcc g++ git

curl -L https://github.com/bazelbuild/bazelisk/releases/download/v1.10.1/bazelisk-linux-amd64 -o ./bazel
chmod +x bazel && sudo mv bazel /usr/local/bin/

git clone https://github.com/ankitects/anki
  1. Build card info pages in ts
cd anki/ts/card-info
bazel build card-info
  1. Build card info pages in web
cd anki/qt/aqt/data/web
bazel build pages
  1. Build lib
cd anki/qt/aqt/data/lib
bazel build lib
  1. Copy required files to assets dir
cp anki/.bazel/bin/qt/aqt/data/web/pages/card-info.html  \
Anki-Android/AnkiDroid/src/main/assets/pages/card-info.html

cp anki/.bazel/bin/qt/aqt/data/web/pages/card-info.html  \
Anki-Android/AnkiDroid/src/main/assets/pages/card-info.css

cp anki/.bazel/bin/qt/aqt/data/web/pages/card-info.html  \
Anki-Android/AnkiDroid/src/main/assets/pages/card-info.js

How Has This Been Tested?

Currently getting errors, fixing errors
image

Learning (optional, can help others)

Describe the research stage

Links to blog posts, patterns, libraries or addons used to solve this problem

Checklist

Please, go through these checks before submitting the PR.

  • You have not changed whitespace unnecessarily (it makes diffs hard to read)
  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • Your code follows the style of the project (e.g. never omit braces in if statements)
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@dae
Copy link

dae commented Jun 3, 2022

If it works like in iOS, you would modify the shouldIntercept routine to check if the .html/.js file is requested, and return the data/content type in the response of that function. If file:// is causing issues, you could try a custom scheme like ankischeme://card-info.html. You'd tell the webview to load that URL, and then should be able to feed it data via shouldIntercept. When shouldIntercept receives the request for i18nResources, it needs to feed the received data to the i18n_resources method, and return the result.

Also note you probably can't just copy the js from the latest desktop version, as AnkiDroid is using an older backend (something like Anki 2.1.35?). The frontend html/js should match the version of the backend being used. And if David has not exposed i18nResources in the separate ankidroid backend module, it would need to be updated to make that method available before you'd be able to call it in AnkiDroid.

@krmanik
Copy link
Owner Author

krmanik commented Jun 3, 2022

Thanks, I will check and try to push some updates to backend or wait till it gets updated.

@dae
Copy link

dae commented Jun 4, 2022

This is how you might start on it:

diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
index 7eb988d67..ed308b3ed 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt
@@ -387,8 +387,14 @@ open class Reviewer : AbstractFlashcardViewer() {
                 showResetCardDialog()
             }
             R.id.action_mark_card -> {
-                Timber.i("Reviewer:: Mark button pressed")
-                onMark(mCurrentCard)
+                Timber.i("Card Viewer:: Card Info")
+                // openCardInfo()
+                val intent = Intent(this, AnkiWebview::class.java)
+                intent.putExtra("cardId", mCurrentCard!!.id)
+                startActivityWithoutAnimation(intent)
+// access card info screen via 'mark card' action, since card info is not available by default
+//                Timber.i("Reviewer:: Mark button pressed")
+//                onMark(mCurrentCard)
             }
             R.id.action_replay -> {
                 Timber.i("Reviewer:: Replay audio button pressed (from menu)")
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiWebview.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiWebview.kt
index 95e15f1a5..f4b9ae6a7 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiWebview.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiWebview.kt
@@ -3,8 +3,14 @@ package com.ichi2.anki.pages
 import android.os.Bundle
 import android.webkit.*
 import com.ichi2.anki.AnkiActivity
+import com.ichi2.anki.AnkiDroidApp
 import com.ichi2.anki.R
+import com.ichi2.libanki.Collection
+import com.ichi2.libanki.backend.RustDroidBackend
+import com.ichi2.libanki.backend.RustDroidV16Backend
 import timber.log.Timber
+import java.io.ByteArrayInputStream
+import java.io.InputStream
 
 class AnkiWebview : AnkiActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -12,13 +18,14 @@ class AnkiWebview : AnkiActivity() {
         setContentView(R.layout.anki_webview)
         val webview: WebView = findViewById(R.id.anki_wb)
         webview.settings.javaScriptEnabled = true
-        webview.settings.allowFileAccess = true
         webview.webChromeClient = WebChromeClient()
-        webview.webViewClient = AnkiWebChromeClient()
-        webview.loadUrl("file:///android_asset/pages/card-info.html")
+        // passing col in here is presumably the wrong way to do this
+        webview.webViewClient = AnkiWebChromeClient(col)
+        webview.loadUrl("https://127.0.0.1/card-info.html")
     }
 
-    class AnkiWebChromeClient : WebViewClient() {
+    class AnkiWebChromeClient(val col: Collection) : WebViewClient() {
+
         override fun onLoadResource(view: WebView?, url: String?) {
             if (url.equals("file:///_anki/i18nResources")) {
                 view?.loadUrl("file:///android_asset/i18nResources")
@@ -30,8 +37,32 @@ class AnkiWebview : AnkiActivity() {
             view: WebView?,
             request: WebResourceRequest?
         ): WebResourceResponse? {
-            Timber.d("request %d", request?.method)
-            return super.shouldInterceptRequest(view, request)
+            val streamResponse = { data: InputStream, mime: String ->
+                WebResourceResponse(
+                    mime, "utf-8", 200, "OK",
+                    HashMap(), data
+                )
+            }
+            val fileResponse = { path: String, mime: String ->
+                view?.context?.assets?.open("pages$path")?.let { streamResponse(it, mime) }
+            }
+
+            return request?.url?.let { url ->
+                Timber.i("************** request %s", url)
+                when (val path = url.path) {
+                    "/card-info.html" -> fileResponse(path, "text/html")
+                    "/card-info.css" -> fileResponse(path, "text/css")
+                    "/card-info.js" -> fileResponse(path, "text/javascript")
+                    "/_anki/i18nResources" -> {
+                        val data = ByteArrayInputStream(col.backend.i18nResources().toByteArray())
+                        streamResponse(data, "application/binary")
+                    }
+                    else -> {
+                        Timber.i("************** ignore %s", url.path)
+                        null
+                    }
+                }
+            } ?: super.shouldInterceptRequest(view, request)
         }
     }
 }
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.kt
index df84e952c..3d58b6197 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.kt
@@ -19,6 +19,7 @@ import BackendProto.Backend.ExtractAVTagsOut
 import BackendProto.Backend.RenderCardOut
 import android.content.Context
 import androidx.annotation.VisibleForTesting
+import com.google.protobuf.ByteString
 import com.ichi2.libanki.Collection
 import com.ichi2.libanki.DB
 import com.ichi2.libanki.DeckConfig
@@ -85,4 +86,7 @@ interface DroidBackend {
 
     @Throws(BackendNotSupportedException::class)
     fun renderCardForTemplateManager(templateRenderContext: TemplateRenderContext): RenderCardOut
+
+    @Throws(BackendNotSupportedException::class)
+    fun i18nResources(): ByteString
 }
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java
index 8e9cd11d9..4f0bcbae9 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java
@@ -18,6 +18,7 @@ package com.ichi2.libanki.backend;
 
 import android.content.Context;
 
+import com.google.protobuf.ByteString;
 import com.ichi2.libanki.Collection;
 import com.ichi2.libanki.DB;
 import com.ichi2.libanki.TemplateManager;
@@ -108,4 +109,11 @@ public class JavaDroidBackend implements DroidBackend {
     public @NonNull Backend.RenderCardOut renderCardForTemplateManager(@NonNull TemplateManager.TemplateRenderContext templateRenderContext) throws BackendNotSupportedException {
         throw new BackendNotSupportedException();
     }
+
+
+    @NonNull
+    @Override
+    public ByteString i18nResources() throws BackendNotSupportedException {
+        throw new BackendNotSupportedException();
+    }
 }
diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.kt
index 7bf98cd3a..072e98026 100644
--- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.kt
@@ -19,6 +19,7 @@ package com.ichi2.libanki.backend
 import BackendProto.Backend.ExtractAVTagsOut
 import BackendProto.Backend.RenderCardOut
 import android.content.Context
+import com.google.protobuf.ByteString
 import com.ichi2.libanki.Collection
 import com.ichi2.libanki.DB
 import com.ichi2.libanki.TemplateManager.TemplateRenderContext
@@ -94,6 +95,8 @@ open class RustDroidBackend(
     override fun renderCardForTemplateManager(templateRenderContext: TemplateRenderContext): RenderCardOut {
         throw BackendNotSupportedException()
     }
+    
+    override fun i18nResources() = backend.backend.i18nResources().json
 
     companion object {
         const val UNUSED_VALUE = 0

It won't work at the moment though, because the backend is on 2.1.34, and the Svelte card info screen was introduced in 2.1.50.

A custom scheme proved problematic - fetch() didn't support it, and relative links were not resolved. So we use standard https URLs, but intercept the requests so a server is not required.

@krmanik
Copy link
Owner Author

krmanik commented Jun 4, 2022

Thanks, It will help greatly. I will test it now. Thanks again.

It won't work at the moment though, because the backend is on 2.1.34, and the Svelte card info screen was introduced in 2.1.50.

After the backend updates the following errors seems to be resolved. Also I will try/build JS files on Anki 2.1.34 branch and use the backend for testing (graphs.html).
image

A custom scheme proved problematic - fetch() didn't support it, and relative links were not resolved. So we use standard https URLs, but intercept the requests so a server is not required.

The implementation in the patch really helped in serving the files without a server. Thanks

@krmanik
Copy link
Owner Author

krmanik commented Jun 4, 2022

For graphs.html, it worked. I have added backend code for graphData. I will refine this PR for graphs and push to AnkiDroid.

One questions for this line, I passed empty string and 100 for days. What will be the possible parameters? (I will check code to understand the required params)

override fun graphData(): ByteString = backend.backend.graphs("", 100).toByteString()

@dae
Copy link

dae commented Jun 4, 2022

In that version of Anki, those two params (configured via the bar at the top) were sent in the POST body as json, so shouldIntercept() will need to extract the JSON from the request, and then pass the two values into graphData().

@krmanik
Copy link
Owner Author

krmanik commented Jun 4, 2022

Thanks, I understood it, I try it.
Now it needs to handle preferences for night mode and language, which will also needs to implement similar to i18n and graphData. I will check the Anki codebase for implementation idea.

@dae
Copy link

dae commented Jun 5, 2022

The language is currently hard-coded to English in backendv1impl:ensureBackend(). Night mode is set by requesting graphs.html#night instead of just graphs.html

@krmanik
Copy link
Owner Author

krmanik commented Jun 5, 2022

Thanks, I understood this.

dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 21, 2022
dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 25, 2022
dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 25, 2022
dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 28, 2022
dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 28, 2022
dae added a commit to ankitects/Anki-Android that referenced this pull request Jun 28, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankitects/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankitects/Anki-Android that referenced this pull request Jun 29, 2022
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Jun 29, 2022
streamResponse(data, "application/binary")
}
"/_anki/graphs" -> {
val data = ByteArrayInputStream(col.backend.graphs("deck:current", 365).toByteArray())
Copy link

Choose a reason for hiding this comment

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

You should be able to use graphsRaw() now, and pass it the bytes from the request body.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks, I am implementing it.

streamResponse(data, "application/binary")
}
"/_anki/getGraphPreferences" -> {
val data = ByteArrayInputStream(col.backend.getGraphPreferences().toByteArray())
Copy link

Choose a reason for hiding this comment

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

getGraphPreferencesRaw()

"/graphs.js" -> fileResponse(path, "text/javascript")
"/_anki/i18nResources" -> {
val byteArray: ByteArray = "en-US".toByteArray()
val data = ByteArrayInputStream(col.backend.i18nResourcesRaw(byteArray))
Copy link

Choose a reason for hiding this comment

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

Pass i18nResourcesRaw the bytes from the request body

Copy link

Choose a reason for hiding this comment

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

Also, all should be col.xxx now, not col.backend.xxx

Copy link

Choose a reason for hiding this comment

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

col.newBackend will give you a CollectionV16 which should have those properties

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks

@krmanik
Copy link
Owner Author

krmanik commented Jun 30, 2022

After building aar files with web pages https://github.com/krmanik/Anki-Android-Backend/pull/1. I have updated the current PR with new changes to backend. Going through anki codebase I tried to figure out the input and added the input where required when request made from pages to backend. (I have used V16 backend in AnkiDroid->Settings to make it works because earlier version does not contains tables.)

The graphs page assets loaded from aar files stored in web directory.

This is result of graphs stats, but I didn't understand args for i18nResourcesRaw.

@mikehardy
Copy link

it does seem like the local webserver is the way to go, to be clear, I'm not against that, I appreciate doing quick proof-of-concepts (of, lack-of-proof-of-concepts? 😅 ) on the other styles to quickly demonstrate why localhost appears best

@krmanik
Copy link
Owner Author

krmanik commented Jul 1, 2022

it does seem like the local webserver is the way to go, to be clear, I'm not against that, I appreciate doing quick proof-of-concepts (of, lack-of-proof-of-concepts? 😅 ) on the other styles to quickly demonstrate why localhost appears best

I will create PR for it to upstream when implementation complete.

@dae
Copy link

dae commented Jul 2, 2022

Edit: It is implemented on col.backend

To keep things neat, GUI code should avoid calling the backend directly, so we'll need to add two small methods to BackendImportExport.kt that pass the request on to the backend, like is done in BackendStats.kt

@dae
Copy link

dae commented Jul 2, 2022

But the pages are not loading.

If you look at the desktop code, it calls this after the document has finished loading:

self.web.eval(f"anki.setupImportCsvPage('{path}');")

@krmanik
Copy link
Owner Author

krmanik commented Jul 2, 2022

To keep things neat, GUI code should avoid calling the backend directly, so we'll need to add two small methods to BackendImportExport.kt that pass the request on to the backend, like is done in BackendStats.kt

I have added code for it in libanki.

If you look at the desktop code, it calls this after the document has finished loading:

self.web.eval(f"anki.setupImportCsvPage('{path}');")

Thanks, In AnkiDroid side, due to scope storage policy extra work needed to make it work for getting path from file picker and passing it to webview.

@krmanik
Copy link
Owner Author

krmanik commented Jul 3, 2022

For testing, I used fix path in onPageFinished and created test.csv file in cache dir. It is working as expected. All the errors are resolved for import tsv.
Thanks

anki.setupImportCsvPage('/data/user/0/com.ichi2.anki/cache/test.csv')

image

dorrin-sot pushed a commit to dorrin-sot/Anki-Android that referenced this pull request Jul 14, 2022
@krmanik
Copy link
Owner Author

krmanik commented Jul 18, 2022

What will be the approach for changing i18n languages?

I checked the code in utils.ts, lang is taken from returned i18n resources.

export async function setupI18n(args: { modules: ModuleName[] }): Promise<void> {
    const resources = await i18n.i18nResources(I18n.I18nResourcesRequest.create(args));
    const json = JSON.parse(new TextDecoder().decode(resources.json));

    const newBundles: FluentBundle[] = [];
    for (const res in json.resources) {
        const text = json.resources[res];
        const lang = json.langs[res];
        const bundle = new FluentBundle([lang, "en-US"]);
        const resource = new FluentResource(text);
        bundle.addResource(resource);
        newBundles.push(bundle);
    }

    setBundles(newBundles);
    langs = json.langs;

    document.dir = direction();
}

@dae
Copy link

dae commented Jul 18, 2022

The active language is set at AnkiDroid startup (updateContextWithLanguage), and the TS code will use whichever language is currently active.

@krmanik
Copy link
Owner Author

krmanik commented Jul 19, 2022

These screen are ported graphs, card-info and import-csv. The only issues I am trying solve is closing the webview when import finished.

In Anki codebase the do_import method import then dismiss the dialog but in case of webview activity, should JS interface be used or are there any other option to know about the import finished? (May be another post request should be sent when import finished)

import_csv_dialog.py

    def do_import(self, data: bytes) -> None:
        request = ImportCsvRequest()
        request.ParseFromString(data)
        self._on_accepted(request)
        super().reject()

In current AnkiDroid codebase I have done following

val csvIntent = Intent(context, AnkiPagesWebview::class.java)
csvIntent.putExtra("web_page", "import-csv")
csvIntent.putExtra("csv_path", tempOutDir)
val activity = context as AnkiActivity
activity.startActivityForResultWithoutAnimation(csvIntent, PICK_TEXT_FILE)

class AnkiWebChromeClient(private val webPageName: String, private val cardId: Long, private val path: String) : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (webPageName == "import-csv") {
view?.evaluateJavascript("anki.setupImportCsvPage('$path');", null)
}

@dae
Copy link

dae commented Jul 19, 2022

The way I handle this in AnkiMobile is to have each screen subclass a base handler. Each page/subclass declares the endpoints it requires, and can handle special behavior like closing the displayed page.

@krmanik
Copy link
Owner Author

krmanik commented Jul 19, 2022

The way I handle this in AnkiMobile is to have each screen subclass a base handler. Each page/subclass declares the endpoints it requires, and can handle special behavior like closing the displayed page.

I will try to implement the handler class.

BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 19, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 19, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 19, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 19, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 20, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 20, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
BrayanDSO added a commit to BrayanDSO/Anki-Android that referenced this pull request Aug 21, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
mikehardy pushed a commit to ankidroid/Anki-Android that referenced this pull request Aug 25, 2022
This introduces a dependency to NanoHTTPD, a java server library

Used krmanik#23 as base of this commit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants