diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml
index c051cf5a9d90..10ab72e2a15a 100644
--- a/AnkiDroid/src/main/AndroidManifest.xml
+++ b/AnkiDroid/src/main/AndroidManifest.xml
@@ -463,6 +463,10 @@
android:theme="@style/Theme.AppCompat.NoActionBar"
android:configChanges="keyboardHidden|screenSize" />
+
+
{
+ Timber.i("Navigating to addons")
+ val intent = Intent(this, AddonsBrowserActivity::class.java)
+ startActivityWithAnimation(intent, ActivityTransitionAnimation.Direction.START)
+ }
+
R.id.nav_settings -> {
Timber.i("Navigating to settings")
openSettings()
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt
index f65111d33f24..65e03f6cd843 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt
@@ -52,10 +52,11 @@ class AddonData(
val author: Map? = null,
val license: String? = null,
val homepage: String? = null,
- val dist: DistInfo? = null
+ val dist: String? = null
)
@Serializable
+// Note: used in the original code as a type for the AddonData.dist property
data class DistInfo(val tarball: String)
/**
@@ -151,7 +152,9 @@ fun getAddonModelFromAddonData(addonData: AddonData): Pair *
+ * *
+ * *
+ * 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 . *
+ ****************************************************************************************/
+
+package com.ichi2.anki.jsaddons
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.BundleCompat
+import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import com.ichi2.anki.R
+import com.ichi2.utils.cancelable
+import com.ichi2.utils.create
+import com.ichi2.utils.customView
+import com.ichi2.utils.positiveButton
+
+/**
+ * Shows all available details for the addon identified by the [AddonModel] passed as an argument.
+ */
+class AddonDetailsDialog : DialogFragment() {
+
+ override fun onStart() {
+ super.onStart()
+ dialog?.window?.setLayout(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val addonModel =
+ BundleCompat.getParcelable(requireArguments(), KEY_ADDON_MODEL, AddonModel::class.java)
+ ?: error("No addon identifier was provided!")
+ val contentView =
+ requireActivity().layoutInflater.inflate(R.layout.dialog_addon_details, null).apply {
+ findViewById(R.id.addon_name).text = addonModel.name
+ findViewById(R.id.addon_description).text = addonModel.description
+ findViewById(R.id.addon_type).text = addonModel.addonType
+ findViewById(R.id.addon_author).text = addonModel.author["name"]
+ findViewById(R.id.addon_version).text = addonModel.version
+ findViewById(R.id.addon_js_api_version).text = addonModel.ankidroidJsApi
+ findViewById(R.id.addon_license).text = addonModel.license
+ findViewById(R.id.addon_homepage).text = addonModel.homepage
+ }
+ return AlertDialog.Builder(requireContext()).create {
+ customView(contentView)
+ cancelable(true)
+ positiveButton(R.string.close)
+ }
+ }
+
+ companion object {
+ private const val KEY_ADDON_MODEL = "key_addon_model"
+
+ fun newInstance(addonModel: AddonModel): AddonDetailsDialog = AddonDetailsDialog().apply {
+ arguments = bundleOf(KEY_ADDON_MODEL to addonModel)
+ }
+ }
+}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonModel.kt
index 7f6b641a7431..d23f88615276 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonModel.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonModel.kt
@@ -17,8 +17,11 @@
package com.ichi2.anki.jsaddons
import android.content.SharedPreferences
+import android.os.Parcelable
import androidx.core.content.edit
+import kotlinx.parcelize.Parcelize
+@Parcelize
data class AddonModel(
val name: String,
val addonTitle: String,
@@ -32,8 +35,8 @@ data class AddonModel(
val author: Map,
val license: String,
val homepage: String,
- val dist: DistInfo
-) {
+ val dist: String
+) : Parcelable {
/**
* Update preferences for addons with boolean remove, the preferences will be used to store the information about
* enabled and disabled addon. So, that other method will return content of script to reviewer or note editor
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonStorage.kt b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonStorage.kt
new file mode 100644
index 000000000000..4d24e1e4eb97
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonStorage.kt
@@ -0,0 +1,77 @@
+/****************************************************************************************
+ * Copyright (c) 2022 Mani *
+ * *
+ * *
+ * 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 . *
+ ***************************************************************************************/
+
+package com.ichi2.anki.jsaddons
+
+import android.content.Context
+import com.ichi2.anki.BackupManager
+import com.ichi2.anki.CollectionHelper
+import com.ichi2.annotations.NeedsTest
+import java.io.File
+
+/**
+ * Implemented functions for getting addons related directory for current profile
+ */
+@NeedsTest("Addons directory test")
+class AddonStorage(val context: Context) {
+ private val currentAnkiDroidDirectory = CollectionHelper.getCurrentAnkiDroidDirectory(context)
+ private val addonsHomeDir = File(currentAnkiDroidDirectory, "addons")
+
+ /**
+ * Get addons directory for current profile
+ * e.g. AnkiDroid/addons/
+ */
+ fun getCurrentProfileAddonDir(): File {
+ if (!addonsHomeDir.exists()) {
+ addonsHomeDir.mkdirs()
+ }
+ return addonsHomeDir
+ }
+
+ /**
+ * Get addon's directory which contains packages and index.js files
+ * e.g. AnkiDroid/addons/some-addon/
+ *
+ * @param addonName
+ * @return some-addon dir e.g. AnkiDroid/addons/some-addon/
+ */
+ fun getSelectedAddonDir(addonName: String): File {
+ return File(addonsHomeDir, addonName)
+ }
+
+ /**
+ * Get package.json for selected addons
+ * e.g. AnkiDroid/addons/some-addon/package/package.json
+ *
+ * @param addonName
+ * @return package.json file
+ */
+ fun getSelectedAddonPackageJson(addonName: String): File {
+ val addonPath = getSelectedAddonDir(addonName)
+ return File(addonPath, "package/package.json")
+ }
+
+ /**
+ * Remove selected addon in list view from addons directory
+ *
+ * @param addonName
+ */
+ fun deleteSelectedAddonPackageDir(addonName: String): Boolean {
+ val dir = getSelectedAddonDir(addonName)
+ return BackupManager.removeDir(dir)
+ }
+}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonsBrowserActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonsBrowserActivity.kt
new file mode 100644
index 000000000000..2658d9618991
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonsBrowserActivity.kt
@@ -0,0 +1,214 @@
+/****************************************************************************************
+ * Copyright (c) 2022 Mani *
+ * *
+ * 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 . *
+ ***************************************************************************************/
+
+package com.ichi2.anki.jsaddons
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.ichi2.anki.AnkiActivity
+import com.ichi2.anki.CollectionManager.TR
+import com.ichi2.anki.R
+import com.ichi2.anki.dialogs.ConfirmationDialog
+import com.ichi2.anki.preferences.sharedPrefs
+import com.ichi2.anki.showThemedToast
+import timber.log.Timber
+import java.io.File
+import java.io.IOException
+
+/**
+ * Shows the addons available in the app along with actions to fetch and remove other addons.
+ */
+// TODO ===== EXTRACT RELATED STRINGS FOR TRANSLATION BEFORE RELEASE =====
+class AddonsBrowserActivity : AnkiActivity() {
+ private val addonsAdapter by lazy {
+ AddonsBrowserAdapter(
+ context = this@AddonsBrowserActivity,
+ onToggleAddon = ::handleAddonModelToggled,
+ onDetailsRequested = { addonModel ->
+ showDialogFragment(this, AddonDetailsDialog.newInstance(addonModel))
+ },
+ onDeleteAddon = ::handleAddonDeletion
+ )
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ if (showedActivityFailedScreen(savedInstanceState)) {
+ return
+ }
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_addons_browser)
+
+ enableToolbar().apply {
+ // Add a home button to the actionbar
+ setHomeButtonEnabled(true)
+ setDisplayHomeAsUpEnabled(true)
+ title = TR.addonsWindowTitle()
+ }
+
+ findViewById(R.id.addons_list).apply {
+ adapter = addonsAdapter
+ }
+ findViewByIdnewReviewernewReviewerOptions
+ newAddonsScreendevOptionsEnabledByUserworkInProgressDevOptions
diff --git a/AnkiDroid/src/main/res/xml/preferences_dev_options.xml b/AnkiDroid/src/main/res/xml/preferences_dev_options.xml
index 9db550cda1e8..f0efeb24e605 100644
--- a/AnkiDroid/src/main/res/xml/preferences_dev_options.xml
+++ b/AnkiDroid/src/main/res/xml/preferences_dev_options.xml
@@ -85,6 +85,10 @@
android:key="@string/new_reviewer_options_key"
android:dependency="@string/new_reviewer_pref_key"
android:fragment="com.ichi2.anki.preferences.ReviewerOptionsFragment"/>
-
+
+
\ No newline at end of file
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
index a84771084bf7..81c477ad2af3 100644
--- a/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/jsaddons/AddonModelTest.kt
@@ -125,11 +125,11 @@ class AddonModelTest : RobolectricTest() {
// first addon name and tgz download url
val addon1 = result.first[0]
assertEquals(addon1.name, "ankidroid-js-addon-progress-bar")
- assertThat(addon1.dist.tarball, endsWith(".tgz"))
+ assertThat(addon1.dist, endsWith(".tgz"))
// second addon name and tgz download url
val addon2 = result.first[1]
assertEquals(addon2.name, "valid-ankidroid-js-addon-test")
- assertThat(addon2.dist.tarball, endsWith(".tgz"))
+ assertThat(addon2.dist, endsWith(".tgz"))
}
}
diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
index 061daa079bb9..97212b4e2b64 100644
--- a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
+++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt
@@ -39,6 +39,7 @@ import com.ichi2.anki.SharedDecksActivity
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.StudyOptionsActivity
import com.ichi2.anki.instantnoteeditor.InstantNoteEditorActivity
+import com.ichi2.anki.jsaddons.AddonsBrowserActivity
import com.ichi2.anki.multimedia.MultimediaActivity
import com.ichi2.anki.notetype.ManageNotetypes
import com.ichi2.anki.preferences.Preferences
@@ -92,7 +93,8 @@ object ActivityList {
get(InstantNoteEditorActivity::class.java),
get(MultimediaActivity::class.java),
get(DeckPickerWidgetConfig::class.java),
- get(CardAnalysisWidgetConfig::class.java)
+ get(CardAnalysisWidgetConfig::class.java),
+ get(AddonsBrowserActivity::class.java)
)
}