addon store initial
This commit is contained in:
+353
-18
@@ -13,11 +13,16 @@ import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonDisabledReason
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonIncognito
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonListing
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonListingPreview
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonStoreApp
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonStoreInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonStorePromoted
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateAttemptInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateStatus
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
|
||||
import org.json.JSONArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -35,6 +40,7 @@ import mozilla.components.feature.addons.ui.displayName
|
||||
import mozilla.components.feature.addons.ui.summary
|
||||
import mozilla.components.feature.addons.ui.translateDescription
|
||||
import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED
|
||||
import java.util.Locale
|
||||
import org.json.JSONObject
|
||||
|
||||
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
@@ -56,6 +62,23 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
"Update downloaded and will be applied after restarting the app."
|
||||
private const val DEFAULT_AMO_SERVER_URL = "https://addons.mozilla.org"
|
||||
private const val PERIODIC_UPDATE_RESTORE_DELAY_MS = 10_000L
|
||||
private const val STORE_INFO_CACHE_TTL_MS = 30L * 60L * 1000L
|
||||
}
|
||||
|
||||
private data class StoreInfoCacheEntry(val value: AddonStoreInfo, val timestampMs: Long)
|
||||
private val storeInfoCache = java.util.concurrent.ConcurrentHashMap<String, StoreInfoCacheEntry>()
|
||||
|
||||
private fun cachedStoreInfo(addonId: String): AddonStoreInfo? {
|
||||
val entry = storeInfoCache[addonId] ?: return null
|
||||
if (System.currentTimeMillis() - entry.timestampMs > STORE_INFO_CACHE_TTL_MS) {
|
||||
storeInfoCache.remove(addonId)
|
||||
return null
|
||||
}
|
||||
return entry.value
|
||||
}
|
||||
|
||||
private fun cacheStoreInfo(addonId: String, info: AddonStoreInfo) {
|
||||
storeInfoCache[addonId] = StoreInfoCacheEntry(info, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override fun getAddons(allowCache: Boolean, callback: (Result<List<AddonInfo>>) -> Unit) {
|
||||
@@ -84,12 +107,25 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val installedAddon = components.core.addonManager.getAddonByID(addonId)
|
||||
(installedAddon ?: components.core.addonManager.getAddons(allowCache = allowCache)
|
||||
.find { it.id == addonId })?.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addonId),
|
||||
isLocalFileInstalled = isLocalFileInstalledAddon(addonId),
|
||||
)
|
||||
val resolved = installedAddon ?: components.core.addonManager.getAddons(
|
||||
allowCache = allowCache,
|
||||
).find { it.id == addonId }
|
||||
|
||||
val isLocalFile = isLocalFileInstalledAddon(addonId)
|
||||
val storeInfo = if (resolved != null && resolved.isInstalled() && !isLocalFile &&
|
||||
resolved.needsAmoEnrichment()
|
||||
) {
|
||||
runCatching { fetchAddonStoreInfo(addonId) }.getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
resolved?.toPigeon(
|
||||
context = context,
|
||||
isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addonId),
|
||||
isLocalFileInstalled = isLocalFile,
|
||||
storeInfo = storeInfo,
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
@@ -108,6 +144,51 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAddonListings(
|
||||
query: String,
|
||||
app: AddonStoreApp,
|
||||
page: Long,
|
||||
pageSize: Long,
|
||||
callback: (Result<List<AddonListing>>) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
fetchAddonListings(
|
||||
query = query.ifBlank { null },
|
||||
app = app,
|
||||
page = page.toInt().coerceAtLeast(1),
|
||||
pageSize = pageSize.toInt().coerceIn(1, 50),
|
||||
sort = if (query.isBlank()) "users" else null,
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFeaturedAddonListings(
|
||||
app: AddonStoreApp,
|
||||
pageSize: Long,
|
||||
callback: (Result<List<AddonListing>>) -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
fetchAddonListings(
|
||||
query = null,
|
||||
app = app,
|
||||
page = 1,
|
||||
pageSize = pageSize.toInt().coerceIn(1, 50),
|
||||
sort = "users",
|
||||
promoted = "recommended",
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
@@ -394,6 +475,8 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
}
|
||||
|
||||
private suspend fun fetchAddonStoreInfo(addonId: String): AddonStoreInfo? {
|
||||
cachedStoreInfo(addonId)?.let { return it }
|
||||
|
||||
val response = components.core.client.fetch(
|
||||
Request(
|
||||
url = addonStoreInfoUrl(addonId),
|
||||
@@ -416,15 +499,205 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
return null
|
||||
}
|
||||
|
||||
return AddonStoreInfo(
|
||||
val language = Locale.getDefault().language
|
||||
val ratings = json.optJSONObject("ratings")
|
||||
val ratingAverage = ratings?.optDouble("average")?.takeIf { !it.isNaN() && it > 0.0 }
|
||||
val ratingReviews = ratings?.optInt("text_count", -1)?.takeIf { it >= 0 }?.toLong()
|
||||
val firstAuthor = json.optJSONArray("authors")?.optJSONObject(0)
|
||||
|
||||
val storeInfo = AddonStoreInfo(
|
||||
latestVersion = latestVersion,
|
||||
latestXpiUrl = latestXpiUrl,
|
||||
ratingAverage = ratingAverage,
|
||||
ratingReviews = ratingReviews,
|
||||
summary = json.pickTranslation("summary", language),
|
||||
description = json.pickTranslation("description", language),
|
||||
homepageUrl = json.pickHomepage(language)
|
||||
?: json.optString("url").takeIf { it.isNotBlank() },
|
||||
detailUrl = json.optString("url").takeIf { it.isNotBlank() },
|
||||
ratingUrl = json.optString("ratings_url").takeIf { it.isNotBlank() },
|
||||
authorName = firstAuthor?.optString("name")?.takeIf { it.isNotBlank() },
|
||||
authorUrl = firstAuthor?.optString("url")?.takeIf { it.isNotBlank() },
|
||||
)
|
||||
cacheStoreInfo(addonId, storeInfo)
|
||||
return storeInfo
|
||||
}
|
||||
|
||||
private fun addonStoreInfoUrl(addonId: String): String {
|
||||
val baseUrl = components.addonCollection?.serverURL?.trimEnd('/') ?: DEFAULT_AMO_SERVER_URL
|
||||
return "$baseUrl/api/v5/addons/addon/$addonId/"
|
||||
return "${amoBaseUrl()}/api/v5/addons/addon/$addonId/"
|
||||
}
|
||||
|
||||
private fun amoBaseUrl(): String {
|
||||
return components.addonCollection?.serverURL?.trimEnd('/') ?: DEFAULT_AMO_SERVER_URL
|
||||
}
|
||||
|
||||
private suspend fun fetchAddonListings(
|
||||
query: String?,
|
||||
app: AddonStoreApp,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
sort: String? = null,
|
||||
promoted: String? = null,
|
||||
): List<AddonListing> {
|
||||
val params = mutableListOf<String>()
|
||||
params += "app=${app.queryValue()}"
|
||||
params += "type=extension"
|
||||
params += "page=$page"
|
||||
params += "page_size=$pageSize"
|
||||
if (!query.isNullOrBlank()) {
|
||||
params += "q=${java.net.URLEncoder.encode(query, "UTF-8")}"
|
||||
}
|
||||
sort?.let { params += "sort=$it" }
|
||||
promoted?.let { params += "promoted=$it" }
|
||||
params += "lang=${Locale.getDefault().language}"
|
||||
|
||||
val url = "${amoBaseUrl()}/api/v5/addons/search/?${params.joinToString("&")}"
|
||||
val response = components.core.client.fetch(
|
||||
Request(
|
||||
url = url,
|
||||
method = Request.Method.GET,
|
||||
headers = MutableHeaders("Accept" to "application/json"),
|
||||
),
|
||||
)
|
||||
if (response.status !in 200..299) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val body = response.body.useStream { stream ->
|
||||
String(stream.readAllBytes(), Charsets.UTF_8)
|
||||
}
|
||||
val json = JSONObject(body)
|
||||
val results = json.optJSONArray("results") ?: return emptyList()
|
||||
val language = Locale.getDefault().language
|
||||
val listings = mutableListOf<AddonListing>()
|
||||
for (index in 0 until results.length()) {
|
||||
val entry = results.optJSONObject(index) ?: continue
|
||||
entry.toAddonListing(language)?.let { listings += it }
|
||||
}
|
||||
return listings
|
||||
}
|
||||
|
||||
private fun JSONObject.toAddonListing(language: String): AddonListing? {
|
||||
val id = optString("guid").takeIf { it.isNotBlank() } ?: return null
|
||||
val currentVersion = optJSONObject("current_version") ?: return null
|
||||
val file = currentVersion.optJSONObject("file")
|
||||
?: currentVersion.optJSONArray("files")?.optJSONObject(0)
|
||||
val downloadUrl = file?.optString("url").orEmpty()
|
||||
val latestVersion = currentVersion.optString("version").orEmpty()
|
||||
if (downloadUrl.isBlank() || latestVersion.isBlank()) return null
|
||||
|
||||
val name = pickTranslation("name", language) ?: return null
|
||||
val ratings = optJSONObject("ratings")
|
||||
val ratingAverage = ratings?.optDouble("average")?.takeIf { !it.isNaN() && it > 0.0 }
|
||||
val ratingReviews = ratings?.optInt("text_count", -1)?.takeIf { it >= 0 }?.toLong()
|
||||
val author = optJSONArray("authors")?.optJSONObject(0)
|
||||
val averageDailyUsers = optInt("average_daily_users", -1).takeIf { it >= 0 }?.toLong()
|
||||
|
||||
val previews = mutableListOf<AddonListingPreview>()
|
||||
optJSONArray("previews")?.let { arr ->
|
||||
for (i in 0 until arr.length()) {
|
||||
val p = arr.optJSONObject(i) ?: continue
|
||||
val imageUrl = p.optString("image_url").takeIf { it.isNotBlank() } ?: continue
|
||||
previews += AddonListingPreview(
|
||||
imageUrl = imageUrl,
|
||||
thumbnailUrl = p.optString("thumbnail_url").takeIf { it.isNotBlank() },
|
||||
caption = p.pickTranslation("caption", language),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val permissions = file?.stringArray("permissions").orEmpty()
|
||||
val hostPermissions = file?.stringArray("host_permissions").orEmpty()
|
||||
val optionalPermissions = file?.stringArray("optional_permissions").orEmpty()
|
||||
val dataCollectionPermissions = file?.stringArray("data_collection_permissions").orEmpty()
|
||||
val fileSize = file?.optLong("size", -1L)?.takeIf { it >= 0L }
|
||||
|
||||
val license = currentVersion.optJSONObject("license")
|
||||
val licenseName = license?.pickTranslation("name", language)
|
||||
val licenseUrl = license?.pickTranslatedUrl("url", language)
|
||||
|
||||
val supportUrl = pickTranslatedUrl("support_url", language)
|
||||
val supportEmail = pickTranslation("support_email", language)
|
||||
|
||||
val categories = mutableListOf<String>()
|
||||
optJSONObject("categories")?.let { cats ->
|
||||
val keys = cats.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val arr = cats.optJSONArray(key) ?: continue
|
||||
for (i in 0 until arr.length()) {
|
||||
val c = arr.optString(i)
|
||||
if (!c.isNullOrBlank() && !categories.contains(c)) categories += c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AddonListing(
|
||||
id = id,
|
||||
name = name,
|
||||
summary = pickTranslation("summary", language),
|
||||
description = pickTranslation("description", language),
|
||||
iconUrl = optString("icon_url").takeIf { it.isNotBlank() },
|
||||
latestVersion = latestVersion,
|
||||
downloadUrl = downloadUrl,
|
||||
ratingAverage = ratingAverage,
|
||||
ratingReviews = ratingReviews,
|
||||
authorName = author?.optString("name")?.takeIf { it.isNotBlank() },
|
||||
authorUrl = author?.optString("url")?.takeIf { it.isNotBlank() },
|
||||
homepageUrl = pickHomepage(language),
|
||||
detailUrl = optString("url").orEmpty(),
|
||||
ratingUrl = optString("ratings_url").takeIf { it.isNotBlank() },
|
||||
averageDailyUsers = averageDailyUsers,
|
||||
promoted = pickPromoted(),
|
||||
previews = previews,
|
||||
permissions = permissions,
|
||||
hostPermissions = hostPermissions,
|
||||
optionalPermissions = optionalPermissions,
|
||||
dataCollectionPermissions = dataCollectionPermissions,
|
||||
fileSize = fileSize,
|
||||
lastUpdated = optString("last_updated").takeIf { it.isNotBlank() && it != "null" },
|
||||
licenseName = licenseName,
|
||||
licenseUrl = licenseUrl,
|
||||
supportUrl = supportUrl,
|
||||
supportEmail = supportEmail,
|
||||
categories = categories,
|
||||
hasPrivacyPolicy = optBoolean("has_privacy_policy", false),
|
||||
slug = optString("slug").takeIf { it.isNotBlank() && it != "null" },
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.stringArray(key: String): List<String> {
|
||||
val arr = optJSONArray(key) ?: return emptyList()
|
||||
val out = mutableListOf<String>()
|
||||
for (i in 0 until arr.length()) {
|
||||
val s = arr.optString(i)
|
||||
if (!s.isNullOrBlank() && s != "null") out += s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun JSONObject.pickPromoted(): AddonStorePromoted {
|
||||
val categories = optJSONObject("promoted")?.optJSONArray("category")
|
||||
?: optJSONArray("promoted")
|
||||
val singleCategory = optJSONObject("promoted")?.optString("category")
|
||||
val candidates = buildList<String> {
|
||||
if (categories is JSONArray) {
|
||||
for (i in 0 until categories.length()) {
|
||||
categories.optString(i)?.let { add(it) }
|
||||
}
|
||||
}
|
||||
if (!singleCategory.isNullOrBlank()) add(singleCategory)
|
||||
}
|
||||
return when {
|
||||
candidates.any { it.equals("recommended", ignoreCase = true) } -> AddonStorePromoted.RECOMMENDED
|
||||
candidates.any { it.equals("line", ignoreCase = true) } -> AddonStorePromoted.LINE
|
||||
else -> AddonStorePromoted.NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun AddonStoreApp.queryValue(): String = when (this) {
|
||||
AddonStoreApp.ANDROID -> "android"
|
||||
AddonStoreApp.FIREFOX -> "firefox"
|
||||
}
|
||||
|
||||
private fun saveManualUpdateAttempt(
|
||||
@@ -687,10 +960,62 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Addon.needsAmoEnrichment(): Boolean {
|
||||
val hasRating = (rating?.average ?: 0f) > 0f
|
||||
val hasDescription = translatableDescription.values.any { it.isNotBlank() }
|
||||
return !hasRating || !hasDescription
|
||||
}
|
||||
|
||||
private fun JSONObject.pickHomepage(language: String): String? {
|
||||
return pickTranslatedUrl("homepage", language)
|
||||
}
|
||||
|
||||
// AMO represents URL-bearing translated fields either as a flat translated map
|
||||
// (locale → url), a plain string, or a nested { url: {locale: url, ...}, outgoing: {...} }.
|
||||
private fun JSONObject.pickTranslatedUrl(key: String, language: String): String? {
|
||||
if (isNull(key)) return null
|
||||
val value = opt(key)
|
||||
if (value is JSONObject && value.has("url")) {
|
||||
return value.pickTranslation("url", language)
|
||||
}
|
||||
return pickTranslation(key, language)
|
||||
}
|
||||
|
||||
private fun JSONObject.pickTranslation(key: String, language: String): String? {
|
||||
if (isNull(key)) return null
|
||||
return when (val value = opt(key)) {
|
||||
is String -> value.takeIf { it.isNotBlank() }
|
||||
is JSONObject -> {
|
||||
val lower = language.lowercase(Locale.ROOT)
|
||||
// Filter out keys whose value is JSONObject.NULL — optString would
|
||||
// return the literal string "null" for those.
|
||||
val populatedKeys = value.keys().asSequence()
|
||||
.filter { !value.isNull(it) }
|
||||
.toList()
|
||||
val defaultLocale = value.optString("_default").takeIf {
|
||||
it.isNotBlank() && it != "null"
|
||||
}
|
||||
val candidateKeys = listOfNotNull(
|
||||
populatedKeys.firstOrNull { it.equals(lower, ignoreCase = true) },
|
||||
populatedKeys.firstOrNull { it.lowercase(Locale.ROOT).startsWith("$lower-") },
|
||||
defaultLocale?.takeIf { populatedKeys.contains(it) },
|
||||
"en-US".takeIf { populatedKeys.contains(it) },
|
||||
populatedKeys.firstOrNull { it != "_default" },
|
||||
)
|
||||
candidateKeys
|
||||
.asSequence()
|
||||
.map { value.optString(it) }
|
||||
.firstOrNull { it.isNotBlank() }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Addon.toPigeon(
|
||||
context: Context,
|
||||
isAutoUpdateEnabled: Boolean,
|
||||
isLocalFileInstalled: Boolean,
|
||||
storeInfo: AddonStoreInfo? = null,
|
||||
): AddonInfo {
|
||||
val installedState = installedState
|
||||
val localizedName = displayName(context)
|
||||
@@ -701,24 +1026,34 @@ private fun Addon.toPigeon(
|
||||
""
|
||||
}
|
||||
|
||||
val geckoRatingAverage = rating?.average?.toDouble()?.takeIf { it > 0.0 }
|
||||
val geckoRatingReviews = rating?.reviews?.toLong()?.takeIf { it > 0L }
|
||||
val resolvedSummary = localizedSummary?.takeIf { it.isNotBlank() } ?: storeInfo?.summary
|
||||
val resolvedDescription = localizedDescription.ifBlank { storeInfo?.description.orEmpty() }
|
||||
val resolvedHomepage = homepageUrl.ifBlank { storeInfo?.homepageUrl.orEmpty() }
|
||||
val resolvedDetailUrl = detailUrl.ifBlank { storeInfo?.detailUrl.orEmpty() }
|
||||
val resolvedRatingUrl = ratingUrl.ifBlank { storeInfo?.ratingUrl.orEmpty() }
|
||||
val resolvedAuthorName = author?.name ?: storeInfo?.authorName
|
||||
val resolvedAuthorUrl = author?.url ?: storeInfo?.authorUrl
|
||||
|
||||
return AddonInfo(
|
||||
id = id,
|
||||
displayName = localizedName,
|
||||
summary = localizedSummary,
|
||||
description = localizedDescription,
|
||||
summary = resolvedSummary,
|
||||
description = resolvedDescription,
|
||||
downloadUrl = downloadUrl,
|
||||
version = version,
|
||||
installedVersion = installedState?.version,
|
||||
translatedPermissions = translatePermissions(context),
|
||||
translatedRequiredDataCollectionPermissions =
|
||||
translateRequiredDataCollectionPermissions(context),
|
||||
authorName = author?.name,
|
||||
authorUrl = author?.url,
|
||||
homepageUrl = homepageUrl,
|
||||
detailUrl = detailUrl,
|
||||
ratingUrl = ratingUrl,
|
||||
ratingAverage = rating?.average?.toDouble(),
|
||||
ratingReviews = rating?.reviews?.toLong(),
|
||||
authorName = resolvedAuthorName,
|
||||
authorUrl = resolvedAuthorUrl,
|
||||
homepageUrl = resolvedHomepage,
|
||||
detailUrl = resolvedDetailUrl,
|
||||
ratingUrl = resolvedRatingUrl,
|
||||
ratingAverage = geckoRatingAverage ?: storeInfo?.ratingAverage,
|
||||
ratingReviews = geckoRatingReviews ?: storeInfo?.ratingReviews,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt,
|
||||
icon = provideIcon()?.toWebPBytes(),
|
||||
|
||||
+539
-206
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,11 @@ export 'src/pigeons/gecko.g.dart'
|
||||
AddonDisabledReason,
|
||||
AddonIncognito,
|
||||
AddonInfo,
|
||||
AddonListing,
|
||||
AddonListingPreview,
|
||||
AddonStoreApp,
|
||||
AddonStoreInfo,
|
||||
AddonStorePromoted,
|
||||
AddonUpdateAttemptInfo,
|
||||
AddonUpdateStatus,
|
||||
AppLinksMode,
|
||||
|
||||
@@ -49,6 +49,22 @@ class GeckoAddonService extends GeckoAddonEvents {
|
||||
return _api.getAddonStoreInfo(addonId);
|
||||
}
|
||||
|
||||
Future<List<AddonListing>> searchAddonListings({
|
||||
required String query,
|
||||
required AddonStoreApp app,
|
||||
int page = 1,
|
||||
int pageSize = 25,
|
||||
}) {
|
||||
return _api.searchAddonListings(query, app, page, pageSize);
|
||||
}
|
||||
|
||||
Future<List<AddonListing>> getFeaturedAddonListings({
|
||||
required AddonStoreApp app,
|
||||
int pageSize = 25,
|
||||
}) {
|
||||
return _api.getFeaturedAddonListings(app, pageSize);
|
||||
}
|
||||
|
||||
Future<void> invokeAddonAction(
|
||||
String extensionId,
|
||||
WebExtensionActionType actionType,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -749,13 +749,113 @@ class AddonInfo {
|
||||
});
|
||||
}
|
||||
|
||||
enum AddonStoreApp { android, firefox }
|
||||
|
||||
enum AddonStorePromoted { none, recommended, line }
|
||||
|
||||
class AddonListingPreview {
|
||||
final String imageUrl;
|
||||
final String? thumbnailUrl;
|
||||
final String? caption;
|
||||
|
||||
const AddonListingPreview({
|
||||
required this.imageUrl,
|
||||
this.thumbnailUrl,
|
||||
this.caption,
|
||||
});
|
||||
}
|
||||
|
||||
class AddonListing {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? summary;
|
||||
final String? description;
|
||||
final String? iconUrl;
|
||||
final String latestVersion;
|
||||
final String downloadUrl;
|
||||
final double? ratingAverage;
|
||||
final int? ratingReviews;
|
||||
final String? authorName;
|
||||
final String? authorUrl;
|
||||
final String? homepageUrl;
|
||||
final String detailUrl;
|
||||
final String? ratingUrl;
|
||||
final int? averageDailyUsers;
|
||||
final AddonStorePromoted promoted;
|
||||
final List<AddonListingPreview> previews;
|
||||
final List<String> permissions;
|
||||
final List<String> hostPermissions;
|
||||
final List<String> optionalPermissions;
|
||||
final List<String> dataCollectionPermissions;
|
||||
final int? fileSize;
|
||||
final String? lastUpdated;
|
||||
final String? licenseName;
|
||||
final String? licenseUrl;
|
||||
final String? supportUrl;
|
||||
final String? supportEmail;
|
||||
final List<String> categories;
|
||||
final bool hasPrivacyPolicy;
|
||||
final String? slug;
|
||||
|
||||
const AddonListing({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.summary,
|
||||
this.description,
|
||||
this.iconUrl,
|
||||
required this.latestVersion,
|
||||
required this.downloadUrl,
|
||||
this.ratingAverage,
|
||||
this.ratingReviews,
|
||||
this.authorName,
|
||||
this.authorUrl,
|
||||
this.homepageUrl,
|
||||
required this.detailUrl,
|
||||
this.ratingUrl,
|
||||
this.averageDailyUsers,
|
||||
this.promoted = AddonStorePromoted.none,
|
||||
this.previews = const [],
|
||||
this.permissions = const [],
|
||||
this.hostPermissions = const [],
|
||||
this.optionalPermissions = const [],
|
||||
this.dataCollectionPermissions = const [],
|
||||
this.fileSize,
|
||||
this.lastUpdated,
|
||||
this.licenseName,
|
||||
this.licenseUrl,
|
||||
this.supportUrl,
|
||||
this.supportEmail,
|
||||
this.categories = const [],
|
||||
this.hasPrivacyPolicy = false,
|
||||
this.slug,
|
||||
});
|
||||
}
|
||||
|
||||
class AddonStoreInfo {
|
||||
final String latestVersion;
|
||||
final String latestXpiUrl;
|
||||
final double? ratingAverage;
|
||||
final int? ratingReviews;
|
||||
final String? summary;
|
||||
final String? description;
|
||||
final String? homepageUrl;
|
||||
final String? detailUrl;
|
||||
final String? ratingUrl;
|
||||
final String? authorName;
|
||||
final String? authorUrl;
|
||||
|
||||
const AddonStoreInfo({
|
||||
required this.latestVersion,
|
||||
required this.latestXpiUrl,
|
||||
this.ratingAverage,
|
||||
this.ratingReviews,
|
||||
this.summary,
|
||||
this.description,
|
||||
this.homepageUrl,
|
||||
this.detailUrl,
|
||||
this.ratingUrl,
|
||||
this.authorName,
|
||||
this.authorUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1853,6 +1953,20 @@ abstract class GeckoAddonsApi {
|
||||
@async
|
||||
AddonStoreInfo? getAddonStoreInfo(String addonId);
|
||||
|
||||
@async
|
||||
List<AddonListing> searchAddonListings(
|
||||
String query,
|
||||
AddonStoreApp app,
|
||||
int page,
|
||||
int pageSize,
|
||||
);
|
||||
|
||||
@async
|
||||
List<AddonListing> getFeaturedAddonListings(
|
||||
AddonStoreApp app,
|
||||
int pageSize,
|
||||
);
|
||||
|
||||
void invokeAddonAction(String extensionId, WebExtensionActionType actionType);
|
||||
|
||||
@async
|
||||
|
||||
Reference in New Issue
Block a user