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..e262cb0cf39f 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/jsaddons/AddonData.kt
@@ -151,6 +151,8 @@ 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..e3360ce595a3 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,15 @@
package com.ichi2.anki.jsaddons
import android.content.SharedPreferences
+import android.os.Parcel
+import android.os.Parcelable
import androidx.core.content.edit
+import kotlinx.parcelize.Parceler
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.TypeParceler
+@Parcelize
+@TypeParceler
data class AddonModel(
val name: String,
val addonTitle: String,
@@ -33,7 +40,7 @@ data class AddonModel(
val license: String,
val homepage: String,
val dist: DistInfo
-) {
+) : 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
@@ -60,3 +67,12 @@ data class AddonModel(
preferences.edit { putStringSet(jsAddonKey, newStrSet) }
}
}
+
+class DistParceler : Parceler {
+
+ override fun DistInfo.write(parcel: Parcel, flags: Int) {
+ parcel.writeString(tarball)
+ }
+
+ override fun create(parcel: Parcel): DistInfo = DistInfo(parcel.readString() ?: "")
+}
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..3e900c990d70
--- /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
+ }
+ findViewById